/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @format
 */

'use strict';

import type {PlatformConfig} from '../AnimatedPlatformConfig';

import AnimatedValue from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
import invariant from 'invariant';

export type AnimatedValueXYConfig = $ReadOnly<{
  useNativeDriver: boolean,
}>;
type ValueXYListenerCallback = (value: {x: number, y: number, ...}) => mixed;

let _uniqueId = 1;

/**
 * 2D Value for driving 2D animations, such as pan gestures. Almost identical
 * API to normal `Animated.Value`, but multiplexed.
 *
 * See https://reactnative.dev/docs/animatedvaluexy
 */
export default class AnimatedValueXY extends AnimatedWithChildren {
  x: AnimatedValue;
  y: AnimatedValue;
  _listeners: {
    [key: string]: {
      x: string,
      y: string,
      ...
    },
    ...
  };

  constructor(
    valueIn?: ?{
      +x: number | AnimatedValue,
      +y: number | AnimatedValue,
      ...
    },
    config?: ?AnimatedValueXYConfig,
  ) {
    super();
    const value: any = valueIn || {x: 0, y: 0}; // @flowfixme: shouldn't need `: any`
    if (typeof value.x === 'number' && typeof value.y === 'number') {
      this.x = new AnimatedValue(value.x);
      this.y = new AnimatedValue(value.y);
    } else {
      invariant(
        value.x instanceof AnimatedValue && value.y instanceof AnimatedValue,
        'AnimatedValueXY must be initialized with an object of numbers or ' +
          'AnimatedValues.',
      );
      this.x = value.x;
      this.y = value.y;
    }
    this._listeners = {};
    if (config && config.useNativeDriver) {
      this.__makeNative();
    }
  }

  /**
   * Directly set the value. This will stop any animations running on the value
   * and update all the bound properties.
   *
   * See https://reactnative.dev/docs/animatedvaluexy#setvalue
   */
  setValue(value: {x: number, y: number, ...}) {
    this.x.setValue(value.x);
    this.y.setValue(value.y);
  }

  /**
   * Sets an offset that is applied on top of whatever value is set, whether
   * via `setValue`, an animation, or `Animated.event`. Useful for compensating
   * things like the start of a pan gesture.
   *
   * See https://reactnative.dev/docs/animatedvaluexy#setoffset
   */
  setOffset(offset: {x: number, y: number, ...}) {
    this.x.setOffset(offset.x);
    this.y.setOffset(offset.y);
  }

  /**
   * Merges the offset value into the base value and resets the offset to zero.
   * The final output of the value is unchanged.
   *
   * See https://reactnative.dev/docs/animatedvaluexy#flattenoffset
   */
  flattenOffset(): void {
    this.x.flattenOffset();
    this.y.flattenOffset();
  }

  /**
   * Sets the offset value to the base value, and resets the base value to
   * zero. The final output of the value is unchanged.
   *
   * See https://reactnative.dev/docs/animatedvaluexy#extractoffset
   */
  extractOffset(): void {
    this.x.extractOffset();
    this.y.extractOffset();
  }

  __getValue(): {
    x: number,
    y: number,
    ...
  } {
    return {
      x: this.x.__getValue(),
      y: this.y.__getValue(),
    };
  }

  /**
   * Stops any animation and resets the value to its original.
   *
   * See https://reactnative.dev/docs/animatedvaluexy#resetanimation
   */
  resetAnimation(
    callback?: (value: {x: number, y: number, ...}) => void,
  ): void {
    this.x.resetAnimation();
    this.y.resetAnimation();
    callback && callback(this.__getValue());
  }

  /**
   * Stops any running animation or tracking. `callback` is invoked with the
   * final value after stopping the animation, which is useful for updating
   * state to match the animation position with layout.
   *
   * See https://reactnative.dev/docs/animatedvaluexy#stopanimation
   */
  stopAnimation(callback?: (value: {x: number, y: number, ...}) => void): void {
    this.x.stopAnimation();
    this.y.stopAnimation();
    callback && callback(this.__getValue());
  }

  /**
   * Adds an asynchronous listener to the value so you can observe updates from
   * animations.  This is useful because there is no way to synchronously read
   * the value because it might be driven natively.
   *
   * Returns a string that serves as an identifier for the listener.
   *
   * See https://reactnative.dev/docs/animatedvaluexy#addlistener
   */
  addListener(callback: ValueXYListenerCallback): string {
    const id = String(_uniqueId++);
    const jointCallback = ({value: number}: any) => {
      callback(this.__getValue());
    };
    this._listeners[id] = {
      x: this.x.addListener(jointCallback),
      y: this.y.addListener(jointCallback),
    };
    return id;
  }

  /**
   * Unregister a listener. The `id` param shall match the identifier
   * previously returned by `addListener()`.
   *
   * See https://reactnative.dev/docs/animatedvaluexy#removelistener
   */
  removeListener(id: string): void {
    this.x.removeListener(this._listeners[id].x);
    this.y.removeListener(this._listeners[id].y);
    delete this._listeners[id];
  }

  /**
   * Remove all registered listeners.
   *
   * See https://reactnative.dev/docs/animatedvaluexy#removealllisteners
   */
  removeAllListeners(): void {
    this.x.removeAllListeners();
    this.y.removeAllListeners();
    this._listeners = {};
  }

  /**
   * Converts `{x, y}` into `{left, top}` for use in style.
   *
   * See https://reactnative.dev/docs/animatedvaluexy#getlayout
   */
  getLayout(): {[key: string]: AnimatedValue, ...} {
    return {
      left: this.x,
      top: this.y,
    };
  }

  /**
   * Converts `{x, y}` into a useable translation transform.
   *
   * See https://reactnative.dev/docs/animatedvaluexy#gettranslatetransform
   */
  getTranslateTransform(): Array<{[key: string]: AnimatedValue, ...}> {
    return [{translateX: this.x}, {translateY: this.y}];
  }

  __attach(): void {
    this.x.__addChild(this);
    this.y.__addChild(this);
    super.__attach();
  }

  __detach(): void {
    this.x.__removeChild(this);
    this.y.__removeChild(this);
    super.__detach();
  }

  __makeNative(platformConfig: ?PlatformConfig) {
    this.x.__makeNative(platformConfig);
    this.y.__makeNative(platformConfig);
    super.__makeNative(platformConfig);
  }
}
