import interact from "interactjs"
import { onUnmounted, ref } from "vue"
import type {
  ClonedDraggableItem,
  DraggableConfig,
  DroppableConfig,
} from "./types"
import {
  animateElements,
  clearClonedElements,
  cloneDraggableElements,
  collectDraggableElements,
  createGhostElement,
  findClosestElement,
  getAverageSizeOfClonedElements,
  removeGhostElement,
} from "./helperFunctions"

let nextElementConfigIndex = 0
let ghostElement: HTMLElement | null = null
const elementConfigIndex: { [key: number]: DraggableConfig } = {}

const getDraggableConfig = (element: HTMLElement): DraggableConfig => {
  const configIndex = parseInt(element?.dataset?.configIndex || "")
  const draggableConfig = elementConfigIndex[configIndex]

  return draggableConfig
}

const isDraggingOrResizingAnything = ref(false)

export default () => {
  const isDragging = ref(false)
  const isDraggingInsideDropzone = ref(false)
  let instance: ReturnType<typeof interact>

  const makeDraggable = (
    element: HTMLElement,
    config: DraggableConfig = {},
  ) => {
    const index = nextElementConfigIndex++
    const ghostPosition = { x: 0, y: 0 }

    // save the config to have access to it later
    elementConfigIndex[index] = config
    element.dataset.configIndex = index.toString()

    instance = interact(element).draggable({
      allowFrom: config.handleElement ? config.handleElement : undefined,
      listeners: {
        start: (event) => {
          ghostElement = createGhostElement(element)
          if (config.anchorToCorner) {
            ghostPosition.x = event.dx
            ghostPosition.y = event.dy
          } else {
            const style = getComputedStyle(ghostElement)

            ghostPosition.x = parseInt(style.left)
            ghostPosition.y = parseInt(style.top)
          }
        },
        move: (event) => {
          if (!ghostElement) {
            return
          }
          if (config.anchorToCorner) {
            ghostPosition.x = event.page.x
            ghostPosition.y = event.page.y
          } else {
            ghostPosition.x += event.dx
            ghostPosition.y += event.dy
          }

          requestAnimationFrame(() => {
            ghostElement!.style.top = ghostPosition.y + "px"
            ghostElement!.style.left = ghostPosition.x + "px"
          })
        },
        end: (event) => {
          config.onDragEnd?.(event.rect.left, event.rect.top)
          element.style.transform = ""

          if (ghostElement) {
            removeGhostElement(ghostElement, element)
          }
        },
      },
    })

    if (config.resize) {
      let breakpoints: number[] = []
      let closestBreakpoint = 0

      const findClosestBreakpoint = (
        value: number,
        breakpoints: number[],
      ): number =>
        breakpoints.reduce((prev, curr) =>
          Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev,
        )

      instance.resizable({
        edges: { left: true, right: true },
        listeners: {
          start: (event) => {
            ghostElement = createGhostElement(element, true)
            const startCols = Number(event.currentTarget.dataset.cols) || 12
            const startWidth = ghostElement.clientWidth
            const colWidth = startWidth / startCols

            for (let i = 1; i <= 12; i++) {
              breakpoints.push(i * colWidth)
            }

            isDraggingOrResizingAnything.value = true
          },
          move: (event) => {
            if (ghostElement) {
              closestBreakpoint = findClosestBreakpoint(
                event.rect.width,
                breakpoints,
              )

              ghostElement.style.width = closestBreakpoint + "px"
            }
          },
          end: (event) => {
            const onResizePayload = {
              draggableElement: event.relatedTarget,
              targetElement: event.target,
              draggableConfig: config,
              cols: breakpoints.indexOf(closestBreakpoint) + 1,
              width: closestBreakpoint,
            }

            breakpoints = []
            closestBreakpoint = 0

            if (ghostElement) {
              removeGhostElement(ghostElement, element)
            }

            config.onResize?.(onResizePayload)
            isDraggingOrResizingAnything.value = false
          },
        },
      })
    }
  }

  const createDropzone = (
    element: HTMLElement,
    droppableConfig: DroppableConfig = {},
  ) => {
    let draggableElements: Element[] = []
    let clonedDraggableElements: ClonedDraggableItem[] = []
    let lastHoveredIndex: number
    let hasClones = false
    let virtualElementSize = { width: 0, height: 0 }

    instance = interact(element).dropzone({
      ondrop: ({ relatedTarget, target }) => {
        const draggableConfig = getDraggableConfig(relatedTarget)
        const onDropPayload = {
          draggableElement: relatedTarget,
          targetElement: target,
          draggableConfig: draggableConfig,
          droppableConfig: droppableConfig,
          index: lastHoveredIndex,
        }

        droppableConfig.onDrop?.(onDropPayload)
        draggableConfig.onDrop?.(onDropPayload)
      },
      ondropmove: (event) => {
        if (droppableConfig.sortable) {
          const closestElement = findClosestElement(
            {
              x: event.dragEvent.pageX,
              y: event.dragEvent.pageY,
            },
            clonedDraggableElements,
            droppableConfig.sortable,
          )

          if (closestElement) {
            const { index } = closestElement

            if (index !== lastHoveredIndex) {
              animateElements(
                index,
                clonedDraggableElements,
                droppableConfig.sortable,
                virtualElementSize,
              )
            }
            lastHoveredIndex = index
          }
        }
      },
      accept: ({ draggableElement }) => {
        const draggableType = getDraggableConfig(draggableElement)?.type

        // if the draggableElement has a type, check if the config includes that type
        if (droppableConfig.groups?.length && draggableType?.length) {
          return droppableConfig.groups.includes(draggableType)
        }

        return false
      },
      ondropactivate: ({ target }: { target: HTMLElement }) => {
        isDragging.value = true
        isDraggingOrResizingAnything.value = true
      },
      ondropdeactivate: () => {
        isDragging.value = false
        isDraggingOrResizingAnything.value = false
        isDraggingInsideDropzone.value = false
        virtualElementSize = { width: 0, height: 0 }

        if (hasClones) {
          clearClonedElements(clonedDraggableElements, draggableElements)
          hasClones = false
        }
        lastHoveredIndex = Infinity
      },
      ondragenter: ({ target }) => {
        isDraggingInsideDropzone.value = true

        if (!hasClones && droppableConfig.sortable) {
          draggableElements = collectDraggableElements(target)
          clonedDraggableElements = cloneDraggableElements(
            draggableElements,
            droppableConfig.cloneZIndex,
          )

          virtualElementSize = getAverageSizeOfClonedElements(
            clonedDraggableElements,
          )

          hasClones = true
        }
      },
      ondragleave: () => {
        isDraggingInsideDropzone.value = false
        lastHoveredIndex = Infinity

        if (droppableConfig.sortable) {
          animateElements(
            Infinity,
            clonedDraggableElements,
            droppableConfig.sortable,
            virtualElementSize,
          )
        }
      },
    })
  }

  onUnmounted(() => instance?.unset())

  return {
    makeDraggable,
    createDropzone,
    isDragging,
    isDraggingInsideDropzone,
    isDraggingOrResizingAnything,
  }
}

export type * from "./types"
