import React, { useEffect, useRef, useState } from 'react';
import { Animated, PanResponder, Easing, Platform } from 'react-native';

import type { ReactElement, ReactNode } from 'react';
import {
  StyledDragableContainer,
  StyledDragableDrawerContainer,
  StyledHandler,
  StyledHandlerContainer,
} from '../StyledDrawer';
import {
  calculateAnimatedToValue,
  calculateSnapPointsData,
  getOffset,
} from './helpers';
import type { SnapPointsData } from './helpers';

export interface DragableDrawerProps {
  /**
   * Drawer's content.
   */
  children?: ReactNode;
  /**
   * Initial visible height percentage of the drawer. Can be used to programmatically control the drawer height. Range: [0,100].
   */
  initialHeightPercentage?: number;
  /**
   * Mininum visible height percentage of the drawer. This will be the lowest point in snap points list. Range: [0,100].
   */
  minimumHeightPercentage?: number;
  /**
   * Callback function is called when the drawer expand to max position.
   */
  onExpanded?: () => void;
  /**
   * Callback function is called when the drawer collapsed to min position.
   */
  onCollapsed?: () => void;
  /**
   * Testing id of the component.
   */
  testID?: string;
  /**
   * Nearest height snap points that the drawer will attach to on release gesture. Range: [0,100]
   */
  snapPoints?: number[];
}

const DragableDrawer = ({
  children,
  initialHeightPercentage,
  minimumHeightPercentage = 10,
  snapPoints = [],
  onExpanded,
  onCollapsed,
  testID,
}: DragableDrawerProps): ReactElement => {
  const [height, setHeight] = useState(0);
  const baseHeightForMeasure = useRef(0);
  const snapPointsData = useRef<SnapPointsData>({
    list: [],
    minHeightOffset: 0,
    maxHeightOffset: 0,
  });

  // Track drag
  const pan = useRef(new Animated.Value(0)).current;
  const offset = useRef(0);
  const offsetBeforePan = useRef(0);
  const [animatedToValue, setAnimatedToValue] = useState<number>(-1);

  useEffect(() => {
    const id = pan.addListener(({ value }) => {
      offset.current = value;
    });

    return () => pan.removeListener(id);
  }, []);

  useEffect(() => {
    if (height > 0) {
      const initialOffset = getOffset(
        height,
        initialHeightPercentage || minimumHeightPercentage
      );
      setAnimatedToValue(initialOffset);
    }
  }, [height]);

  useEffect(() => {
    if (height > 0) {
      pan.setValue(height);
      offset.current = height;

      baseHeightForMeasure.current = height;

      // Calculate snap points information
      snapPointsData.current = calculateSnapPointsData(
        minimumHeightPercentage,
        height,
        snapPoints
      );
    }
  }, [height, minimumHeightPercentage]);

  useEffect(() => {
    if (animatedToValue >= 0) {
      const animation = Animated.timing(pan, {
        toValue: animatedToValue,
        useNativeDriver: Platform.OS !== 'web',
        easing: Easing.inOut(Easing.cubic),
      });

      animation.start(() => {
        if (animatedToValue === 0) {
          onExpanded?.();
        } else if (
          animatedToValue === getOffset(height, minimumHeightPercentage)
        ) {
          onCollapsed?.();
        }
        setAnimatedToValue(-1);
      });

      return () => animation.stop();
    }
  }, [animatedToValue]);

  const panResponder = useRef(
    PanResponder.create({
      onMoveShouldSetPanResponder: () => true,
      onPanResponderGrant: () => {
        offsetBeforePan.current = offset.current;
        pan.setOffset(offset.current);
        pan.setValue(0);
      },
      onPanResponderMove: (_, gesture) => {
        const panDistance = gesture.dy;

        // Moving toward top, stop at highest snap point
        if (offsetBeforePan.current + panDistance < 0) {
          pan.setValue(-offsetBeforePan.current);
          return;
        }

        // Moving toward bottom, stop at lowest snap point
        if (
          offsetBeforePan.current + panDistance >
          snapPointsData.current?.minHeightOffset
        ) {
          pan.setValue(
            baseHeightForMeasure.current -
              baseHeightForMeasure.current * (minimumHeightPercentage / 100) -
              offsetBeforePan.current
          );
          return;
        }

        pan.setValue(panDistance);
      },
      onPanResponderRelease: (_, gesture) => {
        pan.flattenOffset();

        // Attach to nearest snappoint
        const panDistance = gesture.dy;
        const offsetAfterPan = offsetBeforePan.current + panDistance;
        const animatedValue = calculateAnimatedToValue(
          offsetAfterPan,
          snapPointsData.current.list
        );

        setAnimatedToValue(animatedValue);
      },
    })
  ).current;

  return (
    <StyledDragableContainer
      testID={testID}
      enableShadow
      pointerEvents="box-none"
    >
      <StyledDragableDrawerContainer
        enableShadow
        style={{
          transform: [
            { scaleY: baseHeightForMeasure.current > 0 ? 1 : 0 },
            { translateY: pan },
          ],
        }}
        onLayout={({ nativeEvent }) => {
          setHeight(nativeEvent.layout.height);
        }}
      >
        <StyledHandlerContainer {...panResponder.panHandlers}>
          <StyledHandler />
        </StyledHandlerContainer>
        {children}
      </StyledDragableDrawerContainer>
    </StyledDragableContainer>
  );
};

export default DragableDrawer;
