<script lang="ts">
  // Apple-lock-screen-style 4-digit PIN pad for offline pass-and-play (hotseat) games.
  // Modes:
  //   set     — enter a PIN, then repeat it to confirm; emits { kind: 'set', lock }.
  //   verify  — enter a PIN; runs `verify(pin)` on the 4th digit; emits { kind: 'verified' }
  //             or shakes + clears on a wrong PIN.
  //   change  — run `verify` on the current PIN, then offer "set a new PIN" (the set flow)
  //             or, when allowRemove, "remove PIN" ({ kind: 'removed' }).
  // Verification is delegated to the caller's `verify` callback: at creation time it checks a
  // UI-held lock (lib/pin verifyPin); in-game it delegates to the local source (which holds the
  // stored lock). The pad never shows an OK button: entering the 4th digit is the action. PIN
  // storage is a social lock, not cryptography — see lib/pin.ts.
  import Modal from './Modal.svelte';
  import { t } from '../lib/i18n/index.svelte';
  import { newLock, type PinLock } from '../lib/pin';

  /** PinResult is the outcome handed to onresult when the pad completes. */
  export type PinResult =
    | { kind: 'set'; lock: PinLock }
    | { kind: 'verified' }
    | { kind: 'removed' };

  let {
    mode,
    title = '',
    verify,
    allowRemove = false,
    onclose,
    onresult,
  }: {
    mode: 'set' | 'verify' | 'change';
    title?: string;
    /** Verifies an entered PIN (required for 'verify' and 'change'). */
    verify?: (pin: string) => Promise<boolean>;
    allowRemove?: boolean;
    onclose?: () => void;
    onresult: (r: PinResult) => void;
  } = $props();

  const LEN = 4;

  // 'old' verifies the existing PIN (change); 'menu' offers set-new / remove (change);
  // 'enter' collects a PIN; 'confirm' re-enters it (set / change→new). `mode` is fixed
  // for the pad's lifetime (callers mount a fresh pad per operation), so capturing its
  // initial value here is intentional.
  // svelte-ignore state_referenced_locally
  let phase = $state<'old' | 'menu' | 'enter' | 'confirm'>(mode === 'change' ? 'old' : 'enter');
  let buffer = $state('');
  let first = $state(''); // the first entry, awaiting confirmation
  let error = $state(false); // drives the error prompt + shake
  let shakeSeq = $state(0); // bumped on each error to replay the shake animation
  let busy = $state(false); // guards the async check between the 4th digit and the outcome

  function promptText(): string {
    if (error) return phase === 'confirm' ? t('pin.mismatch') : t('pin.wrong');
    switch (phase) {
      case 'old':
        return t('pin.enterCurrent');
      case 'confirm':
        return t('pin.repeat');
      default:
        return mode === 'verify' ? t('pin.enter') : t('pin.create');
    }
  }

  function fail(): void {
    buffer = '';
    error = true;
    shakeSeq += 1;
  }

  async function commit(): Promise<void> {
    busy = true;
    try {
      // Let the 4th dot register before the verdict: a beat so the fill (and, on failure, the shake)
      // reads as a deliberate response rather than an instant flicker.
      await new Promise((r) => setTimeout(r, 250));
      if (phase === 'old') {
        if (verify && (await verify(buffer))) {
          buffer = '';
          error = false;
          phase = 'menu';
        } else fail();
      } else if (phase === 'enter' && mode === 'verify') {
        if (verify && (await verify(buffer))) onresult({ kind: 'verified' });
        else fail();
      } else if (phase === 'enter') {
        first = buffer;
        buffer = '';
        error = false;
        phase = 'confirm';
      } else if (phase === 'confirm') {
        if (buffer === first) onresult({ kind: 'set', lock: await newLock(buffer) });
        else {
          first = '';
          phase = 'enter';
          fail();
        }
      }
    } finally {
      busy = false;
    }
  }

  function press(d: string): void {
    if (busy || phase === 'menu' || buffer.length >= LEN) return;
    error = false;
    buffer += d;
    if (buffer.length === LEN) void commit();
  }

  function back(): void {
    if (busy || phase === 'menu') return;
    buffer = buffer.slice(0, -1);
  }

  function startNew(): void {
    error = false;
    first = '';
    buffer = '';
    phase = 'enter';
  }

  function onkey(e: KeyboardEvent): void {
    if (e.key >= '0' && e.key <= '9') {
      press(e.key);
      e.preventDefault();
    } else if (e.key === 'Backspace') {
      back();
      e.preventDefault();
    } else if (e.key === 'Escape') {
      onclose?.();
    }
  }

  const keys = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
  const slots = Array.from({ length: LEN }, (_, i) => i);
</script>

<svelte:window onkeydown={onkey} />

<Modal {title} {onclose} overlayKeyboard>
  <div class="pad">
    {#if phase === 'menu'}
      <div class="menu">
        <button class="opt" onclick={startNew}>{t('pin.setNew')}</button>
        {#if allowRemove}
          <button class="opt danger" onclick={() => onresult({ kind: 'removed' })}>
            {t('pin.remove')}
          </button>
        {/if}
      </div>
    {:else}
      <p class="prompt" class:err={error}>{promptText()}</p>
      {#key shakeSeq}
        <div class="dots" class:shake={error} aria-hidden="true">
          {#each slots as i}
            <span class="dot" class:on={i < buffer.length}></span>
          {/each}
        </div>
      {/key}
      <div class="grid">
        {#each keys as k}
          <button class="key" onclick={() => press(k)} aria-label={k}>{k}</button>
        {/each}
        <span class="key spacer"></span>
        <button class="key" onclick={() => press('0')} aria-label="0">0</button>
        <button class="key back" onclick={back} aria-label="⌫" disabled={buffer.length === 0}>⌫</button>
      </div>
    {/if}
  </div>
</Modal>

<style>
  .pad {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    padding-top: 4px;
  }
  .prompt {
    margin: 0;
    font-size: 0.95rem;
    color: var(--muted, var(--text));
    min-height: 1.2em;
  }
  .prompt.err {
    color: var(--danger);
  }
  .dots {
    display: flex;
    gap: 16px;
  }
  .dot {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    border: 1.5px solid var(--text);
    box-sizing: border-box;
  }
  .dot.on {
    background: var(--text);
  }
  .shake {
    animation: shake 0.32s ease;
  }
  @keyframes shake {
    0%, 100% { transform: translateX(0); }
    20% { transform: translateX(-7px); }
    40% { transform: translateX(7px); }
    60% { transform: translateX(-5px); }
    80% { transform: translateX(5px); }
  }
  .grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 12px;
  }
  .key {
    width: 62px;
    height: 62px;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    font-size: 1.4rem;
    font-weight: 500;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .key:active {
    background: var(--accent);
    color: var(--accent-text);
  }
  .key.back {
    border-color: transparent;
    background: transparent;
    font-size: 1.2rem;
  }
  .key.back:disabled {
    opacity: 0.3;
  }
  .key.spacer {
    border: none;
    background: none;
    pointer-events: none;
  }
  .menu {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 100%;
  }
  .opt {
    padding: 14px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    border-radius: var(--radius);
    font-weight: 600;
  }
  .opt.danger {
    border-color: var(--danger);
    color: var(--danger);
  }
</style>
