// The local move preview: it produces the same EvalResult the network `evaluate`
// returns (legality, score, the words formed, the inferred orientation), computed
// on-device from a loaded dictionary so composing a move needs no round trip. It
// adapts the client's letter-space board and placements into the validator's
// index space (lib/dict/validate.ts), assembling the per-variant ruleset from the
// static geometry/constants (lib/premiums.ts) and the server-sent tile values
// (lib/alphabet.ts). The server stays authoritative — submit re-validates — so any
// throw here is caught by the caller, which falls back to the network preview.

import { validatePlay, Horizontal, type Board as VBoard, type Ruleset, type Placement as VPlacement, type Dict } from './validate';
import { playDirection } from './direction';
import { noRepeatScore } from './norepeat';
import type { Board as ClientBoard } from '../board';
import type { PlacedTile } from '../client';
import type { EvalResult, Variant } from '../model';
import { premiumGrid, centre, BOARD_SIZE, RACK_SIZE, BINGO, type Premium } from '../premiums';
import { alphabetValues, indexForLetter, letterForIndex } from '../alphabet';

function letterMultOf(p: Premium): number {
  return p === 'DL' ? 2 : p === 'TL' ? 3 : 1;
}
function wordMultOf(p: Premium): number {
  return p === 'DW' ? 2 : p === 'TW' ? 3 : 1;
}

// buildBoard adapts the client's letter-space board into the validator's
// index-space read view, precomputing the letter indices once.
function buildBoard(variant: Variant, board: ClientBoard): VBoard {
  const grid: ({ letter: number; blank: boolean } | null)[] = new Array(BOARD_SIZE * BOARD_SIZE).fill(null);
  let empty = true;
  for (let r = 0; r < BOARD_SIZE; r++) {
    for (let c = 0; c < BOARD_SIZE; c++) {
      const cell = board[r][c];
      if (cell) {
        grid[r * BOARD_SIZE + c] = { letter: indexForLetter(variant, cell.letter), blank: cell.blank };
        empty = false;
      }
    }
  }
  const inBounds = (r: number, c: number): boolean => r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE;
  return {
    inBounds,
    filled: (r, c) => inBounds(r, c) && grid[r * BOARD_SIZE + c] !== null,
    cellAt: (r, c) => grid[r * BOARD_SIZE + c]!,
    isEmpty: () => empty,
  };
}

// buildRuleset assembles the per-variant scoring rules the validator needs.
function buildRuleset(variant: Variant, multipleWords: boolean): Ruleset {
  const prem = premiumGrid(variant);
  const ctr = centre(variant);
  const values = alphabetValues(variant);
  return {
    cols: BOARD_SIZE,
    center: ctr.row * BOARD_SIZE + ctr.col,
    rackSize: RACK_SIZE,
    bingo: BINGO[variant],
    values,
    letterMult: (r, c) => letterMultOf(prem[r][c]),
    wordMult: (r, c) => wordMultOf(prem[r][c]),
    ignoreCrossWords: !multipleWords,
  };
}

// decodeWord turns a word's alphabet indices back into a lower-cased string, the
// form the network EvalResult carries (the caption renders it directly).
function decodeWord(variant: Variant, letters: readonly number[]): string {
  let s = '';
  for (const i of letters) s += letterForIndex(variant, i);
  return s.toLowerCase();
}

/**
 * evaluateLocal computes the move preview for tiles placed on board in the given
 * game, returning the same shape as the network `evaluate`. dict is the loaded
 * dictionary reader for the game's (variant, version); multipleWords is the game's
 * cross-word rule. playedWords is the game's already-played words (decoded, as the
 * history carries them) when the game takes the no-repeat-words rule, and absent
 * otherwise — with it a play repeating a word is reported illegal and an already-played
 * cross-word scores nothing, exactly as the server decides. It may throw if the
 * variant's alphabet table is not loaded — the caller guards with hasAlphabet and falls
 * back to the network on any failure.
 */
export function evaluateLocal(
  dict: Dict,
  variant: Variant,
  board: ClientBoard,
  tiles: PlacedTile[],
  multipleWords: boolean,
  playedWords?: ReadonlySet<string>,
): EvalResult {
  const vboard = buildBoard(variant, board);
  const rs = buildRuleset(variant, multipleWords);
  const vtiles: VPlacement[] = tiles.map((t) => ({
    row: t.row,
    col: t.col,
    letter: indexForLetter(variant, t.letter),
    blank: t.blank,
  }));

  const dir = playDirection(vboard, rs, dict, vtiles);
  const res = validatePlay(vboard, rs, dict, dir, vtiles);
  if (!res.legal || !res.move) {
    return { legal: false, score: 0, words: [], dir: '' };
  }
  const m = res.move;
  const words = [m.main, ...m.cross].map((w) => decodeWord(variant, w.letters));
  const score = playedWords ? noRepeatScore(m, playedWords, (l) => decodeWord(variant, l)) : m.score;
  if (score === null) {
    // A real word, but one this game has already used: illegal, and named so the caption can say
    // why rather than leaving the player staring at a word they know is in the dictionary.
    return { legal: false, score: 0, words: [], dir: '', repeatedWord: words[0] };
  }
  return { legal: true, score, words, dir: dir === Horizontal ? 'H' : 'V' };
}
