/* ============================================================
   ClaraHA controls — sliders, number entry, knob, toggle, segmented
   buttons and a per-band bar-graph editor, driven by the schema in
   claraha-params.js. Exposed as window.HaControls.
   ============================================================ */
(function () {
  const { useState, useEffect, useRef } = React;

  /* ---- Value formatting ---- */
  function fmtNum(x, maxDec = 3) {
    if (!Number.isFinite(x)) return x > 0 ? "∞" : "−∞";
    if (x !== 0 && Math.abs(x) < 0.01) return x.toPrecision(3).replace(/\.?0+e/, "e");
    return String(+x.toFixed(maxDec));
  }
  const toDisplay = (p, v) => v * (p.display || 1);
  const fromDisplay = (p, d) => d / (p.display || 1);
  function fmtParam(p, v) {
    const d = toDisplay(p, v);
    const u = p.units === ":1" ? ":1" : p.units ? " " + p.units : "";
    return fmtNum(d, p.units === "Hz" || p.units === "ms" ? 1 : 3) + u;
  }

  /* ---- Slider position <-> value mapping (positions 0..1) ---- */
  const POW2_STEPS = [0, ...Array.from({ length: 11 }, (_, i) => 2 ** (i - 12))]; // 0, 2^-12 .. 2^-2
  function makeScale(p) {
    const [lo, hi] = p.ui || p.bounds;
    const clampV = v => Math.min(hi, Math.max(lo, v));
    if (p.scale === "log") {
      const a = Math.log(lo), b = Math.log(hi);
      return { lo, hi, toPos: v => (Math.log(clampV(Math.max(v, lo))) - a) / (b - a), fromPos: t => Math.exp(a + t * (b - a)) };
    }
    if (p.scale === "oneMinusLog") {
      // 1 - a spans 2^0 .. 2^-14, so the slider moves through time constants evenly
      return { lo: 0, hi: 1 - 2 ** -14,
        toPos: v => Math.min(1, Math.max(0, -Math.log2(Math.max(1 - v, 2 ** -14)) / 14)),
        fromPos: t => 1 - 2 ** (-14 * t) };
    }
    if (p.scale === "pow2") {
      return { lo: 0, hi: POW2_STEPS[POW2_STEPS.length - 1],
        toPos: v => { const i = nearestPow2Index(v); return i / (POW2_STEPS.length - 1); },
        fromPos: t => POW2_STEPS[Math.round(t * (POW2_STEPS.length - 1))] };
    }
    return { lo, hi, toPos: v => (clampV(v) - lo) / (hi - lo), fromPos: t => lo + t * (hi - lo) };
  }
  function nearestPow2Index(v) {
    let best = 0;
    POW2_STEPS.forEach((s, i) => { if (Math.abs(s - v) < Math.abs(POW2_STEPS[best] - v)) best = i; });
    return best;
  }
  function snap(p, v) {
    if (p.scale === "pow2") return POW2_STEPS[nearestPow2Index(v)];
    if (p.scale === "log" || p.scale === "oneMinusLog") return +v.toPrecision(4);
    if (p.step) return +(Math.round(v / p.step) * p.step).toFixed(6);
    return v;
  }

  /* ---- NumberField: commits on Enter / blur, shows display units ---- */
  function NumberField({ param, value, onChange, disabled, width, warn }) {
    const [draft, setDraft] = useState(null);
    const d = toDisplay(param, value);
    const shown = draft !== null ? draft : Number.isFinite(d) ? String(+d.toPrecision(8)) : fmtNum(d);
    const commit = () => {
      if (draft === null) return;
      const d = parseFloat(draft);
      setDraft(null);
      if (Number.isFinite(d)) onChange(fromDisplay(param, d));
    };
    return (
      <span className="ha-num">
        <input className={"ha-num-input" + (warn ? " ha-num-input--warn" : "")} type="text" inputMode="decimal" value={shown} disabled={disabled}
          style={width ? { width } : undefined} title={warn || undefined}
          aria-label={param.label}
          onChange={e => setDraft(e.target.value)} onBlur={commit}
          onKeyDown={e => { if (e.key === "Enter") e.currentTarget.blur(); if (e.key === "Escape") setDraft(null); }} />
        {param.units && <span className="ha-num-unit">{param.units}</span>}
      </span>
    );
  }

  /* ---- ParamSlider: range input + number entry + recommended-range band ---- */
  function ParamSlider({ param, value, onChange, disabled, label, readout, hint, testId }) {
    const sc = makeScale(param);
    const pos = sc.toPos(value);
    const rec = param.rec && !param.perBand ? [sc.toPos(param.rec[0]), sc.toPos(param.rec[1])] : null;
    const outOfRec = param.rec && (value < param.rec[0] - 1e-12 || value > param.rec[1] + 1e-12);
    return (
      <div className={"ha-param" + (disabled ? " ha-param--disabled" : "")} data-testid={testId || "param-" + param.key}>
        <div className="ha-param-head">
          <span className="ha-param-label" title={param.help}>{label || param.label}</span>
          {readout && <span className="ha-param-readout">{readout}</span>}
          <NumberField param={param} value={value} disabled={disabled} onChange={v => onChange(clampBounds(param, v))}
            warn={outOfRec ? `Outside the recommended range ${fmtNum(param.rec[0])}–${fmtNum(param.rec[1])}` : null} />
        </div>
        <div className="ha-slider-wrap">
          <div className="ha-slider-track">
            {rec && <span className={"ha-slider-rec" + (outOfRec ? " ha-slider-rec--off" : "")}
              style={{ left: rec[0] * 100 + "%", width: Math.max(0.6, (rec[1] - rec[0]) * 100) + "%" }}
              title={`Recommended ${fmtNum(param.rec[0])}–${fmtNum(param.rec[1])}`} />}
          </div>
          <input type="range" className="ha-slider" min={0} max={1000} step={1} value={Math.round(pos * 1000)}
            disabled={disabled} aria-label={label || param.label}
            onChange={e => onChange(clampBounds(param, snap(param, sc.fromPos(+e.target.value / 1000))))} />
        </div>
        {param.ends && (
          <div className="ha-slider-ends"><span>{param.ends[0]}</span><span>{param.ends[1]}</span></div>
        )}
        {hint && <div className="ha-param-hint">{hint}</div>}
      </div>
    );
  }
  function clampBounds(p, v) { return Math.min(p.bounds[1], Math.max(p.bounds[0], v)); }

  /* ---- Knob: drag vertically, scroll, or use arrow keys ---- */
  function Knob({ value, min, max, step = 0.5, onChange, label, format, size = 120, disabled, testId }) {
    const dragRef = useRef(null);
    const svgRef = useRef(null);
    const t = (Math.min(max, Math.max(min, value)) - min) / (max - min);
    const A0 = -135, A1 = 135, ang = A0 + t * (A1 - A0);
    const r = size / 2 - 10, cx = size / 2, cy = size / 2;
    const pt = a => [cx + r * Math.sin(a * Math.PI / 180), cy - r * Math.cos(a * Math.PI / 180)];
    const arc = (a, b) => {
      const [x0, y0] = pt(a), [x1, y1] = pt(b);
      return `M ${x0} ${y0} A ${r} ${r} 0 ${b - a > 180 ? 1 : 0} 1 ${x1} ${y1}`;
    };
    const zeroT = min < 0 && max > 0 ? -min / (max - min) : 0;
    const zeroAng = A0 + zeroT * (A1 - A0);
    const set = v => onChange(Math.min(max, Math.max(min, Math.round(v / step) * step)));

    useEffect(() => {
      // Wheel must be non-passive to stop the tab from scrolling while turning.
      const el = svgRef.current;
      if (!el) return;
      const onWheel = e => { if (disabled) return; e.preventDefault(); set(value + (e.deltaY < 0 ? step : -step) * 2); };
      el.addEventListener("wheel", onWheel, { passive: false });
      return () => el.removeEventListener("wheel", onWheel);
    });

    const onPointerDown = e => {
      if (disabled) return;
      // Stop the browser starting a text-selection drag; that also skips the
      // default focus, so focus explicitly to keep keyboard control working.
      e.preventDefault();
      e.currentTarget.focus();
      e.currentTarget.setPointerCapture(e.pointerId);
      dragRef.current = { y: e.clientY, v: value };
    };
    const onPointerMove = e => {
      if (!dragRef.current) return;
      const dy = dragRef.current.y - e.clientY;
      set(dragRef.current.v + dy * (max - min) / 200);
    };
    const onPointerUp = () => { dragRef.current = null; };
    const onKeyDown = e => {
      if (disabled) return;
      const k = { ArrowUp: step, ArrowRight: step, ArrowDown: -step, ArrowLeft: -step, PageUp: step * 10, PageDown: -step * 10 }[e.key];
      if (k) { e.preventDefault(); set(value + k); }
      if (e.key === "Home") set(min);
      if (e.key === "End") set(max);
    };
    const [hx, hy] = pt(ang);
    return (
      <div className={"ha-knob" + (disabled ? " ha-knob--disabled" : "")} data-testid={testId}>
        <svg ref={svgRef} width={size} height={size} viewBox={`0 0 ${size} ${size}`}
          role="slider" tabIndex={disabled ? -1 : 0} aria-label={label}
          aria-valuemin={min} aria-valuemax={max} aria-valuenow={value} aria-valuetext={format ? format(value) : String(value)}
          onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerUp} onPointerCancel={onPointerUp}
          onKeyDown={onKeyDown} onDoubleClick={() => !disabled && set(min < 0 && max > 0 ? 0 : min)}>
          <path d={arc(A0, A1)} className="ha-knob-track" />
          {Math.abs(ang - zeroAng) > 0.5 && <path d={ang > zeroAng ? arc(zeroAng, ang) : arc(ang, zeroAng)} className="ha-knob-fill" />}
          <circle cx={cx} cy={cy} r={r - 12} className="ha-knob-face" />
          <line x1={cx + (r - 30) * Math.sin(ang * Math.PI / 180)} y1={cy - (r - 30) * Math.cos(ang * Math.PI / 180)}
            x2={cx + (r - 14) * Math.sin(ang * Math.PI / 180)} y2={cy - (r - 14) * Math.cos(ang * Math.PI / 180)} className="ha-knob-pointer" />
          <circle cx={hx} cy={hy} r={4} className="ha-knob-dot" />
          <text x={cx} y={cy + 5} textAnchor="middle" className="ha-knob-value">{format ? format(value) : value}</text>
        </svg>
        <div className="ha-knob-label">{label}</div>
      </div>
    );
  }

  /* ---- Toggle switch (light surface) ---- */
  function HaToggle({ checked, onChange, label, sub, disabled, testId }) {
    return (
      <label className={"ha-toggle" + (disabled ? " ha-toggle--disabled" : "")} data-testid={testId}>
        <input type="checkbox" checked={checked} disabled={disabled} onChange={e => onChange(e.target.checked)} />
        <span className="ha-sw" />
        <span className="ha-toggle-text">
          <span className="ha-toggle-label">{label}</span>
          {sub && <span className="ha-toggle-sub">{sub}</span>}
        </span>
      </label>
    );
  }

  /* ---- Segmented buttons ---- */
  function Segmented({ options, value, onChange, disabled, small, testId, ariaLabel }) {
    return (
      <div className={"ha-seg" + (small ? " ha-seg--sm" : "")} role="radiogroup" aria-label={ariaLabel} data-testid={testId}>
        {options.map(o => (
          <button key={o.id} type="button" role="radio" aria-checked={value === o.id}
            className={"ha-seg-btn" + (value === o.id ? " ha-seg-btn--active" : "") + (o.ghost ? " ha-seg-btn--ghost" : "")}
            disabled={disabled || o.disabled} title={o.title} onClick={() => onChange(o.id)}>
            {o.label}
          </button>
        ))}
      </div>
    );
  }

  /* ---- BandGraphEditor: graphic-EQ style per-band values ----
     Click selects a band; dragging sets values and paints across bands.
     Arrow keys: left/right move the selection, up/down nudge its value. */
  function BandGraphEditor({ param, values, onChange, labels, selected: selProp, onSelect: onSelProp, errorBands = [], warnBands = [], height = 96, disabled, testId }) {
    const svgRef = useRef(null);
    const dragRef = useRef(null);
    // Selection is controlled when the parent passes onSelect, local otherwise.
    const [selLocal, setSelLocal] = useState(0);
    const selected = onSelProp ? selProp : selLocal;
    const onSelect = onSelProp || setSelLocal;
    const sc = makeScale(param);
    const n = values.length;
    const W = 600, H = height, padT = 6, padB = 6;
    const plotH = H - padT - padB;
    const bw = W / n;
    const yOf = v => padT + (1 - sc.toPos(v)) * plotH;
    const baseV = sc.lo < 0 && sc.hi > 0 ? 0 : sc.lo;
    const y0 = yOf(baseV);

    const eventToBand = e => {
      const rect = svgRef.current.getBoundingClientRect();
      const x = (e.clientX - rect.left) / rect.width * W;
      const y = (e.clientY - rect.top) / rect.height * H;
      const b = Math.max(0, Math.min(n - 1, Math.floor(x / bw)));
      const t = Math.max(0, Math.min(1, 1 - (y - padT) / plotH));
      return { b, v: clampBounds(param, snap(param, sc.fromPos(t))) };
    };
    const writeBands = (from, to, v) => {
      const next = values.slice();
      for (let b = Math.min(from, to); b <= Math.max(from, to); b++) next[b] = v;
      onChange(next);
    };
    const onPointerDown = e => {
      if (disabled) return;
      e.preventDefault(); // no text-selection drag; focus by hand instead
      svgRef.current.focus();
      svgRef.current.setPointerCapture(e.pointerId);
      const { b, v } = eventToBand(e);
      dragRef.current = { last: b, moved: false, startB: b };
      onSelect && onSelect(b);
      if (e.shiftKey || e.altKey) { writeBands(b, b, v); dragRef.current.moved = true; }
    };
    const onPointerMove = e => {
      if (!dragRef.current) return;
      const { b, v } = eventToBand(e);
      dragRef.current.moved = true;
      writeBands(dragRef.current.last, b, v);
      dragRef.current.last = b;
      onSelect && onSelect(b);
    };
    const onPointerUp = () => { dragRef.current = null; };
    const onKeyDown = e => {
      if (disabled || selected == null) return;
      if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
        e.preventDefault();
        onSelect && onSelect(Math.max(0, Math.min(n - 1, selected + (e.key === "ArrowRight" ? 1 : -1))));
      } else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
        e.preventDefault();
        const t = sc.toPos(values[selected]) + (e.key === "ArrowUp" ? 1 : -1) * (param.scale === "pow2" ? 1 / (POW2_STEPS.length - 1) : 0.01);
        writeBands(selected, selected, clampBounds(param, snap(param, sc.fromPos(Math.max(0, Math.min(1, t))))));
      }
    };
    const errSet = new Set(errorBands), warnSet = new Set(warnBands);
    const labelEvery = n > 12 ? 4 : 1;
    return (
      <div className={"ha-bands" + (disabled ? " ha-bands--disabled" : "")} data-testid={testId}>
        <svg ref={svgRef} className="ha-bands-svg" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none"
          tabIndex={disabled ? -1 : 0} role="group" aria-label={`${param.label} per band`}
          onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerUp} onPointerCancel={onPointerUp}
          onKeyDown={onKeyDown}>
          <line x1={0} x2={W} y1={y0} y2={y0} className="ha-bands-base" />
          {values.map((v, b) => {
            const y = yOf(v);
            const cls = "ha-bands-bar" + (b === selected ? " ha-bands-bar--sel" : "") +
              (errSet.has(b) ? " ha-bands-bar--err" : warnSet.has(b) ? " ha-bands-bar--warn" : "");
            const isZeroPow2 = param.scale === "pow2" && v === 0;
            return (
              <g key={b}>
                <rect x={b * bw + 1.5} y={padT} width={bw - 3} height={plotH} className="ha-bands-hit" />
                {isZeroPow2
                  ? <line x1={b * bw + 4} x2={(b + 1) * bw - 4} y1={y0 - 1} y2={y0 - 1} className={cls + " ha-bands-off"} />
                  : <rect x={b * bw + 2} y={Math.min(y, y0)} width={bw - 4} height={Math.max(1.5, Math.abs(y0 - y))} rx={2} className={cls} />}
              </g>
            );
          })}
        </svg>
        {/* Labels live in HTML: the SVG stretches non-uniformly, which would distort text */}
        {labels && (
          <div className="ha-bands-lbls" style={{ gridTemplateColumns: `repeat(${n}, 1fr)` }}>
            {labels.map((l, b) => (
              <span key={b} className={"ha-bands-lbl" + (b === selected ? " ha-bands-lbl--sel" : "")}>
                {b % labelEvery === 0 || b === selected ? l : ""}
              </span>
            ))}
          </div>
        )}
        {selected != null && (
          <div className="ha-bands-sel">
            <span>Band {selected + 1}{labels ? ` · ${labels[selected]}` : ""}</span>
            <NumberField param={param} value={values[selected]} disabled={disabled}
              onChange={v => writeBands(selected, selected, clampBounds(param, v))} />
          </div>
        )}
      </div>
    );
  }

  window.HaControls = { fmtNum, fmtParam, toDisplay, makeScale, snap, POW2_STEPS, NumberField, ParamSlider, Knob, HaToggle, Segmented, BandGraphEditor };
})();
