// The no-repeat-words rule of Russian "Эрудит": a word laid on the board belongs to the game from
// then on and cannot be laid again. This is the client-side port of the server's rule
// (backend/internal/engine/norepeat.go) and must stay in step with it — it decides both what the
// offline engine accepts and what the online move preview scores.
//
// The rule applies in two different ways:
//
//   - the play's main word must not already be there — such a play is illegal, and is neither
//     accepted nor offered by the move generator;
//   - a perpendicular cross-word that is already there is allowed, because it is incidental to
//     laying the main word, but it scores nothing.
//
// It is a rule of that variant only, and it is pinned per game rather than derived from the
// variant, so a game started before the rule existed plays on without it.

import type { Move } from './validate';
import type { MoveRecord } from '../model';

/**
 * wordKey identifies a word for the rule. Words are alphabet-index letters, so the key ignores
 * which tile carried a letter: a word spelled with a blank is the same word as one spelled with
 * real tiles, exactly as the server sees it (there the words are compared decoded).
 */
export function wordKey(letters: readonly number[]): string {
  return letters.join(',');
}

/**
 * playedWordKeys builds the rule's lookup set from a game's move journal — pass every play's
 * words (the main word and its cross-words). A game with the rule off never needs it.
 */
export function playedWordKeys(words: Iterable<readonly number[]>): Set<string> {
  const played = new Set<string>();
  for (const w of words) {
    if (w.length > 0) played.add(wordKey(w));
  }
  return played;
}

/**
 * playedWordsFromHistory is the rule's lookup set for a game whose history the server decoded for
 * us: every word its plays have formed, main words and cross-words alike. It lives here, with the
 * rest of the rule and behind the same lazy import as the evaluator, rather than in the game screen.
 *
 * The history is case-folded to lower, because the codec upper-cases those words for display
 * (lib/codec decodeMove) while evaluateLocal decodes a candidate word to lower — comparing the two
 * raw would never match, and the rule would silently never fire.
 */
export function playedWordsFromHistory(moves: readonly MoveRecord[]): Set<string> {
  const played = new Set<string>();
  for (const m of moves) {
    if (m.action === 'play') for (const w of m.words) played.add(w.toLowerCase());
  }
  return played;
}

/**
 * noRepeatScore applies the rule to an already validated move against the set of words already
 * played. It returns null when the move's main word is one of them — the play is illegal — and
 * otherwise the move's score with every already-played cross-word dropped from the total.
 *
 * keyOf turns a candidate word's letters into the key the played set is built with. The offline
 * engine keeps its journal in alphabet-index space and uses the default; the online preview reads
 * the history the server sent, whose words are already decoded, and passes a decoding key instead.
 */
export function noRepeatScore(
  m: Move,
  played: ReadonlySet<string>,
  keyOf: (letters: readonly number[]) => string = wordKey,
): number | null {
  if (played.has(keyOf(m.main.letters))) return null;
  let score = m.score;
  for (const cw of m.cross) {
    if (played.has(keyOf(cw.letters))) score -= cw.score;
  }
  return score;
}
