import React, { useCallback, useEffect, useImperativeHandle, useRef } from 'react';

import RNAnimated, {
  AnimatableValue,
  AnimatableValueObject,
  Easing,
  runOnJS,
  useAnimatedRef,
  useAnimatedStyle,
  useSharedValue,
  withDelay,
  withSequence,
  withSpring,
  withTiming,
} from 'react-native-reanimated';
import { useNativeMergeStyles } from '@cleartrip/ct-design-style-manager';

import {
  IAnimatedComponentProps,
  IAnimatedStyle,
  IAnimationConfig,
  IAnimationOptions,
  IAnimationTransformValues,
  ISpringConfig,
  SpringConfig,
} from '../type';
import { IAnimatedStyleClass } from '../AnimatedStyle/IAnimatedStyleClass';
import { ViewStyle, TextStyle, ImageStyle } from '@cleartrip/ct-design-types';

type DefaultStyle = ViewStyle | TextStyle | ImageStyle;

const isEmpty = (value: unknown): boolean => {
  if (value == null) return true;
  if (Array.isArray(value)) return value.length === 0;
  if (typeof value === 'object') return Object.keys(value).length === 0;
  return false;
};

const getAnimatedValue = (animatedStyle: IAnimationTransformValues) => {
  const animatedValues: AnimatableValue = {};

  if (typeof animatedStyle?.translateX !== 'undefined') {
    animatedValues.translateX = animatedStyle.translateX;
  }
  if (typeof animatedStyle?.translateY !== 'undefined') {
    animatedValues.translateY = animatedStyle.translateY;
  }
  if (typeof animatedStyle?.scale !== 'undefined') {
    animatedValues.scale = animatedStyle.scale;
  }
  if (typeof animatedStyle?.opacity !== 'undefined') {
    animatedValues.opacity = animatedStyle.opacity;
  }
  if (typeof animatedStyle?.height !== 'undefined') {
    animatedValues.height = animatedStyle.height;
  }
  if (typeof animatedStyle?.width !== 'undefined') {
    animatedValues.width = animatedStyle.width;
  }
  if (typeof animatedStyle?.top !== 'undefined') {
    animatedValues.top = parseInt(animatedStyle.top as string, 10);
  }
  if (typeof animatedStyle?.left !== 'undefined') {
    animatedValues.left = parseInt(animatedStyle.left as string, 10);
  }
  if (typeof animatedStyle?.color !== 'undefined') {
    animatedValues.color = animatedStyle.color;
  }
  if (typeof animatedStyle?.backgroundColor !== 'undefined') {
    animatedValues.backgroundColor = animatedStyle.backgroundColor;
  }

  return animatedValues;
};

const getEasing = (timingFunction: string) => {
  switch (timingFunction) {
    case 'ease-in-out':
      return Easing.inOut(Easing.ease);
    case 'ease-in':
      return Easing.in(Easing.ease);
    case 'ease-out':
      return Easing.out(Easing.ease);
  }
  return Easing.linear;
};

const getSpringConfig = (springConfig?: ISpringConfig) => {
  if (springConfig) {
    const { mass, tension, friction } = springConfig;
    const config: SpringConfig = {};

    if (mass) {
      config.mass = mass;
    }
    if (tension) {
      config.stiffness = tension;
    }
    if (friction) {
      config.damping = friction;
    }
    return config;
  }

  return {};
};

const getAnimatedStyle = (animated: IAnimatedStyleClass) => {
  if (animated.getNativeIsSequence?.()) {
    const animatedStyles = animated.getNativeAnimatedSequenceStyles();
    const firstAnimatedStyle = animatedStyles[0][0];

    if (!firstAnimatedStyle) {
      return null;
    }

    return firstAnimatedStyle.getNativeAnimatedStyle();
  }
  return animated.getNativeAnimatedStyle();
};

const getInitialAnimatedValue = (animated: IAnimatedStyleClass) => {
  let initialAnimatedValue: IAnimationTransformValues = {};
  if (animated.getNativeIsSequence()) {
    const sequenceStyles = animated.getNativeAnimatedSequenceStyles();

    for (let i = 0; i < sequenceStyles.length; i++) {
      const animatedStyle = sequenceStyles[i][0];
      const animatedStyleValue = animatedStyle.getNativeAnimatedStyle();
      if (animatedStyleValue && animatedStyleValue.from && !isEmpty(animatedStyleValue.from)) {
        Object.keys(animatedStyleValue.from).forEach((key) => {
          if (
            typeof initialAnimatedValue[key as keyof typeof initialAnimatedValue] === 'undefined' &&
            animatedStyleValue.from?.[key as keyof IAnimationTransformValues] !== 'undefined'
          ) {
            initialAnimatedValue[key as keyof IAnimationTransformValues] =
              animatedStyleValue.from?.[key as keyof IAnimationTransformValues];
          }
        });
      }
    }
  } else {
    initialAnimatedValue = animated.getNativeAnimatedStyle()?.from ?? {};
  }

  const initialValue: IAnimationTransformValues = {};
  Object.keys(initialAnimatedValue).forEach((key) => {
    if (typeof initialAnimatedValue[key as keyof typeof initialAnimatedValue] !== 'undefined') {
      initialValue[key as keyof IAnimationTransformValues] = initialAnimatedValue[key];
    }
  });

  return initialValue;
};

const Container = ({ children, animated, styleConfig = {}, onTransitionEnd }: IAnimatedComponentProps) => {
  const { root: rootStyles = [] } = styleConfig || {};
  const { to, config: animatedConfig } = getAnimatedStyle(animated) || {};

  // ref to onTransitionEnd so it can be accessed from worklet context
  const onTransitionEndRef = useRef(onTransitionEnd);
  useEffect(() => {
    onTransitionEndRef.current = onTransitionEnd;
  }, [onTransitionEnd]);

  const animatedValue = useSharedValue(getInitialAnimatedValue(animated));

  const animatedStyle = useAnimatedStyle(() => {
    const style: IAnimatedStyle = {};

    if (typeof animatedValue.value.height !== 'undefined') {
      style.height = animatedValue.value.height;
    }
    if (typeof animatedValue.value.width !== 'undefined') {
      style.width = animatedValue.value.width;
    }
    if (typeof animatedValue.value.opacity !== 'undefined') {
      style.opacity = animatedValue.value.opacity;
    }
    if (typeof animatedValue.value.color !== 'undefined') {
      style.color = animatedValue.value.color;
    }
    if (typeof animatedValue.value.backgroundColor !== 'undefined') {
      style.backgroundColor = animatedValue.value.backgroundColor;
    }
    if (typeof animatedValue.value.top !== 'undefined') {
      style.top = parseInt(String(animatedValue.value.top), 10);
    }
    if (typeof animatedValue.value.left !== 'undefined') {
      style.left = parseInt(String(animatedValue.value.left), 10);
    }

    const transform = [];
    if (typeof animatedValue.value.translateX !== 'undefined') {
      // @ts-ignore
      transform.push({ translateX: animatedValue.value.translateX });
    }
    if (typeof animatedValue.value.translateY !== 'undefined') {
      // @ts-ignore
      transform.push({ translateY: animatedValue.value.translateY });
    }
    if (typeof animatedValue.value.scale !== 'undefined') {
      // @ts-ignore
      transform.push({ scale: animatedValue.value.scale });
    }

    if (transform.length > 0) {
      style.transform = transform;
    }

    return style as DefaultStyle;
  });

  const containerRef = useAnimatedRef();

  useEffect(() => {
    if (animatedConfig?.immediate) {
      animated.getNativeElementRef()?.current?.start?.();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const handleAnimationStart = useCallback(
    (animatedValues: IAnimationTransformValues = {}, config: IAnimationConfig = {}) => {
      const {
        duration = 1000,
        timingFunction = 'ease-in',
        delay,
        animationType,
        springConfig,
        onComplete,
      } = config ?? {};
      const targetValues = getAnimatedValue(animatedValues);

      const completeTargetValues = animated.getNativeIsSequence()
        ? {
            ...(typeof animatedValue.value.translateX !== 'undefined' && {
              translateX: animatedValue.value.translateX,
            }),
            ...(typeof animatedValue.value.translateY !== 'undefined' && {
              translateY: animatedValue.value.translateY,
            }),
            ...(typeof animatedValue.value.scale !== 'undefined' && { scale: animatedValue.value.scale }),
            ...(typeof animatedValue.value.opacity !== 'undefined' && { opacity: animatedValue.value.opacity }),
            ...(typeof animatedValue.value.height !== 'undefined' && { height: animatedValue.value.height }),
            ...(typeof animatedValue.value.width !== 'undefined' && { width: animatedValue.value.width }),
            ...targetValues,
          }
        : targetValues;

      // Both callbacks need to be wrapped with runOnJS since they're called from worklet context
      const handleComplete = (finished?: boolean) => {
        'worklet';
        if (finished) {
          if (onComplete) {
            runOnJS(onComplete)();
          }
          if (onTransitionEndRef.current) {
            runOnJS(onTransitionEndRef.current)();
          }
        }
      };

      let animation: AnimatableValueObject;
      if (animationType === 'spring') {
        animation = withSpring(completeTargetValues, getSpringConfig(springConfig), handleComplete);
      } else {
        animation = withTiming(completeTargetValues, { duration, easing: getEasing(timingFunction) }, handleComplete);
      }

      const withDelayAnimation = delay ? withDelay(delay, animation) : animation;

      if (animated.getNativeIsSequence()) {
        return withDelayAnimation;
      }

      animatedValue.value = withDelayAnimation as IAnimationTransformValues;
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [],
  );

  const handleSequenceAnimationStart = (animated: IAnimatedStyleClass) => {
    const sequenceStyles = [];

    let accumulatedToValues = {
      ...(typeof animatedValue.value.translateX !== 'undefined' && { translateX: animatedValue.value.translateX }),
      ...(typeof animatedValue.value.translateY !== 'undefined' && { translateY: animatedValue.value.translateY }),
      ...(typeof animatedValue.value.scale !== 'undefined' && { scale: animatedValue.value.scale }),
      ...(typeof animatedValue.value.opacity !== 'undefined' && { opacity: animatedValue.value.opacity }),
      ...(typeof animatedValue.value.height !== 'undefined' && { height: animatedValue.value.height }),
      ...(typeof animatedValue.value.width !== 'undefined' && { width: animatedValue.value.width }),
    };

    for (let i = 0; i < animated.getNativeAnimatedSequenceStyles().length; i++) {
      const animatedStyle = animated.getNativeAnimatedSequenceStyles()[i][0];
      const animatedStyleValue = animatedStyle.getNativeAnimatedStyle();

      const toValues = animatedStyleValue?.to ?? {};

      accumulatedToValues = {
        ...accumulatedToValues,
        ...toValues,
      };

      // @ts-ignore
      sequenceStyles.push(handleAnimationStart(accumulatedToValues, animatedStyleValue?.config));
    }

    animatedValue.value = withSequence(
      ...(sequenceStyles as unknown as AnimatableValue[]),
    ) as IAnimationTransformValues;
  };

  useImperativeHandle(
    animated.getNativeElementRef(),
    () => {
      return {
        start(animatedStyle?: IAnimationOptions) {
          if (containerRef?.current) {
            if (animated.getNativeIsSequence()) {
              handleSequenceAnimationStart(animated);
            } else {
              if (animatedStyle) {
                const { to = {}, config = animatedConfig } = animatedStyle;
                handleAnimationStart(to, config);
              } else {
                handleAnimationStart(to, animatedConfig);
              }
            }
          }
        },
      };
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [],
  );

  // @ts-ignore
  const nativeStyle = useNativeMergeStyles(rootStyles, [rootStyles]);
  return (
    <RNAnimated.View ref={containerRef} style={[nativeStyle, animatedStyle]}>
      {children}
    </RNAnimated.View>
  );
};

export default React.memo(Container);
