<script lang="ts">
  import { onDestroy } from 'svelte';
  import { SvelteSet } from 'svelte/reactivity';
  import { app } from '../lib/app.svelte';
  import { t } from '../lib/i18n/index.svelte';
  import { ERUDIT_KEYS, SPLASH_TILES, splashSchedule, type SplashStep } from '../lib/splash';

  // Animate normally, but fall back to a static frame (no per-tile drop, no looping) under
  // reduced motion or the mock build — the latter so the Playwright smoke is not held up by
  // the overlay intercepting taps while it animates over the lobby.
  const instant = $derived(app.reduceMotion || import.meta.env.MODE === 'mock');

  // The keys of the tiles currently on screen; the schedule adds and (on a clear) removes them.
  const revealed = new SvelteSet<string>();
  let timers: ReturnType<typeof setTimeout>[] = [];

  function stop(): void {
    for (const id of timers) clearTimeout(id);
    timers = [];
  }
  function later(fn: () => void, ms: number): void {
    timers.push(setTimeout(fn, ms));
  }
  function finish(): void {
    stop();
    app.splashDone = true;
  }
  function clearVerticals(): void {
    for (const k of [...revealed]) if (!ERUDIT_KEYS.has(k)) revealed.delete(k);
  }
  function apply(step: SplashStep): void {
    if (step.kind === 'reveal' && step.key) revealed.add(step.key);
    else if (step.kind === 'check') {
      // Dismiss at a word boundary once the lobby's first load has settled.
      if (app.lobbyReady) finish();
    } else if (step.kind === 'clear') clearVerticals();
  }

  function runAnimated(): void {
    const sch = splashSchedule();
    for (const step of sch.prefix) later(() => apply(step), step.atMs);
    const loop = (): void => {
      if (app.splashDone) return;
      for (const step of sch.cycle) later(() => apply(step), step.atMs);
      later(loop, sch.cycleMs);
    };
    later(loop, sch.prefixMs);
  }

  function showStatic(): void {
    stop();
    for (const k of ERUDIT_KEYS) revealed.add(k);
    clearVerticals();
  }

  // Drive the splash. The effect re-runs if `instant` flips (the reduce-motion preference
  // loads early in bootstrap, possibly just after this overlay mounts), swapping the
  // animation for the static frame.
  $effect(() => {
    if (instant) {
      showStatic();
      return;
    }
    runAnimated();
    return stop;
  });

  // The static path has no word boundary to wait for, so dismiss as soon as the lobby settles.
  $effect(() => {
    if (instant && app.lobbyReady) app.splashDone = true;
  });

  onDestroy(stop);
</script>

<div class="splash" role="status" aria-busy="true">
  <span class="sr">{t('common.loading')}</span>
  <div class="grid" aria-hidden="true">
    {#each SPLASH_TILES as tile (tile.key)}
      {#if revealed.has(tile.key)}
        <div
          class="tile"
          class:drop={!instant}
          style="grid-row: {tile.row + 1}; grid-column: {tile.col + 1};"
        >
          <span class="letter">{tile.letter}</span>
          <span class="val">{tile.value}</span>
        </div>
      {/if}
    {/each}
  </div>
</div>

<style>
  .splash {
    position: fixed;
    inset: 0;
    /* Above the lobby and any modal (Modal.svelte z 40), below toasts (z 50) so an offline
       error toast still shows over the splash. */
    z-index: 45;
    background: var(--bg);
    display: grid;
    place-items: center;
  }
  .grid {
    /* Six columns wide, eight rows tall — capped so the whole crossword fits a phone portrait
       (width) and is not oversized on desktop. */
    --tile: min(14vw, 9vh, 56px);
    display: grid;
    grid-template-columns: repeat(6, var(--tile));
    grid-template-rows: repeat(8, var(--tile));
    gap: calc(var(--tile) * 0.12);
  }
  /* The tile mirrors a placed board tile (game/Board.svelte .cell.filled): the same cream
     stock, bottom edge and drop shadow, with the letter top-left and value bottom-right. */
  .tile {
    position: relative;
    background: var(--tile-bg);
    color: var(--tile-text);
    border-radius: 4px;
    box-shadow:
      inset 0 -2px 0 var(--tile-edge),
      2px 0 3px -1px rgba(0, 0, 0, 0.4);
  }
  /* "Falling" tile: it appears a touch larger and translucent and settles to size and full
     opacity as it lands. */
  .tile.drop {
    animation: tile-drop 180ms ease-out;
  }
  @keyframes tile-drop {
    from {
      transform: scale(1.15);
      opacity: 0.4;
    }
    to {
      transform: scale(1);
      opacity: 1;
    }
  }
  @media (prefers-reduced-motion: reduce) {
    .tile.drop {
      animation: none;
    }
  }
  .letter {
    position: absolute;
    top: 6%;
    left: 10%;
    font-size: calc(var(--tile) * 0.5);
    font-weight: 700;
    line-height: 1;
  }
  .val {
    position: absolute;
    right: 8%;
    bottom: 5%;
    font-size: calc(var(--tile) * 0.28);
    font-weight: 600;
  }
  /* Visually-hidden live label for assistive tech; the tiles themselves are decorative. */
  .sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
</style>
