import React, {
  createContext,
  useEffect,
  useRef,
  useLayoutEffect,
  useMemo,
  useState,
  useContext,
  type RefCallback,
  type MutableRefObject,
  type Context,
  type Ref,
} from 'react';
import { useDrag, useDrop } from 'react-dnd';

import { theme } from '../style';

import View from './common/View';

function useMergedRefs<T>(
  ref1: RefCallback<T> | MutableRefObject<T>,
  ref2: RefCallback<T> | MutableRefObject<T>,
): Ref<T> {
  return useMemo(() => {
    function ref(value) {
      [ref1, ref2].forEach(ref => {
        if (typeof ref === 'function') {
          ref(value);
        } else if (ref != null) {
          ref.current = value;
        }
      });
    }

    return ref;
  }, [ref1, ref2]);
}

type DragState = {
  state: 'start-preview' | 'start' | 'end';
  type?: string;
  item?: unknown;
};

export type OnDragChangeCallback = (drag: DragState) => Promise<void> | void;
type UseDraggableArgs = {
  item: unknown;
  type: string;
  canDrag: boolean;
  onDragChange: OnDragChangeCallback;
};

export function useDraggable({
  item,
  type,
  canDrag,
  onDragChange,
}: UseDraggableArgs) {
  let _onDragChange = useRef(onDragChange);

  const [, dragRef] = useDrag({
    type,
    item: () => {
      _onDragChange.current({ state: 'start-preview', type, item });

      setTimeout(() => {
        _onDragChange.current({ state: 'start' });
      }, 0);

      return { type, item };
    },
    collect: monitor => ({ isDragging: monitor.isDragging() }),

    end(item) {
      _onDragChange.current({ state: 'end', type, item });
    },

    canDrag() {
      return canDrag;
    },
  });

  useLayoutEffect(() => {
    _onDragChange.current = onDragChange;
  });

  return { dragRef };
}
type DropPosition = 'top' | 'bottom';

export type OnDropCallback = (
  id: unknown,
  dropPos: DropPosition,
  targetId: unknown,
) => Promise<void> | void;

type OnLongHoverCallback = () => Promise<void> | void;

type UseDroppableArgs = {
  types: string | string[];
  id: unknown;
  onDrop: OnDropCallback;
  onLongHover?: OnLongHoverCallback;
};

export function useDroppable({
  types,
  id,
  onDrop,
  onLongHover,
}: UseDroppableArgs) {
  let ref = useRef(null);
  let [dropPos, setDropPos] = useState<DropPosition>(null);

  let [{ isOver }, dropRef] = useDrop({
    accept: types,
    drop({ item }) {
      onDrop(item.id, dropPos, id);
    },
    hover(_, monitor) {
      let hoverBoundingRect = ref.current.getBoundingClientRect();
      let hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
      let clientOffset = monitor.getClientOffset();
      let hoverClientY = clientOffset.y - hoverBoundingRect.top;
      let pos: DropPosition = hoverClientY < hoverMiddleY ? 'top' : 'bottom';

      setDropPos(pos);
    },
    collect(monitor) {
      return { isOver: monitor.isOver() };
    },
  });

  useEffect(() => {
    let timeout;
    if (onLongHover && isOver) {
      timeout = setTimeout(onLongHover, 700);
    }

    return () => timeout && clearTimeout(timeout);
  }, [isOver]);

  return {
    dropRef: useMergedRefs(dropRef, ref),
    dropPos: isOver ? dropPos : null,
  };
}

type ItemPosition = 'first' | 'last';
export const DropHighlightPosContext: Context<ItemPosition> =
  createContext(null);

type DropHighlightProps = {
  pos: 'top' | 'bottom';
  offset?: {
    top?: number;
    bottom?: number;
  };
};
export function DropHighlight({ pos, offset }: DropHighlightProps) {
  let itemPos = useContext(DropHighlightPosContext);

  if (pos == null) {
    return null;
  }

  let topOffset = (itemPos === 'first' ? 2 : 0) + (offset?.top || 0);
  let bottomOffset = (itemPos === 'last' ? 2 : 0) + (offset?.bottom || 0);

  let posStyle =
    pos === 'top' ? { top: -2 + topOffset } : { bottom: -1 + bottomOffset };

  return (
    <View
      style={[
        {
          position: 'absolute',
          left: 2,
          right: 2,
          borderRadius: 3,
          height: 3,
          background: theme.pageTextLink,
          zIndex: 10000,
          pointerEvents: 'none',
        },
        posStyle,
      ]}
    />
  );
}