/**
 * 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 java.util.List;

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

import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.SizeMonitoringFrameLayout;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.ViewManagerRegistry;

/**
 * FlatNativeViewHierarchyManager is the only class that performs View manipulations. All of this
 * class methods can only be called from UI thread by {@link FlatUIViewOperationQueue}.
 */
/* package */ final class FlatNativeViewHierarchyManager extends NativeViewHierarchyManager
    implements ViewResolver {

  /* package */ FlatNativeViewHierarchyManager(ViewManagerRegistry viewManagers) {
    super(viewManagers, new FlatRootViewManager());
  }

  @Override
  public View getView(int reactTag) {
    return super.resolveView(reactTag);
  }

  @Override
  public void addRootView(
      int tag,
      SizeMonitoringFrameLayout view,
      ThemedReactContext themedContext) {
    FlatViewGroup root = new FlatViewGroup(themedContext);
    view.addView(root);

    // When unmounting, ReactInstanceManager.detachViewFromInstance() will check id of the
    // top-level View (SizeMonitoringFrameLayout) and pass it back to JS. We want that View's id to
    // be set, otherwise NativeViewHierarchyManager will not be able to cleanup properly.
    view.setId(tag);

    addRootViewGroup(tag, root, themedContext);
  }

  /**
   * Updates DrawCommands and AttachDetachListeners of a FlatViewGroup specified by a reactTag.
   *
   * @param reactTag reactTag to lookup FlatViewGroup by
   * @param drawCommands if non-null, new draw commands to execute during the drawing.
   * @param listeners if non-null, new attach-detach listeners.
   */
  /* package */ void updateMountState(
      int reactTag,
      @Nullable DrawCommand[] drawCommands,
      @Nullable AttachDetachListener[] listeners,
      @Nullable NodeRegion[] nodeRegions) {
    FlatViewGroup view = (FlatViewGroup) resolveView(reactTag);
    if (drawCommands != null) {
      view.mountDrawCommands(drawCommands);
    }
    if (listeners != null) {
      view.mountAttachDetachListeners(listeners);
    }
    if (nodeRegions != null) {
      view.mountNodeRegions(nodeRegions);
    }
  }

  /**
   * Updates DrawCommands and AttachDetachListeners of a clipping FlatViewGroup specified by a
   * reactTag.
   *
   * @param reactTag The react tag to lookup FlatViewGroup by.
   * @param drawCommands If non-null, new draw commands to execute during the drawing.
   * @param drawViewIndexMap Mapping of react tags to the index of the corresponding DrawView
   *   command in the draw command array.
   * @param commandMaxBot At each index i, the maximum bottom value (or right value in the case of
   *   horizontal clipping) value of all draw commands at or below i.
   * @param commandMinTop At each index i, the minimum top value (or left value in the case of
   *   horizontal clipping) value of all draw commands at or below i.
   * @param listeners If non-null, new attach-detach listeners.
   * @param nodeRegions Node regions to mount.
   * @param regionMaxBot At each index i, the maximum bottom value (or right value in the case of
   *   horizontal clipping) value of all node regions at or below i.
   * @param regionMinTop At each index i, the minimum top value (or left value in the case of
   *   horizontal clipping) value of all draw commands at or below i.
   * @param willMountViews Whether we are going to also send a mountViews command in this state
   *   cycle.
   */
  /* package */ void updateClippingMountState(
      int reactTag,
      @Nullable DrawCommand[] drawCommands,
      SparseIntArray drawViewIndexMap,
      float[] commandMaxBot,
      float[] commandMinTop,
      @Nullable AttachDetachListener[] listeners,
      @Nullable NodeRegion[] nodeRegions,
      float[] regionMaxBot,
      float[] regionMinTop,
      boolean willMountViews) {
    FlatViewGroup view = (FlatViewGroup) resolveView(reactTag);
    if (drawCommands != null) {
      view.mountClippingDrawCommands(
          drawCommands,
          drawViewIndexMap,
          commandMaxBot,
          commandMinTop,
          willMountViews);
    }
    if (listeners != null) {
      view.mountAttachDetachListeners(listeners);
    }
    if (nodeRegions != null) {
      view.mountClippingNodeRegions(nodeRegions, regionMaxBot, regionMinTop);
    }
  }

  /* package */ void updateViewGroup(int reactTag, int[] viewsToAdd, int[] viewsToDetach) {
    View view = resolveView(reactTag);
    if (view instanceof FlatViewGroup) {
      ((FlatViewGroup) view).mountViews(this, viewsToAdd, viewsToDetach);
      return;
    }

    ViewGroup viewGroup = (ViewGroup) view;
    ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(reactTag);
    List<View> listOfViews = new ArrayList<>(viewsToAdd.length);

    // batch the set of additions - some view managers can take advantage of the batching to
    // decrease operations, etc.
    for (int viewIdToAdd : viewsToAdd) {
      int tag = Math.abs(viewIdToAdd);
      listOfViews.add(resolveView(tag));
    }
    viewManager.addViews(viewGroup, listOfViews);
  }

  /**
   * Updates View bounds, possibly re-measuring and re-layouting it if the size changed.
   *
   * @param reactTag reactTag to lookup a View by
   * @param left left coordinate relative to parent
   * @param top top coordinate relative to parent
   * @param right right coordinate relative to parent
   * @param bottom bottom coordinate relative to parent
   */
  /* package */ void updateViewBounds(int reactTag, int left, int top, int right, int bottom) {
    View view = resolveView(reactTag);
    int width = right - left;
    int height = bottom - top;
    if (view.getWidth() != width || view.getHeight() != height) {
      // size changed, we need to measure and layout the View
      view.measure(
          MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
          MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
      view.layout(left, top, right, bottom);
    } else {
      // same size, only location changed, there is a faster route.
      view.offsetLeftAndRight(left - view.getLeft());
      view.offsetTopAndBottom(top - view.getTop());
    }
  }

  /* package */ void setPadding(
      int reactTag,
      int paddingLeft,
      int paddingTop,
      int paddingRight,
      int paddingBottom) {
    resolveView(reactTag).setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
  }

  /* package */ void dropViews(SparseIntArray viewsToDrop) {
    for (int i = 0, count = viewsToDrop.size(); i < count; i++) {
      int viewToDrop = viewsToDrop.keyAt(i);
      View view = null;
      if (viewToDrop > 0) {
        try {
          view = resolveView(viewToDrop);
          dropView(view);
        } catch (Exception e) {
          // the view is already dropped, nothing we can do
        }
      } else {
        // Root views are noted with a negative tag from StateBuilder.
        removeRootView(-viewToDrop);
      }

      int parentTag = viewsToDrop.valueAt(i);
      // this only happens for clipped, non-root views - clipped because there is no parent, and
      // not a root view (because we explicitly pass -1 for root views).
      if (parentTag > 0 && view != null && view.getParent() == null) {
        // this can only happen if the parent exists (if the parent were removed first, it'd also
        // remove the child, so trying to explicitly remove the child afterwards would crash at
        // the resolveView call above) - we also explicitly check for a null parent, implying that
        // we are either clipped (or that we already removed the child from its parent, in which
        // case this will essentially be a no-op).
        View parent = resolveView(parentTag);
        if (parent instanceof FlatViewGroup) {
          ((FlatViewGroup) parent).onViewDropped(view);
        }
      }
    }
  }

  @Override
  protected void dropView(View view) {
    super.dropView(view);

    // As a result of removeClippedSubviews, some views have strong references but are not attached
    // to a parent. consequently, when the parent gets removed, these Views don't get cleaned up,
    // because they aren't children (they also aren't removed from mTagsToViews, thus causing a
    // leak). To solve this, we ask for said detached views and explicitly drop them.
    if (view instanceof FlatViewGroup) {
      FlatViewGroup flatViewGroup = (FlatViewGroup) view;
      if (flatViewGroup.getRemoveClippedSubviews()) {
        SparseArray<View> detachedViews = flatViewGroup.getDetachedViews();
        for (int i = 0, size = detachedViews.size(); i < size; i++) {
          View detachedChild = detachedViews.valueAt(i);
          try {
             dropView(detachedChild);
          } catch (Exception e) {
             // if the view is already dropped, ignore any exceptions
             // in reality, we should find out the edge cases that cause
             // this to happen and properly fix them.
          }
          // trigger onDetachedFromWindow and clean up this detached/clipped view
          flatViewGroup.removeDetachedView(detachedChild);
        }
      }
    }
  }

  /* package */ void detachAllChildrenFromViews(int[] viewsToDetachAllChildrenFrom) {
    for (int viewTag : viewsToDetachAllChildrenFrom) {
      View view = resolveView(viewTag);
      if (view instanceof FlatViewGroup) {
        ((FlatViewGroup) view).detachAllViewsFromParent();
        continue;
      }

      ViewGroup viewGroup = (ViewGroup) view;
      ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(viewTag);
      viewManager.removeAllViews(viewGroup);
    }
  }
}
