import { useEffect, useRef, type PointerEvent as ReactPointerEvent, type RefObject } from 'react'
import { localToWorld, screenToWorld, worldToLocal, type Point } from '@/lib/geometry'
import { useUpdateObject } from '@/lib/objects'
import { HANDLE_PX, SELECT_COLOR, objectTransform } from './shared'
import { useEditorStore } from './store'
import type { EditorObject } from './types'

const ROTATE_OFFSET_PX = 28 // distance of the rotate knob above the object
const MIN_OBJ_CM = 1 // smallest allowed dimension
const ROTATE_SNAP_DEG = 15

const corners: [number, number][] = [
  [-1, -1],
  [1, -1],
  [1, 1],
  [-1, 1],
]

/**
 * Handles for the selected object: a transparent body to move, four corner
 * handles to resize (opposite corner stays put, honoring rotation via the
 * object-local frame), and a knob to rotate (snaps to 15°, free with Shift).
 * Each gesture updates liveObject for instant feedback and fires exactly one
 * PATCH on release.
 */
export function SelectionOverlay({
  object,
  gardenId,
  svgRef,
}: {
  object: EditorObject
  gardenId: number
  svgRef: RefObject<SVGSVGElement | null>
}) {
  const setLiveObject = useEditorStore((s) => s.setLiveObject)
  const setObjectDragging = useEditorStore((s) => s.setObjectDragging)
  const scale = useEditorStore((s) => s.viewport.scale)
  const update = useUpdateObject(gardenId)

  // Detach for an in-flight gesture. If the overlay unmounts mid-drag (the
  // object is deleted or deselected), this runs on unmount so window listeners
  // don't leak and objectDragging can't stay stuck true (which would freeze pan).
  const cleanupRef = useRef<(() => void) | null>(null)
  useEffect(
    () => () => {
      cleanupRef.current?.()
      cleanupRef.current = null
    },
    [],
  )

  const halfW = object.widthCm / 2
  const halfH = object.heightCm / 2
  const handleCm = HANDLE_PX / scale
  const rotateOffsetCm = ROTATE_OFFSET_PX / scale

  // The svg's screen rect doesn't move during a drag, so snapshot it once at
  // gesture start rather than calling getBoundingClientRect (a layout reflow) on
  // every pointermove.
  const makePointerWorld = () => {
    const rect = svgRef.current?.getBoundingClientRect()
    return (e: { clientX: number; clientY: number }): Point => {
      const vp = useEditorStore.getState().viewport
      const local = rect ? { x: e.clientX - rect.left, y: e.clientY - rect.top } : { x: e.clientX, y: e.clientY }
      return screenToWorld(local, vp)
    }
  }

  // Common gesture scaffolding: mark dragging, track the pointer on window, and
  // on release fire one PATCH built from the final liveObject. onMove receives
  // the live pointer event so modifier keys (Shift) reflect their current state.
  const begin = (
    e: ReactPointerEvent,
    onMove: (e: PointerEvent) => EditorObject,
    fields: (final: EditorObject) => Parameters<typeof update.mutate>[0],
  ) => {
    e.stopPropagation()
    e.preventDefault()
    setObjectDragging(true)
    const move = (ev: PointerEvent) => setLiveObject(onMove(ev))
    const detach = () => {
      window.removeEventListener('pointermove', move)
      window.removeEventListener('pointerup', finish)
      window.removeEventListener('pointercancel', finish)
    }
    const finish = () => {
      detach()
      cleanupRef.current = null
      const final = useEditorStore.getState().liveObject
      setObjectDragging(false)
      setLiveObject(null)
      if (final) update.mutate(fields(final))
    }
    // On unmount mid-gesture: detach and reset, but don't fire a PATCH.
    cleanupRef.current = () => {
      detach()
      setObjectDragging(false)
      setLiveObject(null)
    }
    window.addEventListener('pointermove', move)
    window.addEventListener('pointerup', finish)
    window.addEventListener('pointercancel', finish)
  }

  const base = { ...object }
  const center0: Point = { x: base.xCm, y: base.yCm }

  const startMove = (e: ReactPointerEvent) => {
    const pointerWorld = makePointerWorld()
    const start = pointerWorld(e.nativeEvent)
    begin(
      e,
      (ev) => {
        const world = pointerWorld(ev)
        return { ...base, xCm: base.xCm + (world.x - start.x), yCm: base.yCm + (world.y - start.y) }
      },
      (f) => ({ id: base.id, version: base.version, xCm: f.xCm, yCm: f.yCm }),
    )
  }

  const startResize = (e: ReactPointerEvent, sx: number, sy: number) => {
    // The corner opposite the dragged one stays fixed (in the object's local
    // frame, which is anchored at the original center).
    const oppositeLocal: Point = { x: -sx * halfW, y: -sy * halfH }
    const pointerWorld = makePointerWorld()
    begin(
      e,
      (ev) => {
        const world = pointerWorld(ev)
        const p = worldToLocal(world, center0, base.rotationDeg)
        const newW = Math.max(MIN_OBJ_CM, Math.abs(p.x - oppositeLocal.x))
        const newH = Math.max(MIN_OBJ_CM, Math.abs(p.y - oppositeLocal.y))
        const draggedLocal: Point = { x: oppositeLocal.x + sx * newW, y: oppositeLocal.y + sy * newH }
        const newCenterLocal: Point = {
          x: (oppositeLocal.x + draggedLocal.x) / 2,
          y: (oppositeLocal.y + draggedLocal.y) / 2,
        }
        const c = localToWorld(newCenterLocal, center0, base.rotationDeg)
        return { ...base, xCm: c.x, yCm: c.y, widthCm: newW, heightCm: newH }
      },
      (f) => ({ id: base.id, version: base.version, xCm: f.xCm, yCm: f.yCm, widthCm: f.widthCm, heightCm: f.heightCm }),
    )
  }

  const startRotate = (e: ReactPointerEvent) => {
    const pointerWorld = makePointerWorld()
    begin(
      e,
      (ev) => {
        const world = pointerWorld(ev)
        // +90° because the knob points up (local -y) at rotation 0.
        let deg = (Math.atan2(world.y - center0.y, world.x - center0.x) * 180) / Math.PI + 90
        // Read Shift live off the move event so toggling mid-drag switches
        // between snapped and free rotation.
        if (!ev.shiftKey) deg = Math.round(deg / ROTATE_SNAP_DEG) * ROTATE_SNAP_DEG
        deg = ((deg % 360) + 360) % 360
        return { ...base, rotationDeg: deg }
      },
      (f) => ({ id: base.id, version: base.version, rotationDeg: f.rotationDeg }),
    )
  }

  return (
    <g transform={objectTransform(object)}>
      {/* Transparent body: drag to move. */}
      {object.shape === 'circle' ? (
        <ellipse cx={0} cy={0} rx={halfW} ry={halfH} fill="transparent" pointerEvents="all" style={{ cursor: 'move' }} onPointerDown={startMove} />
      ) : (
        <rect x={-halfW} y={-halfH} width={object.widthCm} height={object.heightCm} fill="transparent" pointerEvents="all" style={{ cursor: 'move' }} onPointerDown={startMove} />
      )}

      {/* Selection outline. */}
      {object.shape === 'circle' ? (
        <ellipse cx={0} cy={0} rx={halfW} ry={halfH} fill="none" stroke={SELECT_COLOR} strokeWidth={1.5} vectorEffect="non-scaling-stroke" pointerEvents="none" />
      ) : (
        <rect x={-halfW} y={-halfH} width={object.widthCm} height={object.heightCm} fill="none" stroke={SELECT_COLOR} strokeWidth={1.5} vectorEffect="non-scaling-stroke" pointerEvents="none" />
      )}

      {/* Rotate knob. */}
      <line x1={0} y1={-halfH} x2={0} y2={-halfH - rotateOffsetCm} stroke={SELECT_COLOR} strokeWidth={1} vectorEffect="non-scaling-stroke" pointerEvents="none" />
      <circle cx={0} cy={-halfH - rotateOffsetCm} r={handleCm * 0.7} fill={SELECT_COLOR} style={{ cursor: 'grab' }} onPointerDown={startRotate} />

      {/* Resize corners. */}
      {corners.map(([sx, sy]) => (
        <rect
          key={`${sx},${sy}`}
          x={sx * halfW - handleCm / 2}
          y={sy * halfH - handleCm / 2}
          width={handleCm}
          height={handleCm}
          fill="#ffffff"
          stroke={SELECT_COLOR}
          strokeWidth={1}
          vectorEffect="non-scaling-stroke"
          style={{ cursor: 'nwse-resize' }}
          onPointerDown={(e) => startResize(e, sx, sy)}
        />
      ))}
    </g>
  )
}
