import { useEffect, useRef } from 'react'
import { HARMONIES, harmonyById, harmonyColors, normHue, reHueSlots } from '../hooks/colorMath.js'

/* taxonomy-ok: a pure-canvas hue + harmony picker in the SpectrumControls
 * color-picker family. It nests no KOL component — its only import is the
 * colorMath support module in hooks/ — but belongs beside its siblings
 * SpectrumControls / SwatchControls, not down in atoms. */

/*
 * PaletteHarmonyWheel — a hue ring with a draggable base-hue handle and
 * satellite markers at the active harmony's scheme hues. Dragging (or the
 * arrow keys) rotates the base hue; every change emits the full set of
 * harmony colors so the caller never re-derives them.
 *
 * The scheme table is DATA-INJECTED (`harmonies`, defaulting to the shared
 * HARMONIES); saturation/lightness of the generated colors are injected too,
 * so this stays a pure hue picker — the caller owns S/L.
 *
 * Controlled on `hue`; `onChange({ hue, colors })` fires on press, drag, and
 * arrow keys. `colors` is one hex per role offset of the active harmony
 * (see colorMath.harmonyColors), matching the satellite markers 1:1.
 *
 * PASS `slots` IF YOU ALREADY HAVE A PALETTE (2026-09-03,
 * `editor-set-is-behind-its-source`). Without it, `colors` is built at ONE
 * flat `saturation`/`lightness`, so a Light role and a Dark role both come
 * back at 50% — the palette flattens on the first drag, and kol-fxr's editor
 * had to ignore half the payload because its own wheel re-hues slot by slot.
 * With `slots`, every entry keeps its own S/L and only its hue moves, and a
 * `locked` or empty slot is passed through untouched. `onHueChange(hue)` is
 * the same seam with no payload at all, for a caller that owns the derivation
 * outright — which is what fxr's wheel emits.
 *
 * The ring hues, marker outlines and handle halo are literal color math
 * (hsl / #FFFFFF / rgba) on purpose — a spectrum is not themeable, and the
 * markers sit on fully-saturated ring hues, not on the surface (same
 * rationale as SpectrumControls). No token belongs here.
 *
 * @param {number}        size        px, square (default 248)
 * @param {number}        hue         controlled base hue, 0–360
 * @param {string|object} harmony     active harmony id or object (default 'analogous')
 * @param {number}        saturation  base saturation for emitted colors, 0–100 (default 100)
 * @param {number}        lightness   base lightness for emitted colors, 0–100 (default 50)
 * @param {Array}         harmonies   injectable scheme table (default HARMONIES)
 * @param {Array}         slots       the CURRENT palette in role order (`{hex, locked}` objects or plain hex strings). Given, `colors` re-hues these — each slot keeps its own S/L, locked and empty entries pass through — instead of generating flat ones
 * @param {Function}      onChange    ({ hue, colors }) => void
 * @param {Function}      onHueChange (hue) => void — the payload-free seam, for a caller that derives its own colors
 */

/* Marker outline — white for contrast against the fully-saturated ring hues
 * (theme-independent: the wheel's colors, not the surface, sit behind it). */
const MARKER_STROKE = '#FFFFFF'

export default function PaletteHarmonyWheel({
  size = 248,
  hue,
  harmony = 'analogous',
  saturation = 100,
  lightness = 50,
  slots,
  harmonies = HARMONIES,
  onChange,
  onHueChange,
}) {
  const canvasRef   = useRef(null)
  const draggingRef = useRef(false)
  const emitRef     = useRef(null)

  const active = harmonyById(
    typeof harmony === 'string'
      ? (harmonies.find((h) => h.id === harmony) ?? harmonies[0])
      : harmony,
  )

  /* Emit next hue + its harmony colors. Held in a ref so the pointer/key
   * handlers stay stable while always seeing the latest props.
   *
   * With `slots`, the colors are the CALLER'S palette re-hued — each slot
   * keeping its own saturation and lightness — rather than a fresh flat set.
   * Both fire, so a caller can take the hue and ignore the colors. */
  emitRef.current = (nextHue) => {
    const h = normHue(nextHue)
    const colors = slots?.length
      ? reHueSlots(h, active, slots)
      : harmonyColors(h, active, { saturation, lightness })
    onChange?.({ hue: h, colors })
    onHueChange?.(h)
  }

  const outerR = size / 2 - 8
  const innerR = outerR - 22
  const midR   = (outerR + innerR) / 2

  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) return
    const dpr = window.devicePixelRatio || 1
    canvas.width  = size * dpr
    canvas.height = size * dpr
    const ctx = canvas.getContext('2d')
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
    ctx.clearRect(0, 0, size, size)
    const cx = size / 2
    const cy = size / 2

    /* Angle mapping: hue 0 at 12 o'clock, clockwise. */
    const angleFor = (h) => ((h - 90) * Math.PI) / 180

    /* Hue ring — 360 thin annulus wedges (1.5° overlap hides seams). */
    for (let a = 0; a < 360; a++) {
      const a0 = angleFor(a)
      const a1 = angleFor(a + 1.5)
      ctx.beginPath()
      ctx.arc(cx, cy, outerR, a0, a1)
      ctx.arc(cx, cy, innerR, a1, a0, true)
      ctx.closePath()
      ctx.fillStyle = `hsl(${a}, 100%, 50%)`
      ctx.fill()
    }

    const baseHue = normHue(hue)
    const offsets = [...new Set((active?.roleOffsets ?? [0]).map(normHue))]

    /* Spokes + satellite markers at the scheme hues. */
    for (const off of offsets) {
      const h = normHue(baseHue + off)
      const a = angleFor(h)
      ctx.beginPath()
      ctx.moveTo(cx, cy)
      ctx.lineTo(cx + Math.cos(a) * innerR, cy + Math.sin(a) * innerR)
      ctx.strokeStyle = 'rgba(128, 128, 128, 0.4)'
      ctx.lineWidth = 1
      ctx.stroke()
      if (off === 0) continue /* base hue gets the big handle below */
      const x = cx + Math.cos(a) * midR
      const y = cy + Math.sin(a) * midR
      ctx.beginPath()
      ctx.arc(x, y, 5, 0, Math.PI * 2)
      ctx.fillStyle = `hsl(${h}, 100%, 50%)`
      ctx.fill()
      ctx.strokeStyle = MARKER_STROKE
      ctx.lineWidth = 1.5
      ctx.stroke()
    }

    /* Base-hue handle. */
    const ba = angleFor(baseHue)
    const bx = cx + Math.cos(ba) * midR
    const by = cy + Math.sin(ba) * midR
    ctx.beginPath()
    ctx.arc(bx, by, 9, 0, Math.PI * 2)
    ctx.fillStyle = `hsl(${baseHue}, 100%, 50%)`
    ctx.fill()
    ctx.strokeStyle = MARKER_STROKE
    ctx.lineWidth = 2
    ctx.stroke()
    ctx.beginPath()
    ctx.arc(bx, by, 10.5, 0, Math.PI * 2)
    ctx.strokeStyle = 'rgba(0, 0, 0, 0.35)'
    ctx.lineWidth = 1
    ctx.stroke()
  }, [size, hue, active, outerR, innerR, midR])

  const pointFromEvent = (e) => {
    const rect = canvasRef.current.getBoundingClientRect()
    const dx = e.clientX - rect.left - rect.width / 2
    const dy = e.clientY - rect.top - rect.height / 2
    return { dist: Math.hypot(dx, dy), hue: normHue((Math.atan2(dy, dx) * 180) / Math.PI + 90) }
  }

  const onPointerDown = (e) => {
    const { dist, hue: h } = pointFromEvent(e)
    if (dist < innerR - 14 || dist > outerR + 10) return
    draggingRef.current = true
    e.currentTarget.setPointerCapture(e.pointerId)
    emitRef.current?.(h)
  }

  const onPointerMove = (e) => {
    if (!draggingRef.current) return
    emitRef.current?.(pointFromEvent(e).hue)
  }

  const endDrag = () => { draggingRef.current = false }

  const onKeyDown = (e) => {
    const step = e.shiftKey ? 15 : 3
    if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
      e.preventDefault()
      emitRef.current?.(normHue(hue + step))
    } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
      e.preventDefault()
      emitRef.current?.(normHue(hue - step))
    }
  }

  return (
    <canvas
      ref={canvasRef}
      role="slider"
      aria-label="Base hue"
      aria-valuemin={0}
      aria-valuemax={359}
      aria-valuenow={Math.round(normHue(hue))}
      tabIndex={0}
      className="cursor-pointer touch-none"
      style={{ width: size, height: size }}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={endDrag}
      onPointerCancel={endDrag}
      onKeyDown={onKeyDown}
    />
  )
}
