/* ============================================================================
   cards.jsx — icons, tiny synth audio engine, and a card renderer per type.
   Exposes: Icon, Card  (+ helpers) on window.
   ========================================================================== */
const { useState, useRef, useEffect, useCallback } = React;

/* ---- icons (simple stroke set) ----------------------------------------- */
const PATHS = {
  search:  "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-4-4",
  shuffle: "M3 6h3l9 12h3M3 18h3l3-4M17 6h3M14 10l3-4M17 18l3 2-2-2 2-2",
  focus:   "M4 8V5a1 1 0 0 1 1-1h3M20 8V5a1 1 0 0 0-1-1h-3M4 16v3a1 1 0 0 0 1 1h3M20 16v3a1 1 0 0 1-1 1h-3",
  grid:    "M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z",
  columns: "M4 4h4v16H4zM10 4h4v16h-4zM16 4h4v16h-4z",
  scatter: "M6 5h7v6H6zM14 9h5v5h-5zM4 14h6v5H4z",
  zin:     "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-4-4M8 11h6M11 8v6",
  zout:    "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-4-4M8 11h6",
  sun:     "M12 4V2M12 22v-2M5 5 3.5 3.5M20.5 20.5 19 19M4 12H2M22 12h-2M5 19l-1.5 1.5M20.5 3.5 19 5M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z",
  moon:    "M20 14.5A8 8 0 0 1 9.5 4 8 8 0 1 0 20 14.5z",
  sort:    "M7 4v16M7 20l-3-3M7 4l3 3M14 7h6M14 12h4M14 17h2",
  x:       "M6 6l12 12M18 6 6 18",
  play:    "M8 5v14l11-7z",
  pause:   "M8 5h3v14H8zM14 5h3v14h-3z",
  arrow:   "M7 17 17 7M9 7h8v8",
  chevL:   "M15 5l-7 7 7 7",
  chevR:   "M9 5l7 7-7 7",
  quote:   "M9 7c-2 0-3 1.6-3 3.5S7.3 14 9 14M18 7c-2 0-3 1.6-3 3.5S16.3 14 18 14",
  note:    "M5 4h11l3 3v13H5zM9 9h6M9 13h6M9 17h3",
  link:    "M10 14a4 4 0 0 0 6 0l2-2a4 4 0 0 0-6-6l-1 1M14 10a4 4 0 0 0-6 0l-2 2a4 4 0 0 0 6 6l1-1",
  photo:   "M4 5h16v14H4zM4 15l4-4 5 5M14 13l2-2 4 4M16 9h.01",
  video:   "M4 6h12v12H4zM16 10l4-2v8l-4-2",
  mic:     "M12 4a3 3 0 0 0-3 3v4a3 3 0 0 0 6 0V7a3 3 0 0 0-3-3zM6 11a6 6 0 0 0 12 0M12 17v3",
  audio:   "M4 10v4M8 6v12M12 9v6M16 4v16M20 10v4",
};
function Icon({ name, size = 18, stroke = 1.7, fill = false, style }) {
  const p = PATHS[name] || "";
  return (
    <svg width={size} height={size} viewBox="0 0 24 24"
         fill={fill ? "currentColor" : "none"}
         stroke={fill ? "none" : "currentColor"}
         strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round"
         style={style} aria-hidden="true">
      <path d={p} />
    </svg>
  );
}

const TYPE_META = {
  audio:   { glyph: "audio",  label: "audio" },
  quote:   { glyph: "quote",  label: "quote" },
  photo:   { glyph: "photo",  label: "photograph" },
  video:   { glyph: "video",  label: "video" },
  podcast: { glyph: "mic",    label: "podcast" },
  note:    { glyph: "note",   label: "note" },
  link:    { glyph: "link",   label: "link" },
};

/* ---- tiny WebAudio synth so music cards actually play a motif ---------- */
const AudioEngine = (() => {
  let ctx = null;
  let active = null; // {id, stop}
  const listeners = new Set();
  const get = () => (ctx ||= new (window.AudioContext || window.webkitAudioContext)());
  const midi = (m) => 440 * Math.pow(2, (m - 69) / 12);

  function stop() {
    if (active) { active.stop(); active = null; }
    listeners.forEach((fn) => fn(null));
  }
  function play(id, motif, tempo, onProgress, onDone) {
    stop();
    const ac = get();
    if (ac.state === "suspended") ac.resume();
    const beat = 60 / tempo;
    const t0 = ac.currentTime + 0.05;
    let t = t0;
    const master = ac.createGain();
    master.gain.value = 0.18;
    master.connect(ac.destination);
    const nodes = [];
    motif.forEach(([n, d]) => {
      const dur = d * beat;
      const osc = ac.createOscillator();
      const g = ac.createGain();
      osc.type = "triangle";
      osc.frequency.value = midi(n);
      // soft attack/release
      g.gain.setValueAtTime(0.0001, t);
      g.gain.exponentialRampToValueAtTime(0.9, t + 0.04);
      g.gain.setValueAtTime(0.9, t + Math.max(0.05, dur - 0.12));
      g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
      osc.connect(g); g.connect(master);
      osc.start(t); osc.stop(t + dur + 0.02);
      nodes.push(osc);
      t += dur;
    });
    const total = t - t0;
    let raf;
    const tick = () => {
      const p = (ac.currentTime - t0) / total;
      if (p >= 1) { onProgress(1); stop(); onDone && onDone(); return; }
      onProgress(Math.max(0, p));
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    active = {
      id,
      stop: () => {
        cancelAnimationFrame(raf);
        nodes.forEach((o) => { try { o.stop(); } catch (e) {} });
        try { master.disconnect(); } catch (e) {}
        onProgress(0);
      },
    };
    listeners.forEach((fn) => fn(id));
  }
  function subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); }
  return { play, stop, subscribe, activeId: () => active && active.id };
})();

/* deterministic pseudo-random from string seed */
function seedRand(seed) {
  let h = 2166136261;
  for (let i = 0; i < seed.length; i++) { h ^= seed.charCodeAt(i); h = Math.imul(h, 16777619); }
  return () => { h += 0x6D2B79F5; let x = Math.imul(h ^ (h >>> 15), 1 | h); x ^= x + Math.imul(x ^ (x >>> 7), 61 | x); return ((x ^ (x >>> 14)) >>> 0) / 4294967296; };
}

/* ---- shared bits ------------------------------------------------------- */
function CardHead({ item }) {
  const m = TYPE_META[item.type];
  return (
    <div className="card-head">
      <span className="type-tag">
        <span className="glyph"><Icon name={m.glyph} size={15} stroke={1.6} /></span>
        {m.label}
      </span>
      <span className="date">{item.date}</span>
    </div>
  );
}
function SourceLink({ src }) {
  if (!src) return null;
  return (
    <a className="source" href={src.url} target="_blank" rel="noreferrer"
       onClick={(e) => e.stopPropagation()}>
      <Icon name="arrow" size={12} /> {src.label}
    </a>
  );
}
function Foot({ item, onTag }) {
  return (
    <div className="card-foot">
      {item.tags.map((t) => (
        <button key={t} className="tag" onClick={(e) => { e.stopPropagation(); onTag && onTag(t); }}>{t}</button>
      ))}
      <SourceLink src={item.source} />
    </div>
  );
}
function Note({ children }) {
  if (!children) return null;
  return <p className="note-text" style={{ WebkitLineClamp: 5 }}>{children}</p>;
}

/* ---- audio ------------------------------------------------------------- */
function AudioCard({ item, onTag }) {
  const [playing, setPlaying] = useState(false);
  const [prog, setProg] = useState(0);
  const aRef = useRef(null);
  const waveRef = useRef(null);
  const scrubbing = useRef(false);
  const bars = useRef(null);
  const hasFile = !!item.audioUrl;
  if (!bars.current) {
    const rnd = seedRand(item.id);
    bars.current = Array.from({ length: 40 }, (_, i) => {
      const env = Math.sin((i / 39) * Math.PI); // arch
      return 0.18 + (0.35 + env * 0.5) * rnd();
    });
  }
  useEffect(() => AudioEngine.subscribe((id) => { if (id !== item.id) { setPlaying(false); setProg(0); } }), [item.id]);
  const toggle = (e) => {
    e.stopPropagation();
    if (hasFile) {
      const a = aRef.current;
      if (!a) return;
      if (playing) { a.pause(); }
      else {
        AudioEngine.stop();
        if (window.__liveAudio && window.__liveAudio !== a) { try { window.__liveAudio.pause(); } catch (_) {} }
        window.__liveAudio = a;
        a.play().catch(() => {});
      }
    } else if (item.motif) {
      if (playing) { AudioEngine.stop(); setPlaying(false); setProg(0); }
      else { AudioEngine.play(item.id, item.motif, item.tempo, setProg, () => setPlaying(false)); setPlaying(true); }
    }
  };
  const seekToClientX = (clientX) => {
    const a = aRef.current, el = waveRef.current;
    if (!a || !el || !a.duration) return;
    const r = el.getBoundingClientRect();
    const frac = Math.min(1, Math.max(0, (clientX - r.left) / r.width));
    a.currentTime = frac * a.duration;
    setProg(frac);
  };
  const nudge = (delta) => {
    const a = aRef.current;
    if (!a || !a.duration) return;
    a.currentTime = Math.min(a.duration, Math.max(0, a.currentTime + delta));
    setProg(a.currentTime / a.duration);
  };
  const onWaveDown = (e) => {
    if (!hasFile) return;
    e.stopPropagation();
    scrubbing.current = true;
    try { e.currentTarget.setPointerCapture(e.pointerId); } catch (_) {}
    seekToClientX(e.clientX);
  };
  const onWaveMove = (e) => { if (scrubbing.current) { e.stopPropagation(); seekToClientX(e.clientX); } };
  const onWaveUp = (e) => {
    if (!scrubbing.current) return;
    scrubbing.current = false;
    try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (_) {}
  };
  const onWaveKey = (e) => {
    if (!hasFile) return;
    if (e.key === "ArrowLeft") { e.preventDefault(); nudge(-5); }
    else if (e.key === "ArrowRight") { e.preventDefault(); nudge(5); }
  };
  const filled = Math.floor(prog * bars.current.length);
  const lit = hasFile || playing;
  return (
    <div className="card audio-card">
      <CardHead item={item} />
      <div className="audio-piece">{item.piece}</div>
      <div className="audio-perf">{item.performer}</div>
      {hasFile && (
        <audio ref={aRef} src={item.audioUrl} preload="metadata"
               onPlay={() => setPlaying(true)} onPause={() => setPlaying(false)}
               onEnded={() => { setPlaying(false); setProg(0); }}
               onTimeUpdate={(e) => { const a = e.currentTarget; if (a.duration && !scrubbing.current) setProg(a.currentTime / a.duration); }} />
      )}
      <div className="audio">
        <button className="pp" onClick={toggle} aria-label={playing ? "pause" : "play"}>
          <Icon name={playing ? "pause" : "play"} size={20} fill stroke={0} />
        </button>
        <div ref={waveRef}
             className={"wave" + (hasFile ? " seekable" : "")}
             role={hasFile ? "slider" : undefined}
             tabIndex={hasFile ? 0 : undefined}
             aria-label={hasFile ? "seek" : undefined}
             aria-valuemin={hasFile ? 0 : undefined}
             aria-valuemax={hasFile ? 100 : undefined}
             aria-valuenow={hasFile ? Math.round(prog * 100) : undefined}
             onPointerDown={onWaveDown} onPointerMove={onWaveMove}
             onPointerUp={onWaveUp} onPointerCancel={onWaveUp}
             onKeyDown={onWaveKey}>
          {bars.current.map((h, i) => (
            <i key={i} className={i <= filled && lit ? "on" : ""}
               style={{ height: (h * 100) + "%", transform: i === filled && playing ? "scaleY(1.15)" : "none" }} />
          ))}
        </div>
        <div className="meta"><div className="t">{item.duration}</div></div>
      </div>
      <div className="card-body"><Note>{item.note}</Note></div>
      <Foot item={item} onTag={onTag} />
    </div>
  );
}

/* ---- podcast ----------------------------------------------------------- */
function PodcastCard({ item, onTag }) {
  const [playing, setPlaying] = useState(false);
  const toggle = (e) => { e.stopPropagation(); setPlaying((p) => !p); };
  return (
    <div className="card pod-card">
      <CardHead item={item} />
      <div className="pod-head">
        <button className="pp" onClick={toggle} aria-label="play clip">
          <Icon name={playing ? "pause" : "play"} size={18} fill stroke={0} />
        </button>
        <div className="pi">
          <div className="show">{item.show}</div>
          <div className="ep">{item.episode} · {item.duration}</div>
        </div>
      </div>
      <div className={"eq" + (playing ? " playing" : "")}>
        {Array.from({ length: 9 }).map((_, i) => <i key={i} />)}
      </div>
      <div className="card-body">
        <p className="note-text" style={{ fontFamily: "var(--font-quote)", fontStyle: "italic", fontSize: 14.5, color: "var(--text)", WebkitLineClamp: 4 }}>{item.transcript}</p>
        <Note>{item.note}</Note>
      </div>
      <Foot item={item} onTag={onTag} />
    </div>
  );
}

/* ---- quote ------------------------------------------------------------- */
function QuoteCard({ item, onTag }) {
  return (
    <div className="card quote-card">
      {item.image && (
        <div className="quote-figure">
          <img src={item.image} alt={item.alt || ""} loading="lazy" />
        </div>
      )}
      <CardHead item={item} />
      <div className="quote">
        <blockquote><span className="mark">“</span>{item.text}<span className="mark">”</span></blockquote>
        <div className="attr"><b>{item.author}</b> · {item.work}</div>
      </div>
      <div className="card-body"><Note>{item.note}</Note></div>
      <Foot item={item} onTag={onTag} />
    </div>
  );
}

/* ---- note -------------------------------------------------------------- */
function NoteCard({ item, onTag }) {
  return (
    <div className="card note-card">
      <CardHead item={item} />
      <div className="card-body"><p className="note-big">{item.text}</p></div>
      <Foot item={item} onTag={onTag} />
    </div>
  );
}

/* ---- link -------------------------------------------------------------- */
function LinkCard({ item, onTag }) {
  return (
    <div className="card link-card">
      <CardHead item={item} />
      <a className="linkrow" href={item.source ? item.source.url : "#"} target="_blank" rel="noreferrer" style={{ textDecoration: "none" }}>
        <div className="ld">{item.domain}</div>
        <div className="lt">{item.title}</div>
        <div className="lx">{item.excerpt}</div>
      </a>
      <div className="card-body"><Note>{item.note}</Note></div>
      <Foot item={item} onTag={onTag} />
    </div>
  );
}

/* ---- photo ------------------------------------------------------------- */
function PhotoCard({ item, onTag, ratio }) {
  const r = ratio || item.ratio || 1;
  return (
    <div className="card photo-card">
      <div className="ph" style={{ aspectRatio: r, "--ph-tint": `oklch(0.6 0.12 ${item.tone})` }}>
        {item.image
          ? <img src={item.image} alt={item.alt} loading="lazy" />
          : <span className="ph-cap"><span className="arrow">↳</span> {item.alt}</span>}
      </div>
      <CardHead item={item} />
      <div className="card-body"><Note>{item.note}</Note></div>
      <Foot item={item} onTag={onTag} />
    </div>
  );
}

/* ---- video ------------------------------------------------------------- */
// Downgrade a YouTube still to the guaranteed hqdefault.jpg, once, when the
// preferred (maxres) image 404s or comes back as the grey placeholder.
function fallbackThumb(img, vid) {
  if (!vid || img.dataset.fb) return;
  img.dataset.fb = "1";
  img.src = `https://i.ytimg.com/vi/${vid}/hqdefault.jpg`;
}

function VideoCard({ item, onTag }) {
  const [open, setOpen] = useState(false);
  // poster: explicit `item.poster`, else the YouTube still derived from `vid`
  // (maxres → hqdefault fallback, since maxres isn't published for every video)
  const poster = item.poster || (item.vid ? `https://i.ytimg.com/vi/${item.vid}/maxresdefault.jpg` : null);
  return (
    <div className="card video-card">
      <div className="video-poster" style={{ "--ph-tint": "oklch(0.55 0.12 20)" }}>
        {open ? (
          <iframe
            src={item.embedUrl || `https://www.youtube-nocookie.com/embed/${item.vid}?autoplay=1&rel=0`}
            title={item.title} allow="autoplay; encrypted-media" allowFullScreen />
        ) : (
          <>
            {poster && (
              <img className="video-thumb" src={poster} alt={item.title} loading="lazy"
                   onError={(e) => fallbackThumb(e.currentTarget, item.vid)}
                   onLoad={(e) => {
                     // maxresdefault returns a 120×90 grey placeholder with HTTP 200
                     // when the real hi-res still doesn't exist — onError never fires,
                     // so catch it by the tell-tale tiny dimensions and downgrade.
                     const img = e.currentTarget;
                     if (img.naturalWidth <= 120 && /maxresdefault/.test(img.src)) {
                       fallbackThumb(img, item.vid);
                     }
                   }} />
            )}
            <button className="play-btn" onClick={(e) => { e.stopPropagation(); setOpen(true); }} aria-label="play video">
              <Icon name="play" size={22} fill stroke={0} style={{ marginLeft: 3 }} />
            </button>
            <div className="stamp">{item.stamp}</div>
          </>
        )}
      </div>
      <CardHead item={item} />
      <div className="card-body" style={{ paddingTop: 8 }}>
        <div style={{ fontFamily: "var(--font-ui)", fontWeight: 600, fontSize: 15, marginBottom: 6 }}>{item.title}</div>
        <Note>{item.note}</Note>
      </div>
      <Foot item={item} onTag={onTag} />
    </div>
  );
}

/* ---- dispatcher -------------------------------------------------------- */
function Card({ item, onTag, onOpen, draggable }) {
  const inner = (() => {
    switch (item.type) {
      case "audio":   return <AudioCard item={item} onTag={onTag} />;
      case "podcast": return <PodcastCard item={item} onTag={onTag} />;
      case "quote":   return <QuoteCard item={item} onTag={onTag} />;
      case "note":    return <NoteCard item={item} onTag={onTag} />;
      case "link":    return <LinkCard item={item} onTag={onTag} />;
      case "photo":   return <PhotoCard item={item} onTag={onTag} />;
      case "video":   return <VideoCard item={item} onTag={onTag} />;
      default:        return null;
    }
  })();
  // wrap so the focus affordance & drag work without breaking inner links
  return inner;
}

Object.assign(window, { Icon, Card, AudioEngine, TYPE_META, seedRand });
