<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';

  let {
    slots,
    variant,
    selected,
    landscape = false,
    shuffling = false,
    draggingId = null,
    dropIndex = null,
    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;
    /** Landscape layout: size the tiles to share the (narrow) left-panel width instead of the
     *  viewport-width measure, so seven tiles never overflow the panel. */
    landscape?: boolean;
    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;
    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} class:landscape 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}
      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}
</div>

<style>
  .rack {
    display: flex;
    gap: 5px;
    align-items: center;
    /* Reserve 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;
    flex: 0 0 auto;
    width: min(12.5vw, 46px);
    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;
    font-size: 1.4rem;
    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;
  }
  /* Landscape: the rack sits in a fixed-width left panel, so the tiles share the panel width
     (capped at the portrait size) and shrink below it when the panel is narrow, instead of the
     viewport-width measure that would overflow seven tiles in a column. */
  .rack.landscape .tile {
    flex: 1 1 0;
    width: auto;
    min-width: 0;
    max-width: 46px;
  }
  /* 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%;
  }
  .val {
    position: absolute;
    right: 4px;
    bottom: 1px;
    font-size: 0.7rem;
    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: 1.7rem;
  }
</style>
