package com.airbnb.android.react.navigation;

import android.annotation.TargetApi;
import android.app.Activity;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.AnimRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.transition.Fade;
import android.util.Log;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;

import com.airbnb.android.R;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;

import java.util.Map;
import java.util.Stack;

import static com.airbnb.android.react.navigation.ReactNativeIntents.EXTRA_CODE;

/**
 * Owner of the navigation stack for a given Activity. There should be one per activity.
 */
public class ScreenCoordinator {
  private static final String TAG = ScreenCoordinator.class.getSimpleName();
  static final String EXTRA_PAYLOAD = "payload";
  private static final String TRANSITION_GROUP = "transitionGroup";

  enum PresentAnimation {
    Modal(R.anim.slide_up, R.anim.delay, R.anim.delay, R.anim.slide_down),
    Push(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right),
    Fade(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out);

    @AnimRes int enter;
    @AnimRes int exit;
    @AnimRes int popEnter;
    @AnimRes int popExit;

    PresentAnimation(int enter, int exit, int popEnter, int popExit) {
      this.enter = enter;
      this.exit = exit;
      this.popEnter = popEnter;
      this.popExit = popExit;
    }
  }

  private final Stack<BackStack> backStacks = new Stack<>();
  private final AppCompatActivity activity;
  private final ScreenCoordinatorLayout container;
  private ReactNavigationCoordinator reactNavigationCoordinator = ReactNavigationCoordinator.sharedInstance;

  private int stackId = 0;
  /**
   * When we dismiss a back stack, the fragment manager would normally execute the latest fragment's
   * pop exit animation. However, if we present A as a modal, push, B, then dismiss(), the latest
   * pop exit animation would be from when B was pushed, not from when A was presented.
   * We want the dismiss animation to be the popExit of the original present transaction.
   */
  @AnimRes private int nextPopExitAnim;

  public ScreenCoordinator(AppCompatActivity activity, ScreenCoordinatorLayout container,
      @Nullable Bundle savedInstanceState) {
    this.activity = activity;
    this.container = container;
    container.setFragmentManager(activity.getSupportFragmentManager());
    // TODO: restore state
  }

  void onSaveInstanceState(Bundle outState) {
    // TODO
  }

  public void pushScreen(String moduleName) {
    pushScreen(moduleName, null, null);
  }

  public void pushScreen(String moduleName, @Nullable Bundle props, @Nullable Bundle options) {
    Fragment fragment = ReactNativeFragment.newInstance(moduleName, props);
    pushScreen(fragment, options);
  }

  public void pushScreen(Fragment fragment) {
    pushScreen(fragment, null);
  }

  public void pushScreen(Fragment fragment, @Nullable Bundle options) {
    FragmentTransaction ft = activity.getSupportFragmentManager().beginTransaction()
            .setAllowOptimization(true);
    Fragment currentFragment = getCurrentFragment();
    if (currentFragment == null) {
      throw new IllegalStateException("There is no current fragment. You must present one first.");
    }

    if (ViewUtils.isAtLeastLollipop() && options != null && options.containsKey(TRANSITION_GROUP)) {
        setupFragmentForSharedElement(currentFragment,  fragment, ft, options);
    } else {
      PresentAnimation anim = PresentAnimation.Push;
      ft.setCustomAnimations(anim.enter, anim.exit, anim.popEnter, anim.popExit);
    }
    BackStack bsi = getCurrentBackStack();
    ft
            .detach(currentFragment)
            .add(container.getId(), fragment)
            .addToBackStack(null)
            .commit();
    bsi.pushFragment(fragment);
    Log.d(TAG, toString());
  }

  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  private void setupFragmentForSharedElement(
          Fragment outFragment, Fragment inFragment, FragmentTransaction transaction, Bundle options) {
    FragmentSharedElementTransition transition = new FragmentSharedElementTransition();
    inFragment.setSharedElementEnterTransition(transition);
    inFragment.setSharedElementReturnTransition(transition);
    Fade fade = new Fade();
    inFragment.setEnterTransition(fade);
    inFragment.setReturnTransition(fade);
    ViewGroup rootView = (ViewGroup) outFragment.getView();
    ViewGroup transitionGroup = ViewUtils.findViewGroupWithTag(
            rootView,
            R.id.react_shared_element_group_id,
            options.getString(TRANSITION_GROUP));
    AutoSharedElementCallback.addSharedElementsToFragmentTransaction(transaction, transitionGroup);
  }

  public void presentScreen(String moduleName) {
    presentScreen(moduleName, null, null, null);
  }

  public void presentScreen(
      String moduleName,
      @Nullable Bundle props,
      @Nullable Bundle options,
      @Nullable Promise promise) {
    // TODO: use options
    Fragment fragment = ReactNativeFragment.newInstance(moduleName, props);
    presentScreen(fragment, PresentAnimation.Modal, promise);
  }

  public void presentScreen(Fragment fragment) {
    presentScreen(fragment, null);
  }

  private Boolean isFragmentTranslucent(Fragment fragment) {
    Bundle bundle = fragment.getArguments();
    if (bundle != null) {
      String moduleName = bundle.getString(ReactNativeIntents.EXTRA_MODULE_NAME);
      if (moduleName != null) {
        ReadableMap config = reactNavigationCoordinator.getInitialConfigForModuleName(moduleName);
        if (config != null && config.hasKey("screenColor")) {
          return Color.alpha(config.getInt("screenColor")) < 255;
        }
      }
    }
    return false;
  }

  public void presentScreen(Fragment fragment, @Nullable Promise promise) {
    presentScreen(fragment, PresentAnimation.Modal, promise);
  }

  public void presentScreen(Fragment fragment, PresentAnimation anim, @Nullable Promise promise) {
    if (fragment == null) {
      throw new IllegalArgumentException("Fragment must not be null.");
    }
    BackStack bsi = new BackStack(getNextStackTag(), anim, promise);
    backStacks.push(bsi);
    // TODO: dry this up with pushScreen
    FragmentTransaction ft = activity.getSupportFragmentManager().beginTransaction()
        .setAllowOptimization(true)
        .setCustomAnimations(anim.enter, anim.exit, anim.popEnter, anim.popExit);

    Fragment currentFragment = getCurrentFragment();
    if (currentFragment != null && !isFragmentTranslucent(fragment)) {
      container.willDetachCurrentScreen();
      ft.detach(currentFragment);
    }
    ft
        .add(container.getId(), fragment)
        .addToBackStack(bsi.getTag())
        .commit();
    activity.getSupportFragmentManager().executePendingTransactions();
    bsi.pushFragment(fragment);
    Log.d(TAG, toString());
  }

  public void dismissAll() {
    while (!backStacks.isEmpty()) {
      dismiss(0, null, false);
      activity.getFragmentManager().executePendingTransactions();
    }
  }

  public void onBackPressed() {
    pop();
  }

  public void pop() {
    BackStack bsi = getCurrentBackStack();
    if (bsi.getSize() == 1) {
      dismiss();
      return;
    }
    bsi.popFragment();
    activity.getSupportFragmentManager().popBackStack();
    Log.d(TAG, toString());
  }

  public void dismiss() {
    dismiss(Activity.RESULT_OK, null);
  }

  public void dismiss(int resultCode, Map<String, Object> payload) {
    dismiss(resultCode, payload, true);
  }

  private void dismiss(int resultCode, Map<String, Object> payload, boolean finishIfEmpty) {
    BackStack bsi = backStacks.pop();
    Promise promise = bsi.getPromise();
    deliverPromise(promise, resultCode, payload);
    // This is needed so we can override the pop exit animation to slide down.
    PresentAnimation anim = bsi.getAnimation();

    if (backStacks.isEmpty()) {
      if (finishIfEmpty) {
        activity.supportFinishAfterTransition();
        return;
      }
    } else {
      // This will be used when the fragment delegates its onCreateAnimation to this.
      nextPopExitAnim = anim.popExit;
    }

    activity.getSupportFragmentManager()
            .popBackStackImmediate(bsi.getTag(), FragmentManager.POP_BACK_STACK_INCLUSIVE);
    Log.d(TAG, toString());
  }

  public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
    if (!enter && nextPopExitAnim != 0) {
      // If this fragment was pushed on to the stack, it's pop exit animation will be
      // slide out right. However, we want it to be slide down in this case.
      int anim = nextPopExitAnim;
      nextPopExitAnim = 0;
      return AnimationUtils.loadAnimation(activity, anim);
    }
    return null;
  }

  private void deliverPromise(Promise promise, int resultCode, Map<String, Object> payload) {
    if (promise != null) {
      Map<String, Object> newPayload =
              MapBuilder.of(EXTRA_CODE, resultCode, EXTRA_PAYLOAD, payload);
      promise.resolve(ConversionUtil.toWritableMap(newPayload));
    }
  }

  private String getNextStackTag() {
    return getStackTag(stackId++);
  }

  private String getStackTag(int id) {
    return "STACK" + id;
  }

  @Nullable
  private Fragment getCurrentFragment() {
    return activity.getSupportFragmentManager().findFragmentById(container.getId());
  }

  private BackStack getCurrentBackStack() {
    return backStacks.peek();
  }

  @NonNull
  private FrameLayout createContainerView() {
    return new FrameLayout(activity);
  }

  @Override
  public String toString() {
    final StringBuilder sb = new StringBuilder("ScreenCoordinator{");
    for (int i = 0; i < backStacks.size(); i++) {
      sb.append("Back stack ").append(i).append(":\t").append(backStacks.get(i));
    }
    sb.append('}');
    return sb.toString();
  }
}