import { computed, ref, type Ref } from 'vue';

const DEFAULT_DRAG_DATA_TYPE = 'text/plain';

type UseDragOptionsBase = {
  /** DataTransfer MIME type used by setDragData/getDragData. */
  dataType?: string;
};

type UseDragOptionsWithItems<TItem> = UseDragOptionsBase & {
  /** Optional sortable item model. When provided, `onDrop` reorders it. */
  items: Ref<TItem[]>;
  /** Returns the stable drag ID for an item in `items`. */
  getItemId: (item: TItem) => string;
};

type UseDragOptionsWithoutItems = UseDragOptionsBase & {
  /** Disallow sortable item state unless `getItemId` is also supplied. */
  items?: never;
  /** Disallow item ID resolution without sortable item state. */
  getItemId?: never;
};

export type UseDragOptions<TItem = unknown> =
  | UseDragOptionsWithItems<TItem>
  | UseDragOptionsWithoutItems;

export type DragGhostOptions = {
  /** Width of the generated drag preview in pixels. */
  width: number;
  /** Height of the generated drag preview in pixels. */
  height: number;
  /** Classes applied to the generated preview element. */
  className?: string;
  /** Horizontal cursor offset inside the preview. */
  offsetX?: number;
  /** Vertical cursor offset inside the preview. */
  offsetY?: number;
};

export function reorderItems<T>(
  items: T[],
  draggedItem: T,
  targetItem: T,
): T[] {
  const draggedIndex = items.indexOf(draggedItem);
  const targetIndex = items.indexOf(targetItem);

  if (draggedIndex === -1 || targetIndex === -1 || draggedIndex === targetIndex)
    return items;

  const nextItems = [...items];
  const [removedItem] = nextItems.splice(draggedIndex, 1);
  if (removedItem === undefined) return items;
  nextItems.splice(targetIndex, 0, removedItem);

  return nextItems;
}

export function setDragData(
  event: DragEvent,
  value: string,
  dataType = DEFAULT_DRAG_DATA_TYPE,
) {
  event.dataTransfer?.setData(dataType, value);
}

export function getDragData(
  event: DragEvent,
  dataType = DEFAULT_DRAG_DATA_TYPE,
) {
  return event.dataTransfer?.getData(dataType) || undefined;
}

export function setDragGhost(event: DragEvent, options: DragGhostOptions) {
  if (!event.dataTransfer?.setDragImage) return undefined;

  const ghost = document.createElement('div');
  ghost.setAttribute('aria-hidden', 'true');
  ghost.className = options.className ?? '';
  Object.assign(ghost.style, {
    position: 'fixed',
    top: '-10000px',
    left: '-10000px',
    width: `${options.width}px`,
    height: `${options.height}px`,
    boxSizing: 'border-box',
  });

  document.body.appendChild(ghost);
  event.dataTransfer.setDragImage(
    ghost,
    options.offsetX ?? options.width / 2,
    options.offsetY ?? Math.min(24, options.height / 2),
  );

  return () => ghost.remove();
}

export function useDrag<TItem = unknown>(options: UseDragOptions<TItem> = {}) {
  const dataType = options.dataType ?? DEFAULT_DRAG_DATA_TYPE;
  const originalItems = ref<TItem[]>();
  const draggedItemId = ref<string>();
  const activeItemId = ref<string>();
  let cleanupDragGhost: (() => void) | undefined;

  function setData(event: DragEvent, value: string) {
    setDragData(event, value, dataType);
  }

  function getData(event: DragEvent) {
    return getDragData(event, dataType);
  }

  function setGhost(event: DragEvent, ghostOptions: DragGhostOptions) {
    cleanupDragGhost?.();
    cleanupDragGhost = setDragGhost(event, ghostOptions);
    return cleanupDragGhost;
  }

  function cleanupGhost() {
    cleanupDragGhost?.();
    cleanupDragGhost = undefined;
  }

  function startDrag(
    event: DragEvent,
    itemId: string,
    ghostOptions?: DragGhostOptions,
  ) {
    originalItems.value = options.items ? [...options.items.value] : undefined;
    draggedItemId.value = itemId;
    setData(event, itemId);

    if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
    if (ghostOptions) setGhost(event, ghostOptions);
  }

  function dragOver(event: DragEvent, itemId: string) {
    if (!draggedItemId.value) return;

    event.preventDefault();
    activeItemId.value = itemId;
    if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
  }

  function getDropData(event: DragEvent) {
    return getData(event) ?? draggedItemId.value;
  }

  function resetDragState() {
    originalItems.value = undefined;
    draggedItemId.value = undefined;
    activeItemId.value = undefined;
    cleanupGhost();
  }

  function getItemId(item: TItem) {
    return options.getItemId?.(item);
  }

  const draggedItem = computed(() =>
    options.items?.value.find(
      (item) => getItemId(item) === draggedItemId.value,
    ),
  );
  const activeItem = computed(() =>
    options.items?.value.find((item) => getItemId(item) === activeItemId.value),
  );

  function onDrop(event: DragEvent, targetItemId: string) {
    event.preventDefault();

    const draggedId = getDropData(event);

    if (draggedId && options.items) {
      const draggedItem = options.items.value.find(
        (item) => getItemId(item) === draggedId,
      );
      const targetItem = options.items.value.find(
        (item) => getItemId(item) === targetItemId,
      );

      if (draggedItem && targetItem) {
        options.items.value = reorderItems(
          options.items.value,
          draggedItem,
          targetItem,
        );
      }
    }

    resetDragState();
  }

  return {
    activeItemId,
    activeItem,
    draggedItemId,
    draggedItem,
    originalItems,
    reorderItems,
    setDragData: setData,
    getDragData: getData,
    setDragGhost: setGhost,
    cleanupDragGhost: cleanupGhost,
    startDrag,
    dragOver,
    getDropData,
    onDrop,
    resetDragState,
  };
}
