import * as React from "react";
import { Animated, EasingFunction, StyleProp, ViewStyle } from "react-native";
import { usePrevious } from "@applicaster/zapp-react-native-utils/reactHooks/utils";
import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";

type AnimatedInterpolatedStyle = any;

type AnimationConfig = {
  duration: number;
  easing: EasingFunction;
  styles: (animatedValue: Animated.Value) => {
    [Key: string]: AnimatedInterpolatedStyle | StyleProp<any>;
  };
  delay?: number;
  onAnimationEnd?: () => void;
};

export type AnimatedInConfig =
  | AnimationConfig
  | {
      in: AnimationConfig;
      out: AnimationConfig;
    };

type Props = {
  visible: boolean;
  animationConfig: AnimatedInConfig;
  children: React.ReactElement<any>;
  staticStyles?: ViewStyle;
};

function getAnimation(config: AnimatedInConfig, phase: "in" | "out") {
  return config[phase] || config;
}

export function AnimatedInOut({
  visible,
  animationConfig,
  staticStyles = {},
  children,
}: Props) {
  const [animatedValue] = React.useState(new Animated.Value(visible ? 1 : 0));
  const animationRef = React.useRef<Animated.CompositeAnimation | null>(null);
  const delayTimerRef = React.useRef<NodeJS.Timeout | null>(null);

  const previousVisible = usePrevious(toBooleanWithDefaultFalse(visible));

  const startAnimation = React.useCallback(
    (toValue: number, config: AnimationConfig) => {
      if (delayTimerRef.current) {
        clearTimeout(delayTimerRef.current);
        delayTimerRef.current = null;
      }

      if (animationRef.current) {
        animationRef.current.stop();
        animationRef.current = null;
      }

      const { duration, easing, delay = 0, onAnimationEnd = noop } = config;

      const runAnimation = () => {
        animationRef.current = Animated.timing(animatedValue, {
          duration,
          toValue,
          easing,
          useNativeDriver: true,
        });

        animationRef.current.start(({ finished }) => {
          if (finished) {
            animationRef.current = null;
            onAnimationEnd();
          }
        });
      };

      if (delay > 0) {
        delayTimerRef.current = setTimeout(runAnimation, delay);
      } else {
        runAnimation();
      }
    },
    [animatedValue]
  );

  React.useEffect(() => {
    if (previousVisible === undefined) {
      animatedValue.setValue(visible ? 1 : 0);

      return;
    }

    if (!previousVisible && visible) {
      startAnimation(1.0, getAnimation(animationConfig, "in"));
    }

    if (previousVisible && !visible) {
      startAnimation(0.0, getAnimation(animationConfig, "out"));
    }
  }, [
    visible,
    previousVisible,
    animatedValue,
    startAnimation,
    animationConfig,
  ]);

  React.useEffect(() => {
    return () => {
      if (delayTimerRef.current) {
        clearTimeout(delayTimerRef.current);
        delayTimerRef.current = null;
      }

      if (animationRef.current) {
        animationRef.current.stop();
        animationRef.current = null;
      }

      animatedValue.stopAnimation();
    };
  }, [animatedValue]);

  const styles = visible
    ? getAnimation(animationConfig, "in").styles
    : getAnimation(animationConfig, "out").styles;

  return (
    <Animated.View
      renderToHardwareTextureAndroid={!!animationRef.current}
      style={[styles(animatedValue), staticStyles]}
    >
      {children}
    </Animated.View>
  );
}
