import { useEffect, useRef, useState } from 'react';
import cx from '@src/cx.mjs';
import { useSettings } from '@src/settings.mjs';
import { __pianoroll } from '@strudel/draw';
import {
  getAudioContext,
  getAnalyserById,
  getAnalyzerData,
  getSuperdoughAudioController,
} from '@strudel/webaudio';

// Visor: dibuja EN VIVO lo que suena, sin escribir nada en el código.
//  - PENTAGRAMA (piano-roll): notas de lo que se está tocando ahora.
//  - ONDA: osciloscopio estable (disparo por cruce por cero + auto-ganancia).
//  - ESPECTRO: bandas logarítmicas (graves→agudos) con peak-hold.
// Para que funcione con CUALQUIER código (sin .scope()) pinchamos dos
// AnalyserNode al bus maestro de superdough (destinationGain).

const CKEY = 'strudel-visor-cycles'; // zoom del pentagrama (compases visibles)
const ID_T = 'visorTime'; // analizador de forma de onda
const ID_F = 'visorFreq'; // analizador de espectro
const FFT = 2048;

const CYAN = '#00e5ff';
const ORANGE = '#ff7a18';

// ── osciloscopio estable ────────────────────────────────────────────
function drawOnda(ctx, data, gainRef) {
  const w = ctx.canvas.width;
  const h = ctx.canvas.height;
  const n = data.length;
  const mid = h / 2;

  // línea central tenue
  ctx.strokeStyle = 'rgba(255,255,255,0.12)';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(0, mid);
  ctx.lineTo(w, mid);
  ctx.stroke();

  // disparo: primer cruce por cero ascendente (estabiliza la imagen)
  let start = 0;
  const search = Math.floor(n / 2);
  for (let i = 1; i < search; i++) {
    if (data[i - 1] <= 0 && data[i] > 0) {
      start = i;
      break;
    }
  }

  // auto-ganancia suave: normaliza al pico reciente para que se vea igual de
  // bien con el tema flojo o fuerte (con límite para no ampliar el silencio).
  let peak = 0;
  for (let i = 0; i < n; i++) {
    const a = Math.abs(data[i]);
    if (a > peak) peak = a;
  }
  const amp = mid * 0.92; // altura útil (deja margen arriba/abajo)
  const norm = 1 / Math.max(peak, 0.02); // multiplicador para que el pico llene amp
  gainRef.g += (norm - gainRef.g) * 0.15; // suavizado entre frames
  const k = amp * Math.min(gainRef.g, 6); // tope de amplificación x6

  const len = n - start;
  ctx.lineWidth = 2;
  ctx.strokeStyle = CYAN;
  ctx.shadowBlur = 8;
  ctx.shadowColor = CYAN;
  ctx.beginPath();
  for (let i = 0; i < len; i++) {
    const x = (i / (len - 1)) * w;
    let s = data[start + i] * k;
    if (s > amp) s = amp;
    else if (s < -amp) s = -amp;
    const y = mid - s;
    if (i === 0) ctx.moveTo(x, y);
    else ctx.lineTo(x, y);
  }
  ctx.stroke();
  ctx.shadowBlur = 0;
}

// ── espectro con bandas logarítmicas + peak-hold ────────────────────
function drawEspectro(ctx, data, sampleRate, peaks) {
  const w = ctx.canvas.width;
  const h = ctx.canvas.height;
  const bins = data.length; // dB, lineal de 0..nyquist
  const nyquist = sampleRate / 2;
  const BANDS = peaks.length;
  const fmin = 30;
  const fmax = Math.min(16000, nyquist);
  const minDb = -90;
  const maxDb = -10;
  const bw = w / BANDS;

  for (let b = 0; b < BANDS; b++) {
    const f0 = fmin * Math.pow(fmax / fmin, b / BANDS);
    const f1 = fmin * Math.pow(fmax / fmin, (b + 1) / BANDS);
    let i0 = Math.floor((f0 / nyquist) * bins);
    let i1 = Math.floor((f1 / nyquist) * bins);
    if (i1 <= i0) i1 = i0 + 1;
    let m = -Infinity;
    for (let i = i0; i < i1 && i < bins; i++) if (data[i] > m) m = data[i];
    if (m === -Infinity) m = minDb;
    let v = (m - minDb) / (maxDb - minDb);
    v = v < 0 ? 0 : v > 1 ? 1 : v;

    const x = b * bw;
    const bh = v * h;
    // color por frecuencia: graves naranjas → agudos cian (tema neón)
    const hue = 25 + (b / BANDS) * 165;
    ctx.fillStyle = `hsl(${hue}, 90%, 55%)`;
    ctx.fillRect(x, h - bh, Math.max(1, bw * 0.8), bh);

    // peak-hold: cae despacio
    peaks[b] = Math.max(v, peaks[b] - 0.015);
    const py = h - peaks[b] * h;
    ctx.fillStyle = 'rgba(255,255,255,0.85)';
    ctx.fillRect(x, py - 1.5, Math.max(1, bw * 0.8), 1.5);
  }
}

export function VisorTab({ context }) {
  const { fontFamily } = useSettings();
  const rollRef = useRef(null);
  const scopeRef = useRef(null);
  const specRef = useRef(null);

  const [cycles, setCycles] = useState(() => {
    try {
      return Number(localStorage.getItem(CKEY)) || 4;
    } catch {
      return 4;
    }
  });
  const [show, setShow] = useState({ roll: true, scope: true, spec: true });
  const [playing, setPlaying] = useState(false);

  const cyclesRef = useRef(cycles);
  const showRef = useRef(show);
  cyclesRef.current = cycles;
  showRef.current = show;

  useEffect(() => {
    try {
      localStorage.setItem(CKEY, String(cycles));
    } catch {
      /* noop */
    }
  }, [cycles]);

  useEffect(() => {
    const roll = rollRef.current;
    const scope = scopeRef.current;
    const spec = specRef.current;
    const rctx = roll?.getContext('2d');
    const sctx = scope?.getContext('2d');
    const fctx = spec?.getContext('2d');
    let raf;
    let lastT = null;
    let lastF = null;
    const gainRef = { g: 1 };
    const peaks = new Float32Array(64); // bandas del espectro

    // el piano-roll usa tamaños de texto en píxeles → dpr 1; onda/espectro
    // los dibujo yo, así que ahí sí uso dpr para que se vean nítidos.
    const dpr = Math.min(2, window.devicePixelRatio || 1);
    const fit = (cv, scale) => {
      if (!cv) return;
      const w = Math.round(cv.clientWidth * scale);
      const h = Math.round(cv.clientHeight * scale);
      if (w && h && (cv.width !== w || cv.height !== h)) {
        cv.width = w;
        cv.height = h;
      }
    };

    const ensureAnalysers = () => {
      try {
        getAudioContext();
        const nt = getAnalyserById(ID_T, FFT, 0);
        if (nt !== lastT) {
          getSuperdoughAudioController().output.destinationGain.connect(nt);
          lastT = nt;
        }
        const nf = getAnalyserById(ID_F, FFT, 0.7);
        if (nf !== lastF) {
          getSuperdoughAudioController().output.destinationGain.connect(nf);
          lastF = nf;
        }
      } catch {
        /* audio aún no listo */
      }
    };

    const frame = () => {
      const scheduler = context.editorRef?.current?.repl?.scheduler;
      const started = !!scheduler?.started;
      setPlaying((p) => (p !== started ? started : p));

      const S = showRef.current;
      fit(roll, 1);
      fit(scope, dpr);
      fit(spec, dpr);
      rctx && rctx.clearRect(0, 0, roll.width, roll.height);
      sctx && sctx.clearRect(0, 0, scope.width, scope.height);
      fctx && fctx.clearRect(0, 0, spec.width, spec.height);

      if (started && scheduler.pattern) {
        // PENTAGRAMA
        if (S.roll && rctx) {
          const cyc = cyclesRef.current;
          const playhead = 0.5;
          const from = -cyc * playhead;
          const to = cyc * (1 - playhead);
          let haps = [];
          try {
            haps = scheduler.pattern.queryArc(scheduler.now() + from, scheduler.now() + to).filter((h) => h.hasOnset());
          } catch {
            haps = [];
          }
          try {
            __pianoroll({
              ctx: rctx,
              haps,
              time: scheduler.now(),
              cycles: cyc,
              playhead,
              fold: 1,
              labels: 1,
              background: 'transparent',
              playheadColor: ORANGE,
            });
          } catch {
            /* noop */
          }
        }

        // ONDA + ESPECTRO
        if (S.scope || S.spec) {
          ensureAnalysers();
          if (S.scope && sctx) {
            try {
              drawOnda(sctx, getAnalyzerData('time', ID_T), gainRef);
            } catch {
              /* noop */
            }
          }
          if (S.spec && fctx) {
            try {
              const sr = getAudioContext().sampleRate || 44100;
              drawEspectro(fctx, getAnalyzerData('frequency', ID_F), sr, peaks);
            } catch {
              /* noop */
            }
          }
        }
      }
      raf = requestAnimationFrame(frame);
    };
    raf = requestAnimationFrame(frame);
    return () => cancelAnimationFrame(raf);
  }, [context]);

  const toggle = (k) => setShow((s) => ({ ...s, [k]: !s[k] }));
  const chip = (on) =>
    cx(
      'px-2 py-0.5 rounded text-xs font-bold border cursor-pointer transition-colors select-none',
      on ? 'border-cyan-400 text-cyan-300 bg-cyan-400/10' : 'border-muted opacity-50 hover:opacity-80',
    );

  return (
    <div className="w-full h-full text-foreground flex flex-col overflow-hidden" style={{ fontFamily }}>
      <div className="flex items-center gap-3 px-2 py-2 border-b border-muted shrink-0 flex-wrap">
        <span className="text-xs opacity-70">zoom</span>
        <input
          type="range"
          min="1"
          max="16"
          step="1"
          value={cycles}
          onChange={(e) => setCycles(Number(e.target.value))}
          className="accent-cyan-400"
          title="compases visibles en el pentagrama"
        />
        <span className="text-xs tabular-nums w-14">{cycles} comp.</span>
        <span className="ml-auto flex gap-1.5">
          <span className={chip(show.roll)} onClick={() => toggle('roll')}>
            pentagrama
          </span>
          <span className={chip(show.scope)} onClick={() => toggle('scope')}>
            onda
          </span>
          <span className={chip(show.spec)} onClick={() => toggle('spec')}>
            espectro
          </span>
        </span>
      </div>

      <div className="relative grow min-h-0 flex flex-col">
        {!playing && (
          <div className="absolute inset-0 flex items-center justify-center pointer-events-none z-10">
            <span className="text-sm opacity-60">dale al play para ver el pentagrama y las ondas</span>
          </div>
        )}
        {show.roll && (
          <div className="relative grow min-h-0 border-b border-muted">
            <span className="absolute top-1 left-2 text-[10px] uppercase tracking-widest opacity-40 z-10">
              pentagrama
            </span>
            <canvas ref={rollRef} className="w-full h-full block" />
          </div>
        )}
        {show.scope && (
          <div className="relative shrink-0 border-b border-muted" style={{ height: 120 }}>
            <span className="absolute top-1 left-2 text-[10px] uppercase tracking-widest opacity-40 z-10">onda</span>
            <canvas ref={scopeRef} className="w-full h-full block" />
          </div>
        )}
        {show.spec && (
          <div className="relative shrink-0" style={{ height: 120 }}>
            <span className="absolute top-1 left-2 text-[10px] uppercase tracking-widest opacity-40 z-10">
              espectro
            </span>
            <canvas ref={specRef} className="w-full h-full block" />
          </div>
        )}
      </div>
    </div>
  );
}
