import { memo } from 'react';

import { cn } from '@/lib/utils';

import { MAX_GUESSES, WORD_LENGTH, type BoardState, type Pattern } from './engine';

/**
 * The board. One component for all three jobs — you playing, the replay, and
 * the gallery thumbnail — because three near-identical boards is how they drift.
 */

const TILE_CLASS: Record<string, string> = {
  G: 'bg-tile-exact text-tile-exact-fg border-tile-exact',
  Y: 'bg-tile-present text-tile-present-fg border-tile-present',
  X: 'bg-tile-absent text-tile-absent-fg border-tile-absent',
};

/**
 * A glyph per state, shown only in high-contrast mode.
 *
 * Green/yellow/grey is a colour-only distinction, which is exactly why the
 * original game ships a high-contrast mode. A second channel means the result
 * survives deuteranopia, a projector with the colour balance wrong, and a
 * screenshot printed in black and white.
 */
const TILE_GLYPH: Record<string, string> = { G: '●', Y: '◆', X: '' };

function Tile({
  letter,
  tile,
  index,
  revealing,
  compact,
}: {
  letter: string;
  tile: Pattern[number] | null;
  index: number;
  revealing: boolean;
  compact?: boolean;
}) {
  const filled = letter !== '';
  return (
    <div
      className={cn(
        'relative grid place-items-center select-none font-semibold uppercase',
        'aspect-square rounded-md border-2 transition-colors',
        compact ? 'text-[0.55rem] border' : 'text-xl sm:text-2xl',
        tile
          ? TILE_CLASS[tile]
          : filled
            ? 'border-muted/60 bg-surface text-fg'
            : 'border-border bg-surface-2/40 text-fg',
        // The flip is a rotation about X with the colour landing at the
        // half-way point, staggered along the row. `prefers-reduced-motion`
        // clamps it to nothing globally, which is why announceRow() exists.
        revealing && tile && 'motion-safe:animate-[tile-flip_520ms_ease-enter_both]',
        filled && !tile && 'motion-safe:animate-[tile-pop_120ms_ease-enter]',
      )}
      style={revealing ? { animationDelay: `${index * 100}ms` } : undefined}
      aria-hidden="true"
    >
      {letter}
      {tile ? (
        <span className="pointer-events-none absolute bottom-0 right-0.5 text-[0.5em] leading-none opacity-0 [:root[data-contrast='high']_&]:opacity-90">
          {TILE_GLYPH[tile]}
        </span>
      ) : null}
    </div>
  );
}

function Row({
  guess,
  pattern,
  revealing,
  shake,
  compact,
}: {
  guess: string;
  pattern: Pattern | null;
  revealing: boolean;
  shake?: boolean;
  compact?: boolean;
}) {
  const letters = guess.padEnd(WORD_LENGTH, ' ').slice(0, WORD_LENGTH);
  return (
    <div
      className={cn(
        'grid gap-1 sm:gap-1.5',
        shake && 'motion-safe:animate-[tile-shake_600ms_ease-enter]',
      )}
      style={{ gridTemplateColumns: `repeat(${WORD_LENGTH}, minmax(0, 1fr))` }}
    >
      {[...letters].map((letter, i) => (
        <Tile
          key={i}
          letter={letter.trim()}
          tile={pattern ? (pattern[i] as Pattern[number]) : null}
          index={i}
          revealing={revealing}
          compact={compact}
        />
      ))}
    </div>
  );
}

export const Board = memo(function Board({
  state,
  compact,
}: {
  state: BoardState;
  compact?: boolean;
}) {
  const rows = state.rows;
  const draftRow = rows.length < MAX_GUESSES && state.status === 'playing' ? state.draft : null;
  const blanks = MAX_GUESSES - rows.length - (draftRow === null ? 0 : 1);

  return (
    <div
      className={cn(
        'grid gap-1 sm:gap-1.5',
        // An explicit width, not `w-full max-w-sm`. An empty board has no
        // intrinsic width, so inside a shrink-to-fit parent `w-full` resolves
        // to nothing and the board collapses to a stub — which is exactly the
        // state the interactive board starts in.
        compact ? 'w-[7rem]' : 'w-[min(100%,20rem)] min-w-[15rem]',
      )}
      role="img"
      aria-label={
        rows.length === 0
          ? 'Empty board, six guesses remaining.'
          : `${rows.length} of ${MAX_GUESSES} guesses played.`
      }
    >
      {rows.map((row, i) => (
        <Row
          key={`${row.guess}-${i}`}
          guess={row.guess}
          pattern={row.pattern}
          revealing={i === rows.length - 1}
          compact={compact}
        />
      ))}
      {draftRow !== null ? (
        <Row
          guess={draftRow}
          pattern={null}
          revealing={false}
          shake={state.invalid !== null}
          compact={compact}
        />
      ) : null}
      {Array.from({ length: Math.max(0, blanks) }, (_, i) => (
        <Row key={`blank-${i}`} guess="" pattern={null} revealing={false} compact={compact} />
      ))}
    </div>
  );
});

export default Board;
