import { useEffect, useMemo } from 'react';

import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';

import { play, type BoardState } from './engine';
import { useSolver } from './useSolver';
import { allowedNow, ANSWERS, primeGuessList } from './words';

const ROWS = ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'];

type TileState = 'G' | 'Y' | 'X';

/** Best-known state of each letter, for tinting the keys. */
function letterStates(board: BoardState): Record<string, TileState> {
  const rank = { X: 0, Y: 1, G: 2 } as const;
  const out: Record<string, TileState> = {};
  for (const { guess, pattern } of board.rows) {
    for (let i = 0; i < guess.length; i += 1) {
      const letter = guess[i]!;
      const tile = pattern[i] as TileState;
      const current = out[letter];
      if (!current || rank[tile] > rank[current]) out[letter] = tile;
    }
  }
  return out;
}

/**
 * A used key takes the tile token at FULL opacity. The old `/70` on absent
 * made the grey key a fourth colour the board never shows; at full strength
 * the key is the tile, and the ink on it is measured there (7.75 dark, 5.74
 * light — see index.css).
 */
const KEY_TINT: Record<TileState, string> = {
  G: 'bg-tile-exact text-tile-exact-fg hover:bg-tile-exact',
  Y: 'bg-tile-present text-tile-present-fg hover:bg-tile-present',
  X: 'bg-tile-absent text-tile-absent-fg hover:bg-tile-absent',
};

/** The board's high-contrast glyphs, repeated on the keys so the keyboard has the same second channel. */
const KEY_GLYPH: Record<TileState, string> = { G: '●', Y: '◆', X: '' };

/**
 * One key. Letter keys are `flex-1` with no minimum width: ten of them plus
 * nine gaps have to fit 326px on a phone, which is roughly 29px each — the
 * 44px target is the height. ENTER and DEL are wider and set in mono, the
 * way a command is.
 */
const KEY =
  'min-h-11 min-w-0 select-none rounded-control transition-[background-color,color,transform] duration-1 ease-enter ' +
  'motion-safe:active:scale-[0.94] disabled:pointer-events-none disabled:opacity-40';

export function Keyboard({
  state,
  onChange,
}: {
  state: BoardState;
  onChange: (next: BoardState) => void;
}) {
  const states = useMemo(() => letterStates(state), [state]);
  const done = state.status !== 'playing';
  const solver = useSolver(ANSWERS);

  useEffect(() => {
    void primeGuessList();
  }, []);

  const press = (key: string) => {
    if (done) return;
    if (key === 'enter') {
      if (state.draft.length !== 5) {
        onChange({ ...state, invalid: 'not enough letters' });
        return;
      }
      onChange(play(state, state.draft, allowedNow()));
      return;
    }
    if (key === 'back') {
      onChange({ ...state, draft: state.draft.slice(0, -1), invalid: null });
      return;
    }
    if (state.draft.length < 5) {
      onChange({ ...state, draft: state.draft + key, invalid: null });
    }
  };

  // A physical keyboard is how anyone on a laptop will actually play, and
  // wiring only the on-screen keys is the most common way that gets forgotten.
  useEffect(() => {
    const handler = (event: KeyboardEvent) => {
      if (event.metaKey || event.ctrlKey || event.altKey) return;
      const target = event.target as HTMLElement | null;
      if (target && /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName)) return;
      if (event.key === 'Enter') press('enter');
      else if (event.key === 'Backspace') press('back');
      else if (/^[a-zA-Z]$/.test(event.key)) press(event.key.toLowerCase());
      else return;
      event.preventDefault();
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  });

  const hint = solver.state;

  return (
    // Own cap, not the shell's. Unconstrained, these flex rows stretch to the
    // page and the keys drift a screen away from the board they belong to.
    <div
      // 28rem caps a letter key at 39px on a 1024px iPad; a coarse pointer
      // gets 32rem, which is the narrowest width at which ten keys and nine
      // 6px gaps are all 44px. The phone never reaches either cap.
      className="grid w-full max-w-md gap-1 pb-[max(0px,var(--safe-bottom))] sm:gap-1.5 [@media(pointer:coarse)]:max-w-lg"
    >
      {/* The solver, live, on whatever word you picked — not a recording. This
          is the difference between a demo and a video: it answers for boards no
          rollout on this site ever covered. */}
      {hint.status !== 'unavailable' && !done ? (
        <div className="mb-1 flex flex-wrap items-center gap-x-3 gap-y-1.5">
          <Button
            type="button"
            variant="outline"
            size="touch"
            onClick={() => solver.ask(state.rows)}
            disabled={hint.status === 'thinking'}
          >
            {hint.status === 'thinking' ? 'Thinking…' : 'What would the best player guess?'}
          </Button>
          <span aria-live="polite" className="font-mono text-mono-data text-muted">
            {hint.status === 'ready' ? (
              <>
                <span className="font-semibold uppercase text-fg">{hint.suggestion.guess}</span>
                {' — '}
                {hint.suggestion.bits.toFixed(2)} bits from{' '}
                {hint.suggestion.candidatesBefore.toLocaleString('en-US')} remaining
                {hint.suggestion.viable ? null : (
                  <>
                    {' · '}
                    <span className="text-warning">cannot win — it is buying information</span>
                  </>
                )}
              </>
            ) : null}
          </span>
        </div>
      ) : null}
      {ROWS.map((row, index) => (
        <div key={row} className="flex justify-center gap-1 sm:gap-1.5">
          {index === 2 ? (
            <button
              type="button"
              onClick={() => press('enter')}
              disabled={done}
              className={cn(
                KEY,
                'flex-[1.6] bg-surface-2 px-1 font-mono text-label uppercase text-fg hover:bg-surface-3',
              )}
            >
              Enter
            </button>
          ) : null}
          {[...row].map((letter) => {
            const used = states[letter];
            return (
              <button
                key={letter}
                type="button"
                onClick={() => press(letter)}
                disabled={done}
                aria-label={letter.toUpperCase()}
                className={cn(
                  KEY,
                  'relative flex-1 font-sans text-[0.8125rem] font-semibold uppercase',
                  used ? KEY_TINT[used] : 'bg-surface-2 text-fg hover:bg-surface-3',
                )}
              >
                {letter}
                {used && KEY_GLYPH[used] ? (
                  <span
                    aria-hidden="true"
                    className="pointer-events-none absolute bottom-0.5 right-0.5 text-[0.5em] leading-none opacity-0 [:root[data-contrast='high']_&]:opacity-100"
                  >
                    {KEY_GLYPH[used]}
                  </span>
                ) : null}
              </button>
            );
          })}
          {index === 2 ? (
            <button
              type="button"
              onClick={() => press('back')}
              disabled={done}
              aria-label="Backspace"
              className={cn(
                KEY,
                'flex-[1.6] bg-surface-2 px-1 font-mono text-label uppercase text-fg hover:bg-surface-3',
              )}
            >
              Del
            </button>
          ) : null}
        </div>
      ))}
    </div>
  );
}

export default Keyboard;
