import {
  Props,
  Pose,
  PoserState,
  PoseMap,
  ChildPosers,
  GetTransitionProps,
  GetInstantTransition,
  StartAction,
  StopAction,
  ResolveTarget,
  TransformPose,
  AddTransitionDelay,
  ConvertTransitionDefinition,
  TransitionFactory,
  TransitionMapFactory,
  TransitionMap,
  SetValue,
  SetValueNative,
  ApplyMap
} from '../types';
import { getPoseValues } from '../inc/selectors';
import { invariant } from 'hey-listen';

type AnimationsPromiseList = Array<Promise<any>>;

type SetterFactoryProps<V, A, C, P, TD> = {
  state: PoserState<V, A, C, P>;
  poses: PoseMap<A, TD>;
  getInstantTransition: GetInstantTransition<V, A>;
  startAction: StartAction<V, A, C>;
  stopAction: StopAction<C>;
  setValue: SetValue<V>;
  setValueNative: SetValueNative;
  addActionDelay: AddTransitionDelay<A>;
  getTransitionProps: GetTransitionProps<V>;
  resolveTarget: ResolveTarget<V>;
  convertTransitionDefinition: ConvertTransitionDefinition<V, A, TD>;
  transformPose?: TransformPose<V, A, C, P, TD>;
  posePriority?: string[];
  forceRender?: (props: Props) => any;
};

export const resolveProp = (target: any, props: Props) =>
  typeof target === 'function' ? target(props) : target;

const poseDefault = <A, TD>(
  pose: Pose<A, TD>,
  prop: string,
  defaultValue: any,
  resolveProps: Props
) =>
  pose && pose[prop] !== undefined
    ? resolveProp(pose[prop], resolveProps)
    : defaultValue;

const startChildAnimations = <V, A, C, P, TD>(
  children: ChildPosers<V, A, C, P>,
  next: string,
  pose: Pose<A, TD>,
  props: Props
) => {
  const animations: Array<Promise<any>> = [];
  const delay = poseDefault<A, TD>(pose, 'delayChildren', 0, props);
  const stagger = poseDefault<A, TD>(pose, 'staggerChildren', 0, props);
  const staggerDirection = poseDefault<A, TD>(
    pose,
    'staggerDirection',
    1,
    props
  );

  const maxStaggerDuration = (children.size - 1) * stagger;
  const generateStaggerDuration =
    staggerDirection === 1
      ? (i: number) => i * stagger
      : (i: number) => maxStaggerDuration - i * stagger;

  Array.from(children).forEach((child, i) => {
    animations.push(
      child.set(next, {
        delay: delay + generateStaggerDuration(i)
      })
    );
  });

  return animations;
};

const resolveTransition = <V, A, TD>(
  transition: TransitionMap<A, TD> | TransitionMapFactory<A, TD>,
  key: string,
  value: V,
  props: Props,
  convertTransitionDefinition: ConvertTransitionDefinition<V, A, TD>,
  getInstantTransition: GetInstantTransition<V, A>
): A => {
  let resolvedTransition: ReturnType<TransitionFactory<A, TD>>;

  /**
   * transition: () => {}
   */
  if (typeof transition === 'function') {
    const resolvedTransitionMap = transition(props);

    /**
     * transition: () => { d: () => tweenFn }
     */
    resolvedTransition = resolveTransition(
      resolvedTransitionMap,
      key,
      value,
      props,
      convertTransitionDefinition,
      getInstantTransition
    );
    // Or if it's a keyed object
  } else if (transition[key] || transition.default) {
    const keyTransition = transition[key] || transition.default;

    /**
     * transition: {
     *  x: () => {}
     * }
     */
    if (typeof keyTransition === 'function') {
      resolvedTransition = (keyTransition as TransitionFactory<A, TD>)(props);

      /**
       * transition: {
       *  x: { type: 'tween' } || false
       * }
       */
    } else {
      resolvedTransition = keyTransition;
    }

    /**
     * transition: { type: 'tween' } || false
     */
  } else {
    resolvedTransition = (transition as any) as typeof resolvedTransition;
  }

  return resolvedTransition === false
    ? getInstantTransition(value, props)
    : convertTransitionDefinition(value, resolvedTransition, props);
};

const findInsertionIndex = (
  poseList: string[],
  priorityList: string[],
  priorityIndex: number
) => {
  let insertionIndex = 0;

  for (let i = priorityIndex - 1; i >= 0; i--) {
    const nextHighestPriorityIndex = poseList.indexOf(priorityList[i]);
    if (nextHighestPriorityIndex !== -1) {
      insertionIndex = nextHighestPriorityIndex + 1;
      break;
    }
  }

  return insertionIndex;
};

const applyValues = <V>(
  toApply: ApplyMap,
  values: Map<string, V>,
  props: Props,
  setValue: SetValue<V>,
  setValueNative: SetValueNative
) => {
  invariant(
    typeof toApply === 'object',
    'applyAtStart and applyAtEnd must be of type object'
  );
  return Object.keys(toApply).forEach(key => {
    const valueToSet = resolveProp(toApply[key], props);
    values.has(key)
      ? setValue(values.get(key), valueToSet)
      : setValueNative(key, valueToSet, props);
  });
};

const createPoseSetter = <V, A, C, P, TD>(
  setterProps: SetterFactoryProps<V, A, C, P, TD>
) => {
  const {
    state,
    poses,
    startAction,
    stopAction,
    getInstantTransition,
    addActionDelay,
    getTransitionProps,
    resolveTarget,
    transformPose,
    posePriority,
    convertTransitionDefinition,
    setValue,
    setValueNative,
    forceRender
  } = setterProps;

  return (next: string, nextProps: Props = {}, propagate: boolean = true) => {
    const { children, values, props, activeActions, activePoses } = state;
    const { delay = 0 } = nextProps;
    const hasChildren = children.size;

    const baseTransitionProps = {
      ...props,
      ...nextProps
    };

    let nextPose = poses[next];

    const getChildAnimations = (): AnimationsPromiseList =>
      hasChildren && propagate
        ? startChildAnimations(children, next, nextPose, baseTransitionProps)
        : [];

    const getParentAnimations = (): AnimationsPromiseList => {
      if (!nextPose) return [];

      const { applyAtStart } = nextPose;

      // Apply initial values, if set
      if (applyAtStart) {
        applyValues(
          applyAtStart,
          values,
          baseTransitionProps,
          setValue,
          setValueNative
        );
        if (forceRender) forceRender(baseTransitionProps);
      }

      if (transformPose) nextPose = transformPose(nextPose, next, state);

      const { preTransition, transition: getTransition, applyAtEnd } = nextPose;

      // Run pre-transition prep, if set
      if (preTransition) preTransition(baseTransitionProps);

      const animations = Object.keys(getPoseValues(nextPose)).map(key => {
        // Add pose to the pose order
        const valuePoses = activePoses.has(key)
          ? activePoses.get(key)
          : (activePoses.set(key, []), activePoses.get(key));

        // Remove pose if it already exists
        const existingIndex = valuePoses.indexOf(next);
        if (existingIndex !== -1) valuePoses.splice(existingIndex, 1);

        // Insert pose in its priority order
        const priority = posePriority ? posePriority.indexOf(next) : 0;
        const insertionIndex =
          priority <= 0
            ? 0
            : findInsertionIndex(valuePoses, posePriority, priority);
        valuePoses.splice(insertionIndex, 0, next);

        return insertionIndex === 0
          ? new Promise(complete => {
              const value = values.get(key);

              const transitionProps = {
                ...baseTransitionProps,
                key,
                value
              };

              // Resolve target from pose
              const target = resolveTarget(
                value,
                resolveProp(nextPose[key], transitionProps)
              );

              // Stop the current action
              if (activeActions.has(key)) stopAction(activeActions.get(key));

              // Get the transition
              const resolveTransitionProps = {
                to: target,
                ...transitionProps,
                ...getTransitionProps(value, target, transitionProps)
              };

              let transition = resolveTransition<V, A, TD>(
                getTransition,
                key,
                value,
                resolveTransitionProps,
                convertTransitionDefinition,
                getInstantTransition
              );

              // Add delay if defined on pose
              const poseDelay =
                delay || resolveProp(nextPose.delay, transitionProps);
              if (poseDelay) {
                transition = addActionDelay(poseDelay, transition);
              }

              // Start transition
              activeActions.set(key, startAction(value, transition, complete));
            })
          : Promise.resolve();
      });

      return applyAtEnd
        ? [
            Promise.all(animations).then(() => {
              applyValues(
                applyAtEnd,
                values,
                baseTransitionProps,
                setValue,
                setValueNative
              );
            })
          ]
        : animations;
    };

    // Check before and afterChildren props to check if we need to reorder these animations
    if (nextPose && hasChildren) {
      // parent before children
      if (resolveProp(nextPose.beforeChildren, baseTransitionProps)) {
        return Promise.all(getParentAnimations()).then(() =>
          Promise.all(getChildAnimations())
        );

        // children before parent
      } else if (resolveProp(nextPose.afterChildren, baseTransitionProps)) {
        return Promise.all(getChildAnimations()).then(() =>
          Promise.all(getParentAnimations())
        );
      }
    }

    // Otherwise, run all animations in parallel
    return Promise.all([...getParentAnimations(), ...getChildAnimations()]);
  };
};

export default createPoseSetter;
