<script lang="ts">
  import type { RackSlot } from '../lib/placement';
  import { BLANK } from '../lib/placement';
  import { valueForLetter } from '../lib/alphabet';
  import type { Variant } from '../lib/model';
  import { usesStarBlank, BLANK_STAR } from '../lib/variants';
  import type { Snippet } from 'svelte';

  let {
    slots,
    variant,
    selected,
    shuffling = false,
    draggingId = null,
    dropIndex = null,
    frozen = false,
    confirm,
    ondown,
  }: {
    // Each slot carries a stable id that travels with its tile through a shuffle, so the
    // keyed list reorders (rather than relabelling in place) and the hop animation fires.
    slots: (RackSlot & { id: number })[];
    variant: Variant;
    selected: number | null;
    shuffling?: boolean;
    // While a rack tile is being dragged to reorder it, draggingId is its id (hidden here —
    // the drag ghost stands in) and dropIndex is the slot where a gap opens.
    draggingId?: number | null;
    dropIndex?: number | null;
    /** frozen disables tile interaction (offline in an online game): tiles cannot be picked up to
     *  compose or reorder a move until the connection returns. The confirm control is gated
     *  separately by the parent. */
    frozen?: boolean;
    /** The confirm-move control, rendered as the fixed 7th slot of the rack while a play is staged
     *  (the parent owns the button and its enablement; the rack only positions it). */
    confirm?: Snippet;
    ondown: (e: PointerEvent, index: number) => void;
  } = $props();

  // Used slots are hidden (the rack shifts left, freeing room on the right for the
  // MakeMove control); the slot still exists in the model for per-tile recall. While
  // reordering, the dragged tile is lifted out (the ghost shows it).
  const visible = $derived(slots.filter((s) => !s.used));
  const shown = $derived(draggingId == null ? visible : visible.filter((s) => s.id !== draggingId));

  // hop flies a tile to its shuffled position along a low parabola (apogee ≈ half a tile
  // height). The duration scales with the horizontal distance — i.e. the arc length — so
  // the longest swap (slot 1 ↔ 7) takes ~0.3s and shorter swaps land sooner. It runs only
  // while a shuffle is in progress (and motion is not reduced); ordinary reflow
  // (placing/recalling a tile) is instant.
  function hop(node: HTMLElement, { from, to }: { from: DOMRect; to: DOMRect }, active: boolean) {
    const dx = from.left - to.left;
    const dy = from.top - to.top;
    const dist = Math.hypot(dx, dy);
    if (!active || dist < 2) return { duration: 0 };
    const span = node.parentElement?.getBoundingClientRect().width || dist;
    const lift = (to.height || from.height) * 0.5;
    return {
      duration: Math.max(120, Math.min(300, (dist / span) * 340)),
      css: (t: number, u: number) =>
        `transform: translate(${dx * u}px, ${dy * u - Math.sin(Math.PI * t) * lift}px);`,
    };
  }
</script>

<div class="rack" class:reordering={draggingId != null || dropIndex != null} data-rack>
  {#each shown as slot, i (slot.id)}
    <button
      class="tile"
      class:selected={selected === slot.index}
      class:shift={dropIndex != null && i >= dropIndex}
      data-rack-index={slot.index}
      disabled={frozen}
      animate:hop={shuffling}
      onpointerdown={(e) => ondown(e, slot.index)}
    >
      {#if slot.letter === BLANK}
        {#if usesStarBlank(variant)}<span class="star">{BLANK_STAR}</span>{/if}
      {:else}
        <span class="letter">{slot.letter}</span>
        <span class="val">{valueForLetter(variant, slot.letter)}</span>
      {/if}
    </button>
  {/each}
  {@render confirm?.()}
</div>

<style>
  /* Seven equal columns filling the whole tray width, so the tiles are square and as large as the
     row allows — the same in portrait and landscape. The tile glyphs use cqw against the rack width
     (the query container) so they track the tile size, the way the board sizes its labels on its
     .scaler. */
  .rack {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 5px;
    align-items: start;
    container-type: inline-size;
    /* Reserve about one tile's height so an empty rack (e.g. a finished game) keeps the footer the
       same size as during play — no layout jump between states. */
    min-height: min(12.5vw, 46px);
  }
  .tile {
    position: relative;
    aspect-ratio: 1;
    background: var(--tile-bg);
    color: var(--tile-text);
    border: none;
    border-radius: 5px;
    box-shadow: inset 0 -3px 0 var(--tile-edge);
    font-weight: 700;
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
    /* iOS shows a tap/active highlight that can linger on the neighbour sliding into a
       dragged tile's slot; suppress it so only our own styles mark a tile. */
    -webkit-tap-highlight-color: transparent;
  }
  .tile.selected {
    outline: 3px solid var(--accent);
    outline-offset: -3px;
  }
  /* Frozen (offline in an online game): the tray is inert until the connection returns. */
  .tile:disabled {
    opacity: 0.5;
  }
  /* While reordering, tiles at/after the drop slot slide right to open a gap there (one
     tile width plus the rack gap), so the drop position is visible. */
  .rack.reordering .tile {
    transition: transform 0.14s ease;
  }
  .tile.shift {
    transform: translateX(calc(100% + 5px));
  }
  .letter {
    position: absolute;
    top: 8%;
    left: 14%;
    font-size: 6vmin; /* cqw fallback for Chrome < 105 — approximates the portrait rack ≈ viewport width */
    font-size: 6cqw;
  }
  .val {
    position: absolute;
    right: 4px;
    bottom: 1px;
    font-size: 3.1vmin; /* cqw fallback for Chrome < 105 */
    font-size: 3.1cqw;
    font-weight: 600;
  }
  /* Erudit's blank ("звёздочка") shows its star horizontally centred on the otherwise empty
     tile face; the top offset centres its ink against the neighbouring letters' block, nudged
     up a pixel to sit right by eye (it is slightly larger than them so it reads). */
  .star {
    position: absolute;
    top: calc(0.5% - 1px);
    left: 0;
    right: 0;
    text-align: center;
    font-size: 7.6vmin; /* cqw fallback for Chrome < 105 */
    font-size: 7.6cqw;
  }
</style>
