import React, { Component, createContext, useContext, useEffect, useRef, useState } from 'react';
import {
  Animated,
  GestureResponderEvent,
  LayoutChangeEvent,
  PanResponder,
  PanResponderGestureState,
  PanResponderInstance,
  StyleProp,
  View,
  ViewProps,
  ViewStyle,
} from 'react-native';

export type TDropResult = {
  dropX: number | null;
  dropY: number | null;
};

const OPACITY_VISIBLE = 0.8;
const OPACITY_HIDDEN = 0;

const getMeasure = (component: React.RefObject<View>): Promise<TMeasureValue> => {
  return new Promise((resolve, reject) => {
    component.measure((x, y, width, height, pageX, pageY) => {
      resolve({ x, y, width, height, pageX, pageY });
    });
  });
};

export interface DragDropProps extends ViewProps {
  children: React.ReactNode;
  hideWhenDrop?: boolean;
  onDraggableCheck?: (e: GestureResponderEvent, gesture: PanResponderGestureState) => boolean;
  onDragStart?: (e: GestureResponderEvent, gesture: PanResponderGestureState) => void;
  onDragEnd?: (gesture: PanResponderGestureState) => TDropResult | null | undefined | boolean;
  onRemove?: () => void;
}

export type TMeasureValue = {
  x: number;
  y: number;
  width: number;
  height: number;
  pageX: number;
  pageY: number;
};

export const DragDrop = ({
  style,
  children,
  hideWhenDrop = true,
  onDraggableCheck,
  onDragStart,
  onDragEnd,
  onRemove,
  ...props
}: DragDropProps) => {
  const dragDropContext = useContext(DragDropContext);
  const global = useRef({ value: { x: 0, y: 0 } }).current;
  const dragging = useRef<boolean>(false);
  const ref = useRef<React.RefObject<View> | null>(null);
  const measure = useRef<TMeasureValue | null>(null);

  const onStartShouldSetPanResponder = (e: GestureResponderEvent, gesture: PanResponderGestureState) => {
    dragging.current = !onDraggableCheck ? true : onDraggableCheck(e, gesture);
    if (!dragging.current) return dragging.current;

    if (dragDropContext.start && ref.current != null) {
      dragging.current = true;
    } else {
      dragging.current = false;
    }

    return dragging.current;
  };

  const onPanResponderGrant = async (e: GestureResponderEvent, gesture: PanResponderGestureState) => {
    if (dragDropContext.start && ref.current != null) {
      const measureValue: TMeasureValue = await getMeasure(ref.current);
      measure.current = measureValue;
      dragDropContext.start({
        style,
        children,
        layoutStyle: {
          left: measureValue.pageX,
          top: measureValue.pageY,
          width: measureValue.width,
          height: measureValue.height,
        },
      });
      dragDropContext.opacity.setValue(OPACITY_VISIBLE);
    }

    if (onDragStart) {
      onDragStart(e, gesture);
    }
  };

  const onPanResponderMove = (e: GestureResponderEvent, gesture: PanResponderGestureState) => {
    if (!dragging.current) return;

    dragDropContext.pan.setValue({
      x: global.value.x + gesture.dx,
      y: global.value.y + gesture.dy,
    });
  };

  const moveToStartPosition = () => {
    const identify = Math.random();
    if (dragDropContext.endStart) dragDropContext.endStart(identify);

    global.value = { x: 0, y: 0 };
    Animated.timing(dragDropContext.pan, {
      toValue: { x: global.value.x, y: global.value.y },
      duration: 200,
      useNativeDriver: true,
    }).start(() => {
      if (dragDropContext.endFinish) {
        dragDropContext.opacity.setValue(OPACITY_HIDDEN);
        dragDropContext.endFinish(identify);
      }
    });
  };

  const onPanResponderRelease = (e: GestureResponderEvent, gesture: PanResponderGestureState) => {
    let dropResult: TDropResult | null | undefined | boolean = null;
    if (!!onDragEnd) {
      dropResult = onDragEnd(gesture);
    }

    if (!dragging.current) {
      moveToStartPosition();
      return;
    }
    dragging.current = false;

    if (!!dropResult && !!measure.current) {
      if (typeof dropResult === 'boolean') {
        Animated.timing(dragDropContext.opacity, {
          toValue: 0,
          duration: 200,
          useNativeDriver: true,
        }).start(() => {
          handleRemove();
        });
      } else {
        if (dropResult?.dropX == null) {
          global.value.x = global.value.x + gesture.dx;
        } else {
          global.value.x = dropResult.dropX - measure.current.pageX; // dropX 가 절대좌표이므로 toValue.x 에 상대좌표를 넣으려면 현 DragDrop 의 절대좌표 x 를 빼줘야 한다.
        }
        if (dropResult?.dropY == null) {
          global.value.y = global.value.y + gesture.dy;
        } else {
          global.value.y = dropResult.dropY - measure.current.pageY; // dropY 가 절대좌표이므로 toValue.y 에 상대좌표를 넣으려면 현 DragDrop 의 절대좌표 y 를 빼줘야 한다.
        }

        Animated.timing(dragDropContext.pan, {
          toValue: { x: global.value.x, y: global.value.y },
          useNativeDriver: true,
          duration: 200,
        }).start(() => {
          handleRemove();
        });
        if (hideWhenDrop) {
          Animated.timing(dragDropContext.opacity, {
            toValue: 0,
            duration: 200,
            useNativeDriver: true,
          }).start();
        }
      }
    } else {
      moveToStartPosition();
    }
  };

  const onPanResponderReject = (e: GestureResponderEvent, gesture: PanResponderGestureState) => {
    console.log('reject');

    if (dragDropContext.endStart && dragDropContext.endFinish) {
      const identify = Math.random();
      dragDropContext.endStart(identify);
      dragDropContext.endFinish(identify);
      dragDropContext.opacity.setValue(OPACITY_HIDDEN);
    }

    if (onDragEnd) onDragEnd(gesture);
    moveToStartPosition();
  };

  const onPanResponderTerminate = (e: GestureResponderEvent, gesture: PanResponderGestureState) => {
    console.log('terminate');

    if (dragDropContext.endStart && dragDropContext.endFinish) {
      const identify = Math.random();
      dragDropContext.endStart(identify);
      dragDropContext.endFinish(identify);
      dragDropContext.opacity.setValue(OPACITY_HIDDEN);
    }

    if (onDragEnd) onDragEnd(gesture);
    moveToStartPosition();
  };

  const createPanResponder = () => {
    return PanResponder.create({
      onStartShouldSetPanResponder: onStartShouldSetPanResponder,
      onMoveShouldSetPanResponder: () => dragging.current,
      onPanResponderGrant: onPanResponderGrant,
      onPanResponderMove: onPanResponderMove,
      onPanResponderRelease: onPanResponderRelease,
      onPanResponderReject: onPanResponderReject,
      onPanResponderTerminate: onPanResponderTerminate,
    });
  };

  const [panResponder, setPanResponder] = useState<PanResponderInstance>(createPanResponder());

  const handleRemove = () => {
    if (dragDropContext.endStart && dragDropContext.endFinish) {
      const identify = Math.random();
      dragDropContext.endStart(identify);
      dragDropContext.endFinish(identify);
    }
    if (onRemove) {
      onRemove();
    }
  };

  useEffect(() => {
    dragDropContext.pan.setValue({ x: 0, y: 0 });
    dragDropContext.pan.addListener((value) => {});
  }, []);

  useEffect(() => {
    dragDropContext.pan.setValue({ x: 0, y: 0 });
    global.value = { x: 0, y: 0 };
    dragDropContext.opacity.setValue(OPACITY_VISIBLE);
    setPanResponder(createPanResponder());
  }, [onDragStart, onDragEnd, onRemove, hideWhenDrop, children]);

  useEffect(() => {
    setPanResponder(createPanResponder());
  }, [dragDropContext]);

  return (
    <View {...props} {...panResponder.panHandlers} style={style} ref={ref}>
      {children}
    </View>
  );
};

export type DragDropContextLayoutStyle = {
  left: number;
  top: number;
  width: number;
  height: number;
};

export type DragDropContextStartOptions = {
  children?: React.ReactNode | null;
  style?: StyleProp<ViewStyle>;
  layoutStyle: DragDropContextLayoutStyle;
};

export type DragDropContextType = {
  pan: Animated.ValueXY;
  opacity: Animated.Value;
  children?: React.ReactNode | null;
  style?: StyleProp<ViewStyle>;
  start?: (options: DragDropContextStartOptions) => void;
  endStart?: (identify: number) => void;
  endFinish?: (identify: number) => void;
  endAnimationIdentify?: number;
};
const initialValue = {
  pan: new Animated.ValueXY({ x: 0, y: 0 }),
  opacity: new Animated.Value(0),
  children: null,
};
export const DragDropContext = createContext<DragDropContextType>(initialValue);
export type DragDropProviderProps = {
  children?: React.ReactNode;
};

export const DragDropProvider = ({ children }: DragDropProviderProps) => {
  const measure = useRef<TMeasureValue | null>(null);
  const ref = useRef<React.RefObject<View> | null>(null);
  const pan = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current;
  const opacity = useRef(new Animated.Value(0)).current;
  const [rect, setRect] = useState({ left: 0, top: 0, width: 0, height: 0 });
  const [show, setShow] = useState(false);

  const start = async (options: DragDropContextStartOptions) => {
    value.endAnimationIdentify = undefined;
    setValue({ ...value, style: options.style, children: options.children, start, endStart, endFinish });
    if (ref.current) {
      measure.current = await getMeasure(ref.current);
      if (
        typeof options.layoutStyle.left !== 'undefined' &&
        options.layoutStyle.left != null &&
        typeof options.layoutStyle.top !== 'undefined' &&
        options.layoutStyle.top != null &&
        typeof options.layoutStyle.width !== 'undefined' &&
        options.layoutStyle.width != null &&
        typeof options.layoutStyle.height !== 'undefined' &&
        options.layoutStyle.height != null
      ) {
        setRect({
          left: options.layoutStyle.left - measure.current.pageX,
          top: options.layoutStyle.top - measure.current.pageY,
          width: options.layoutStyle.width,
          height: options.layoutStyle.height,
        });
        setShow(true);
      }
    }
  };

  const endStart = (identify: number) => {
    value.endAnimationIdentify = identify;
  };

  const endFinish = (identify: number) => {
    if (value.endAnimationIdentify == identify) {
      setShow(false);
    }
  };

  const [value, setValue] = useState<DragDropContextType>({
    pan: pan,
    opacity: opacity,
    children: null,
    style: {},
    start: start,
    endStart: endStart,
    endFinish: endFinish,
  });

  return (
    <DragDropContext.Provider value={value}>
      {children || null}
      <View
        style={[
          value.style,
          {
            position: 'absolute',
            marginLeft: 0,
            marginTop: 0,
            marginRight: 0,
            marginBottom: 0,
            left: 0,
            top: 0,
            width: 0,
            height: 0,
            opacity: 0,
            overflow: 'hidden',
          },
        ]}
        ref={ref}
      />
      {show && (
        <>
          <Animated.View
            style={[
              { transform: pan.getTranslateTransform(), opacity: opacity },
              value.style,
              { position: 'absolute', marginLeft: 0, marginTop: 0, marginRight: 0, marginBottom: 0, ...rect },
            ]}
          >
            {value.children}
          </Animated.View>
          <Animated.View
            style={[
              { transform: pan.getTranslateTransform(), opacity: 0.3 },
              value.style,
              {
                position: 'absolute',
                backgroundColor: 'white',
                marginLeft: 0,
                marginTop: 0,
                marginRight: 0,
                marginBottom: 0,
                ...rect,
              },
            ]}
          />
        </>
      )}
    </DragDropContext.Provider>
  );
};
