import type { Node as PMNode } from "@tiptap/pm/model"
import type { ResizeHandle } from "./docs-graphic-layout.ts"
import { usesPageLayer } from "./docs-graphic-position.ts"
import { parseGraphicAttrs } from "./docs-graphic-types.ts"

export const DOCS_GRAPHIC_SNAP_THRESHOLD_PX = 6

export type SnapRect = {
  x: number
  y: number
  width: number
  height: number
}

export type SnapGuideLine = {
  axis: "x" | "y"
  position: number
}

export type PageSnapContext = {
  pageWidth: number
  pageHeight: number
  margins: { top: number; right: number; bottom: number; left: number }
  otherRects: SnapRect[]
}

export type DocsGraphicSnapGuideState = {
  pageIndex: number
  pageWidth: number
  pageHeight: number
  guides: SnapGuideLine[]
}

function rectXEdges(rect: SnapRect): [number, number, number] {
  return [rect.x, rect.x + rect.width / 2, rect.x + rect.width]
}

function rectYEdges(rect: SnapRect): [number, number, number] {
  return [rect.y, rect.y + rect.height / 2, rect.y + rect.height]
}

export function buildPageSnapLines(ctx: PageSnapContext): { x: number[]; y: number[] } {
  const { pageWidth, pageHeight, margins, otherRects } = ctx
  const x = new Set<number>([
    0,
    margins.left,
    pageWidth / 2,
    pageWidth - margins.right,
    pageWidth,
  ])
  const y = new Set<number>([
    0,
    margins.top,
    pageHeight / 2,
    pageHeight - margins.bottom,
    pageHeight,
  ])

  for (const rect of otherRects) {
    for (const edge of rectXEdges(rect)) x.add(edge)
    for (const edge of rectYEdges(rect)) y.add(edge)
  }

  return { x: [...x], y: [...y] }
}

function bestAxisSnap(
  edges: number[],
  lines: number[],
  threshold: number
): { delta: number; guide: number | null } {
  let bestDist = threshold + 1
  let bestDelta = 0
  let bestGuide: number | null = null

  for (const edge of edges) {
    for (const line of lines) {
      const delta = line - edge
      const dist = Math.abs(delta)
      if (dist <= threshold && dist < bestDist) {
        bestDist = dist
        bestDelta = delta
        bestGuide = line
      }
    }
  }

  return { delta: bestDist <= threshold ? bestDelta : 0, guide: bestGuide }
}

export function snapMoveRect(
  rect: SnapRect,
  ctx: PageSnapContext,
  threshold = DOCS_GRAPHIC_SNAP_THRESHOLD_PX
): { rect: SnapRect; guides: SnapGuideLine[] } {
  const lines = buildPageSnapLines(ctx)
  const guides: SnapGuideLine[] = []

  const snapX = bestAxisSnap(rectXEdges(rect), lines.x, threshold)
  const snapY = bestAxisSnap(rectYEdges(rect), lines.y, threshold)

  if (snapX.guide != null) guides.push({ axis: "x", position: snapX.guide })
  if (snapY.guide != null) guides.push({ axis: "y", position: snapY.guide })

  return {
    rect: {
      ...rect,
      x: Math.round(rect.x + snapX.delta),
      y: Math.round(rect.y + snapY.delta),
    },
    guides,
  }
}

function movingEdgesForHandle(handle: ResizeHandle): {
  x: Array<"start" | "center" | "end">
  y: Array<"start" | "center" | "end">
} {
  const x: Array<"start" | "center" | "end"> = []
  const y: Array<"start" | "center" | "end"> = []
  if (handle.includes("w")) x.push("start")
  if (handle.includes("e")) x.push("end")
  if (handle.includes("n")) y.push("start")
  if (handle.includes("s")) y.push("end")
  if (x.length === 0 && y.length === 0) {
    x.push("end")
    y.push("end")
  }
  return { x, y }
}

function edgeValue(rect: SnapRect, axis: "x" | "y", kind: "start" | "center" | "end"): number {
  if (axis === "x") {
    if (kind === "start") return rect.x
    if (kind === "center") return rect.x + rect.width / 2
    return rect.x + rect.width
  }
  if (kind === "start") return rect.y
  if (kind === "center") return rect.y + rect.height / 2
  return rect.y + rect.height
}

function setEdgeValue(
  rect: SnapRect,
  axis: "x" | "y",
  kind: "start" | "center" | "end",
  value: number
): SnapRect {
  const next = { ...rect }
  if (axis === "x") {
    if (kind === "start") {
      const right = rect.x + rect.width
      next.x = value
      next.width = Math.max(1, right - value)
    } else if (kind === "end") {
      next.width = Math.max(1, value - rect.x)
    } else {
      const half = rect.width / 2
      next.x = value - half
    }
  } else {
    if (kind === "start") {
      const bottom = rect.y + rect.height
      next.y = value
      next.height = Math.max(1, bottom - value)
    } else if (kind === "end") {
      next.height = Math.max(1, value - rect.y)
    } else {
      const half = rect.height / 2
      next.y = value - half
    }
  }
  return {
    x: Math.round(next.x),
    y: Math.round(next.y),
    width: Math.round(next.width),
    height: Math.round(next.height),
  }
}

function snapEdge(
  rect: SnapRect,
  axis: "x" | "y",
  kind: "start" | "center" | "end",
  lines: number[],
  threshold: number
): { rect: SnapRect; guide: number | null } {
  const current = edgeValue(rect, axis, kind)
  let bestDist = threshold + 1
  let bestLine: number | null = null

  for (const line of lines) {
    const dist = Math.abs(line - current)
    if (dist <= threshold && dist < bestDist) {
      bestDist = dist
      bestLine = line
    }
  }

  if (bestLine == null) return { rect, guide: null }
  return { rect: setEdgeValue(rect, axis, kind, bestLine), guide: bestLine }
}

export function snapResizeRect(
  handle: ResizeHandle,
  rect: SnapRect,
  ctx: PageSnapContext,
  threshold = DOCS_GRAPHIC_SNAP_THRESHOLD_PX
): { rect: SnapRect; guides: SnapGuideLine[] } {
  const lines = buildPageSnapLines(ctx)
  const moving = movingEdgesForHandle(handle)
  let next = { ...rect }
  const guides: SnapGuideLine[] = []

  for (const kind of moving.x) {
    const snapped = snapEdge(next, "x", kind, lines.x, threshold)
    next = snapped.rect
    if (snapped.guide != null) guides.push({ axis: "x", position: snapped.guide })
  }

  for (const kind of moving.y) {
    const snapped = snapEdge(next, "y", kind, lines.y, threshold)
    next = snapped.rect
    if (snapped.guide != null) guides.push({ axis: "y", position: snapped.guide })
  }

  return { rect: next, guides: dedupeGuides(guides) }
}

function dedupeGuides(guides: SnapGuideLine[]): SnapGuideLine[] {
  const seen = new Set<string>()
  const out: SnapGuideLine[] = []
  for (const guide of guides) {
    const key = `${guide.axis}:${guide.position}`
    if (seen.has(key)) continue
    seen.add(key)
    out.push(guide)
  }
  return out
}

export function collectOtherPageGraphicRects(
  doc: PMNode,
  pageIndex: number,
  excludePos: number | null
): SnapRect[] {
  const rects: SnapRect[] = []
  doc.descendants((node, pos) => {
    if (node.type.name !== "docsGraphic") return
    const attrs = parseGraphicAttrs(node.attrs as Record<string, unknown>)
    if (!usesPageLayer(attrs)) return
    if (attrs.pageIndex !== pageIndex) return
    if (excludePos != null && pos === excludePos) return
    rects.push({
      x: attrs.pageX,
      y: attrs.pageY,
      width: attrs.width,
      height: attrs.height,
    })
  })
  return rects
}

export function readPageSnapContextFromStack(
  stack: HTMLElement | null,
  otherRects: SnapRect[]
): PageSnapContext | null {
  if (!stack) return null
  const pageWidth = Number.parseFloat(stack.dataset.docsPageWidth ?? "0")
  const pageHeight = Number.parseFloat(stack.dataset.docsPageHeight ?? "0")
  if (pageWidth <= 0 || pageHeight <= 0) return null
  return {
    pageWidth,
    pageHeight,
    margins: {
      top: Number.parseFloat(stack.dataset.docsPageMarginTop ?? "0") || 0,
      right: Number.parseFloat(stack.dataset.docsPageMarginRight ?? "0") || 0,
      bottom: Number.parseFloat(stack.dataset.docsPageMarginBottom ?? "0") || 0,
      left: Number.parseFloat(stack.dataset.docsPageMarginLeft ?? "0") || 0,
    },
    otherRects,
  }
}
