/* ============================================================================
   board.jsx — toolbar, layouts (mosaic / grid / scatter), filters, focus mode,
   and the Tweaks panel.  Depends on cards.jsx (Card, Icon, AudioEngine).
   ========================================================================== */
const { useState: uS, useRef: uR, useEffect: uE, useMemo, useCallback: uCB } = React;

const COL_W = 340;   // mosaic column / scatter card width (px)
const TILE = 308;    // uniform grid tile size (px)

const TYPE_ORDER = ["audio", "quote", "photo", "video", "podcast", "note", "link"];

const FONTS = {
  grotesk:   { ui: "'Space Grotesk'", body: "'Hanken Grotesk'", quote: "'Newsreader'" },
  editorial: { ui: "'Space Grotesk'", body: "'Newsreader'",     quote: "'Newsreader'" },
  mono:      { ui: "'Space Mono'",    body: "'Hanken Grotesk'", quote: "'Space Mono'" },
};
const GAP = { cozy: 14, regular: 22, roomy: 34 };

const ACCENTS = [
  "oklch(0.72 0.11 66)",   // amber (default)
  "oklch(0.64 0.12 32)",   // clay
  "oklch(0.66 0.07 168)",  // sage
  "oklch(0.62 0.09 250)",  // slate-blue
];

const EMPTY = [];
function normalizeItems(arr) {
  return (Array.isArray(arr) ? arr : []).map((it, i) => ({
    ...it,
    id: it.id || (it.type || "item") + "-" + i,
    note: it.comment != null ? it.comment : it.note,
    year: it.year != null ? it.year : (parseInt(it.date, 10) || 0),
    tags: it.tags || [],
  }));
}

function searchText(it) {
  return [it.note, it.text, it.title, it.excerpt, it.piece, it.performer,
          it.author, it.work, it.transcript, it.show, it.episode, it.alt,
          (it.tags || []).join(" ")].filter(Boolean).join(" ").toLowerCase();
}

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "paper",
  "accent": "oklch(0.72 0.11 66)",
  "typeset": "grotesk",
  "texture": "grain",
  "cardStyle": "hairline",
  "density": "regular"
}/*EDITMODE-END*/;

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);

  // ---- product state ----
  const [data, setData] = uS(null);      // null = loading
  const [loadErr, setLoadErr] = uS(false);
  const [dragOver, setDragOver] = uS(false);
  const fileRef = uR(null);
  const ALL = data || EMPTY;
  const [query, setQuery] = uS("");
  const [types, setTypes] = uS([]);          // active type filters (empty = all)
  const [tag, setTag] = uS(null);            // active hashtag
  const [sort, setSort] = uS("added");       // added | newest | oldest | shuffle
  const [shuffleSeed, setShuffleSeed] = uS(0);
  const [layout, setLayout] = uS("mosaic");  // mosaic | grid | scatter
  const [focus, setFocus] = uS(null);        // index into visible[]
  const [gen, setGen] = uS(0);               // bump to replay entrance anim

  // load items.json (works when served over http; see local-preview fallback)
  uE(() => {
    let alive = true;
    fetch("items.json", { cache: "no-store" })
      .then((r) => { if (!r.ok) throw new Error("http " + r.status); return r.json(); })
      .then((d) => { if (alive) setData(normalizeItems(d)); })
      .catch(() => { if (alive) setLoadErr(true); });
    return () => { alive = false; };
  }, []);

  const loadFromFile = (file) => {
    if (!file) return;
    const fr = new FileReader();
    fr.onload = () => {
      try { setData(normalizeItems(JSON.parse(fr.result))); setLoadErr(false); }
      catch (err) { alert("Couldn't parse that JSON:\n" + err.message); }
    };
    fr.readAsText(file);
  };

  // apply design tweaks to <html>
  uE(() => {
    const r = document.documentElement;
    r.dataset.theme = t.theme;
    r.dataset.texture = t.texture;
    r.dataset.cardstyle = t.cardStyle;
    r.style.setProperty("--accent", t.accent);
    r.style.setProperty("--accent-ink", t.accent);
    const f = FONTS[t.typeset] || FONTS.grotesk;
    r.style.setProperty("--font-ui", f.ui + ", system-ui, sans-serif");
    r.style.setProperty("--font-body", f.body + ", system-ui, sans-serif");
    r.style.setProperty("--font-quote", f.quote + ", Georgia, serif");
    r.style.setProperty("--gap", (GAP[t.density] || 22) + "px");
  }, [t]);

  // ---- pipeline: filter -> sort ----
  const visible = useMemo(() => {
    let list = ALL.slice();
    if (types.length) list = list.filter((i) => types.includes(i.type));
    if (tag) list = list.filter((i) => (i.tags || []).includes(tag));
    if (query.trim()) {
      const q = query.toLowerCase();
      list = list.filter((i) => searchText(i).includes(q));
    }
    if (sort === "newest") list.sort((a, b) => b.year - a.year);
    else if (sort === "oldest") list.sort((a, b) => a.year - b.year);
    else if (sort === "shuffle") {
      const r = seedRand("s" + shuffleSeed);
      list = list.map((x) => [x, r()]).sort((a, b) => a[1] - b[1]).map((x) => x[0]);
    }
    return list;
  }, [ALL, types, tag, query, sort, shuffleSeed]);

  // type counts for chips
  const counts = useMemo(() => {
    const c = {};
    ALL.forEach((i) => { c[i.type] = (c[i.type] || 0) + 1; });
    return c;
  }, [ALL]);

  // bump entrance animation when the set/order changes
  uE(() => { setGen((g) => g + 1); }, [types, tag, query, sort, shuffleSeed, layout]);

  // ---- scatter positions ----
  const boardRef = uR(null);
  const [pos, setPos] = uS({});
  uE(() => {
    if (layout !== "scatter") return;
    const wEl = boardRef.current;
    const W = (wEl ? wEl.clientWidth : 1200) - 52;
    const cw = COL_W;
    const cols = Math.max(1, Math.floor(W / (cw + 28)));
    const colH = new Array(cols).fill(30);
    const next = {};
    visible.forEach((it, idx) => {
      const r = seedRand(it.id + "p");
      const col = idx % cols;
      const x = 26 + col * (cw + 28) + (r() - 0.5) * 30;
      const y = colH[col] + (r() - 0.5) * 16;
      next[it.id] = { x, y, rot: (r() - 0.5) * 4 };
      colH[col] = y + 250 + r() * 160;
    });
    setPos(next);
  }, [layout, visible]);

  const scatterHeight = useMemo(() => {
    if (layout !== "scatter") return undefined;
    let max = 800;
    Object.values(pos).forEach((p) => { if (p.y > max) max = p.y; });
    return max + 420;
  }, [pos, layout]);

  // ---- handlers ----
  const toggleType = (ty) => setTypes((p) => p.includes(ty) ? p.filter((x) => x !== ty) : [...p, ty]);
  const onTag = (tg) => setTag((cur) => cur === tg ? null : tg);
  const cycleSort = () => setSort((s) => s === "added" ? "newest" : s === "newest" ? "oldest" : "added");
  const doShuffle = () => { setSort("shuffle"); setShuffleSeed((s) => s + 1); };
  const sortLabel = { added: "Recent", newest: "Newest", oldest: "Oldest", shuffle: "Shuffled" }[sort];

  // ---- focus mode ----
  const openFocus = (id) => { const i = visible.findIndex((x) => x.id === id); if (i >= 0) setFocus(i); };
  const closeFocus = () => { setFocus(null); AudioEngine.stop(); };
  const step = uCB((d) => setFocus((f) => f == null ? f : (f + d + visible.length) % visible.length), [visible.length]);
  uE(() => {
    if (focus == null) return;
    const onKey = (e) => {
      if (e.key === "Escape") closeFocus();
      else if (e.key === "ArrowRight") { AudioEngine.stop(); step(1); }
      else if (e.key === "ArrowLeft") { AudioEngine.stop(); step(-1); }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [focus, step]);

  // scatter drag
  const dragRef = uR(null);
  const onScatterDown = (e, id) => {
    if (layout !== "scatter") return;
    if (window.matchMedia("(max-width: 720px), (pointer: coarse)").matches) return;
    if (e.target.closest("button,a,input,iframe")) return;
    const start = { mx: e.clientX, my: e.clientY, ...(pos[id] || { x: 0, y: 0 }) };
    dragRef.current = id;
    const card = e.currentTarget;
    card.classList.add("dragging");
    const move = (ev) => {
      setPos((p) => ({ ...p, [id]: { ...p[id], x: start.x + (ev.clientX - start.mx), y: Math.max(10, start.y + (ev.clientY - start.my)) } }));
    };
    const up = () => {
      card.classList.remove("dragging");
      dragRef.current = null;
      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", up);
    };
    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
  };

  const cw = COL_W, tile = TILE;

  return (
    <div className="app">
      {/* ---------- top bar ---------- */}
      <div className="topbar">
        <a className="tbtn icon back-btn" href="../" title="Back to javier-artiles.com" aria-label="Back to main site">
          <Icon name="chevL" size={18} />
        </a>
        <div className="brand">
          <a href="../" style={{ color: "inherit", textDecoration: "none" }} title="Back to javier-artiles.com">
            <h1>Inspiration Board</h1>
          </a>
        </div>
        <div className="spacer" />

        <div className="search">
          <span className="ic"><Icon name="search" size={15} /></span>
          <input value={query} onChange={(e) => setQuery(e.target.value)}
                 placeholder="Search notes, tags, sources…" />
        </div>

        <div className="tools">
          <button className="tbtn sort-btn" onClick={cycleSort} title="Sort by date" aria-label={`Sort: ${sortLabel}`}>
            <Icon name="sort" size={16} /> <span className="btn-label">{sortLabel}</span>
          </button>
          <button className="tbtn icon" onClick={doShuffle} title="Shuffle" aria-label="Shuffle items">
            <Icon name="shuffle" size={17} />
          </button>

          <div className="zoom">
            <button data-active={layout === "mosaic" ? 1 : 0} onClick={() => setLayout("mosaic")} title="Mosaic" aria-label="Mosaic layout" style={layoutBtn(layout === "mosaic")}><Icon name="columns" size={16} /></button>
            <button data-active={layout === "grid" ? 1 : 0} onClick={() => setLayout("grid")} title="Grid" aria-label="Grid layout" style={layoutBtn(layout === "grid")}><Icon name="grid" size={16} /></button>
            <button data-active={layout === "scatter" ? 1 : 0} onClick={() => setLayout("scatter")} title="Pinboard" aria-label="Pinboard layout" style={layoutBtn(layout === "scatter")}><Icon name="scatter" size={16} /></button>
          </div>

          <button className="tbtn icon" onClick={() => visible.length && setFocus(0)} title="Focus / gallery mode" aria-label="Open focus mode" style={{ marginLeft: 2 }}>
            <Icon name="focus" size={17} />
          </button>
          <button className="tbtn icon" onClick={() => setTweak("theme", t.theme === "paper" ? "ink" : "paper")} title="Toggle theme" aria-label={`Use ${t.theme === "paper" ? "dark" : "light"} theme`}>
            <Icon name={t.theme === "paper" ? "moon" : "sun"} size={17} />
          </button>
        </div>
      </div>

      {/* ---------- filter rail ---------- */}
      <div className="subbar">
        <span className="label">Type</span>
        <div className="chips">
          {TYPE_ORDER.filter((ty) => counts[ty] > 0).map((ty) => (
            <button key={ty} className="chip" data-active={types.includes(ty) ? 1 : 0} onClick={() => toggleType(ty)}>
              <span style={{ color: "var(--accent-use)", display: "flex" }}><Icon name={TYPE_META[ty].glyph} size={13} stroke={1.7} /></span>
              {TYPE_META[ty].label}
              <span className="ct">{counts[ty]}</span>
            </button>
          ))}
        </div>
        {(tag || types.length || query) && <div className="divider-v" />}
        {tag && (
          <div className="facet">#{tag}
            <button onClick={() => setTag(null)} aria-label="clear tag"><Icon name="x" size={12} /></button>
          </div>
        )}
        {(types.length > 0) && (
          <button className="facet" onClick={() => setTypes([])} style={{ cursor: "pointer" }}>
            clear types <Icon name="x" size={12} />
          </button>
        )}
      </div>

      {/* ---------- board ---------- */}
      <div className="board-wrap" ref={boardRef}
           onDragOver={loadErr ? (e) => { e.preventDefault(); setDragOver(true); } : undefined}
           onDragLeave={loadErr ? () => setDragOver(false) : undefined}
           onDrop={loadErr ? (e) => { e.preventDefault(); setDragOver(false); loadFromFile(e.dataTransfer.files && e.dataTransfer.files[0]); } : undefined}>
        {data === null && !loadErr && (
          <div className="loader"><div className="spin" /><div className="lt">loading items.json…</div></div>
        )}
        {loadErr && (
          <div className={"onboard" + (dragOver ? " over" : "")}>
            <div className="glyph"><Icon name="note" size={30} /></div>
            <h2>Preview your board locally</h2>
            <p>Opened straight from your disk, the browser won't let the page read <code>items.json</code>. Drop the file anywhere here — or pick it — to preview now. Once this folder is served over http (your live site, or a dev server) the board loads on its own.</p>
            <input ref={fileRef} type="file" accept="application/json,.json" style={{ display: "none" }} onChange={(e) => loadFromFile(e.target.files[0])} />
            <button className="pick" onClick={() => fileRef.current && fileRef.current.click()}><Icon name="arrow" size={15} /> Choose items.json</button>
            <div className="ot">or run this in the folder, then refresh</div>
            <code>python3 -m http.server 8000</code>
          </div>
        )}
        <div className={"board " + layout}
             style={{ "--col-w": cw + "px", "--tile": tile + "px", minHeight: scatterHeight }}>
          {data && !loadErr && visible.length === 0 && (
            <div className="empty"><Icon name="search" size={26} /><div>nothing matches — try clearing a filter</div></div>
          )}
          {visible.map((it) => {
            const p = pos[it.id];
            return (
              <div className="frame" key={it.id + "-" + gen}
                   style={layout === "scatter" && p ? { left: p.x, top: p.y, transform: `rotate(${p.rot}deg)`, zIndex: 1 } : undefined}
                   onPointerDown={(e) => onScatterDown(e, it.id)}>
                <button className="expand" onClick={(e) => { e.stopPropagation(); openFocus(it.id); }} title="Open in focus">
                  <Icon name="focus" size={15} />
                </button>
                <Card item={it} onTag={onTag} />
              </div>
            );
          })}
        </div>
      </div>

      {/* ---------- focus / gallery ---------- */}
      {focus != null && visible[focus] && (
        <div className="focus" onClick={closeFocus}>
          <div className="focus-top" onClick={(e) => e.stopPropagation()}>
            <span className="counter">{String(focus + 1).padStart(2, "0")} / {String(visible.length).padStart(2, "0")}</span>
            <span className="ftitle">focus mode · {TYPE_META[visible[focus].type].label}</span>
            <button className="x" onClick={closeFocus} aria-label="close"><Icon name="x" size={18} /></button>
          </div>
          <div className="focus-stage" onClick={(e) => e.stopPropagation()}>
            <button className="nav" onClick={() => { AudioEngine.stop(); step(-1); }} aria-label="previous"><Icon name="chevL" size={22} /></button>
            <div className="focus-card" key={visible[focus].id}>
              <Card item={visible[focus]} onTag={(tg) => { setTag(tg); closeFocus(); }} />
            </div>
            <button className="nav" onClick={() => { AudioEngine.stop(); step(1); }} aria-label="next"><Icon name="chevR" size={22} /></button>
          </div>
          <div className="kbd-hint" onClick={(e) => e.stopPropagation()}>
            <span><kbd>←</kbd> <kbd>→</kbd> browse</span>
            <span><kbd>esc</kbd> close</span>
          </div>
        </div>
      )}

      {/* ---------- tweaks ---------- */}
      <TweaksPanel title="Tweaks">
        <TweakSection label="Mood" />
        <TweakRadio label="Theme" value={t.theme}
          options={[{ value: "paper", label: "Paper" }, { value: "ink", label: "Ink" }]}
          onChange={(v) => setTweak("theme", v)} />
        <TweakColor label="Accent" value={t.accent} options={ACCENTS}
          onChange={(v) => setTweak("accent", v)} />
        <TweakSelect label="Background" value={t.texture}
          options={[{ value: "none", label: "None" }, { value: "grain", label: "Grain" }, { value: "grid", label: "Grid" }, { value: "dots", label: "Dots" }]}
          onChange={(v) => setTweak("texture", v)} />

        <TweakSection label="Type" />
        <TweakRadio label="Typeset" value={t.typeset}
          options={[{ value: "grotesk", label: "Grotesk" }, { value: "editorial", label: "Editorial" }, { value: "mono", label: "Mono" }]}
          onChange={(v) => setTweak("typeset", v)} />

        <TweakSection label="Cards" />
        <TweakRadio label="Style" value={t.cardStyle}
          options={[{ value: "hairline", label: "Hairline" }, { value: "raised", label: "Raised" }]}
          onChange={(v) => setTweak("cardStyle", v)} />
        <TweakRadio label="Density" value={t.density}
          options={[{ value: "cozy", label: "Cozy" }, { value: "regular", label: "Regular" }, { value: "roomy", label: "Roomy" }]}
          onChange={(v) => setTweak("density", v)} />
      </TweaksPanel>
    </div>
  );
}

function layoutBtn(active) {
  return active ? { color: "var(--bg)", background: "var(--text)" } : undefined;
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
