import * as React from "react";
import { View } from "react-native";
import { append, compose, identity, tail, take, update } from "ramda";

import { AnimationManager } from "./AnimationManager";
import { Scene } from "./Scene";

import {
  getSceneByRoute,
  shouldSkipAnimationForPreviousWebViewScene,
} from "./utils";
import { v4 as uuid_v4 } from "uuid";
import { isScreenActiveForRoute } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useIsScreenActive";

export const NAV_ACTION_PUSH = "PUSH";

export const NAV_ACTION_REPLACE = "REPLACE";

export const NAV_ACTION_BACK = "POP";

type SceneProps = {
  children: React.ReactNode;
  overlayStyle: any;
  style: any;
  animating: boolean;
};

/**
 * This function returns an object with the children and the pathname
 * used as key. this prevents scenes unmounting when transitions occur
 * @param {Object} children
 * @returns { children: Object, key: string }
 */
function prepareScene(children) {
  return {
    children,
    key: children.props.pathname,
    screenUniqueId: uuid_v4(),
  };
}

/**
 * 1. If there is more than one scene, get back to the previous scene.
 * 2. If there is no scene to go back to, clone the new children as the scene to go back to.
 * @param {any} props Component's props
 * @param {any} state Component's state
 */
function scenesForBackTransition(props: SceneProps, { scenes }) {
  return scenes.length > 1
    ? identity(scenes)
    : [prepareScene(props.children), scenes[0]];
}

type Props = {
  transitionConfig: any;
  children: React.ReactNode;
  navigator: {
    previousAction: string;
    currentRoute: string;
    videoModalState?: {
      visible?: boolean;
      mode?: string;
    };
    screenData: {
      [key: string]: any;
    };
  };
  contentStyle: {
    [key: string]: any;
  };
  plugins: any;
  layoutData: {
    isTabletPortrait: boolean;
    isTablet: boolean;
    width: number;
    height: number;
  };
};

type State = {
  animatedValue: any;
  animating: boolean;
  duration: number;
  easing: any;
  from: any;
  to: any;
  scenes: any;
};

const styles = {
  container: { flex: 1, overflow: "hidden" },
} as const;

export class TransitionerComponent extends React.PureComponent<Props, State> {
  constructor(props: Props) {
    super(props);

    this.animationManager = new AnimationManager(props);

    this.state = {
      scenes: [prepareScene(props.children)], // starting with index: 0
      ...this.animationManager.initialState(),
    };

    this.pushTransitionStart = this.pushTransitionStart.bind(this);
    this.pushTransitionEnd = this.pushTransitionEnd.bind(this);
    this.backTransitionStart = this.backTransitionStart.bind(this);
    this.backTransitionEnd = this.backTransitionEnd.bind(this);
  }

  /**
   * Clone new children, append to scenes array,
   * and trigger push animation with callbacks.
   */
  pushTransitionStart() {
    this.setState(
      {
        scenes: append(prepareScene(this.props.children), this.state.scenes),
        ...this.animationManager.stateForAction(this.state, NAV_ACTION_PUSH),
      },
      () => {
        this.animationManager.animate(this.state, this.pushTransitionEnd);
      }
    );
  }

  /**
   * Trigger back animation.
   * The last scene in the array is removed in the "backTransitionEnd" callback,
   * only after the animation is complete.
   */
  backTransitionStart() {
    const previousRoute = this.props.navigator?.currentRoute;
    const currentRoute = this.props.navigator?.currentRoute;

    const diffLengthRoutes =
      (previousRoute?.match(/river\//g) || []).length -
      (currentRoute?.match(/river\//g) || []).length;

    const stepsBack =
      diffLengthRoutes && diffLengthRoutes > 0 ? diffLengthRoutes : 1;

    this.setState(
      {
        scenes: scenesForBackTransition(this.props, this.state),
        ...this.animationManager.stateForAction(
          this.state,
          NAV_ACTION_BACK,
          stepsBack
        ),
      },
      () => {
        this.animationManager.animate(this.state, () =>
          this.backTransitionEnd(stepsBack)
        );
      }
    );
  }

  shouldSkipAnimationForRoute(route) {
    // We are skipping animations for some routes, likes hooks & the player mainly

    return route.includes("/playable") || route.includes("/hook");
  }

  shouldResetStateForRoute(route) {
    // We are resetting the transitioner state for root routes

    return (route?.match(/river\//g) || []).length === 1;
  }

  transitionWithoutAnimation(resetState) {
    const { scenes } = this.state;
    const newScene = prepareScene(this.props.children);
    const isLastSceneHook = scenes[scenes.length - 1].key.startsWith("/hook");

    let newState;

    if (resetState) {
      newState = {
        scenes: [newScene],
        to: { index: 0 },
      };
    } else if (isLastSceneHook || scenes.length === 1) {
      newState = { scenes: update(scenes.length - 1, newScene, scenes) };
    } else {
      newState = {
        scenes: compose(tail, append(newScene))(scenes),
      };
    }

    this.setState(newState);
  }

  componentDidUpdate(prevProps: Props) {
    const currentRoute = this.props.navigator?.currentRoute;
    const previousRoute = prevProps.navigator?.currentRoute;
    const isGoingBack = this.props.navigator.previousAction === NAV_ACTION_BACK;

    if (currentRoute !== previousRoute) {
      if (this.state.animating && !isGoingBack) {
        // this happens when a new transition is triggered before the previous one ended
        // Instant modification of active scene without animations.

        return this.transitionWithoutAnimation();
      }

      // Skipping animation when coming back from the 'landscape' WebView component"
      if (
        isGoingBack &&
        shouldSkipAnimationForPreviousWebViewScene({
          previousScene: getSceneByRoute({
            route: previousRoute,
            scenes: this.state.scenes,
          }),
          nextScene: getSceneByRoute({
            route: currentRoute,
            scenes: this.state.scenes,
          }),
          layoutData: this.props.layoutData,
        })
      ) {
        return this.transitionWithoutAnimation();
      }

      // TODO: Add explicit check for differnt orientation and skip animated transition in that case.
      // Now it checks for playable and hooks, but in theory we can have non-player screens
      // with different orientation
      if (
        this.shouldSkipAnimationForRoute(previousRoute) ||
        this.shouldSkipAnimationForRoute(currentRoute)
      ) {
        // we are going back, in this case we need to have only one scene using resetState
        const resetState = isGoingBack ? true : undefined;

        return this.transitionWithoutAnimation(resetState);
      }

      switch (this.props.navigator.previousAction) {
        case NAV_ACTION_PUSH:
          this.pushTransitionStart();
          break;

        case NAV_ACTION_BACK: {
          this.backTransitionStart();
          break;
        }

        case NAV_ACTION_REPLACE: {
          // Instant modification of active scene without animations.
          this.transitionWithoutAnimation(
            this.shouldResetStateForRoute(currentRoute)
          );

          break;
        }

        default:
          // Should never get here - disabling animation just to be on the safe side.

          this.setState({ animating: false });
          break;
      }
    }
  }

  // Sets state.animating to false
  pushTransitionEnd() {
    this.setState({
      animating: false,
    });
  }

  // Sets state.animating to false
  // and remove last scene from scenes stack
  backTransitionEnd(stepsBack) {
    this.setState({
      animating: false,
      scenes: take(this.state.scenes.length - stepsBack, this.state.scenes),
    });
  }

  // On initial state, both "to" and "from" point to a single scene
  // so only a single scene should be rendered,
  // instead of dangerous nulls or duplicates.
  renderSingleScene({ scenes, to }) {
    const { contentStyle, navigator, children } = this.props;

    const { currentRoute, videoModalState } = navigator;
    const screenData = children.props.screenData;
    const pathname = scenes[to.index].key;

    return (
      <View style={styles.container}>
        {scenes[to.index] ? (
          <Scene
            {...{ style: to?.style || {} }}
            pathname={pathname}
            isActive={isScreenActiveForRoute(
              pathname,
              currentRoute,
              videoModalState
            )}
            contentStyle={contentStyle}
            key={scenes[to.index].key}
            screenUniqueId={scenes[to.index].screenUniqueId}
            screenData={screenData}
          >
            {scenes[to.index].children}
          </Scene>
        ) : null}
      </View>
    );
  }

  // Render two scenes consecutively -
  // the scenes' indexes determines which scene should be drawn first.
  // Looks redundant, but when written like this
  // we do not need to pass or generate keys.
  renderScenes({ scenes, from, to }) {
    const { contentStyle, navigator } = this.props;

    const { currentRoute, videoModalState } = navigator;

    const fromScene = scenes[from.index] && (
      <Scene
        key={scenes[from.index].key}
        pathname={scenes[from.index].key}
        isActive={isScreenActiveForRoute(
          scenes[from.index].key,
          currentRoute,
          videoModalState
        )}
        style={from.style}
        pointerEvents="none"
        overlayStyle={from.overlayStyle}
        animating={this.state.animating}
        contentStyle={contentStyle}
        screenUniqueId={scenes[from.index].screenUniqueId}
        screenData={scenes[from.index].children.props.screenData}
      >
        {scenes[from.index].children}
      </Scene>
    );

    const toScene = scenes[to.index] && (
      <Scene
        pathname={scenes[to.index].key}
        isActive={isScreenActiveForRoute(
          scenes[to.index].key,
          currentRoute,
          videoModalState
        )}
        key={scenes[to.index].key}
        style={to.style}
        overlayStyle={to.overlayStyle}
        animating={this.state.animating}
        contentStyle={contentStyle}
        screenUniqueId={scenes[to.index].screenUniqueId}
        screenData={this.props.children.props.screenData}
      >
        {scenes[to.index].children}
      </Scene>
    );

    return from.index <= to.index ? (
      <View style={styles.container}>
        {fromScene}
        {toScene}
      </View>
    ) : (
      <View style={styles.container}>
        {toScene}
        {fromScene}
      </View>
    );
  }

  render() {
    const { from, to } = this.state;

    if (from.index === to.index) {
      return this.renderSingleScene(this.state);
    }

    return this.renderScenes(this.state);
  }
}
