import type { Action, CustomBinding } from 'state/constants'
import { getPagePoint } from 'state/helpers'
import { getShapeUtils, Shape } from 'shapes'
import { mutables } from 'state/mutables'
import { nanoid } from 'nanoid'
import { TLPointerInfo, Utils } from '@tldraw/core'
import type { ArrowShape } from 'shapes/arrow'
import Vec from '@tldraw/vec'

export const translateHandle: Action = (data, payload: TLPointerInfo) => {
  const { initialPoint, snapshot, pointedHandleId } = mutables

  if (!pointedHandleId) return

  let delta = Vec.sub(mutables.currentPoint, initialPoint)

  data.pageState.selectedIds.forEach((id) => {
    const initialShape = snapshot.page.shapes[id] as ArrowShape

    const shape = data.page.shapes[id] as ArrowShape

    if (payload.shiftKey) {
      const A = initialShape.handles[pointedHandleId === 'start' ? 'end' : 'start'].point
      const B = initialShape.handles[pointedHandleId].point
      const C = Vec.add(B, delta)
      const angle = Vec.angle(A, C)
      const adjusted = Vec.rotWith(C, A, Utils.snapAngleToSegments(angle, 24) - angle)
      delta = Vec.add(delta, Vec.sub(adjusted, C))
    }

    const handlePoints = {
      start: [...initialShape.handles.start.point],
      end: [...initialShape.handles.end.point],
    }

    handlePoints[pointedHandleId] = Vec.add(handlePoints[pointedHandleId], delta)

    // Create binding

    const oppositeHandleId = pointedHandleId === 'start' ? 'end' : 'start'
    const oppositeHandle = shape.handles[oppositeHandleId]
    const handlePoint = Vec.add(handlePoints[pointedHandleId], initialShape.point)

    let minDistance = Infinity
    let toShape: Shape | undefined
    const oppositeBindingTargetId = Object.values(data.page.bindings).find(
      (binding) => binding.fromId === shape.id && binding.handleId === oppositeHandleId
    )?.toId

    if (!payload.metaKey) {
      // Find colliding shape with center nearest to point
      Object.values(data.page.shapes)
        .filter(
          (shape) =>
            !data.pageState.selectedIds.includes(shape.id) && shape.id !== oppositeBindingTargetId
        )
        .forEach((potentialTarget) => {
          const utils = getShapeUtils(potentialTarget)

          if (!utils.canBind) return

          const bounds = utils.getBounds(potentialTarget)

          if (Utils.pointInBounds(handlePoint, bounds)) {
            const dist = Vec.dist(handlePoint, utils.getCenter(potentialTarget))
            if (dist < minDistance) {
              minDistance = dist
              toShape = potentialTarget
            }
          }
        })
    }

    const oldBinding = Object.values(data.page.bindings).find(
      (binding) => binding.fromId === shape.id && binding.handleId === pointedHandleId
    )

    // If we have a binding target
    if (toShape) {
      if (!oldBinding || oldBinding.toId !== toShape.id) {
        if (oldBinding) {
          delete data.page.bindings[oldBinding.id]
        }

        // Create a new binding between shape and toShape
        const binding: CustomBinding = {
          id: nanoid(),
          fromId: shape.id,
          toId: toShape.id,
          handleId: pointedHandleId,
        }

        data.page.bindings[binding.id] = binding
      }

      // The `updateBoundShapes` action will take it from here.
      return
    }

    // If we didn't find a toShape, clear out the old binding (if present)
    if (oldBinding) {
      delete data.page.bindings[oldBinding.id]
    }

    const offset = Utils.getCommonTopLeft([handlePoints.start, handlePoints.end])
    shape.handles.start.point = Vec.sub(handlePoints.start, offset)
    shape.handles.end.point = Vec.sub(handlePoints.end, offset)
    shape.point = Vec.add(initialShape.point, offset)
  })
}
