/**
 * 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.flat;

import javax.annotation.Nullable;

import java.util.ArrayList;

import android.util.SparseIntArray;
import android.view.View;
import android.view.ViewGroup;

import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.NoSuchNativeViewException;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.TouchTargetHelper;
import com.facebook.react.uimanager.UIViewOperationQueue;

/**
 * FlatUIViewOperationQueue extends {@link UIViewOperationQueue} to add
 * FlatUIImplementation-specific methods that need to run in UI thread.
 */
/* package */ final class FlatUIViewOperationQueue extends UIViewOperationQueue {

  private static final int[] MEASURE_BUFFER = new int[4];

  private final FlatNativeViewHierarchyManager mNativeViewHierarchyManager;
  private final ProcessLayoutRequests mProcessLayoutRequests = new ProcessLayoutRequests();

  private final class ProcessLayoutRequests implements UIOperation {
    @Override
    public void execute() {
      FlatViewGroup.processLayoutRequests();
    }
  }

  /**
   * UIOperation that updates DrawCommands for a View defined by reactTag.
   */
  private final class UpdateMountState implements UIOperation {

    private final int mReactTag;
    private final @Nullable DrawCommand[] mDrawCommands;
    private final @Nullable AttachDetachListener[] mAttachDetachListeners;
    private final @Nullable NodeRegion[] mNodeRegions;

    private UpdateMountState(
        int reactTag,
        @Nullable DrawCommand[] drawCommands,
        @Nullable AttachDetachListener[] listeners,
        @Nullable NodeRegion[] nodeRegions) {
      mReactTag = reactTag;
      mDrawCommands = drawCommands;
      mAttachDetachListeners = listeners;
      mNodeRegions = nodeRegions;
    }

    @Override
    public void execute() {
      mNativeViewHierarchyManager.updateMountState(
          mReactTag,
          mDrawCommands,
          mAttachDetachListeners,
          mNodeRegions);
    }
  }

  /**
   * UIOperation that updates DrawCommands for a View defined by reactTag.
   */
  private final class UpdateClippingMountState implements UIOperation {

    private final int mReactTag;
    private final @Nullable DrawCommand[] mDrawCommands;
    private final SparseIntArray mDrawViewIndexMap;
    private final float[] mCommandMaxBot;
    private final float[] mCommandMinTop;
    private final @Nullable AttachDetachListener[] mAttachDetachListeners;
    private final @Nullable NodeRegion[] mNodeRegions;
    private final float[] mRegionMaxBot;
    private final float[] mRegionMinTop;
    private final boolean mWillMountViews;

    private UpdateClippingMountState(
        int reactTag,
        @Nullable DrawCommand[] drawCommands,
        SparseIntArray drawViewIndexMap,
        float[] commandMaxBot,
        float[] commandMinTop,
        @Nullable AttachDetachListener[] listeners,
        @Nullable NodeRegion[] nodeRegions,
        float[] regionMaxBot,
        float[] regionMinTop,
        boolean willMountViews) {
      mReactTag = reactTag;
      mDrawCommands = drawCommands;
      mDrawViewIndexMap = drawViewIndexMap;
      mCommandMaxBot = commandMaxBot;
      mCommandMinTop = commandMinTop;
      mAttachDetachListeners = listeners;
      mNodeRegions = nodeRegions;
      mRegionMaxBot = regionMaxBot;
      mRegionMinTop = regionMinTop;
      mWillMountViews = willMountViews;
    }

    @Override
    public void execute() {
      mNativeViewHierarchyManager.updateClippingMountState(
          mReactTag,
          mDrawCommands,
          mDrawViewIndexMap,
          mCommandMaxBot,
          mCommandMinTop,
          mAttachDetachListeners,
          mNodeRegions,
          mRegionMaxBot,
          mRegionMinTop,
          mWillMountViews);
    }
  }

  private final class UpdateViewGroup implements UIOperation {

    private final int mReactTag;
    private final int[] mViewsToAdd;
    private final int[] mViewsToDetach;

    private UpdateViewGroup(int reactTag, int[] viewsToAdd, int[] viewsToDetach) {
      mReactTag = reactTag;
      mViewsToAdd = viewsToAdd;
      mViewsToDetach = viewsToDetach;
    }

    @Override
    public void execute() {
      mNativeViewHierarchyManager.updateViewGroup(mReactTag, mViewsToAdd, mViewsToDetach);
    }
  }

  /**
   * UIOperation that updates View bounds for a View defined by reactTag.
   */
  public final class UpdateViewBounds implements UIOperation {

    private final int mReactTag;
    private final int mLeft;
    private final int mTop;
    private final int mRight;
    private final int mBottom;

    private UpdateViewBounds(int reactTag, int left, int top, int right, int bottom) {
      mReactTag = reactTag;
      mLeft = left;
      mTop = top;
      mRight = right;
      mBottom = bottom;
    }

    @Override
    public void execute() {
      mNativeViewHierarchyManager.updateViewBounds(mReactTag, mLeft, mTop, mRight, mBottom);
    }
  }

  private final class SetPadding implements UIOperation {

    private final int mReactTag;
    private final int mPaddingLeft;
    private final int mPaddingTop;
    private final int mPaddingRight;
    private final int mPaddingBottom;

    private SetPadding(
        int reactTag,
        int paddingLeft,
        int paddingTop,
        int paddingRight,
        int paddingBottom) {
      mReactTag = reactTag;
      mPaddingLeft = paddingLeft;
      mPaddingTop = paddingTop;
      mPaddingRight = paddingRight;
      mPaddingBottom = paddingBottom;
    }

    @Override
    public void execute() {
      mNativeViewHierarchyManager.setPadding(
          mReactTag,
          mPaddingLeft,
          mPaddingTop,
          mPaddingRight,
          mPaddingBottom);
    }
  }

  private final class DropViews implements UIOperation {

    private final SparseIntArray mViewsToDrop;

    private DropViews(ArrayList<Integer> viewsToDrop, ArrayList<Integer> parentsForViewsToDrop) {
      SparseIntArray sparseIntArray = new SparseIntArray();
      for (int i = 0, count = viewsToDrop.size(); i < count; i++) {
        sparseIntArray.put(viewsToDrop.get(i), parentsForViewsToDrop.get(i));
      }
      mViewsToDrop = sparseIntArray;
    }

    @Override
    public void execute() {
      mNativeViewHierarchyManager.dropViews(mViewsToDrop);
    }
  }

  private final class MeasureVirtualView implements UIOperation {

    private final int mReactTag;
    private final float mScaledX;
    private final float mScaledY;
    private final float mScaledWidth;
    private final float mScaledHeight;
    private final Callback mCallback;
    private final boolean mRelativeToWindow;

    private MeasureVirtualView(
        int reactTag,
        float scaledX,
        float scaledY,
        float scaledWidth,
        float scaledHeight,
        boolean relativeToWindow,
        Callback callback) {
      mReactTag = reactTag;
      mScaledX = scaledX;
      mScaledY = scaledY;
      mScaledWidth = scaledWidth;
      mScaledHeight = scaledHeight;
      mCallback = callback;
      mRelativeToWindow = relativeToWindow;
    }

    @Override
    public void execute() {
      try {
        // Measure native View
        if (mRelativeToWindow) {
          // relative to the window
          mNativeViewHierarchyManager.measureInWindow(mReactTag, MEASURE_BUFFER);
        } else {
          // relative to the root view
          mNativeViewHierarchyManager.measure(mReactTag, MEASURE_BUFFER);
        }
      } catch (NoSuchNativeViewException noSuchNativeViewException) {
        // Invoke with no args to signal failure and to allow JS to clean up the callback
        // handle.
        mCallback.invoke();
        return;
      }

      float nativeViewX = MEASURE_BUFFER[0];
      float nativeViewY = MEASURE_BUFFER[1];
      float nativeViewWidth = MEASURE_BUFFER[2];
      float nativeViewHeight = MEASURE_BUFFER[3];

      // Calculate size of the virtual child inside native View.
      float x = PixelUtil.toDIPFromPixel(mScaledX * nativeViewWidth + nativeViewX);
      float y = PixelUtil.toDIPFromPixel(mScaledY * nativeViewHeight + nativeViewY);
      float width = PixelUtil.toDIPFromPixel(mScaledWidth * nativeViewWidth);
      float height = PixelUtil.toDIPFromPixel(mScaledHeight * nativeViewHeight);

      if (mRelativeToWindow) {
        mCallback.invoke(x, y, width, height);
      } else {
        mCallback.invoke(0, 0, width, height, x, y);
      }
    }
  }

  public final class DetachAllChildrenFromViews implements UIOperation {
    private @Nullable int[] mViewsToDetachAllChildrenFrom;

    public void setViewsToDetachAllChildrenFrom(int[] viewsToDetachAllChildrenFrom) {
      mViewsToDetachAllChildrenFrom = viewsToDetachAllChildrenFrom;
    }

    @Override
    public void execute() {
      mNativeViewHierarchyManager.detachAllChildrenFromViews(mViewsToDetachAllChildrenFrom);
    }
  }

  private final class FindTargetForTouchOperation implements UIOperation {

    private final int mReactTag;
    private final float mTargetX;
    private final float mTargetY;
    private final Callback mCallback;
    private final int[] NATIVE_VIEW_BUFFER = new int[1];

    private FindTargetForTouchOperation(
        final int reactTag,
        final float targetX,
        final float targetY,
        final Callback callback) {
      super();
      mReactTag = reactTag;
      mTargetX = targetX;
      mTargetY = targetY;
      mCallback = callback;
    }

    @Override
    public void execute() {
      try {
        mNativeViewHierarchyManager.measure(mReactTag, MEASURE_BUFFER);
      } catch (IllegalViewOperationException e) {
        mCallback.invoke();
        return;
      }

      // Because React coordinates are relative to root container, and measure() operates
      // on screen coordinates, we need to offset values using root container location.
      final float containerX = (float) MEASURE_BUFFER[0];
      final float containerY = (float) MEASURE_BUFFER[1];

      View view = mNativeViewHierarchyManager.getView(mReactTag);
      final int touchTargetReactTag = TouchTargetHelper.findTargetTagForTouch(
          mTargetX,
          mTargetY,
          (ViewGroup) view,
          NATIVE_VIEW_BUFFER);

      try {
        mNativeViewHierarchyManager.measure(
            NATIVE_VIEW_BUFFER[0],
            MEASURE_BUFFER);
      } catch (IllegalViewOperationException e) {
        mCallback.invoke();
        return;
      }

      NodeRegion region = NodeRegion.EMPTY;
      boolean isNativeView = NATIVE_VIEW_BUFFER[0] == touchTargetReactTag;
      if (!isNativeView) {
        // NATIVE_VIEW_BUFFER[0] is a FlatViewGroup, touchTargetReactTag is the touch target and
        // isn't an Android View - try to get its NodeRegion
        view = mNativeViewHierarchyManager.getView(NATIVE_VIEW_BUFFER[0]);
        if (view instanceof FlatViewGroup) {
          region = ((FlatViewGroup) view).getNodeRegionForTag(mReactTag);
        }
      }

      int resultTag = region == NodeRegion.EMPTY ? touchTargetReactTag : region.mTag;
      float x = PixelUtil.toDIPFromPixel(region.getLeft() + MEASURE_BUFFER[0] - containerX);
      float y = PixelUtil.toDIPFromPixel(region.getTop() + MEASURE_BUFFER[1] - containerY);
      float width = PixelUtil.toDIPFromPixel(isNativeView ?
          MEASURE_BUFFER[2] : region.getRight() - region.getLeft());
      float height = PixelUtil.toDIPFromPixel(isNativeView ?
          MEASURE_BUFFER[3] : region.getBottom() - region.getTop());
      mCallback.invoke(resultTag, x, y, width, height);
    }
  }

  /**
   * Used to delay view manager command dispatch until after the view hierarchy is updated.
   * Mirrors command operation dispatch, but is only used in Nodes for view manager commands.
   */
  public final class ViewManagerCommand implements UIOperation {

    private final int mReactTag;
    private final int mCommand;
    private final @Nullable ReadableArray mArgs;

    public ViewManagerCommand(
        int reactTag,
        int command,
        @Nullable ReadableArray args) {
      mReactTag = reactTag;
      mCommand = command;
      mArgs = args;
    }

    @Override
    public void execute() {
      mNativeViewHierarchyManager.dispatchCommand(mReactTag, mCommand, mArgs);
    }
  }

  public FlatUIViewOperationQueue(
      ReactApplicationContext reactContext,
      FlatNativeViewHierarchyManager nativeViewHierarchyManager) {
    super(reactContext, nativeViewHierarchyManager);

    mNativeViewHierarchyManager = nativeViewHierarchyManager;
  }

  /**
   * Enqueues a new UIOperation that will update DrawCommands for a View defined by reactTag.
   */
  public void enqueueUpdateMountState(
      int reactTag,
      @Nullable DrawCommand[] drawCommands,
      @Nullable AttachDetachListener[] listeners,
      @Nullable NodeRegion[] nodeRegions) {
    enqueueUIOperation(new UpdateMountState(
        reactTag,
        drawCommands,
        listeners,
        nodeRegions));
  }

  /**
   * Enqueues a new UIOperation that will update DrawCommands for a View defined by reactTag.
   */
  public void enqueueUpdateClippingMountState(
      int reactTag,
      @Nullable DrawCommand[] drawCommands,
      SparseIntArray drawViewIndexMap,
      float[] commandMaxBot,
      float[] commandMinTop,
      @Nullable AttachDetachListener[] listeners,
      @Nullable NodeRegion[] nodeRegions,
      float[] regionMaxBot,
      float[] regionMinTop,
      boolean willMountViews) {
    enqueueUIOperation(new UpdateClippingMountState(
        reactTag,
        drawCommands,
        drawViewIndexMap,
        commandMaxBot,
        commandMinTop,
        listeners,
        nodeRegions,
        regionMaxBot,
        regionMinTop,
        willMountViews));
  }

  public void enqueueUpdateViewGroup(int reactTag, int[] viewsToAdd, int[] viewsToDetach) {
    enqueueUIOperation(new UpdateViewGroup(reactTag, viewsToAdd, viewsToDetach));
  }

  /**
   * Creates a new UIOperation that will update View bounds for a View defined by reactTag.
   */
  public UpdateViewBounds createUpdateViewBounds(
      int reactTag,
      int left,
      int top,
      int right,
      int bottom) {
    return new UpdateViewBounds(reactTag, left, top, right, bottom);
  }

  public ViewManagerCommand createViewManagerCommand(
      int reactTag,
      int command,
      @Nullable ReadableArray args) {
    return new ViewManagerCommand(reactTag, command, args);
  }

  /* package */ void enqueueFlatUIOperation(UIOperation operation) {
    enqueueUIOperation(operation);
  }

  public void enqueueSetPadding(
      int reactTag,
      int paddingLeft,
      int paddingTop,
      int paddingRight,
      int paddingBottom) {
    enqueueUIOperation(
        new SetPadding(reactTag, paddingLeft, paddingTop, paddingRight, paddingBottom));
  }

  public void enqueueDropViews(
      ArrayList<Integer> viewsToDrop,
      ArrayList<Integer> parentsOfViewsToDrop) {
    enqueueUIOperation(new DropViews(viewsToDrop, parentsOfViewsToDrop));
  }

  public void enqueueMeasureVirtualView(
      int reactTag,
      float scaledX,
      float scaledY,
      float scaledWidth,
      float scaledHeight,
      boolean relativeToWindow,
      Callback callback) {
    enqueueUIOperation(new MeasureVirtualView(
        reactTag,
        scaledX,
        scaledY,
        scaledWidth,
        scaledHeight,
        relativeToWindow,
        callback));
  }

  public void enqueueProcessLayoutRequests() {
    enqueueUIOperation(mProcessLayoutRequests);
  }

  public DetachAllChildrenFromViews enqueueDetachAllChildrenFromViews() {
    DetachAllChildrenFromViews op = new DetachAllChildrenFromViews();
    enqueueUIOperation(op);
    return op;
  }

  @Override
  public void enqueueFindTargetForTouch(
      final int reactTag,
      final float targetX,
      final float targetY,
      final Callback callback) {
    enqueueUIOperation(
        new FindTargetForTouchOperation(reactTag, targetX, targetY, callback));
  }
}
