// nyw.co — spatial map redesign
// Pan/zoom canvas, projects placed as coordinates, blueprint + terminal + datacenter HUD.

const { useState, useRef, useEffect, useCallback, useMemo } = React;

// Read server-injected layout; fall back to hardcoded defaults if absent.
const __NYW = (typeof window !== 'undefined' && window.__NYW) || null;

// ─── Tweak defaults ─────────────────────────────────────────────────────────
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "palette": ["#f7f5ef","#0a0a0a","#5b5b58","#1a4dff"],
  "motion": "full",
  "gridDensity": "normal",
  "showTerminal": true,
  "showMinimap": true
}/*EDITMODE-END*/;

// Each palette = [bg, ink, muted, accent]
const PALETTES = [
  ["#f7f5ef","#0a0a0a","#5b5b58","#1a4dff"],  // Paper blueprint
  ["#0a0a0a","#f1efe6","#888579","#ffb43a"],  // Amber CRT
  ["#050805","#dff5e2","#6a8a72","#39ff7a"],  // Phosphor green
  ["#070b10","#cce6ff","#5a7591","#3ad6ff"],  // Datacenter cyan
  ["#fafafa","#0a0a0a","#7a7a7a","#0a0a0a"],  // Monochrome
];
const PALETTE_NAMES = ["Paper", "Amber CRT", "Phosphor", "Datacenter", "Mono"];

// ─── Data ───────────────────────────────────────────────────────────────────
const FILES = __NYW?.files || [
  { id:"F01", num:"File 01", title:"Between the question and the answer.", box:{x:-1080,y:-560,w:940,h:540} },
  { id:"F02", num:"File 02", title:"Between you and what's actually there.", box:{x:-1080,y:80,w:700,h:460} },
  { id:"F03", num:"File 03 — The Lab", title:"Between an idea and a finished thing.", box:{x:200,y:-460,w:940,h:980} },
];

const PROJECTS = __NYW?.projects || [
  { id:"01", file:"F01", name:"curious.fit",   url:"https://curious.fit",   tag:"A curiosity tool for kids that encourages them to ask better questions, go deeper, and find out for themselves.", tags:["kids","curiosity","learning"], status:"live", stack:["TS","Next","Edge"], updated:"2026·05·12", x:-880, y:-420 },
  { id:"02", file:"F01", name:"always.coach",  url:"https://always.coach",  tag:"A coaching platform that helps you get where you want to go, without judgment or scrutiny.", tags:["coaching","habits","growth"], status:"wip", stack:["TS","Next","AI"], updated:"2026·05·09", x:-420, y:-200 },
  { id:"03", file:"F02", name:"sesn.info",     url:"https://sesn.info",     tag:"Find activity sessions near you, by city and sport.", tags:["sport","local","discovery"], status:"live", stack:["TS","Next","Geo"], updated:"2026·04·30", x:-820, y:240 },
  { id:"04", file:"F03", name:"bulb.fit",      url:"https://bulb.fit",      tag:"An end‑to‑end idea engine that brings your inspirations and frustrations to a finished product.", tags:["ideas","builder","tools"], status:"wip", stack:["TS","Next","AI"], updated:"2026·05·14", x:320, y:-360 },
  { id:"05", file:"F03", name:"itsnot.tech",   url:"https://itsnot.tech",   tag:"A personal toolbox of hand‑built web tools, with new ones added live every week.", tags:["weekly","tools","open"], status:"live", stack:["HTML","CSS","JS"], updated:"2026·05·15", x:720, y:300 },
];

// World bounds for minimap
const WORLD = __NYW?.world || { minX:-1200, minY:-640, maxX:1200, maxY:640 };

// ─── Spatial canvas hook ────────────────────────────────────────────────────
function useSpatial(){
  const [t, setT] = useState({ x: 0, y: 0, z: 1 });
  const ref = useRef(null);
  const drag = useRef(null);

  const clampZ = (z) => Math.max(0.4, Math.min(2.4, z));

  const onPointerDown = (e) => {
    if (e.target.closest('.node-card, .hud, .twk-panel, .term, .manifesto, .minimap, .zoom-ctrl')) return;
    if (e.button !== 0 && e.pointerType === 'mouse') return;
    e.currentTarget.setPointerCapture(e.pointerId);
    drag.current = { startX: e.clientX, startY: e.clientY, tx: t.x, ty: t.y };
    e.currentTarget.classList.add('is-dragging');
  };
  const onPointerMove = (e) => {
    if (!drag.current) return;
    const dx = e.clientX - drag.current.startX;
    const dy = e.clientY - drag.current.startY;
    setT(prev => ({ ...prev, x: drag.current.tx + dx, y: drag.current.ty + dy }));
  };
  const onPointerUp = (e) => {
    drag.current = null;
    e.currentTarget.classList.remove('is-dragging');
  };
  const onWheel = (e) => {
    e.preventDefault();
    const rect = e.currentTarget.getBoundingClientRect();
    const cx = e.clientX - rect.left - rect.width/2;
    const cy = e.clientY - rect.top - rect.height/2;
    const dz = e.deltaY < 0 ? 1.1 : 1/1.1;
    setT(prev => {
      const nz = clampZ(prev.z * dz);
      const actualDz = nz / prev.z;
      // Zoom toward cursor: keep the world point under cursor fixed
      const nx = cx - (cx - prev.x) * actualDz;
      const ny = cy - (cy - prev.y) * actualDz;
      return { x: nx, y: ny, z: nz };
    });
  };
  const zoomBy = (factor) => setT(prev => ({ ...prev, z: clampZ(prev.z * factor) }));
  const reset = () => setT({ x: 0, y: 0, z: 1 });
  const centerOn = (wx, wy, z = 1) => setT({ x: -wx * z, y: -wy * z, z });

  // Keyboard: arrows pan
  useEffect(() => {
    const onKey = (e) => {
      if (e.target.matches('input, textarea')) return;
      const STEP = 80;
      if (e.key === 'ArrowUp')    setT(p => ({ ...p, y: p.y + STEP }));
      if (e.key === 'ArrowDown')  setT(p => ({ ...p, y: p.y - STEP }));
      if (e.key === 'ArrowLeft')  setT(p => ({ ...p, x: p.x + STEP }));
      if (e.key === 'ArrowRight') setT(p => ({ ...p, x: p.x - STEP }));
      if (e.key === '+' || e.key === '=') zoomBy(1.15);
      if (e.key === '-' || e.key === '_') zoomBy(1/1.15);
      if (e.key === '0') reset();
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  return { t, ref, handlers: { onPointerDown, onPointerMove, onPointerUp, onWheel }, zoomBy, reset, centerOn };
}

// ─── Live clock ─────────────────────────────────────────────────────────────
function useClock(){
  const [now, setNow] = useState(() => new Date());
  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(id);
  }, []);
  return now;
}
const pad = (n) => String(n).padStart(2,'0');
const fmtClock = (d) => `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())} UTC`;
const fmtDate  = (d) => `${d.getUTCFullYear()}·${pad(d.getUTCMonth()+1)}·${pad(d.getUTCDate())}`;

// ─── Terminal feed ──────────────────────────────────────────────────────────
const TERM_SEED = [
  { t:"system", lv:"OK ", msg:"boot complete — 5 projects indexed" },
  { t:"net",    lv:"GET", msg:"itsnot.tech/tools  →  200 OK" },
  { t:"deploy", lv:"OK ", msg:"itsnot.tech  ↑ weekly drop  v.34" },
  { t:"net",    lv:"GET", msg:"curious.fit/api  →  200 OK" },
  { t:"build",  lv:".. ", msg:"always.coach  ▸ feature/streaks" },
  { t:"build",  lv:".. ", msg:"bulb.fit  ▸ pipeline rewrite" },
  { t:"net",    lv:"GET", msg:"sesn.info/london  →  200 OK" },
  { t:"note",   lv:"·  ", msg:"thinking about: where listings stop helping" },
  { t:"deploy", lv:"OK ", msg:"sesn.info  ↑ index refresh" },
  { t:"note",   lv:"·  ", msg:"the brief is in the unnamed friction" },
];

function useTerminal(motion){
  const [lines, setLines] = useState(() => TERM_SEED.slice(0, 5).map((l,i) => ({ ...l, id: i, ts: tsFor(i) })));
  const next = useRef(5);
  useEffect(() => {
    if (motion === 'off') return;
    const interval = motion === 'full' ? 2200 : 4200;
    const id = setInterval(() => {
      const i = next.current;
      const item = TERM_SEED[i % TERM_SEED.length];
      next.current = i + 1;
      setLines(prev => {
        const id = (prev[prev.length-1]?.id ?? 0) + 1;
        const merged = [...prev, { ...item, id, ts: tsFor(i) }];
        return merged.slice(-7);
      });
    }, interval);
    return () => clearInterval(id);
  }, [motion]);
  return lines;
}
function tsFor(i){
  const d = new Date();
  d.setSeconds(d.getSeconds() - (10-i) * 7);
  return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}

// ─── Node card ──────────────────────────────────────────────────────────────
function Node({ p }) {
  return (
    <div className="node" style={{ left: p.x, top: p.y }}>
      <span className="node-pin" />
      <div className="node-leader">
        <svg viewBox="0 0 32 32" preserveAspectRatio="none">
          <line x1="0" y1="0" x2="32" y2="32" />
        </svg>
      </div>
      <a className="node-card" href={p.url} target="_blank" rel="noreferrer">
        <span className="corner bl" /><span className="corner br" />
        <div className="node-head">
          <span className="node-num">N°{p.id}</span>
          <span className="node-coord">[{p.x},{p.y}]</span>
          <span className={`node-status ${p.status}`}>
            <span className="pulse-dot" />
            {p.status === 'live' ? 'Live' : 'WIP'}
          </span>
        </div>
        <div className="node-body">
          <h3 className="node-title">
            {p.name}<span className="ext">↗</span>
          </h3>
          <p className="node-tag">{p.tag}</p>
        </div>
        <div className="tags">
          {p.tags.map(t => <span className="tag" key={t}>{t}</span>)}
        </div>
        <div className="node-foot">
          <span className="stack">
            {p.stack.map(s => <span key={s}>{s}</span>)}
          </span>
          <span className="updated">{p.updated}</span>
        </div>
      </a>
    </div>
  );
}

// ─── Zone (file region) ─────────────────────────────────────────────────────
function Zone({ f }) {
  return (
    <div className="zone" style={{ left: f.box.x, top: f.box.y, width: f.box.w, height: f.box.h }}>
      <span className="tick tl" /><span className="tick tr" />
      <span className="tick bl" /><span className="tick br" />
      <span className="zone-label">{f.num}</span>
      <span className="zone-title">{f.title}</span>
    </div>
  );
}

// ─── Minimap ────────────────────────────────────────────────────────────────
function Minimap({ t, viewport, onJump }) {
  const W = 200, H = 116; // inner frame
  const worldW = WORLD.maxX - WORLD.minX;
  const worldH = WORLD.maxY - WORLD.minY;
  const sx = (wx) => ((wx - WORLD.minX) / worldW) * W;
  const sy = (wy) => ((wy - WORLD.minY) / worldH) * H;

  // Viewport rect in world coords: world point at screen center is (-t.x/z, -t.y/z),
  // screen corners map to that ± (viewport/2)/z
  const vw = viewport.w / t.z, vh = viewport.h / t.z;
  const cx = -t.x / t.z, cy = -t.y / t.z;
  const vx = sx(cx - vw/2), vy = sy(cy - vh/2);
  const vwS = (vw / worldW) * W, vhS = (vh / worldH) * H;

  const click = (e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const px = (e.clientX - rect.left) / rect.width;
    const py = (e.clientY - rect.top) / rect.height;
    const wx = WORLD.minX + px * worldW;
    const wy = WORLD.minY + py * worldH;
    onJump(wx, wy);
  };

  return (
    <div className="minimap">
      <div className="minimap-head"><span>Map</span><span>2400×1280</span></div>
      <div className="minimap-frame" onClick={click}>
        {PROJECTS.map(p => (
          <span key={p.id} className="minimap-dot" style={{ left: sx(p.x), top: sy(p.y) }} />
        ))}
        <span className="minimap-view"
              style={{ left: Math.max(0,vx), top: Math.max(0,vy),
                       width: Math.min(W, vwS), height: Math.min(H, vhS) }} />
      </div>
    </div>
  );
}

// ─── Terminal panel ─────────────────────────────────────────────────────────
function Terminal({ lines }) {
  return (
    <aside className="term">
      <div className="term-head">
        <span><b>tail</b> /var/log/nyw.log</span>
        <span className="dots">
          <span className="dot on" /><span className="dot on" /><span className="dot" />
        </span>
      </div>
      <div className="term-body">
        {lines.map((l, i) => (
          <div className={"term-line" + (i === lines.length-1 ? " is-new" : "")} key={l.id}>
            <span className="ts">{l.ts}</span>
            <span className="lv">{l.lv}</span>
            {l.msg}
          </div>
        ))}
      </div>
      <div className="term-prompt">
        <span className="ps">nyw@co</span>
        <span className="ph">~/projects %</span>
        <span className="term-caret" />
      </div>
    </aside>
  );
}

// ─── HUD top bar ────────────────────────────────────────────────────────────
function HUD({ t, projectsLive, projectsWip }) {
  const now = useClock();
  return (
    <header className="hud-top">
      <div className="cell brand">
        <span className="wordmark">NYW.CO</span>
        <span className="sub">▮ NOTHING YET WORKS · V01.2026</span>
      </div>
      <div className="cell">
        <span>System</span>
        <span className="row v"><span className="led" /> ONLINE · SHIPPING</span>
      </div>
      <div className="cell">
        <span>Index</span>
        <span className="v">{projectsLive} LIVE · {projectsWip} WIP</span>
      </div>
      <div className="cell">
        <span>Viewport</span>
        <span className="v">x:{(-t.x/t.z).toFixed(0)}  y:{(-t.y/t.z).toFixed(0)}  z:{t.z.toFixed(2)}×</span>
      </div>
      <div className="cell grow">
        <span>UTC</span>
        <span className="v">{fmtClock(now)} · {fmtDate(now)}</span>
      </div>
    </header>
  );
}

// ─── Manifesto panel ────────────────────────────────────────────────────────
function Manifesto() {
  return (
    <section className="manifesto">
      <div className="lbl">File 00 · About</div>
      <h2>Reading between the lines of everyday life.</h2>
      <p>
        Looking for the small problems no one's named yet — the frictions tucked
        between routines, the gaps people have learned to live with — and writing
        the code that fits. Some live, some still becoming. <em>The work is small; the intent is not.</em>
      </p>
    </section>
  );
}

// ─── Zoom controls ──────────────────────────────────────────────────────────
function ZoomControls({ z, zoomBy, reset }) {
  return (
    <div className="zoom-ctrl">
      <button onClick={() => zoomBy(1.2)} title="Zoom in">+</button>
      <button onClick={() => zoomBy(1/1.2)} title="Zoom out">−</button>
      <button onClick={reset} title="Reset">⌂</button>
      <div className="zoom-readout">{(z*100).toFixed(0)}%</div>
    </div>
  );
}

// ─── Root app ───────────────────────────────────────────────────────────────
function App(){
  const [tw, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const { t, handlers, zoomBy, reset, centerOn } = useSpatial();
  const lines = useTerminal(tw.motion);
  const [viewport, setVp] = useState({ w: window.innerWidth, h: window.innerHeight });

  useEffect(() => {
    const onR = () => setVp({ w: window.innerWidth, h: window.innerHeight });
    window.addEventListener('resize', onR);
    return () => window.removeEventListener('resize', onR);
  }, []);

  // Apply palette as CSS vars on :root
  useEffect(() => {
    const [bg, ink, muted, accent] = tw.palette;
    const r = document.documentElement;
    r.style.setProperty('--bg', bg);
    r.style.setProperty('--paper', bg);
    r.style.setProperty('--ink', ink);
    r.style.setProperty('--muted', muted);
    r.style.setProperty('--accent', accent);
    r.style.setProperty('--accent-rgb', hexToRgb(accent).join(','));
    // Build grid colors from ink alpha
    const a1 = isLight(bg) ? 0.06 : 0.07;
    const a2 = isLight(bg) ? 0.14 : 0.13;
    r.style.setProperty('--grid', toRgba(ink, a1));
    r.style.setProperty('--grid-strong', toRgba(ink, a2));
  }, [tw.palette]);

  // Grid cell size
  const cell = tw.gridDensity === 'tight' ? 24 : tw.gridDensity === 'loose' ? 64 : 40;

  // Compute background offsets so grid stays anchored to world origin
  const bgx = (viewport.w/2 + t.x) + 'px';
  const bgy = (viewport.h/2 + t.y) + 'px';

  const live = PROJECTS.filter(p => p.status === 'live').length;
  const wip  = PROJECTS.filter(p => p.status === 'wip').length;

  return (
    <div className={"app motion-" + tw.motion}>
      <div
        className="viewport"
        style={{
          '--cell': (cell * t.z) + 'px',
          '--bgx': bgx, '--bgy': bgy,
        }}
        {...handlers}
      >
        <div className="canvas" style={{ transform: `translate(${t.x}px, ${t.y}px) scale(${t.z})` }}>
          <div className="origin"><span className="origin-label">[0,0]</span></div>
          {FILES.map(f => <Zone key={f.id} f={f} />)}
          {PROJECTS.map(p => <Node key={p.id} p={p} />)}
        </div>
      </div>

      {tw.motion !== 'off' && <div className="scanline" />}

      <div className="hud">
        <HUD t={t} projectsLive={live} projectsWip={wip} />
        <ZoomControls z={t.z} zoomBy={zoomBy} reset={reset} />
        {tw.showMinimap && <Minimap t={t} viewport={viewport} onJump={(x,y) => centerOn(x,y, t.z)} />}
        <Manifesto />
        {tw.showTerminal && <Terminal lines={lines} />}
        <div className="hint">
          <span>drag to pan</span>
          <kbd>⇧</kbd><kbd>↕</kbd>
          <span>scroll to zoom</span>
          <kbd>0</kbd>
          <span>reset</span>
        </div>
      </div>

      <TweaksPanel title="Tweaks">
        <TweakSection label="Palette" />
        <TweakColor label="Theme" value={tw.palette}
                    options={PALETTES}
                    onChange={(v) => setTweak('palette', v)} />
        <TweakSection label="Motion" />
        <TweakRadio label="Intensity" value={tw.motion}
                    options={['off','subtle','full']}
                    onChange={(v) => setTweak('motion', v)} />
        <TweakSection label="Layout" />
        <TweakRadio label="Grid" value={tw.gridDensity}
                    options={['tight','normal','loose']}
                    onChange={(v) => setTweak('gridDensity', v)} />
        <TweakToggle label="Terminal feed" value={tw.showTerminal}
                     onChange={(v) => setTweak('showTerminal', v)} />
        <TweakToggle label="Minimap" value={tw.showMinimap}
                     onChange={(v) => setTweak('showMinimap', v)} />
        <TweakSection label="Navigation" />
        <TweakButton label="Reset view" onClick={reset} />
      </TweaksPanel>
    </div>
  );
}

// ─── Color helpers ──────────────────────────────────────────────────────────
function hexToRgb(hex){
  const h = hex.replace('#','');
  const n = h.length === 3
    ? h.split('').map(c => parseInt(c+c,16))
    : [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16)];
  return n;
}
function toRgba(hex, a){
  const [r,g,b] = hexToRgb(hex);
  return `rgba(${r},${g},${b},${a})`;
}
function isLight(hex){
  const [r,g,b] = hexToRgb(hex);
  return (r*0.299 + g*0.587 + b*0.114) > 140;
}

// ─── Mount ──────────────────────────────────────────────────────────────────
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
