// Copyright 2004-present Facebook. All Rights Reserved.

package com.facebook.react.uimanager.layoutanimation;

import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;

import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;

import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UiThreadUtil;

/**
 * Class responsible for animation layout changes, if a valid layout animation config has been
 * supplied. If not animation is available, layout change is applied immediately instead of
 * performing an animation.
 *
 * TODO(7613721): Invoke success callback at the end of animation and when animation gets cancelled.
 */
@NotThreadSafe
public class LayoutAnimationController {

  private static final boolean ENABLED = true;

  private final AbstractLayoutAnimation mLayoutCreateAnimation = new LayoutCreateAnimation();
  private final AbstractLayoutAnimation mLayoutUpdateAnimation = new LayoutUpdateAnimation();
  private final AbstractLayoutAnimation mLayoutDeleteAnimation = new LayoutDeleteAnimation();
  private boolean mShouldAnimateLayout;

  public void initializeFromConfig(final @Nullable ReadableMap config) {
    if (!ENABLED) {
      return;
    }

    if (config == null) {
      reset();
      return;
    }

    mShouldAnimateLayout = false;
    int globalDuration = config.hasKey("duration") ? config.getInt("duration") : 0;
    if (config.hasKey(LayoutAnimationType.CREATE.toString())) {
      mLayoutCreateAnimation.initializeFromConfig(
          config.getMap(LayoutAnimationType.CREATE.toString()), globalDuration);
      mShouldAnimateLayout = true;
    }
    if (config.hasKey(LayoutAnimationType.UPDATE.toString())) {
      mLayoutUpdateAnimation.initializeFromConfig(
          config.getMap(LayoutAnimationType.UPDATE.toString()), globalDuration);
      mShouldAnimateLayout = true;
    }
    if (config.hasKey(LayoutAnimationType.DELETE.toString())) {
      mLayoutDeleteAnimation.initializeFromConfig(
          config.getMap(LayoutAnimationType.DELETE.toString()), globalDuration);
      mShouldAnimateLayout = true;
    }
  }

  public void reset() {
    mLayoutCreateAnimation.reset();
    mLayoutUpdateAnimation.reset();
    mLayoutDeleteAnimation.reset();
    mShouldAnimateLayout = false;
  }

  public boolean shouldAnimateLayout(View viewToAnimate) {
    // if view parent is null, skip animation: view have been clipped, we don't want animation to
    // resume when view is re-attached to parent, which is the standard android animation behavior.
    return mShouldAnimateLayout && viewToAnimate.getParent() != null;
  }

  /**
   * Update layout of given view, via immediate update or animation depending on the current batch
   * layout animation configuration supplied during initialization. Handles create and update
   * animations.
   *
   * @param view the view to update layout of
   * @param x the new X position for the view
   * @param y the new Y position for the view
   * @param width the new width value for the view
   * @param height the new height value for the view
   */
  public void applyLayoutUpdate(View view, int x, int y, int width, int height) {
    UiThreadUtil.assertOnUiThread();

    // Determine which animation to use : if view is initially invisible, use create animation,
    // otherwise use update animation. This approach is easier than maintaining a list of tags
    // for recently created views.
    AbstractLayoutAnimation layoutAnimation = (view.getWidth() == 0 || view.getHeight() == 0) ?
        mLayoutCreateAnimation :
        mLayoutUpdateAnimation;

    Animation animation = layoutAnimation.createAnimation(view, x, y, width, height);
    if (animation == null || !(animation instanceof HandleLayout)) {
      view.layout(x, y, x + width, y + height);
    }
    if (animation != null) {
      view.startAnimation(animation);
    }
  }

  /**
   * Animate a view deletion using the layout animation configuration supplied during initialization.
   *
   * @param view     The view to animate.
   * @param listener Called once the animation is finished, should be used to
   *                 completely remove the view.
   */
  public void deleteView(final View view, final LayoutAnimationListener listener) {
    UiThreadUtil.assertOnUiThread();

    AbstractLayoutAnimation layoutAnimation = mLayoutDeleteAnimation;

    Animation animation = layoutAnimation.createAnimation(
        view, view.getLeft(), view.getTop(), view.getWidth(), view.getHeight());

    if (animation != null) {
      disableUserInteractions(view);

      animation.setAnimationListener(new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation anim) {}

        @Override
        public void onAnimationRepeat(Animation anim) {}

        @Override
        public void onAnimationEnd(Animation anim) {
          listener.onAnimationEnd();
        }
      });

      view.startAnimation(animation);
    } else {
      listener.onAnimationEnd();
    }
  }

  /**
   * Disables user interactions for a view and all it's subviews.
   */
  private void disableUserInteractions(View view) {
    view.setClickable(false);
    if (view instanceof ViewGroup) {
      ViewGroup viewGroup = (ViewGroup)view;
      for (int i = 0; i < viewGroup.getChildCount(); i++) {
        disableUserInteractions(viewGroup.getChildAt(i));
      }
    }
  }
}
