/**
 * Copyright (c) 2015-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @noflow
 * @format
 */
'use strict';

import AnimatedValue from './nodes/AnimatedValue';
import NativeAnimatedHelper from './NativeAnimatedHelper';
import findNodeHandle from '../../../exports/findNodeHandle';
import invariant from 'fbjs/lib/invariant';

const { shouldUseNativeDriver } = NativeAnimatedHelper;

export type Mapping = {[key: string]: Mapping} | AnimatedValue;
export type EventConfig = {
  listener?: ?Function,
  useNativeDriver?: boolean,
};

function attachNativeEvent(
  viewRef: any,
  eventName: string,
  argMapping: Array<?Mapping>,
) {
  // Find animated values in `argMapping` and create an array representing their
  // key path inside the `nativeEvent` object. Ex.: ['contentOffset', 'x'].
  const eventMappings = [];

  const traverse = (value, path) => {
    if (value instanceof AnimatedValue) {
      value.__makeNative();

      eventMappings.push({
        nativeEventPath: path,
        animatedValueTag: value.__getNativeTag(),
      });
    } else if (typeof value === 'object') {
      for (const key in value) {
        traverse(value[key], path.concat(key));
      }
    }
  };

  invariant(
    argMapping[0] && argMapping[0].nativeEvent,
    'Native driven events only support animated values contained inside `nativeEvent`.',
  );

  // Assume that the event containing `nativeEvent` is always the first argument.
  traverse(argMapping[0].nativeEvent, []);

  const viewTag = findNodeHandle(viewRef);

  eventMappings.forEach(mapping => {
    NativeAnimatedHelper.API.addAnimatedEventToView(
      viewTag,
      eventName,
      mapping,
    );
  });

  return {
    detach() {
      eventMappings.forEach(mapping => {
        NativeAnimatedHelper.API.removeAnimatedEventFromView(
          viewTag,
          eventName,
          mapping.animatedValueTag,
        );
      });
    },
  };
}

class AnimatedEvent {
  _argMapping: Array<?Mapping>;
  _listeners: Array<Function> = [];
  _callListeners: Function;
  _attachedEvent: ?{
    detach: () => void,
  };
  __isNative: boolean;

  constructor(argMapping: Array<?Mapping>, config?: EventConfig = {}) {
    this._argMapping = argMapping;
    if (config.listener) {
      this.__addListener(config.listener);
    }
    this._callListeners = this._callListeners.bind(this);
    this._attachedEvent = null;
    this.__isNative = shouldUseNativeDriver(config);

    if (process.env.NODE_ENV !== 'production') {
      this._validateMapping();
    }
  }

  __addListener(callback: Function): void {
    this._listeners.push(callback);
  }

  __removeListener(callback: Function): void {
    this._listeners = this._listeners.filter(listener => listener !== callback);
  }

  __attach(viewRef: any, eventName: string) {
    invariant(
      this.__isNative,
      'Only native driven events need to be attached.',
    );

    this._attachedEvent = attachNativeEvent(
      viewRef,
      eventName,
      this._argMapping,
    );
  }

  __detach(viewTag: any, eventName: string) {
    invariant(
      this.__isNative,
      'Only native driven events need to be detached.',
    );

    this._attachedEvent && this._attachedEvent.detach();
  }

  __getHandler() {
    if (this.__isNative) {
      return this._callListeners;
    }

    return (...args: any) => {
      const traverse = (recMapping, recEvt, key) => {
        if (typeof recEvt === 'number' && recMapping instanceof AnimatedValue) {
          recMapping.setValue(recEvt);
        } else if (typeof recMapping === 'object') {
          for (const mappingKey in recMapping) {
            /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This
             * comment suppresses an error when upgrading Flow's support for
             * React. To see the error delete this comment and run Flow. */
            traverse(recMapping[mappingKey], recEvt[mappingKey], mappingKey);
          }
        }
      };

      if (!this.__isNative) {
        this._argMapping.forEach((mapping, idx) => {
          traverse(mapping, args[idx], 'arg' + idx);
        });
      }
      this._callListeners(...args);
    };
  }

  _callListeners(...args: any) {
    this._listeners.forEach(listener => listener(...args));
  }

  _validateMapping() {
    const traverse = (recMapping, recEvt, key) => {
      if (typeof recEvt === 'number') {
        invariant(
          recMapping instanceof AnimatedValue,
          'Bad mapping of type ' +
            typeof recMapping +
            ' for key ' +
            key +
            ', event value must map to AnimatedValue',
        );
        return;
      }
      invariant(
        typeof recMapping === 'object',
        'Bad mapping of type ' + typeof recMapping + ' for key ' + key,
      );
      invariant(
        typeof recEvt === 'object',
        'Bad event of type ' + typeof recEvt + ' for key ' + key,
      );
      for (const mappingKey in recMapping) {
        traverse(recMapping[mappingKey], recEvt[mappingKey], mappingKey);
      }
    };
  }
}

export { AnimatedEvent, attachNativeEvent };
export default { AnimatedEvent, attachNativeEvent };
