// data-translate.jsx — Korean/Japanese → English card lexicon
//
// ENGLISH IS NOT IN THE LEXICON, ON PURPOSE
// An English card needs no translation, so there is nothing here to look up for
// it — `translateText` short-circuits `en` to a pass-through before it ever
// reaches this table. What English DOES need is the other half of the pipeline:
// the reference cards below carry an `en` string per region so the frame engine
// and the rendered demo scene have an English print to work with, and so a
// missing translation falls back to English rather than to Korean.
//
// WHY A GLOSSARY AND NOT A TRANSLATION API
// Card text is a closed vocabulary. A Pokémon card carries maybe 400 distinct
// translatable strings across its whole frame — the stage word, the eleven
// energy types, Weakness/Resistance/Retreat, the game keywords, the trainer
// subtypes, and the species name. Everything else is layout.
//
// Against that, a lookup table beats machine translation on every axis that
// matters here. It is correct on franchise names, where a general translator
// falls back to sounding the name out — 리자몽 comes back as "Rijamong", which
// is what romanizeKo() below produces too, and the card is Charizard. It is
// instant, so it can run at frame rate. It works with no network. And it needs
// no API key, which a static site could not hold anyway.
// See rgs/feature-card-translate.md §5.
//
// PROVENANCE RULES — the same ones data-cards.jsx runs under
//   1. Every entry carries `conf` and `src`.
//        conf: 'card'  — STRONGEST. Confirmed against the English print of the
//                        same card: same set, same collector number, both sides
//                        photographed. Not a translation anyone made — it is
//                        what the publisher printed in both languages. These
//                        need no native-speaker review.
//        conf: 'high'  — printed on every card of that language; transcribed
//                        from card frames and cross-checked against the
//                        franchise's own localised naming.
//        conf: 'check' — believed correct, NOT yet checked by a native reader.
//                        The UI renders these with a dotted underline.
//   2. THE LEXICON HAS NOT HAD A NATIVE-SPEAKER REVIEW PASS. Until it does,
//      this prices nothing real. That review is follow-up #1 in the spec.
//   3. Coverage is deliberately narrow — the full card frame (which generalises
//      to every card) plus species names for what CARD_CATALOG already indexes
//      and the Eeveelutions. Widening it is a data problem: pull official
//      localised names from the feed rather than typing more of them here.

/* ───────────── the lexicon ─────────────
   One row per concept, with its Korean and Japanese surface forms. `ja` is null
   where the Japanese card prints the same glyphs a different way and the pair
   would be a guess. */
const LEXICON = [
  // ── card frame: the words on every card ──
  { ko: '포켓몬',        ja: 'ポケモン',       en: 'Pokémon',        kind: 'frame', conf: 'high',  src: 'card frame' },
  { ko: '기본',          ja: 'たね',           en: 'Basic',          kind: 'frame', conf: 'high',  src: 'card frame, stage line' },
  { ko: '1단계',         ja: '1進化',          en: 'Stage 1',        kind: 'frame', conf: 'high',  src: 'card frame, stage line' },
  { ko: '2단계',         ja: '2進化',          en: 'Stage 2',        kind: 'frame', conf: 'high',  src: 'card frame, stage line' },
  { ko: '진화',          ja: '進化',           en: 'Evolves',        kind: 'frame', conf: 'high',  src: 'card frame' },
  { ko: '특성',          ja: '特性',           en: 'Ability',        kind: 'frame', conf: 'high',  src: 'card frame, ability header' },
  { ko: '약점',          ja: '弱点',           en: 'Weakness',       kind: 'frame', conf: 'high',  src: 'card frame, footer bar' },
  { ko: '저항력',        ja: '抵抗力',         en: 'Resistance',     kind: 'frame', conf: 'high',  src: 'card frame, footer bar' },
  { ko: '후퇴',          ja: 'にげる',         en: 'Retreat',        kind: 'frame', conf: 'high',  src: 'card frame, footer bar' },
  { ko: '에너지',        ja: 'エネルギー',     en: 'Energy',         kind: 'frame', conf: 'high',  src: 'card frame' },
  { ko: '트레이너스',    ja: 'トレーナーズ',   en: "Trainer",        kind: 'frame', conf: 'high',  src: 'trainer card header' },
  { ko: '서포트',        ja: 'サポート',       en: 'Supporter',      kind: 'frame', conf: 'high',  src: 'trainer subtype' },
  { ko: '굿즈',          ja: 'グッズ',         en: 'Item',           kind: 'frame', conf: 'high',  src: 'trainer subtype' },
  { ko: '스타디움',      ja: 'スタジアム',     en: 'Stadium',        kind: 'frame', conf: 'high',  src: 'trainer subtype' },
  { ko: '포켓몬의 도구', ja: 'ポケモンのどうぐ', en: 'Pokémon Tool', kind: 'frame', conf: 'check', src: 'trainer subtype' },

  // ── energy types ──
  { ko: '풀',      ja: '草',           en: 'Grass',      kind: 'type', conf: 'high',  src: 'type name' },
  { ko: '불꽃',    ja: '炎',           en: 'Fire',       kind: 'type', conf: 'high',  src: 'type name' },
  { ko: '물',      ja: '水',           en: 'Water',      kind: 'type', conf: 'high',  src: 'type name' },
  { ko: '번개',    ja: '雷',           en: 'Lightning',  kind: 'type', conf: 'high',  src: 'type name' },
  { ko: '초',      ja: '超',           en: 'Psychic',    kind: 'type', conf: 'high',  src: 'type name' },
  { ko: '격투',    ja: '闘',           en: 'Fighting',   kind: 'type', conf: 'high',  src: 'type name' },
  { ko: '악',      ja: '悪',           en: 'Darkness',   kind: 'type', conf: 'high',  src: 'type name' },
  { ko: '강철',    ja: '鋼',           en: 'Metal',      kind: 'type', conf: 'high',  src: 'type name' },
  { ko: '페어리',  ja: 'フェアリー',   en: 'Fairy',      kind: 'type', conf: 'high',  src: 'type name' },
  { ko: '드래곤',  ja: 'ドラゴン',     en: 'Dragon',     kind: 'type', conf: 'high',  src: 'type name' },
  { ko: '무색',    ja: '無色',         en: 'Colorless',  kind: 'type', conf: 'high',  src: 'type name' },

  // ── game keywords: what attack and ability text is made of ──
  { ko: '데미지',      ja: 'ダメージ',   en: 'damage',              kind: 'keyword', conf: 'high',  src: 'attack text' },
  { ko: '상대',        ja: '相手',       en: "opponent's",          kind: 'keyword', conf: 'high',  src: 'attack text' },
  { ko: '자신',        ja: '自分',       en: 'your own',            kind: 'keyword', conf: 'high',  src: 'attack text' },
  { ko: '벤치',        ja: 'ベンチ',     en: 'Bench',               kind: 'keyword', conf: 'high',  src: 'attack text' },
  { ko: '배틀필드',    ja: 'バトル場',   en: 'Active Spot',         kind: 'keyword', conf: 'check', src: 'attack text' },
  { ko: '트래쉬',      ja: 'トラッシュ', en: 'discard pile',        kind: 'keyword', conf: 'check', src: 'attack text' },
  { ko: '손',          ja: '手札',       en: 'hand',                kind: 'keyword', conf: 'high',  src: 'attack text' },
  { ko: '덱',          ja: '山札',       en: 'deck',                kind: 'keyword', conf: 'high',  src: 'attack text' },
  { ko: '코인',        ja: 'コイン',     en: 'coin',                kind: 'keyword', conf: 'high',  src: 'attack text' },
  { ko: '앞면',        ja: 'オモテ',     en: 'heads',               kind: 'keyword', conf: 'check', src: 'attack text' },
  { ko: '뒷면',        ja: 'ウラ',       en: 'tails',               kind: 'keyword', conf: 'check', src: 'attack text' },
  { ko: '기술',        ja: 'ワザ',       en: 'attack',              kind: 'keyword', conf: 'check', src: 'attack text' },
  { ko: '카드',        ja: 'カード',     en: 'card',                kind: 'keyword', conf: 'high',  src: 'attack text' },
  { ko: '놓다',        ja: 'つける',     en: 'attach',              kind: 'keyword', conf: 'check', src: 'attack text' },
  { ko: '뽑는다',      ja: '引く',       en: 'draw',                kind: 'keyword', conf: 'check', src: 'attack text' },
  { ko: '기절',        ja: 'きぜつ',     en: 'Knocked Out',         kind: 'keyword', conf: 'check', src: 'attack text' },
  // Words taken off the two real cards. '동전' is what the card actually
  // prints for a coin — '코인' above is the loanword and appears elsewhere.
  { ko: '동전',        ja: null,         en: 'coin',                kind: 'keyword', conf: 'card',  src: 'EN print sv2a/MEW 007/165' },
  { ko: '차례',        ja: '番',         en: 'turn',                kind: 'keyword', conf: 'card',  src: 'EN print sv2a/MEW 007/165' },
  { ko: '다음',        ja: '次',         en: 'next',                kind: 'keyword', conf: 'card',  src: 'EN print sv2a/MEW 007/165' },
  { ko: '플레이어',    ja: 'プレイヤー', en: 'player',              kind: 'keyword', conf: 'check', src: 'KO PW promo 2010' },
  { ko: '각각',        ja: null,         en: 'each',                kind: 'keyword', conf: 'check', src: 'KO PW promo 2010' },
  { ko: '모두',        ja: 'すべて',     en: 'all',                 kind: 'keyword', conf: 'check', src: 'KO PW promo 2010' },
  { ko: '붙어',        ja: null,         en: 'attached',            kind: 'keyword', conf: 'check', src: 'KO PW promo 2010' },

  // ── the Pokédex descriptor line, under the art ──
  { ko: '꼬마거북포켓몬', ja: null, en: 'Tiny Turtle Pokémon', kind: 'species', conf: 'card',
    src: 'EN print sv2a/MEW 007/165' },
  { ko: '쥐포켓몬',      ja: null, en: 'Mouse Pokémon',       kind: 'species', conf: 'check',
    src: 'KO PW promo 2010; no EN print of this card exists' },
  { ko: '전국도감',      ja: null, en: 'National Pokédex',    kind: 'frame',   conf: 'check',
    src: 'KO card frame, descriptor line' },
  { ko: '키',            ja: null, en: 'HT',                  kind: 'frame',   conf: 'card',
    src: 'EN print sv2a/MEW 007/165' },
  { ko: '몸무게',        ja: null, en: 'WT',                  kind: 'frame',   conf: 'card',
    src: 'EN print sv2a/MEW 007/165' },

  // ── attack names ──
  // Attack names are the part a romanization helps least with: "roketbakchigi"
  // tells a buyer nothing, and the card is worth what Skull Bash is worth.
  { ko: '껍질에숨기', ja: null, en: 'Withdraw',   kind: 'attack', conf: 'card',
    src: 'EN print sv2a/MEW 007/165' },
  { ko: '로켓박치기', ja: null, en: 'Skull Bash', kind: 'attack', conf: 'card',
    src: 'EN print sv2a/MEW 007/165' },
  { ko: '10만볼트',   ja: null, en: 'Thunderbolt', kind: 'attack', conf: 'check',
    src: 'KO PW promo 2010; standard KO name for Thunderbolt, unconfirmed against an EN print' },
  { ko: '안녕!',      ja: null, en: 'Hello!',      kind: 'attack', conf: 'check',
    src: 'KO PW promo 2010; Korea-exclusive card, no official EN name exists' },

  // ── species: the cards CARD_CATALOG indexes, plus the Eeveelutions,
  //    because a Korean binder is mostly Eeveelutions ──
  { ko: '리자몽',      ja: 'リザードン',   en: 'Charizard',  kind: 'pokemon', conf: 'high',  src: 'official localised name' },
  { ko: '파이리',      ja: 'ヒトカゲ',     en: 'Charmander', kind: 'pokemon', conf: 'high',  src: 'official localised name' },
  { ko: '거북왕',      ja: 'カメックス',   en: 'Blastoise',  kind: 'pokemon', conf: 'high',  src: 'official localised name' },
  { ko: '꼬부기',      ja: 'ゼニガメ',     en: 'Squirtle',   kind: 'pokemon', conf: 'high',  src: 'official localised name' },
  { ko: '이상해꽃',    ja: 'フシギバナ',   en: 'Venusaur',   kind: 'pokemon', conf: 'high',  src: 'official localised name' },
  { ko: '이상해씨',    ja: 'フシギダネ',   en: 'Bulbasaur',  kind: 'pokemon', conf: 'high',  src: 'official localised name' },
  { ko: '피카츄',      ja: 'ピカチュウ',   en: 'Pikachu',    kind: 'pokemon', conf: 'high',  src: 'official localised name' },
  { ko: '라이츄',      ja: 'ライチュウ',   en: 'Raichu',     kind: 'pokemon', conf: 'high',  src: 'official localised name' },
  { ko: '뮤츠',        ja: 'ミュウツー',   en: 'Mewtwo',     kind: 'pokemon', conf: 'high',  src: 'official localised name' },
  { ko: '뮤',          ja: 'ミュウ',       en: 'Mew',        kind: 'pokemon', conf: 'high',  src: 'official localised name' },
  { ko: '이브이',      ja: 'イーブイ',     en: 'Eevee',      kind: 'pokemon', conf: 'high',  src: 'official localised name' },
  { ko: '블래키',      ja: 'ブラッキー',   en: 'Umbreon',    kind: 'pokemon', conf: 'high',  src: 'official localised name' },
  { ko: '에브이',      ja: 'エーフィ',     en: 'Espeon',     kind: 'pokemon', conf: 'check', src: 'official localised name' },
  { ko: '샤미드',      ja: 'シャワーズ',   en: 'Vaporeon',   kind: 'pokemon', conf: 'check', src: 'official localised name' },
  { ko: '쥬피썬더',    ja: 'サンダース',   en: 'Jolteon',    kind: 'pokemon', conf: 'check', src: 'official localised name' },
  { ko: '부스터',      ja: 'ブースター',   en: 'Flareon',    kind: 'pokemon', conf: 'check', src: 'official localised name' },
  { ko: '리피아',      ja: 'リーフィア',   en: 'Leafeon',    kind: 'pokemon', conf: 'high',  src: 'official localised name' },
  { ko: '글레이시아',  ja: 'グレイシア',   en: 'Glaceon',    kind: 'pokemon', conf: 'check', src: 'official localised name' },
  { ko: '님피아',      ja: 'ニンフィア',   en: 'Sylveon',    kind: 'pokemon', conf: 'high',  src: 'official localised name' },
  { ko: '나몰빼미',    ja: 'ナンジャモ',   en: 'Iono',       kind: 'pokemon', conf: 'check', src: 'official localised name' },
];

/* ───────────── phrases ─────────────
   Whole-sentence patterns. Card rules text is templated — the same dozen
   sentence shapes carry almost every attack in the game — so matching a
   sentence beats assembling it word by word, and reads like English instead of
   like a word list. Word-level substitution stays as the fallback.

   These are COMPOSED reference sentences: real sentence patterns built from the
   vocabulary above. They are not transcribed from one specific card, and they
   are marked `composed` so nothing downstream mistakes them for a reading of a
   real card's text. */
const PHRASES = [
  // ── printed on real cards, confirmed against the English print ──
  // Rules text is templated: Withdraw's sentence is on hundreds of cards, so a
  // whole-sentence entry is worth far more than it looks. It only fires on an
  // exact match though — a card that words it differently falls back to
  // word-level assembly, which will read like a word list, not like English.
  { ko: '동전을 1번 던져서 앞면이 나오면 상대의 다음 차례에 이 포켓몬은 기술의 데미지를 받지 않는다.',
    ja: null,
    en: "Flip a coin. If heads, during your opponent's next turn, prevent all damage done to this Pokémon by attacks.",
    conf: 'card', src: 'EN print sv2a/MEW 007/165 — Withdraw' },
  { ko: '위험해지면 등껍질에 손발을 감추어 몸을 지키면서 입에서 물을 내뿜는다.',
    ja: null,
    en: 'When it feels threatened, it draws its limbs inside its shell and sprays water from its mouth.',
    conf: 'card', src: 'EN print sv2a/MEW 007/165 — flavour text' },
  { ko: '상대와 인사를 하자. 그 뒤, 서로의 플레이어는 각각의 덱에서 카드를 1장 뽑는다.',
    ja: null,
    en: 'Say hello to your opponent. After that, each player draws 1 card from their deck.',
    conf: 'check', src: 'KO PW promo 2010 — no EN print exists' },
  { ko: '이 포켓몬에게 붙어 있는 에너지를 모두 트래쉬한다.',
    ja: null,
    en: 'Discard all Energy attached to this Pokémon.',
    conf: 'check', src: 'KO PW promo 2010 — no EN print exists' },
  { ko: '꼬리를 세우고 주변의 상황을 살피다 보면 가끔 꼬리에 번개가 친다.',
    ja: null,
    en: 'It raises its tail to check its surroundings, and sometimes lightning strikes the tail.',
    conf: 'check', src: 'KO PW promo 2010 — flavour text, no EN print exists' },

  // ── composed reference sentences, for the frame-engine demo ──
  { ko: '상대의 배틀 포켓몬에게 120데미지.',
    ja: '相手のバトルポケモンに120ダメージ。',
    en: "120 damage to your opponent's Active Pokémon.",
    conf: 'high', composed: true, src: 'standard attack sentence pattern' },
  { ko: '이 포켓몬이 배틀필드에 있는 한, 상대의 포켓몬은 후퇴할 수 없다.',
    ja: 'このポケモンがバトル場にいるかぎり、相手のポケモンはにげられない。',
    en: "As long as this Pokémon is in the Active Spot, your opponent's Pokémon can't retreat.",
    conf: 'check', composed: true, src: 'standard ability sentence pattern' },
  { ko: '자신의 덱에서 에너지를 1장 붙인다.',
    ja: '自分の山札からエネルギーを1枚つける。',
    en: 'Attach 1 Energy from your deck to this Pokémon.',
    conf: 'check', composed: true, src: 'standard attack sentence pattern' },
];

/* ───────────── romanization ─────────────
   The fallback when nothing matches. An unknown Korean name should surface as
   "Rijamong", not as a blank — a romanized guess is something a human can act
   on, and it is what lets you type the name into a price guide. It is rendered
   as a visibly different state and never carries a price. */

// Revised Romanization of Korean. Hangul syllables are algorithmic: each
// U+AC00..U+D7A3 code point decomposes into initial/medial/final by division.
const RR_INITIAL = ['g','kk','n','d','tt','r','m','b','pp','s','ss','','j','jj','ch','k','t','p','h'];
const RR_MEDIAL  = ['a','ae','ya','yae','eo','e','yeo','ye','o','wa','wae','oe','yo','u','wo','we','wi','yu','eu','ui','i'];
const RR_FINAL   = ['','k','k','k','n','n','n','t','l','l','l','l','l','l','l','l','m','p','p','t','t','ng','t','t','k','t','p','t'];

function romanizeKo(text) {
  let out = '';
  for (const ch of text) {
    const code = ch.codePointAt(0);
    if (code >= 0xAC00 && code <= 0xD7A3) {
      const i = code - 0xAC00;
      out += RR_INITIAL[Math.floor(i / 588)] + RR_MEDIAL[Math.floor((i % 588) / 28)] + RR_FINAL[i % 28];
    } else {
      out += ch;
    }
  }
  return out;
}

// Katakana only — it is what species names and loanwords are printed in, and it
// is the part of Japanese that romanizes by table rather than by dictionary.
// Kanji is left as-is rather than guessed at: a wrong reading is worse than an
// obvious gap.
const KANA_ROMAJI = {
  'ア':'a','イ':'i','ウ':'u','エ':'e','オ':'o','カ':'ka','キ':'ki','ク':'ku','ケ':'ke','コ':'ko',
  'サ':'sa','シ':'shi','ス':'su','セ':'se','ソ':'so','タ':'ta','チ':'chi','ツ':'tsu','テ':'te','ト':'to',
  'ナ':'na','ニ':'ni','ヌ':'nu','ネ':'ne','ノ':'no','ハ':'ha','ヒ':'hi','フ':'fu','ヘ':'he','ホ':'ho',
  'マ':'ma','ミ':'mi','ム':'mu','メ':'me','モ':'mo','ヤ':'ya','ユ':'yu','ヨ':'yo',
  'ラ':'ra','リ':'ri','ル':'ru','レ':'re','ロ':'ro','ワ':'wa','ヲ':'wo','ン':'n',
  'ガ':'ga','ギ':'gi','グ':'gu','ゲ':'ge','ゴ':'go','ザ':'za','ジ':'ji','ズ':'zu','ゼ':'ze','ゾ':'zo',
  'ダ':'da','ヂ':'ji','ヅ':'zu','デ':'de','ド':'do','バ':'ba','ビ':'bi','ブ':'bu','ベ':'be','ボ':'bo',
  'パ':'pa','ピ':'pi','プ':'pu','ペ':'pe','ポ':'po','ー':'', 'ッ':'',
};

function romanizeJa(text) {
  let out = '';
  for (const ch of text) {
    if (KANA_ROMAJI[ch] !== undefined) out += KANA_ROMAJI[ch];
    else if (ch >= 'ぁ' && ch <= 'ゖ') out += KANA_ROMAJI[String.fromCodePoint(ch.codePointAt(0) + 0x60)] || ch;
    else out += ch;
  }
  return out;
}

/* ───────────── the supported source languages ─────────────
   One list, in the order they appear in the UI, so the chips, the OCR worker
   and the frame engine cannot disagree about what is supported.

   ENGLISH IS FIRST AND IS THE DEFAULT. Most cards that cross the counter are
   English — the buy pile from a Fort Leavenworth collection is almost entirely
   English print — so the common case is importing a card, not translating one.
   Japanese and Korean are the second and third options, and they are what the
   glossary and the overlay exist for.

   `ocr` is the Tesseract traineddata to load. English is always in the mix for
   the other two because the collector number and the 'ex' suffix are ASCII on
   every print; English alone loads `eng`, which is ~4 MB against ~15 MB for the
   Korean model — so the default language is also the cheapest one to load. */
const SCAN_LANGS = [
  { id: 'en', label: 'English',  native: 'English', ocr: 'eng',     translates: false },
  { id: 'ja', label: 'Japanese', native: '日本語',   ocr: 'jpn+eng', translates: true },
  { id: 'ko', label: 'Korean',   native: '한국어',   ocr: 'kor+eng', translates: true },
];
const DEFAULT_SCAN_LANG = 'en';
const scanLang = (id) => SCAN_LANGS.find(l => l.id === id) || SCAN_LANGS[0];
// Does this source language need the overlay at all? An English card has
// nothing to translate, so painting plates over it covers the card to tell you
// what it already says.
const langTranslates = (id) => scanLang(id).translates;

/* ───────────── language detection ─────────────
   By script, not by content: Hangul and Kana occupy disjoint code blocks, so a
   character census is both exact and free. Kanji is ambiguous between Japanese
   and Chinese and so is counted only as a weak Japanese signal. */
function detectLang(text) {
  let ko = 0, ja = 0, han = 0, latin = 0;
  for (const ch of text) {
    const c = ch.codePointAt(0);
    if (c >= 0xAC00 && c <= 0xD7A3 || c >= 0x1100 && c <= 0x11FF) ko++;
    else if (c >= 0x3040 && c <= 0x30FF) ja++;
    else if (c >= 0x4E00 && c <= 0x9FFF) han++;
    else if (c >= 0x41 && c <= 0x7A) latin++;
  }
  if (ko > 0 && ko >= ja) return 'ko';
  if (ja > 0 || han > 0) return 'ja';
  return latin > 0 ? 'en' : 'unknown';
}

/* ───────────── lookup index ─────────────
   Built once. Keyed by source string per language, longest-first so that
   segmentation prefers '포켓몬의 도구' over '포켓몬'. */
const LEX_INDEX = (() => {
  const idx = { ko: new Map(), ja: new Map() };
  const add = (map, key, entry) => { if (key && !map.has(key)) map.set(key, entry); };
  LEXICON.forEach(e => { add(idx.ko, e.ko, e); add(idx.ja, e.ja, e); });
  PHRASES.forEach(p => {
    add(idx.ko, p.ko, { ...p, kind: 'phrase' });
    add(idx.ja, p.ja, { ...p, kind: 'phrase' });
  });
  return {
    map: idx,
    keys: {
      ko: [...idx.ko.keys()].sort((a, b) => b.length - a.length),
      ja: [...idx.ja.keys()].sort((a, b) => b.length - a.length),
    },
  };
})();

// Korean particles attach to the stem with no space, so '포켓몬에게' has to
// reduce to '포켓몬' before it will match. Longest-first, and only stripped when
// something is left behind.
const KO_PARTICLES = ['에게서', '에게', '에서', '으로', '까지', '부터', '보다', '와', '과', '의', '은', '는', '이', '가', '을', '를', '에', '로', '도', '만'];

function stripParticle(word) {
  for (const p of KO_PARTICLES) {
    if (word.length > p.length + 1 && word.endsWith(p)) return word.slice(0, -p.length);
  }
  return word;
}

function lookup(text, lang) {
  const map = LEX_INDEX.map[lang];
  if (!map) return null;
  const raw = text.trim();
  return map.get(raw) || (lang === 'ko' ? map.get(stripParticle(raw)) || null : null);
}

/* ───────────── the reference card ─────────────
   What the `frame` engine reads when it maps a detected region to its role.
   It is a COMPOSED card: the identity (name, set, collector number) is a real
   row out of CARD_CATALOG, and the body text is the composed sentence patterns
   above. It is not a transcription of the real Korean print of this card, and
   every consumer of it carries `simulated: true` so the UI can say so.

   Regions are in fractional card coordinates (0..1 of the card quad), which is
   what lets the overlay track a real card held at any angle or distance. The
   numbers are the standard TCG frame proportions.

   EVERY REGION CARRIES ALL THREE LANGUAGES. `en` is not decoration — it is the
   default source language, and it is the fallback when a print has no string
   for the language asked for. Before it existed, selecting English fell through
   to `ko` and an English scan rendered Korean. The English body strings are the
   `en` side of the composed phrases above, so the two cannot drift.

   The English print leads the list because English is the default: most of what
   crosses the counter is an English card being imported, not a foreign one
   being translated. */
const REFERENCE_CARDS = [
  {
    id: 'sv8-238', label: 'Pikachu ex · English print',
    lang: 'en', number: '238/191',
    regions: [
      { role: 'stage',    box: [0.07, 0.030, 0.20, 0.038], en: 'Basic', ko: '기본', ja: 'たね' },
      { role: 'name',     box: [0.07, 0.082, 0.52, 0.062], en: 'Pikachu ex', ko: '피카츄 ex', ja: 'ピカチュウ ex' },
      { role: 'ability',  box: [0.09, 0.592, 0.22, 0.038], en: 'Ability', ko: '특성', ja: '特性' },
      { role: 'abilityBody', box: [0.09, 0.658, 0.82, 0.060],
        en: 'Attach 1 Energy from your deck to this Pokémon.',
        ko: '자신의 덱에서 에너지를 1장 붙인다.',
        ja: '自分の山札からエネルギーを1枚つける。' },
      { role: 'attackBody', box: [0.09, 0.760, 0.82, 0.048],
        en: "120 damage to your opponent's Active Pokémon.",
        ko: '상대의 배틀 포켓몬에게 120데미지.',
        ja: '相手のバトルポケモンに120ダメージ。' },
      { role: 'weakness', box: [0.09, 0.862, 0.26, 0.030], en: 'Weakness', ko: '약점', ja: '弱点' },
      { role: 'retreat',  box: [0.44, 0.862, 0.24, 0.030], en: 'Retreat', ko: '후퇴', ja: 'にげる' },
      { role: 'number',   box: [0.66, 0.932, 0.26, 0.030], en: '238/191', ko: '238/191', ja: '238/191' },
    ],
  },
  {
    id: 'sv8pt5-161', label: 'Umbreon ex · Korean print',
    lang: 'ko', number: '161/131',
    regions: [
      { role: 'stage',    box: [0.07, 0.030, 0.20, 0.038], en: 'Basic', ko: '기본',   ja: 'たね' },
      { role: 'name',     box: [0.07, 0.082, 0.52, 0.062], en: 'Umbreon ex', ko: '블래키 ex', ja: 'ブラッキー ex' },
      { role: 'ability',  box: [0.09, 0.592, 0.22, 0.038], en: 'Ability', ko: '특성',   ja: '特性' },
      { role: 'abilityBody', box: [0.09, 0.658, 0.82, 0.075],
        en: "As long as this Pokémon is in the Active Spot, your opponent's Pokémon can't retreat.",
        ko: '이 포켓몬이 배틀필드에 있는 한, 상대의 포켓몬은 후퇴할 수 없다.',
        ja: 'このポケモンがバトル場にいるかぎり、相手のポケモンはにげられない。' },
      { role: 'attackBody', box: [0.09, 0.760, 0.82, 0.048],
        en: "120 damage to your opponent's Active Pokémon.",
        ko: '상대의 배틀 포켓몬에게 120데미지.',
        ja: '相手のバトルポケモンに120ダメージ。' },
      { role: 'weakness', box: [0.09, 0.862, 0.26, 0.030], en: 'Weakness', ko: '약점',   ja: '弱点' },
      { role: 'retreat',  box: [0.44, 0.862, 0.24, 0.030], en: 'Retreat', ko: '후퇴',   ja: 'にげる' },
      { role: 'number',   box: [0.66, 0.932, 0.26, 0.030], en: '161/131', ko: '161/131', ja: '161/131' },
    ],
  },
  {
    id: 'sv3pt5-199', label: 'Charizard ex · Japanese print',
    lang: 'ja', number: '199/165',
    regions: [
      { role: 'stage',    box: [0.07, 0.030, 0.22, 0.038], en: 'Stage 2', ko: '2단계', ja: '2進化' },
      { role: 'name',     box: [0.07, 0.082, 0.54, 0.062], en: 'Charizard ex', ko: '리자몽 ex', ja: 'リザードン ex' },
      { role: 'ability',  box: [0.09, 0.592, 0.22, 0.038], en: 'Ability', ko: '특성',   ja: '特性' },
      { role: 'abilityBody', box: [0.09, 0.658, 0.82, 0.060],
        en: 'Attach 1 Energy from your deck to this Pokémon.',
        ko: '자신의 덱에서 에너지를 1장 붙인다.',
        ja: '自分の山札からエネルギーを1枚つける。' },
      { role: 'attackBody', box: [0.09, 0.760, 0.82, 0.048],
        en: "120 damage to your opponent's Active Pokémon.",
        ko: '상대의 배틀 포켓몬에게 120데미지.',
        ja: '相手のバトルポケモンに120ダメージ。' },
      { role: 'weakness', box: [0.09, 0.862, 0.26, 0.030], en: 'Weakness', ko: '약점',   ja: '弱点' },
      { role: 'retreat',  box: [0.44, 0.862, 0.24, 0.030], en: 'Retreat', ko: '후퇴',   ja: 'にげる' },
      { role: 'number',   box: [0.66, 0.932, 0.26, 0.030], en: '199/165', ko: '199/165', ja: '199/165' },
    ],
  },
  {
    id: 'sv2-254', label: 'Iono · Korean print',
    lang: 'ko', number: '254/193',
    regions: [
      { role: 'stage',    box: [0.07, 0.030, 0.30, 0.038], en: 'Supporter', ko: '서포트', ja: 'サポート' },
      { role: 'name',     box: [0.07, 0.082, 0.50, 0.062], en: 'Iono', ko: '나몰빼미', ja: 'ナンジャモ' },
      { role: 'attackBody', box: [0.09, 0.700, 0.82, 0.060],
        en: 'Attach 1 Energy from your deck to this Pokémon.',
        ko: '자신의 덱에서 에너지를 1장 붙인다.',
        ja: '自分の山札からエネルギーを1枚つける。' },
      { role: 'trainer',  box: [0.09, 0.790, 0.34, 0.040], en: 'Trainer', ko: '트레이너스', ja: 'トレーナーズ' },
      { role: 'number',   box: [0.66, 0.932, 0.26, 0.030], en: '254/193', ko: '254/193', ja: '254/193' },
    ],
  },
];

const referenceCard = (id) => REFERENCE_CARDS.find(c => c.id === id) || REFERENCE_CARDS[0];

/* ───────────── coverage, for the UI to be honest about ───────────── */
function lexiconStats() {
  const all = LEXICON.concat(PHRASES);
  const total = all.length;
  const check = all.filter(e => e.conf === 'check').length;
  return {
    entries: total,
    verified: total - check,
    needsCheck: check,
    // Confirmed against the publisher's English print of the same card. These
    // are the entries a native-speaker review would have nothing to add to.
    fromPrintedPair: all.filter(e => e.conf === 'card').length,
    species: LEXICON.filter(e => e.kind === 'pokemon').length,
    // ~1,025 species exist. The gap is the point of follow-up #2 in the spec.
    speciesCoverage: LEXICON.filter(e => e.kind === 'pokemon').length / 1025,
  };
}

Object.assign(window, {
  LEXICON, PHRASES, LEX_INDEX, lookup, stripParticle, KO_PARTICLES,
  romanizeKo, romanizeJa, detectLang,
  SCAN_LANGS, DEFAULT_SCAN_LANG, scanLang, langTranslates,
  REFERENCE_CARDS, referenceCard, lexiconStats,
});
