/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.react.animated;

import javax.annotation.Nullable;

import java.util.ArrayList;

import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.OnBatchCompleteListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.modules.core.ReactChoreographer;
import com.facebook.react.uimanager.GuardedFrameCallback;
import com.facebook.react.uimanager.UIManagerModule;

/**
 * Module that exposes interface for creating and managing animated nodes on the "native" side.
 *
 * Animated.js library is based on a concept of a graph where nodes are values or transform
 * operations (such as interpolation, addition, etc) and connection are used to describe how change
 * of the value in one node can affect other nodes.
 *
 * Few examples of the nodes that can be created on the JS side:
 *  - Animated.Value is a simplest type of node with a numeric value which can be driven by an
 *    animation engine (spring, decay, etc) or by calling setValue on it directly from JS
 *  - Animated.add is a type of node that may have two or more input nodes. It outputs the sum of
 *    all the input node values
 *  - interpolate - is actually a method you can call on any node and it creates a new node that
 *    takes the parent node as an input and outputs its interpolated value (e.g. if you have value
 *    that can animate from 0 to 1 you can create interpolated node and set output range to be 0 to
 *    100 and when the input node changes the output of interpolated node will multiply the values
 *    by 100)
 *
 * You can mix and chain nodes however you like and this way create nodes graph with connections
 * between them.
 *
 * To map animated node values to view properties there is a special type of a node: AnimatedProps.
 * It is created by AnimatedImplementation whenever you render Animated.View and stores a mapping
 * from the view properties to the corresponding animated values (so it's actually also a node with
 * connections to the value nodes).
 *
 * Last "special" elements of the the graph are "animation drivers". Those are objects (represented
 * as a graph nodes too) that based on some criteria updates attached values every frame (we have
 * few types of those, e.g., spring, timing, decay). Animation objects can be "started" and
 * "stopped". Those are like "pulse generators" for the rest of the nodes graph. Those pulses then
 * propagate along the graph to the children nodes up to the special node type: AnimatedProps which
 * then can be used to calculate property update map for a view.
 *
 * This class acts as a proxy between the "native" API that can be called from JS and the main class
 * that coordinates all the action: {@link NativeAnimatedNodesManager}. Since all the methods from
 * {@link NativeAnimatedNodesManager} need to be called from the UI thread, we we create a queue of
 * animated graph operations that is then enqueued to be executed in the UI Thread at the end of the
 * batch of JS->native calls (similarily to how it's handled in {@link UIManagerModule}). This
 * isolates us from the problems that may be caused by concurrent updates of animated graph while UI
 * thread is "executing" the animation loop.
 */
@ReactModule(name = NativeAnimatedModule.NAME)
public class NativeAnimatedModule extends ReactContextBaseJavaModule implements
    OnBatchCompleteListener, LifecycleEventListener {

  protected static final String NAME = "NativeAnimatedModule";

  private interface UIThreadOperation {
    void execute(NativeAnimatedNodesManager animatedNodesManager);
  }

  private final Object mOperationsCopyLock = new Object();
  private @Nullable GuardedFrameCallback mAnimatedFrameCallback;
  private @Nullable ReactChoreographer mReactChoreographer;
  private ArrayList<UIThreadOperation> mOperations = new ArrayList<>();
  private volatile @Nullable ArrayList<UIThreadOperation> mReadyOperations = null;

  public NativeAnimatedModule(ReactApplicationContext reactContext) {
    super(reactContext);
  }

  @Override
  public void initialize() {
    getReactApplicationContext().addLifecycleEventListener(this);
  }

  @Override
  public void onHostResume() {
    if (mReactChoreographer == null) {
      // Safe to acquire choreographer here, as onHostResume() is invoked from UI thread.
      mReactChoreographer = ReactChoreographer.getInstance();

      ReactApplicationContext reactCtx = getReactApplicationContext();
      UIManagerModule uiManager = reactCtx.getNativeModule(UIManagerModule.class);

      final NativeAnimatedNodesManager nodesManager = new NativeAnimatedNodesManager(uiManager);
      mAnimatedFrameCallback = new GuardedFrameCallback(reactCtx) {
        @Override
        protected void doFrameGuarded(final long frameTimeNanos) {

          ArrayList<UIThreadOperation> operations;
          synchronized (mOperationsCopyLock) {
            operations = mReadyOperations;
            mReadyOperations = null;
          }

          if (operations != null) {
            for (int i = 0, size = operations.size(); i < size; i++) {
              operations.get(i).execute(nodesManager);
            }
          }

          if (nodesManager.hasActiveAnimations()) {
            nodesManager.runUpdates(frameTimeNanos);
          }

          // TODO: Would be great to avoid adding this callback in case there are no active animations
          // and no outstanding tasks on the operations queue. Apparently frame callbacks can only
          // be posted from the UI thread and therefore we cannot schedule them directly from
          // @ReactMethod methods
          Assertions.assertNotNull(mReactChoreographer).postFrameCallback(
            ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
            mAnimatedFrameCallback);
        }
      };
    }
    enqueueFrameCallback();
  }

  @Override
  public void onBatchComplete() {
    // Note: The order of executing onBatchComplete handler (especially in terms of onBatchComplete
    // from the UIManagerModule) doesn't matter as we only enqueue operations for the UI thread to
    // be executed from here. Thanks to ReactChoreographer all the operations from here are going
    // to be executed *after* all the operations enqueued by UIManager as the callback type that we
    // use for ReactChoreographer (CallbackType.NATIVE_ANIMATED_MODULE) is run after callbacks that
    // UIManager uses.
    ArrayList<UIThreadOperation> operations = mOperations.isEmpty() ? null : mOperations;
    if (operations != null) {
      mOperations = new ArrayList<>();
      synchronized (mOperationsCopyLock) {
        if (mReadyOperations == null) {
          mReadyOperations = operations;
        } else {
          mReadyOperations.addAll(operations);
        }
      }
    }
  }

  @Override
  public void onHostPause() {
    if (mReactChoreographer == null) {
      FLog.e(NAME, "Called NativeAnimated.onHostPause() with a null ReactChoreographer.");
      return;
    }
    clearFrameCallback();
  }

  @Override
  public void onHostDestroy() {
    // do nothing
  }

  @Override
  public String getName() {
    return NAME;
  }

  private void clearFrameCallback() {
    Assertions.assertNotNull(mReactChoreographer).removeFrameCallback(
      ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
      mAnimatedFrameCallback);
  }

  private void enqueueFrameCallback() {
    Assertions.assertNotNull(mReactChoreographer).postFrameCallback(
      ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
      mAnimatedFrameCallback);
  }

  @ReactMethod
  public void createAnimatedNode(final int tag, final ReadableMap config) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.createAnimatedNode(tag, config);
      }
    });
  }

  @ReactMethod
  public void startListeningToAnimatedNodeValue(final int tag) {
    final AnimatedNodeValueListener listener = new AnimatedNodeValueListener() {
      public void onValueUpdate(double value) {
        WritableMap onAnimatedValueData = Arguments.createMap();
        onAnimatedValueData.putInt("tag", tag);
        onAnimatedValueData.putDouble("value", value);
        getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
            .emit("onAnimatedValueUpdate", onAnimatedValueData);
      }
    };

    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.startListeningToAnimatedNodeValue(tag, listener);
      }
    });
  }

  @ReactMethod
  public void stopListeningToAnimatedNodeValue(final int tag) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.stopListeningToAnimatedNodeValue(tag);
      }
    });
  }

  @ReactMethod
  public void dropAnimatedNode(final int tag) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.dropAnimatedNode(tag);
      }
    });
  }

  @ReactMethod
  public void setAnimatedNodeValue(final int tag, final double value) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.setAnimatedNodeValue(tag, value);
      }
    });
  }

  @ReactMethod
  public void setAnimatedNodeOffset(final int tag, final double value) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.setAnimatedNodeOffset(tag, value);
      }
    });
  }

  @ReactMethod
  public void flattenAnimatedNodeOffset(final int tag) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.flattenAnimatedNodeOffset(tag);
      }
    });
  }

  @ReactMethod
  public void extractAnimatedNodeOffset(final int tag) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.extractAnimatedNodeOffset(tag);
      }
    });
  }

  @ReactMethod
  public void startAnimatingNode(
      final int animationId,
      final int animatedNodeTag,
      final ReadableMap animationConfig,
      final Callback endCallback) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.startAnimatingNode(
          animationId,
          animatedNodeTag,
          animationConfig,
          endCallback);
      }
    });
  }

  @ReactMethod
  public void stopAnimation(final int animationId) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.stopAnimation(animationId);
      }
    });
  }

  @ReactMethod
  public void connectAnimatedNodes(final int parentNodeTag, final int childNodeTag) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.connectAnimatedNodes(parentNodeTag, childNodeTag);
      }
    });
  }

  @ReactMethod
  public void disconnectAnimatedNodes(final int parentNodeTag, final int childNodeTag) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.disconnectAnimatedNodes(parentNodeTag, childNodeTag);
      }
    });
  }

  @ReactMethod
  public void connectAnimatedNodeToView(final int animatedNodeTag, final int viewTag) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.connectAnimatedNodeToView(animatedNodeTag, viewTag);
      }
    });
  }

  @ReactMethod
  public void disconnectAnimatedNodeFromView(final int animatedNodeTag, final int viewTag) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.disconnectAnimatedNodeFromView(animatedNodeTag, viewTag);
      }
    });
  }

  @ReactMethod
  public void addAnimatedEventToView(final int viewTag, final String eventName, final ReadableMap eventMapping) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.addAnimatedEventToView(viewTag, eventName, eventMapping);
      }
    });
  }

  @ReactMethod
  public void removeAnimatedEventFromView(final int viewTag, final String eventName, final int animatedValueTag) {
    mOperations.add(new UIThreadOperation() {
      @Override
      public void execute(NativeAnimatedNodesManager animatedNodesManager) {
        animatedNodesManager.removeAnimatedEventFromView(viewTag, eventName, animatedValueTag);
      }
    });
  }
}
