<script>
  import { getJSON } from '$lib/api.js';
  import { lineFrom, matchWord, cellFromPoint } from '$lib/wordsearch.js';

  let { size = 'med', onstatus } = $props();

  // A calm, distinct palette — each found word gets its own colour.
  const PALETTE = ['#a7d3c2', '#f4cf88', '#a9c8ec', '#eab0c0', '#cdb8e6',
                   '#f3a98a', '#c2d99a', '#e3b0d0', '#9fd0d8', '#d9c79a'];

  let theme = $state('');
  let words = $state([]);
  let grid = $state([]);            // array of row strings
  let date = $state('');
  let foundWords = $state([]);      // {word, cells:[[r,c]], ci}
  let sel = $state([]);             // current selection cells
  let selecting = false;
  let startTime = 0;
  let resultMs = $state(0);
  let best = $state(0);
  let loading = $state(true);
  let ready = $state(false);
  let okFlash = $state(false);
  let copied = $state(false);
  let gridEl = $state(null);
  let loadSeq = 0;

  const n = $derived(grid.length);
  const stateKey = $derived(`goodnews:wordsearch:${size}:${date}`);
  const bestKey = $derived(`goodnews:wordsearch:best:${size}`);
  const found = $derived(foundWords.map((w) => w.word));
  const status = $derived(words.length && foundWords.length === words.length ? 'done' : 'playing');
  const selSet = $derived(new Set(sel.map(([r, c]) => r + ',' + c)));
  const cellColor = $derived.by(() => {
    const m = new Map();
    for (const w of foundWords) for (const [r, c] of w.cells) m.set(r + ',' + c, w.ci);
    return m;
  });
  const wordColor = $derived.by(() => {
    const m = new Map();
    for (const w of foundWords) m.set(w.word, w.ci);
    return m;
  });

  async function load() {
    const seq = ++loadSeq;                 // stale-load guard for rapid size switches
    loading = true; ready = false;
    foundWords = []; sel = []; resultMs = 0; startTime = 0;
    try {
      const p = await getJSON('/api/puzzle/wordsearch?variant=' + size);
      if (seq !== loadSeq) return;         // a newer size was selected — abandon
      theme = p.theme; words = p.words; grid = p.grid; date = p.date;
      restore();
      if (!startTime) startTime = Date.now();
      try { best = JSON.parse(localStorage.getItem(bestKey) || '0'); } catch { best = 0; }
    } catch {
      if (seq !== loadSeq) return;
      theme = ''; words = [];
    }
    if (seq !== loadSeq) return;
    loading = false;
    requestAnimationFrame(() => (ready = true));
  }

  function restore() {
    try {
      const s = JSON.parse(localStorage.getItem(stateKey) || 'null');
      if (s && Array.isArray(s.foundWords)) {
        // Keep only finds whose stored cells still spell their word in the CURRENT
        // grid — guards against stale highlights if the day's puzzle changed.
        const valid = s.foundWords.filter((fw) =>
          fw && Array.isArray(fw.cells) && words.includes(fw.word) &&
          fw.cells.map(([r, c]) => (grid[r] && grid[r][c]) || '').join('') === fw.word);
        foundWords = valid;
        startTime = s.startTime || 0;
        resultMs = valid.length === words.length ? (s.ms || 0) : 0;
      }
    } catch { /* ignore */ }
    onstatus?.(summary());
  }
  function persist() {
    try { localStorage.setItem(stateKey, JSON.stringify({ foundWords, startTime, ms: resultMs, status })); }
    catch { /* ignore */ }
    onstatus?.(summary());
  }
  function summary() { return { game: 'wordsearch', size, date, status, found: foundWords.length, total: words.length, ms: resultMs }; }

  function cellAt(e) {
    return cellFromPoint(gridEl.getBoundingClientRect(), e.clientX, e.clientY, n);
  }

  function down(e) {
    if (status === 'done') return;
    selecting = true;
    if (!startTime) startTime = Date.now();
    sel = [cellAt(e)];
    gridEl.setPointerCapture?.(e.pointerId);
    e.preventDefault();
  }
  function move(e) {
    if (!selecting) return;
    sel = lineFrom(sel[0], cellAt(e), n);
  }
  function up() {
    if (!selecting) return;
    selecting = false;
    evaluate(sel);
    sel = [];
  }

  function evaluate(cells) {
    const hit = matchWord(cells, grid, words, found);
    if (!hit) return;
    foundWords = [...foundWords, { word: hit, cells: cells.map((c) => [...c]), ci: foundWords.length % PALETTE.length }];
    okFlash = true; setTimeout(() => (okFlash = false), 500);
    if (foundWords.length === words.length) finish();
    persist();
  }

  function finish() {
    resultMs = startTime ? Date.now() - startTime : 0;
    if (resultMs && (!best || resultMs < best)) {
      best = resultMs;
      try { localStorage.setItem(bestKey, JSON.stringify(best)); } catch { /* ignore */ }
    }
  }

  function fmt(ms) {
    const s = Math.round(ms / 1000);
    return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0');
  }

  function share() {
    const label = { small: 'Small', med: 'Medium', large: 'Large' }[size] || '';
    const text = `Upbeat Bytes · Word Search (${label}) ${date}\n${theme} — cleared in ${fmt(resultMs)}\nupbeatbytes.com/play`;
    if (navigator.share) navigator.share({ text }).catch(() => {});
    else navigator.clipboard?.writeText(text).then(() => { copied = true; setTimeout(() => (copied = false), 1500); });
  }

  // Load on mount and whenever the size changes. Tracks ONLY `size`.
  $effect(() => { size; load(); });
</script>

<div class="wordsearch" class:ready>
  {#if loading}
    <p class="muted">Loading today’s word search…</p>
  {:else if !words.length}
    <p class="muted">Could not load today’s word search.</p>
  {:else}
    <p class="theme">{theme}</p>

    <div class="gridwrap">
      <div class="grid" class:ok={okFlash} class:done={status === 'done'} bind:this={gridEl} style="--n:{n}"
           role="application" aria-label="Word search grid — drag across letters to find words"
           onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up}>
        {#each grid as rowStr, r (r)}
          {#each rowStr.split('') as ch, c (c)}
            {@const key = r + ',' + c}
            <div class="cell" class:sel={selSet.has(key)}
                 style={cellColor.has(key) && !selSet.has(key) ? `background:${PALETTE[cellColor.get(key)]};color:#2a2f36` : ''}>{ch}</div>
          {/each}
        {/each}
      </div>
    </div>

    <div class="palette">
      <p class="plabel">Find these · {foundWords.length}/{words.length}</p>
      <ul class="words">
        {#each words as w (w)}
          <li class:got={found.includes(w)}
              style={wordColor.has(w) ? `background:${PALETTE[wordColor.get(w)]};color:#2a2f36` : ''}>{w}</li>
        {/each}
      </ul>
    </div>

    {#if status === 'done'}
      <div class="result rise">
        <p class="rmark">✦ all found ✦</p>
        <div class="times">
          <div><span class="n">{fmt(resultMs)}</span><span class="l">your time</span></div>
          {#if best}<div><span class="n">{fmt(best)}</span><span class="l">best</span></div>{/if}
        </div>
        <button class="share" onclick={share}>{copied ? 'Copied!' : 'Share result'}</button>
      </div>
    {:else}
      <p class="hint">{foundWords.length} / {words.length} found · drag across the letters</p>
    {/if}
  {/if}
</div>

<style>
  .wordsearch { max-width: 520px; margin: 0 auto; opacity: 0; transform: translateY(6px); }
  .wordsearch.ready { opacity: 1; transform: none; transition: opacity 0.3s ease, transform 0.3s ease; }
  .theme { text-align: center; font-family: var(--serif); font-weight: 600; font-size: 1.6rem;
           color: var(--accent-deep); margin: 0 0 14px; }
  .gridwrap { display: flex; justify-content: center; }
  .grid {
    /* Cells are ~32px on a wide screen (board grows with the grid) and shrink via
       1fr to fit a narrow phone — capped by max-width so it can never overflow. */
    display: grid; grid-template-columns: repeat(var(--n), 1fr); gap: 2px;
    width: 100%; max-width: calc(var(--n) * 32px + 16px); margin: 0 auto 16px;
    touch-action: none; user-select: none; -webkit-user-select: none;
    border-radius: 10px; padding: 6px; background: var(--surface); border: 1px solid var(--line);
  }
  .grid.done { opacity: 0.9; }
  .cell {
    aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
    font-family: var(--label); font-weight: 600; font-size: clamp(0.7rem, 2.6vw, 1.05rem);
    color: var(--ink); border-radius: 5px; background: transparent; text-transform: uppercase;
  }
  .cell.sel { background: var(--accent) !important; color: #fff !important; }
  /* Scale each letter to ~42% of its cell so the letter-to-spacing ratio is uniform
     across Small/Med/Large (Large is the reference). Guarded so older browsers keep
     the clamp() fallback above. */
  @supports (width: 1cqw) {
    .grid { container-type: inline-size; }
    .cell { font-size: calc(100cqw / var(--n) * 0.42); }
  }
  .palette { background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
             padding: 12px 14px 14px; margin: 0 auto 16px; max-width: 440px; box-shadow: var(--shadow); }
  .plabel { text-transform: uppercase; letter-spacing: 0.12em; font-size: 0.62rem; font-family: var(--label);
            color: var(--muted); text-align: center; margin: 0 0 10px; }
  .words { list-style: none; display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; padding: 0; margin: 0; }
  /* Unfound words are plain (transparent + outline) so it's easy to see what's
     left as the board fills; found words get their grid colour via inline style. */
  .words li { font-family: var(--label); font-size: 0.82rem; letter-spacing: 0.04em; color: var(--ink);
              padding: 4px 11px; border-radius: 999px; background: transparent;
              border: 1px solid var(--line); transition: background 0.2s ease, border-color 0.2s ease; }
  .words li.got { border-color: transparent; }
  .hint { text-align: center; color: var(--muted); font-size: 0.84rem; margin: 0; }
  .result { text-align: center; }
  .rmark { font-family: var(--serif); font-style: italic; color: var(--accent-deep); font-size: 1.2rem; margin: 0 0 12px; }
  .times { display: flex; justify-content: center; gap: 26px; margin: 0 0 16px; }
  .times .n { display: block; font-size: 1.5rem; font-weight: 700; font-family: var(--label); }
  .times .l { color: var(--muted); font-size: 0.78rem; }
  .share { background: var(--accent); color: #fff; border: none; border-radius: 999px;
           padding: 11px 26px; font: inherit; font-weight: 600; cursor: pointer; }
  .share:hover { background: var(--accent-deep); }
  .muted { color: var(--muted); text-align: center; }

  /* Mobile: a focused game screen (matches Daily Word). The grid sizes to the
     largest square that fits the space left between the theme and the palette,
     so the whole puzzle is on screen with no page scroll. */
  @media (max-width: 720px) {
    .wordsearch { display: flex; flex-direction: column; height: 100%; max-width: 100%; }
    .theme { flex-shrink: 0; font-size: 1.5rem; margin: 4px 0 8px; }
    .gridwrap { flex: 1; min-height: 0; container-type: size; align-items: center; padding: 2px 0; }
    .grid { width: min(100%, 100cqh, calc(var(--n) * 40px)); max-width: none; margin: 0; }
    .palette { flex-shrink: 0; margin: 10px auto 0; padding: 9px 12px 10px; max-width: 100%;
               width: 100%; box-sizing: border-box; }
    .plabel { margin-bottom: 7px; }
    .words { gap: 6px; }
    .words li { font-size: 0.78rem; padding: 3px 9px; }
    .hint { flex-shrink: 0; padding: 9px 0 calc(env(safe-area-inset-bottom) + 6px); }
    .result { flex-shrink: 0; padding-bottom: calc(env(safe-area-inset-bottom) + 6px); }
  }
</style>
