/* Patches — game logic & UI */
const { useState, useEffect, useRef, useLayoutEffect, useCallback } = React;

/* ---------- geometry helpers ---------- */
const rectFrom = (ar, ac, br, bc) => ({
  r0: Math.min(ar, br), c0: Math.min(ac, bc),
  r1: Math.max(ar, br), c1: Math.max(ac, bc)
});
const rectArea = r => (r.r1 - r.r0 + 1) * (r.c1 - r.c0 + 1);
const inRect = (r, row, col) => row >= r.r0 && row <= r.r1 && col >= r.c0 && col <= r.c1;
const seedsIn = (patches, r) => patches.filter(p => inRect(r, p.seed.r, p.seed.c));
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));

/* shape families: square w=h · wide w>h · tall h>w · free any */
function shapeOK(rect, shape) {
  const w = rect.c1 - rect.c0 + 1, h = rect.r1 - rect.r0 + 1;
  if (shape === 'square') return w === h;
  if (shape === 'wide') return w > h;
  if (shape === 'tall') return h > w;
  return true; // free
}
const SHAPE_LABEL = { square: 'Square', wide: 'Wide', tall: 'Tall', free: 'Freeform' };

/* small silhouette used inside clue chips & the legend */
function ShapeGlyph({ shape, size = 14, stroke = 2 }) {
  const base = { display: 'inline-block', boxSizing: 'border-box', border: `${stroke}px solid currentColor`, borderRadius: 3 };
  if (shape === 'free')
    return (
      <span style={{ position: 'relative', width: size, height: size, display: 'inline-block', verticalAlign: 'middle' }}>
        <i style={{ ...base, position: 'absolute', left: 0, top: 0, width: size * 0.62, height: size * 0.62, opacity: .55 }} />
        <i style={{ ...base, position: 'absolute', right: 0, bottom: 0, width: size * 0.62, height: size * 0.62 }} />
      </span>
    );
  const dims = shape === 'wide' ? [size, size * 0.6] : shape === 'tall' ? [size * 0.6, size] : [size * 0.82, size * 0.82];
  return <span style={{ ...base, width: dims[0], height: dims[1], verticalAlign: 'middle' }} />;
}

/* owners[r][c] = array of patch ids covering that cell */
function computeOwners(patches, placements, rows, cols) {
  const owners = Array.from({ length: rows }, () => Array.from({ length: cols }, () => []));
  patches.forEach(p => {
    const r = placements[p.id];
    if (!r) return;
    for (let row = r.r0; row <= r.r1; row++)
      for (let col = r.c0; col <= r.c1; col++)
        if (row >= 0 && row < rows && col >= 0 && col < cols) owners[row][col].push(p.id);
  });
  return owners;
}

/* ---------- persistence ---------- */
const PROG_KEY = 'patches.progress.v1';
const stateKey = id => 'patches.state.' + id;
const timeKey = id => 'patches.best.' + id;
function loadProgress() {
  try { return JSON.parse(localStorage.getItem(PROG_KEY)) || { unlocked: 1 }; }
  catch (e) { return { unlocked: 1 }; }
}
const fmtTime = s => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;

/* ============================================================= */
function Game({ level, levelIndex, total, onExit, onSolved, onNav, unlocked }) {
  const stageRef = useRef(null);
  const boardRef = useRef(null);
  const cellRef = useRef(40);
  const dragRef = useRef(null);
  const [cell, setCell] = useState(40);
  const [placements, setPlacements] = useState({});
  const [drag, setDrag] = useState(null);          // {ar,ac,br,bc}
  const [flash, setFlash] = useState(null);         // invalid attempt rect
  const [hint, setHint] = useState(null);           // patch id being hinted
  const [justPlaced, setJustPlaced] = useState(null);
  const [won, setWon] = useState(false);
  const [seconds, setSeconds] = useState(0);
  const [guide, setGuide] = useState(false);
  const { rows, cols, patches } = level;
  const gap = Math.max(3, Math.round(cell * 0.055));

  /* load saved placements for this level */
  useEffect(() => {
    let saved = null;
    try { saved = JSON.parse(localStorage.getItem(stateKey(level.id))); } catch (e) {}
    setPlacements(saved && saved.placements ? saved.placements : {});
    setSeconds(saved && saved.seconds ? saved.seconds : 0);
    setWon(false); setDrag(null); setFlash(null); setHint(null);
  }, [level.id]);

  /* timer */
  useEffect(() => {
    if (won) return;
    const t = setInterval(() => setSeconds(s => s + 1), 1000);
    return () => clearInterval(t);
  }, [won, level.id]);

  /* responsive board sizing */
  useLayoutEffect(() => {
    const fit = () => {
      const el = stageRef.current; if (!el) return;
      const availW = el.clientWidth - 8;
      const availH = el.clientHeight - 8;
      const c = Math.floor(Math.min(availW / cols, availH / rows));
      const cc = clamp(c, 18, 132);
      cellRef.current = cc; setCell(cc);
    };
    fit();
    const ro = new ResizeObserver(fit);
    if (stageRef.current) ro.observe(stageRef.current);
    return () => ro.disconnect();
  }, [rows, cols]);

  /* derived */
  const owners = computeOwners(patches, placements, rows, cols);
  const conflictCells = [];
  for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++)
    if (owners[r][c].length > 1) conflictCells.push({ r, c });
  const conflictIds = new Set();
  conflictCells.forEach(({ r, c }) => owners[r][c].forEach(id => conflictIds.add(id)));
  const statusOf = p => {
    const rect = placements[p.id];
    if (!rect) return 'empty';
    if (conflictIds.has(p.id)) return 'conflict';
    return (rectArea(rect) === p.area && shapeOK(rect, p.shape)) ? 'correct' : 'wrong';
  };
  const correctCount = patches.filter(p => statusOf(p) === 'correct').length;

  /* win check */
  useEffect(() => {
    if (won) return;
    const allGood = patches.every(p => statusOf(p) === 'correct');
    if (allGood) {
      setWon(true);
      const best = Number(localStorage.getItem(timeKey(level.id)) || 0);
      if (!best || seconds < best) localStorage.setItem(timeKey(level.id), String(seconds));
      onSolved(levelIndex, seconds);
    }
  });

  /* save placements */
  useEffect(() => {
    try { localStorage.setItem(stateKey(level.id), JSON.stringify({ placements, seconds })); } catch (e) {}
  }, [placements, seconds, level.id]);

  /* pointer → cell */
  const cellFromEvent = e => {
    const b = boardRef.current.getBoundingClientRect();
    const cc = cellRef.current;
    return {
      r: clamp(Math.floor((e.clientY - b.top) / cc), 0, rows - 1),
      c: clamp(Math.floor((e.clientX - b.left) / cc), 0, cols - 1)
    };
  };

  const onDown = e => {
    if (won) return;
    try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) {}
    const { r, c } = cellFromEvent(e);
    dragRef.current = { ar: r, ac: c, br: r, bc: c };
    setDrag(dragRef.current);
    setHint(null);
  };
  const onMove = e => {
    const d = dragRef.current;
    if (!d) return;
    const { r, c } = cellFromEvent(e);
    if (r !== d.br || c !== d.bc) { d.br = r; d.bc = c; setDrag({ ...d }); }
  };
  const onUp = () => {
    const d = dragRef.current;
    dragRef.current = null;
    if (!d) return;
    const isTap = d.ar === d.br && d.ac === d.bc;
    const box = rectFrom(d.ar, d.ac, d.br, d.bc);
    if (isTap) {
      // tap a clue: empty → fill its single cell (1×1); placed → undo
      const onSeed = patches.find(p => p.seed.r === d.ar && p.seed.c === d.ac);
      if (onSeed) {
        if (placements[onSeed.id]) {
          setPlacements(p => { const n = { ...p }; delete n[onSeed.id]; return n; });
        } else {
          setPlacements(p => ({ ...p, [onSeed.id]: { r0: d.ar, c0: d.ac, r1: d.ar, c1: d.ac } }));
          setJustPlaced(onSeed.id);
          setTimeout(() => setJustPlaced(j => (j === onSeed.id ? null : j)), 360);
        }
      } else {
        const owner = owners[d.ar][d.ac];
        const target = owner.length ? owner[owner.length - 1] : null;
        if (target && placements[target]) setPlacements(p => { const n = { ...p }; delete n[target]; return n; });
      }
    } else {
      const hits = seedsIn(patches, box);
      if (hits.length === 1) {
        setPlacements(p => ({ ...p, [hits[0].id]: box }));
        setJustPlaced(hits[0].id);
        setTimeout(() => setJustPlaced(j => (j === hits[0].id ? null : j)), 360);
      } else {
        setFlash(box);
        setTimeout(() => setFlash(null), 360);
      }
    }
    setDrag(null);
  };

  const reset = () => { setPlacements({}); setWon(false); setSeconds(0); };
  const doHint = () => {
    const target = patches.find(p => statusOf(p) !== 'correct');
    if (!target) return;
    setHint(target.id);
    setTimeout(() => setHint(h => (h === target.id ? null : h)), 1700);
  };

  /* drag preview meta */
  let preview = null;
  if (drag && !(drag.ar === drag.br && drag.ac === drag.bc)) {
    const box = rectFrom(drag.ar, drag.ac, drag.br, drag.bc);
    const hits = seedsIn(patches, box);
    preview = {
      box, area: rectArea(box),
      kind: hits.length === 1 ? 'ok' : 'bad',
      patch: hits.length === 1 ? hits[0] : null,
      perfect: hits.length === 1 && rectArea(box) === hits[0].area && shapeOK(box, hits[0].shape)
    };
  }

  /* px box from a rect */
  const pxBox = r => ({
    left: r.c0 * cell + gap, top: r.r0 * cell + gap,
    width: (r.c1 - r.c0 + 1) * cell - gap * 2,
    height: (r.r1 - r.r0 + 1) * cell - gap * 2
  });

  const boardW = cols * cell, boardH = rows * cell;
  const numFont = Math.max(13, Math.round(cell * 0.42));

  return (
    <div className="game">
      <div className="topbar">
        <button className="btn ghost" onClick={onExit} aria-label="Levels">‹ Levels</button>
        <div className="lvl-nav">
          <button className="btn icon" disabled={levelIndex === 0} onClick={() => onNav(levelIndex - 1)}>‹</button>
          <div className="lvl-title">
            <span className="lvl-kicker">Level {levelIndex + 1}</span>
            <span className="lvl-name">{level.name}</span>
          </div>
          <button className="btn icon" disabled={levelIndex + 1 >= total || levelIndex + 1 >= unlocked}
            onClick={() => onNav(levelIndex + 1)}>›</button>
        </div>
        <div className="hud">
          <div className="stat"><span className="stat-num">{correctCount}/{patches.length}</span><span className="stat-lab">sewn</span></div>
          <div className="stat"><span className="stat-num">{fmtTime(seconds)}</span><span className="stat-lab">time</span></div>
        </div>
      </div>

      <div className="board-stage" ref={stageRef}>
        <div className="board" ref={boardRef} style={{ width: boardW, height: boardH }}>
          {/* dashed grid */}
          <svg className="grid-svg" width={boardW} height={boardH}>
            {Array.from({ length: cols - 1 }, (_, i) => (
              <line key={'v' + i} x1={(i + 1) * cell} y1="0" x2={(i + 1) * cell} y2={boardH} />
            ))}
            {Array.from({ length: rows - 1 }, (_, i) => (
              <line key={'h' + i} x1="0" y1={(i + 1) * cell} x2={boardW} y2={(i + 1) * cell} />
            ))}
          </svg>

          {/* placed patches */}
          {patches.map(p => {
            const rect = placements[p.id];
            if (!rect) return null;
            const st = statusOf(p);
            const b = pxBox(rect);
            return (
              <div key={p.id}
                className={`patch ${st === 'conflict' ? 'is-conflict' : ''} ${st === 'correct' ? 'is-correct' : ''} ${justPlaced === p.id ? 'pop' : ''}`}
                style={{ left: b.left, top: b.top, width: b.width, height: b.height,
                         background: p.fill,
                         boxShadow: st === 'correct'
                           ? `3px 4px 0 ${p.shadow}, 0 0 0 2px rgba(255,255,255,.18), 0 8px 22px rgba(0,0,0,.34)`
                           : `3px 4px 0 ${p.shadow}, 0 8px 18px rgba(0,0,0,.32)`,
                         borderRadius: Math.max(8, cell * 0.18) }}>
                <span className={`clue-chip ${st === 'correct' ? 'done' : ''}`}
                  style={{ left: (p.seed.c - rect.c0) * cell + cell / 2 - gap,
                           top: (p.seed.r - rect.r0) * cell + cell / 2 - gap,
                           fontSize: Math.max(11, Math.round(cell * 0.32)),
                           color: st === 'correct' ? p.fill : '#fff' }}>
                  {st === 'correct' && levelIndex === 0
                    ? '✓'
                    : <><ShapeGlyph shape={p.shape} size={Math.max(10, cell * 0.28)} stroke={2} /><b>{p.area}</b></>}
                </span>
              </div>
            );
          })}

          {/* seed cards (unplaced) — silhouette encodes the shape family */}
          {patches.map(p => {
            if (placements[p.id]) return null;
            let w, h;
            if (p.shape === 'wide') { w = Math.round(cell * 0.82); h = Math.round(cell * 0.5); }
            else if (p.shape === 'tall') { w = Math.round(cell * 0.5); h = Math.round(cell * 0.82); }
            else { w = Math.round(cell * 0.62); h = Math.round(cell * 0.62); }
            const left = p.seed.c * cell + (cell - w) / 2, top = p.seed.r * cell + (cell - h) / 2;
            const sFont = Math.min(numFont, Math.round(Math.min(w, h) * 0.6));
            const radius = Math.max(6, cell * 0.14);
            const tabSize = Math.round(w * 0.62);
            return (
              <React.Fragment key={'s' + p.id}>
                {p.shape === 'free' && (
                  <div className="seed-tab" style={{ left: left - tabSize * 0.36, top: top - tabSize * 0.36,
                    width: tabSize, height: tabSize, background: p.shadow, borderRadius: radius }} />)}
                <div className="seed"
                  style={{ left, top, width: w, height: h, background: p.fill,
                           boxShadow: `4px 5px 0 ${p.shadow}, 0 7px 16px rgba(0,0,0,.4)`,
                           borderRadius: radius, fontSize: sFont }}>
                  <span>{p.area}</span>
                </div>
              </React.Fragment>
            );
          })}

          {/* hint outline */}
          {hint && (() => {
            const p = patches.find(x => x.id === hint);
            const b = pxBox(p.solution);
            return <div className="hint-box" style={{ left: b.left, top: b.top, width: b.width, height: b.height,
              borderColor: p.fill, borderRadius: Math.max(8, cell * 0.18) }} />;
          })()}

          {/* drag preview */}
          {preview && (() => {
            const b = pxBox(preview.box);
            return (
              <div className={`preview ${preview.kind} ${preview.perfect ? 'perfect' : ''}`}
                style={{ left: b.left, top: b.top, width: b.width, height: b.height,
                         background: preview.kind === 'ok' ? preview.patch.fill + '66' : 'rgba(210,60,55,.22)',
                         boxShadow: `inset 0 0 0 ${preview.perfect ? 4 : 3}px ${preview.kind === 'ok' ? preview.patch.fill : '#ff5a4d'}`,
                         borderRadius: Math.max(8, cell * 0.18) }}>
                {preview.patch && <span className="preview-count" style={{ fontSize: numFont * 0.72 }}>
                  <ShapeGlyph shape={preview.patch.shape} size={numFont * 0.72} stroke={2} />
                  {preview.area}/{preview.patch.area}
                  {preview.perfect && <b className="pv-ok">{'✓'}</b>}
                </span>}
              </div>
            );
          })()}

          {/* invalid flash */}
          {flash && (() => { const b = pxBox(flash);
            return <div className="flash" style={{ left: b.left, top: b.top, width: b.width, height: b.height,
              borderRadius: Math.max(8, cell * 0.18) }} />; })()}

          {/* interaction layer */}
          <div className="ilayer" onPointerDown={onDown} onPointerMove={onMove}
            onPointerUp={onUp} onPointerCancel={onUp} />
        </div>
      </div>

      <div className="toolbar">
        <button className="btn" onClick={reset}>Reset</button>
        <button className="btn" onClick={doHint}>Hint</button>
        <button className="btn" onClick={() => setGuide(true)}>Shapes</button>
        <p className="tip">Drag a box around each clue — match its <b>area</b> and its <b>shape</b>, with no overlaps. Tap a clue to fill just its own cell (a <b>1</b>), or to undo a patch.</p>
      </div>

      {guide && <ShapesGuide onClose={() => setGuide(false)} />}

      {won && <WinOverlay seconds={seconds} levelIndex={levelIndex} total={total}
        onNext={() => onNav(levelIndex + 1)} onMenu={onExit} onReplay={reset} />}
    </div>
  );
}

/* ============================================================= */
function WinOverlay({ seconds, levelIndex, total, onNext, onMenu, onReplay }) {
  const [confetti] = useState(() => {
    const cols = ['#5d62c4', '#2f9bdc', '#27a857', '#ea4c7d', '#a855c7', '#f4a623'];
    return Array.from({ length: 28 }, (_, i) => ({
      left: Math.random() * 100, delay: Math.random() * 0.5, dur: 1.6 + Math.random() * 1.4,
      rot: Math.random() * 360, color: cols[i % cols.length], size: 8 + Math.random() * 10
    }));
  });
  const last = levelIndex + 1 >= total;
  return (
    <div className="win">
      <div className="confetti">{confetti.map((c, i) => (
        <span key={i} style={{ left: c.left + '%', background: c.color, width: c.size, height: c.size,
          animationDelay: c.delay + 's', animationDuration: c.dur + 's', transform: `rotate(${c.rot}deg)` }} />))}
      </div>
      <div className="win-card">
        <div className="win-check">✓</div>
        <h2>Patched up!</h2>
        <p className="win-sub">Solved in <b>{fmtTime(seconds)}</b></p>
        <div className="win-actions">
          {!last && <button className="btn primary" onClick={onNext}>Next level ›</button>}
          {last && <button className="btn primary" onClick={onMenu}>All levels</button>}
          <button className="btn" onClick={onReplay}>Replay</button>
          <button className="btn ghost" onClick={onMenu}>Levels</button>
        </div>
      </div>
    </div>
  );
}

/* ============================================================= */
const SHAPE_INFO = [
  { shape: 'square', color: '#a855c7', title: 'Square', body: 'Width equals height. A Square clue must be solved by a square patch — 1×1, 2×2, 3×3, and so on.' },
  { shape: 'wide', color: '#e2574c', title: 'Wide', body: 'Width is strictly greater than height. 3×1, 4×2, 6×2 all count — but 2×2 is a square and does NOT satisfy a Wide clue.' },
  { shape: 'tall', color: '#2f9bdc', title: 'Tall', body: 'Height is strictly greater than width. 1×3, 2×4, 2×6 all count — but 3×3 is a square and does NOT satisfy a Tall clue.' },
  { shape: 'free', color: '#f4a623', title: 'Freeform', body: 'Any rectangle, including squares. You decide the shape — freeform clues give the most freedom but the most deduction work.' }
];
function ShapesGuide({ onClose }) {
  return (
    <div className="guide" onClick={onClose}>
      <div className="guide-card" onClick={e => e.stopPropagation()}>
        <button className="guide-x" onClick={onClose} aria-label="Close">×</button>
        <h2>The four shape types</h2>
        <p className="guide-lead">Every clue belongs to a shape family. A patch is only sewn when its <b>area</b> and its <b>shape</b> both match the clue.</p>
        <div className="guide-grid">
          {SHAPE_INFO.map(s => (
            <div className="guide-item" key={s.shape}>
              <div className="guide-swatch" style={{ color: s.color }}>
                {s.shape === 'free'
                  ? <span className="gs-free"><i /><i /></span>
                  : <span className={`gs-${s.shape}`} />}
              </div>
              <div className="guide-text">
                <h3>{s.title}</h3>
                <p>{s.body}</p>
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/* ============================================================= */
function Menu({ levels, unlocked, onPick }) {
  const [guide, setGuide] = useState(false);
  return (
    <div className="menu">
      <div className="menu-head">
        <div className="logo">
          <span className="logo-mark">
            <i style={{ background: '#5d62c4' }} /><i style={{ background: '#f4a623' }} />
            <i style={{ background: '#ea4c7d' }} /><i style={{ background: '#27a857' }} />
          </span>
          <h1>Patches</h1>
        </div>
        <p className="tagline">Sew every clue into a patch. Match its area <b>and</b> its shape — Square, Wide, Tall, or Freeform — and tile the whole board with no overlaps.</p>
        <button className="btn how-btn" onClick={() => setGuide(true)}>How to play · the four shapes</button>
      </div>
      <div className="menu-grid">
        {levels.map((lv, i) => {
          const locked = i + 1 > unlocked;
          let best = 0; try { best = Number(localStorage.getItem(timeKey(lv.id)) || 0); } catch (e) {}
          const done = best > 0;
          return (
            <button key={lv.id} className={`level-card ${locked ? 'locked' : ''} ${done ? 'done' : ''}`}
              disabled={locked} onClick={() => onPick(i)}>
              <span className="lc-num">{i + 1}</span>
              <span className="lc-name">{lv.name}</span>
              <span className="lc-meta">{lv.rows}×{lv.cols} · {lv.patches.length} patches</span>
              {locked && <span className="lc-lock">🔒</span>}
              {done && <span className="lc-done">✓ {fmtTime(best)}</span>}
            </button>
          );
        })}
      </div>
      {guide && <ShapesGuide onClose={() => setGuide(false)} />}
    </div>
  );
}

/* ============================================================= */
function App() {
  const { LEVELS } = window.PatchesLevels;
  const [prog, setProg] = useState(loadProgress);
  const parseHash = () => {
    const m = /^#L(\d+)$/.exec(location.hash);
    return m ? Math.max(0, Math.min(LEVELS.length - 1, +m[1] - 1)) : null;
  };
  const hi = parseHash();
  const [screen, setScreen] = useState(hi != null ? 'game' : 'menu');
  const [idx, setIdx] = useState(hi != null ? hi : 0);

  useEffect(() => {
    const onHash = () => {
      const h = parseHash();
      if (h == null) { setScreen('menu'); }
      else { setIdx(h); setScreen('game'); }
    };
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);

  const save = p => { setProg(p); try { localStorage.setItem(PROG_KEY, JSON.stringify(p)); } catch (e) {} };

  const onSolved = (levelIndex) => {
    if (levelIndex + 2 > prog.unlocked && levelIndex + 1 < LEVELS.length)
      save({ unlocked: levelIndex + 2 });
  };
  const nav = i => { if (i >= 0 && i < LEVELS.length && i + 1 <= prog.unlocked) { setIdx(i); location.hash = 'L' + (i + 1); } };

  if (screen === 'menu')
    return <Menu levels={LEVELS} unlocked={prog.unlocked}
      onPick={i => { setIdx(i); setScreen('game'); location.hash = 'L' + (i + 1); }} />;

  return <Game key={LEVELS[idx].id} level={LEVELS[idx]} levelIndex={idx} total={LEVELS.length}
    unlocked={prog.unlocked} onExit={() => { setScreen('menu'); location.hash = ''; }} onSolved={onSolved} onNav={nav} />;
}

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