import { ConnectionMode } from '../types'
import type {
  Actions,
  Connection,
  ConnectionStatus,
  Dimensions,
  GraphEdge,
  GraphNode,
  HandleType,
  NodeHandleBounds,
  ValidConnectionFunc,
  ValidHandleResult,
  XYPosition,
} from '../types'
import { getEventPosition } from '.'

export interface ConnectionHandle extends XYPosition, Dimensions {
  id: string | null
  type: HandleType | null
  nodeId: string
}

function defaultValidHandleResult(): ValidHandleResult {
  return {
    handleDomNode: null,
    isValid: false,
    connection: { source: '', target: '', sourceHandle: null, targetHandle: null },
    endHandle: null,
  }
}

export function resetRecentHandle(handleDomNode: Element): void {
  handleDomNode?.classList.remove('valid', 'connecting', 'vue-flow__handle-valid', 'vue-flow__handle-connecting')
}

// this functions collects all handles and adds an absolute position
// so that we can later find the closest handle to the mouse position
export function getHandles(
  node: GraphNode,
  handleBounds: NodeHandleBounds,
  type: HandleType,
  currentHandle: string,
): ConnectionHandle[] {
  return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, h) => {
    if (`${node.id}-${h.id}-${type}` !== currentHandle) {
      res.push({
        id: h.id || null,
        type,
        nodeId: node.id,
        x: (node.computedPosition?.x ?? 0) + h.x + h.width / 2,
        y: (node.computedPosition?.y ?? 0) + h.y + h.height / 2,
        width: h.width,
        height: h.height,
      })
    }
    return res
  }, [])
}

export function getClosestHandle(
  event: MouseEvent | TouchEvent,
  doc: Document | ShadowRoot,
  pos: XYPosition,
  connectionRadius: number,
  handles: ConnectionHandle[],
  validator: (handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'>) => ValidHandleResult,
) {
  // we always want to prioritize the handle below the mouse cursor over the closest distance handle,
  // because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
  const { x, y } = getEventPosition(event)
  const domNodes = doc.elementsFromPoint(x, y)

  const handleBelow = domNodes.find((el) => el.classList.contains('vue-flow__handle'))

  if (handleBelow) {
    const handleNodeId = handleBelow.getAttribute('data-nodeid')

    if (handleNodeId) {
      const handleType = getHandleType(undefined, handleBelow)
      const handleId = handleBelow.getAttribute('data-handleid')
      const validHandleResult = validator({ nodeId: handleNodeId, id: handleId, type: handleType })

      if (validHandleResult) {
        const handle = handles.find((h) => h.nodeId === handleNodeId && h.type === handleType && h.id === handleId)

        return {
          handle: {
            id: handleId,
            type: handleType,
            nodeId: handleNodeId,
            x: handle?.x || pos.x,
            y: handle?.y || pos.y,
          },
          validHandleResult,
        }
      }
    }
  }

  // if we couldn't find a handle below the mouse cursor we look for the closest distance based on the connectionRadius
  let closestHandles: { handle: ConnectionHandle; validHandleResult: ValidHandleResult }[] = []
  let minDistance = Number.POSITIVE_INFINITY

  for (const handle of handles) {
    const distance = Math.sqrt((handle.x - pos.x) ** 2 + (handle.y - pos.y) ** 2)

    if (distance <= connectionRadius) {
      const validHandleResult = validator(handle)

      if (distance <= minDistance) {
        if (distance < minDistance) {
          closestHandles = [{ handle, validHandleResult }]
        } else if (distance === minDistance) {
          // when multiple handles are on the same distance we collect all of them
          closestHandles.push({
            handle,
            validHandleResult,
          })
        }

        minDistance = distance
      }
    }
  }

  if (!closestHandles.length) {
    return { handle: null, validHandleResult: defaultValidHandleResult() }
  }

  if (closestHandles.length === 1) {
    return closestHandles[0]
  }

  const hasValidHandle = closestHandles.some(({ validHandleResult }) => validHandleResult.isValid)
  const hasTargetHandle = closestHandles.some(({ handle }) => handle.type === 'target')

  // if multiple handles are layout on top of each other we prefer the one with type = target and the one that is valid
  return (
    closestHandles.find(({ handle, validHandleResult }) =>
      hasTargetHandle ? handle.type === 'target' : hasValidHandle ? validHandleResult.isValid : true,
    ) || closestHandles[0]
  )
}

// checks if  and returns connection in fom of an object { source: 123, target: 312 }
export function isValidHandle(
  event: MouseEvent | TouchEvent,
  handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null,
  connectionMode: ConnectionMode,
  fromNodeId: string,
  fromHandleId: string | null,
  fromType: HandleType,
  isValidConnection: ValidConnectionFunc,
  doc: Document | ShadowRoot,
  edges: GraphEdge[],
  nodes: GraphNode[],
  findNode: Actions['findNode'],
) {
  const isTarget = fromType === 'target'

  const handleDomNode = doc.querySelector(`.vue-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`)
  const { x, y } = getEventPosition(event)
  const elementBelow = doc.elementFromPoint(x, y)

  // we always want to prioritize the handle below the mouse cursor over the closest distance handle,
  // because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
  const handleToCheck = elementBelow?.classList.contains('vue-flow__handle') ? elementBelow : handleDomNode

  const result = defaultValidHandleResult()

  if (handleToCheck) {
    result.handleDomNode = handleToCheck

    const handleType = getHandleType(undefined, handleToCheck)
    const handleNodeId = handleToCheck.getAttribute('data-nodeid')!
    const handleId = handleToCheck.getAttribute('data-handleid')
    const connectable = handleToCheck.classList.contains('connectable')
    const connectableEnd = handleToCheck.classList.contains('connectableend')

    const connection: Connection = {
      source: isTarget ? handleNodeId : fromNodeId,
      sourceHandle: isTarget ? handleId : fromHandleId,
      target: isTarget ? fromNodeId : handleNodeId,
      targetHandle: isTarget ? fromHandleId : handleId,
    }

    result.connection = connection

    const isConnectable = connectable && connectableEnd

    // in strict mode we don't allow target to target or source to source connections
    const isValid =
      isConnectable &&
      (connectionMode === ConnectionMode.Strict
        ? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
        : handleNodeId !== fromNodeId || handleId !== fromHandleId)

    result.endHandle = {
      nodeId: handleNodeId,
      handleId,
      type: handleType as HandleType,
    }

    if (isValid) {
      result.isValid = isValidConnection(connection, {
        edges,
        nodes,
        sourceNode: findNode(connection.source)!,
        targetNode: findNode(connection.target)!,
      })
    }
  }

  return result
}

interface GetHandleLookupParams {
  nodes: GraphNode[]
  nodeId: string
  handleId: string | null
  handleType: string
}

export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) {
  return nodes.reduce<ConnectionHandle[]>((res, node) => {
    const { handleBounds } = node
    let sourceHandles: ConnectionHandle[] = []
    let targetHandles: ConnectionHandle[] = []

    if (handleBounds) {
      sourceHandles = getHandles(node, handleBounds, 'source', `${nodeId}-${handleId}-${handleType}`)
      targetHandles = getHandles(node, handleBounds, 'target', `${nodeId}-${handleId}-${handleType}`)
    }

    res.push(...sourceHandles, ...targetHandles)
    return res
  }, [])
}

export function getHandleType(edgeUpdaterType: HandleType | undefined, handleDomNode: Element | null): HandleType | null {
  if (edgeUpdaterType) {
    return edgeUpdaterType
  } else if (handleDomNode?.classList.contains('target')) {
    return 'target'
  } else if (handleDomNode?.classList.contains('source')) {
    return 'source'
  }

  return null
}

export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) {
  let connectionStatus: ConnectionStatus | null = null

  if (isHandleValid) {
    connectionStatus = 'valid'
  } else if (isInsideConnectionRadius && !isHandleValid) {
    connectionStatus = 'invalid'
  }

  return connectionStatus
}
