// 語彙表（MECH_* / WORLD_*）を、日本語の語順で書き直す。
//
// ゲームの注記いわく「数値を述べる行は、すべてこの一箇所から読まれる」。
// 逆に言えば、ここが崩れていると全画面が崩れる。実際こうなっていた:
//
//   WORLD_PROCS: p + " fire " + pct + "% oftener."
//   → " fire " が「炎」と訳され、全学派が「一撃の作用炎15% 頻繁に。」
//
// 断片ごとに訳しても語順は直らないので、関数ごと日本語で書き下ろす。
// 数値の書式（sNum / sPct / mPct / fmtFig）はそのまま使う。
//
// 1.6.0 で踏んだ落とし穴:
//   表を丸ごと差し替える作りなので、原文の表に鍵が増えると、その鍵は
//   差し替え後の表から消える。mechLine は表に無い鍵を
//       "+" + fmtFig(v) + " " + key + "."
//   で出すので、画面に「+1 keeneye.」「+0.03 procCeil.」が並んだ。
//   差し替えても何も言わずに通っていたので、最後に鍵の突き合わせを入れた。
//
// 二つの声:
//   WORLD_* … 世界の語り口（既定）
//   MECH_*  … 平明な言い方（設定の「簡明な言葉」を選んだとき）

export function rewriteLexicon(html, log = console.log) {
  const subs = [];
  const cut = (name, findRe, to) => subs.push({ name, findRe, to });

  /* ---- 学派の名。WORLD 側は「〜の術」でそろえる ---------------------- */
  cut('WORLD_SCHOOL', /const WORLD_SCHOOL = \{[\s\S]*?\n\};/, `const WORLD_SCHOOL = {
  sight:"見通しの術", crit:"一撃の術",
  travel:"道行きの術", mend:"癒しの術",
  undead:"古きものに抗する術", rarity:"目利きの術",
  ward:"護りの術", fury:"猛り"
};`);

  cut('MECH_SCHOOL', /const MECH_SCHOOL = \{[\s\S]*?\};/, `const MECH_SCHOOL = {sight:"索敵", crit:"クリティカル", travel:"移動", mend:"回復",
  undead:"アンデッド", rarity:"発見", ward:"防御", fury:"猛り"};`);

  /* ---- 世界の言葉（既定） -------------------------------------------- */
  cut('WORLD_ADD', /const WORLD_ADD = \{[\s\S]*?\n\};/, `const WORLD_ADD = {
  critDmg:   (v,f) => "清き一撃の傷が" + (f?"さらに":"") + sNum(v) + "%。",
  ward:      (v,f) => "護りが" + (f?"さらに":"") + sNum(v) + "点。",
  carry:     (v,f) => "積載が" + (f?"さらに":"") + sNum(v) + "ストーン。",
  carryLvl:  (v,f) => "階級ごとの積載が" + (f?"さらに":"") + sNum(v) + "ストーン。",
  /* 「一点」と読ませたいので、1 のときだけ数を言わない */
  wound:     (v,f) => "受ける傷が" + (f?"さらに":"")
                    + (Math.abs(v)===1 ? "一点" : fmtFig(Math.abs(v)) + "点")
                    + (v < 0 ? "重くなる。" : "軽くなる（上限 65%）。"),
  luck:      (v,f) => "d20 判定に" + (f?"さらに":"") + sNum(v) + "。",
  hit:       (v,f) => "一撃の出目に" + (f?"さらに":"") + sNum(v) + "。",
  rarity:    (v,f) => "希少度の目に" + (f?"さらに":"") + sNum(v) + "。",
  restBonus: (v,f) => "壁にもたれてひと息つくたび、さらに " + fmtFig(Math.abs(v)*100) + "% 塞がる。",
  hp:        (v,f) => "HP が" + (f?"さらに":"") + sNum(v) + "。",
  hpLvl:     (v,f) => "階級ごとの HP が" + (f?"さらに":"") + sNum(v) + "。",
  mp:        (v,f) => "MP が" + (f?"さらに":"") + sNum(v) + "。",
  kennel:    v     => "連れの育ちが " + fmtFig(v*100) + "% 早く、実りも "
                    + fmtFig(v*100) + "% 多い。",
  debt:      v     => "その技は MP を求めず、代わりに血を " + fmtFig(v*100)
                    + "% 求める。" + fmtFig(PACT_FLOOR*100) + "% を下回ることはない。",
  lightHour: v     => "百分のうち " + fmtFig(v*100) + " 分、"
                    + LIGHT_MINUTES + " 分のあいだ一撃が +"
                    + fmtFig((LIGHT_HARM-1)*100) + "%、歩みが +"
                    + fmtFig((LIGHT_SPEED-1)*100) + "%。",
  STR:       (v,f) => "STR が" + (f?"さらに":"") + sNum(v) + "。",
  DEX:       (v,f) => "DEX が" + (f?"さらに":"") + sNum(v) + "。",
  CON:       (v,f) => "CON が" + (f?"さらに":"") + sNum(v) + "。",
  INT:       (v,f) => "INT が" + (f?"さらに":"") + sNum(v) + "。",
  WIS:       (v,f) => "WIS が" + (f?"さらに":"") + sNum(v) + "。",
  CHA:       (v,f) => "CHA が" + (f?"さらに":"") + sNum(v) + "。",
  startGold: v     => "入会の時点で " + fmtFig(v) + " ゴールドを携えている。",
  startSpell:v     => (v===1 ? "入会の時点で術を一つ多く修めている。"
                             : "入会の時点で術を " + fmtFig(v) + " つ多く修めている。"),
  dcEase:    s     => s + " の判定が難易度一段ぶん易しくなる。",
  spot:      (v,f) => "見張りに" + (f?"さらに":"") + sNum(v) + "。罠も伏兵も早く目に入る。",
  keeneye:   (v,f) => (f ? "二度目の目はいっそう利く。"
                         : "見張りが見落としたものを、+" + fmtFig(2*v) + " でもう一度見直す。"),
  footing:   (v,f) => "足場に" + (f?"さらに":"") + sNum(v) + "。道が求めるあらゆるセーヴに効く。",
  waywise:   v     => "道を知る者は伏兵を迂回し、近道を見つける。百のうち "
                    + fmtFig(5*v) + "。",
  curio:     v     => "闇は百区間につき" + sNum(v) + " だけ多く櫃を吐き出す。",
  collector: v     => "収集家の目。頂点は "
                    + fmtFig(Math.round(100*v/3)) + "% 多く伝説を手放す。",
  campKeep:  v     => "火を埋めておけば野営は残る。夜の " + sPct(v) + "%。",
  firecatch: v     => "燃え移る火。ひと息が丸一晩の休息になることが " + sPct(v)
                    + "%、野営も費やさない。",
  badheat:   v     => "鍛冶は手ではなく火のせいにする。失敗の " + sPct(v) + "% は代金がかからない。",
  secondheat:v     => "熱いうちにもう一度炭へ。失敗の " + sPct(v)
                    + "% は、半分の見込みでただの打ち直しになる。",
  forge:     v     => "鍛冶の見込みに" + sNum(v) + "。槌はいっそう正しく落ちる。",
  wordDC:    (v,f) => "言葉の定める難易度に" + (f?"さらに":"") + sNum(v) + "。獣はそれを上回らねばならない。",
  procCeil:  (v,f) => "発動の上限が " + fmtFig(Math.abs(v)*100)
                    + "% " + (v<0 ? "下がる。" : "上がる。"),
  tooth:     v     => "蝶番に潜む牙は " + sPct(v) + "% の割合で見抜かれる。",
  hoard:     v     => "討ち取った擬態は、" + sPct(v) + "% の割合で櫃ひとつぶんを呑んでいる。",
  leash:     v     => "その物には飼い手の癖が織り込まれており、" + fmtFig(1 + v) + " 倍なつきやすい。",
  seeking:   v     => "戻らぬ者たちがようやく捜され、歩く区間の "
                    + fmtFig(100 * ROAD_LOST_BASE * Math.min(SEEKING_MAX, v)) + "% 多くで見つかる。",
  stair:     v     => "どの図にも載らぬ階段を探す。区間の " + sPct(v) + "%。",
  trapper:   v     => "見抜いた罠は解体され、その部品は帳場で売れる。",
  campcatch: ()    => "張ったままの野営が落下を受け止め、長い登り返しを省いてくれる。",
  dreamlore: v     => "床は眠る者に、百夜のうち "
                    + fmtFig(100 * DREAM_PLAIN * dreamScaleOf(v)) + " 夜は何かを語り、"
                    + fmtFig(100 * DREAM_TRUE * dreamScaleOf(v)) + " 夜はそれを偽りなく語る。",
  dreamsure: v     => "闇が見せたものは、百度のうち "
                    + fmtFig(100 * Math.min(1, DREAM_HOLD + v)) + " 度まで言ったとおりの場所にある。",
  dreamnamed:()    => "闇に名指しで求めることを覚え、闇はそれを聞き入れる。",
  dreamkeep: ()    => "何かを見せた夜は眠るに値した。野営は費やされない。",
  dozing:    v     => "壁にもたれたひと息でさえ、" + sPct(v) + "% の割合で夢の深さに届く。",
  dreaming:  ()    => "眠りはいっそう深く、夢は朝を二倍遠くまで運ぶ。",
  longdark:  ()    => "闇は長く続き、その底で待つものは二倍の頻度で浮かび上がる。",
  dreams:    v     => "夜に" + (v === 1 ? "もう一度" : fmtFig(v) + " 度多く")
                    + "問い、見せたものを二度見せる。"
};`);

  cut('WORLD_MULT', /const WORLD_MULT = \{[\s\S]*?\n\};/, `const WORLD_MULT = {
  morning:(v,f) => "朝の名残が" + (f?"さらに":"") + mPct(v) + "% " + (v>1?"長い。":"短い。"),
  travel: (v,f) => "歩みの脚が" + (f?"さらに":"") + mPct(v) + "% " + (v<1?"短く走る。":"長く走る。"),
  hunt:   (v,f) => "狩りが" + (f?"さらに":"") + mPct(v) + "% " + (v<1?"短く済む。":"長くかかる。"),
  haul:   (v,f) => "背嚢が" + (v>1?"さらに":"") + mPct(v) + "% " + (v>1?"多く運ぶ。":"少なくしか運べない。"),
  hpPool: (v,f) => "身体そのものが" + (f?"さらに":"") + mPct(v) + "% " + (v>1?"深くなる。":"浅くなる。"),
  mpPool: (v,f) => "井戸そのものが" + (f?"さらに":"") + mPct(v) + "% " + (v>1?"深くなる。":"浅くなる。"),
  hpKnit: (v,f) => "壁にもたれたひと息が" + (f?"さらに":"") + mPct(v) + "% " + (v>1?"多く塞ぐ。":"少なくしか塞がない。"),
  mpWell: (v,f) => "MP の井戸が" + (f?"さらに":"") + mPct(v) + "% " + (v>1?"速く満ちる。":"遅く満ちる。"),
  debuff: (v,f) => "道の患いが" + (f?"さらに":"") + mPct(v) + "% " + (v<1?"短く済む。":"長く続く。"),
  attack: (v,f) => "攻撃が" + (f?"さらに":"") + (v>1?"+":"-") + mPct(v) + "%。",
  harm:   (v,f) => "一撃が" + (f?"さらに":"") + mPct(v) + "% " + (v>1?"重く入る。":"軽く入る。"),
  speed:  (v,f) => "道行きが" + (f?"さらに":"") + mPct(v) + "% " + (v>1?"速い。":"遅い。"),
  procs:  (v,f) => "術の発動が" + (f?"さらに":"") + mPct(v) + "% " + (v>1?"多い。":"少ない。"),
  xp:     (v,f) => "経験値が" + (f?"さらに":"") + (v>1?"+":"-") + mPct(v) + "%。",
  gold:   (v,f) => "ゴールドが" + (f?"さらに":"") + (v>1?"+":"-") + mPct(v) + "%。",
  wares:  (v,f) => "帳場は" + (f?"さらに":"") + mPct(v) + "% "
                 + (v<1 ? "値を引く。" : "値を上乗せする。")
};`);

  cut('WORLD_PROCS', /const WORLD_PROCS = \(school, v, f\) => \{[\s\S]*?\n\};/, `const WORLD_PROCS = (school, v, f) => {
  const p = WORLD_SCHOOL[school] || (school + "の術");
  return p + "の発動が" + (f?"さらに":"") + mPct(v) + "% " + (v>1?"多い。":"少ない。");
};`);

  /* ---- 平明な言葉（設定で切り替えたとき） ----------------------------- */
  cut('MECH_ADD', /const MECH_ADD = \{[\s\S]*?\n\};/, `const MECH_ADD = {
  critDmg:   (v,f) => "クリティカル威力が" + (f?"さらに":"") + sNum(v) + "%。",
  ward:      (v,f) => "防御が" + (f?"さらに":"") + sNum(v) + "。",
  carry:     (v,f) => "荷袋の枠が" + (f?"さらに":"") + sNum(v) + "。",
  carryLvl:  (v,f) => "レベルごとの荷袋の枠が" + (f?"さらに":"") + sNum(v) + "。",
  /* 傷の値は符号を読み違えやすい。3 なら 3 減るので、減るほうで書く */
  wound:     (v,f) => "被ダメージが" + (f?"さらに":"") + (v<0?"+":"-") + fmtFig(Math.abs(v))
                    + (v < 0 ? "。" : "（上限 65%）。"),
  luck:      (v,f) => "d20 判定に" + (f?"さらに":"") + sNum(v) + "。",
  hit:       (v,f) => "命中に" + (f?"さらに":"") + sNum(v) + "。",
  rarity:    (v,f) => "戦利品の目に" + (f?"さらに":"") + sNum(v) + "。",
  restBonus: (v,f) => "休息の回復量が" + (f?"さらに":"") + sPct(v) + "%。",
  hp:        (v,f) => "HP が" + (f?"さらに":"") + sNum(v) + "。",
  hpLvl:     (v,f) => "レベルごとの HP が" + (f?"さらに":"") + sNum(v) + "。",
  mp:        (v,f) => "MP が" + (f?"さらに":"") + sNum(v) + "。",
  kennel:    v     => "ペットの成長 " + fmtFig(v*100) + "% 増、報酬 " + fmtFig(v*100) + "% 増。",
  debt:      v     => "能力の消費 MP が 0 になり、代わりに最大 HP の " + fmtFig(v*100)
                    + "%。下限 " + fmtFig(PACT_FLOOR*100) + "%。",
  lightHour: v     => "毎分 " + fmtFig(v*100) + "%: " + LIGHT_MINUTES + " 分間、ダメージ +"
                    + fmtFig((LIGHT_HARM-1)*100) + "%、速度 +"
                    + fmtFig((LIGHT_SPEED-1)*100) + "%。",
  STR:       (v,f) => "STR が" + (f?"さらに":"") + sNum(v) + "。",
  DEX:       (v,f) => "DEX が" + (f?"さらに":"") + sNum(v) + "。",
  CON:       (v,f) => "CON が" + (f?"さらに":"") + sNum(v) + "。",
  INT:       (v,f) => "INT が" + (f?"さらに":"") + sNum(v) + "。",
  WIS:       (v,f) => "WIS が" + (f?"さらに":"") + sNum(v) + "。",
  CHA:       (v,f) => "CHA が" + (f?"さらに":"") + sNum(v) + "。",
  startGold: v     => "開始時に " + fmtFig(v) + " ゴールド。",
  startSpell:v     => "開始時に能力を " + fmtFig(v) + " つ追加。",
  dcEase:    s     => s + " の判定が DC 1 ぶん易しくなる。",
  spot:      (v,f) => "道中の罠と伏兵の発見に" + (f?"さらに":"") + sNum(v) + "。",
  keeneye:   (v,f) => (f ? "再判定にさらに +2。"
                         : "見逃した罠・伏兵を +" + fmtFig(2*v) + " で再判定。"),
  footing:   (v,f) => "道中のセーヴ（地形・罠）に" + (f?"さらに":"") + sNum(v) + "。",
  waywise:   v     => "道中の伏兵 -" + fmtFig(5*v) + "%、近道 +" + fmtFig(5*v) + "%。",
  curio:     v     => "宝箱のある道中区間が " + sNum(v) + "% 増。",
  collector: v     => "頂点での伝説級入手率 +" + fmtFig(Math.round(100*v/3)) + "%。",
  campKeep:  v     => "長休息が野営を消費しない確率 " + sPct(v) + "%。",
  firecatch: v     => "短休息が野営なしで長休息になる確率 " + sPct(v) + "%。",
  badheat:   v     => "鍛冶の失敗時に代金が戻る確率 " + sPct(v) + "%。",
  secondheat:v     => "無料の再挑戦（成功率は半分）が発生する確率 " + sPct(v) + "%。",
  forge:     v     => "鍛冶の成功率 " + sNum(v) + " ポイント。",
  wordDC:    (v,f) => "言葉のセーヴ DC に" + (f?"さらに":"") + sNum(v) + "。",
  procCeil:  (v,f) => "能力の発動率上限が" + (f?"さらに":"") + sPct(v) + "%。",
  tooth:     v     => "擬態を看破して先制する確率 " + sPct(v) + "%。",
  hoard:     v     => "討伐した擬態が宝箱を落とす確率 " + sPct(v) + "%。",
  leash:     v     => "擬態が仲間を差し出す確率 +" + fmtFig(100 * v) + "%。",
  seeking:   v     => "遭難隊のいる道中区間 +"
                    + fmtFig(100 * ROAD_LOST_BASE * Math.min(SEEKING_MAX, v)) + "%。",
  stair:     v     => "忘れられた階段を見つける確率 " + sPct(v) + "%（区間ごと）。",
  trapper:   v     => "発見した罠を解体して売却、1 階につき " + fmtFig(35*v) + " ゴールド。",
  campcatch: ()    => "野営がある状態での落下は、療養所行きではなく野営の消費で済む。",
  dreamlore: v     => "夢を見る夜 " + sPct(DREAM_PLAIN * dreamScaleOf(v)) + "%、うち正夢 "
                    + sPct(DREAM_TRUE * dreamScaleOf(v)) + "%。",
  dreamsure: v     => "夢で見た区間が当たる確率 " + sPct(Math.min(1, DREAM_HOLD + v)) + "%。",
  dreamnamed:()    => "見たい夢を書類で指定できる。",
  dreamkeep: ()    => "正夢を見た長休息は野営を消費しない。",
  dozing:    v     => "短休息で夢を見る確率 " + sPct(v) + "%。",
  dreaming:  ()    => "夢のあとの「快眠」は HP・MP +20%、持続時間は 2 倍。",
  longdark:  ()    => "夢の武器の出現率が 2 倍（長休息ごとに 0.01% → 0.02%）。",
  dreams:    v     => "長休息ごとの夢の判定 " + sNum(v) + " 回。"
};`);

  cut('MECH_MULT', /const MECH_MULT = \{[\s\S]*?\n\};/, `const MECH_MULT = {
  morning:(v,f) => "「快眠」の持続時間が" + (f?"さらに":"") + mPct(v) + "% " + (v>1?"長い。":"短い。"),
  travel: (v,f) => "移動が" + (f?"さらに":"") + mPct(v) + "% " + (v<1?"速い。":"遅い。"),
  hunt:   (v,f) => "狩りが" + (f?"さらに":"") + mPct(v) + "% " + (v<1?"速い。":"遅い。"),
  haul:   (v,f) => "積載量 " + (v>1?"+":"-") + fmtFig(Math.abs(v-1)*100) + "%。",
  hpPool: (v,f) => "最大 HP が" + (f?"さらに":"") + (v>1?"+":"-") + mPct(v) + "%。",
  mpPool: (v,f) => "最大 MP が" + (f?"さらに":"") + (v>1?"+":"-") + mPct(v) + "%。",
  hpKnit: (v,f) => "短休息の回復量が" + (f?"さらに":"") + mPct(v) + "% " + (v>1?"多い。":"少ない。"),
  mpWell: (v,f) => "MP の自然回復が" + (f?"さらに":"") + mPct(v) + "% " + (v>1?"速い。":"遅い。"),
  debuff: (v,f) => "弱体の持続時間が" + (f?"さらに":"") + mPct(v) + "% " + (v<1?"短い。":"長い。"),
  attack: (v,f) => "攻撃が" + (f?"さらに":"") + (v>1?"+":"-") + mPct(v) + "%。",
  harm:   (v,f) => "与ダメージが" + (f?"さらに":"") + (v>1?"+":"-") + mPct(v) + "%。",
  speed:  (v,f) => "速度が" + (f?"さらに":"") + (v>1?"+":"-") + mPct(v) + "%。",
  procs:  (v,f) => "能力の発動が" + (f?"さらに":"") + mPct(v) + "% " + (v>1?"多い。":"少ない。"),
  xp:     (v,f) => "経験値が" + (f?"さらに":"") + (v>1?"+":"-") + mPct(v) + "%。",
  gold:   (v,f) => "ゴールドが" + (f?"さらに":"") + (v>1?"+":"-") + mPct(v) + "%。",
  wares:  (v,f) => "帳場の価格が" + (f?"さらに":"") + mPct(v) + "% " + (v<1 ? "引き。" : "増し。")
};`);

  // 訳語が変わっても当たるよう、宣言から最初の「; 行末」までで拾う。
  //
  // ここは以前 /;\n/ で終端していた。原文の改行は CRLF なので `;` の次は \r で、
  // \n には当たらない。そのため走査が止まらず、すぐ下の WORLD_PROCS（先に
  // 日本語へ差し替え済み）の末尾まで飲み込んで、return だけが宙に浮いた
  // 「Illegal return statement」になった。\r? を入れて行末で必ず止める。
  cut('MECH_PROCS', /const MECH_PROCS = \(school, v, f\) =>[\s\S]*?;\r?\n/, `const MECH_PROCS = (school, v, f) =>
  (MECH_SCHOOL[school] || school) + "の能力の発動が"
  + (f?"さらに":"") + mPct(v) + "% " + (v>1?"多い。":"少ない。");
`);

  // --- 鍵の突き合わせ ---------------------------------------------------
  // 差し替える前の表にある鍵が、差し替え後にも全部あるか。欠けた鍵は
  // mechLine のフォールバックに落ち、画面に内部語が素で出る。1.6.0 では
  // これで 20 個の鍵が落ちていた（+1 keeneye. / +0.03 procCeil. …）。
  const keysOf = body => new Set(
    [...body.matchAll(/[\n{,][ \t]*([A-Za-z_$][\w$]*)[ \t]*:[ \t]*(?:\(|[A-Za-z_$][\w$]*[ \t]*=>)/g)]
      .map(m => m[1]));
  const lostAll = [];
  for (const s of subs) {
    if (!/_(ADD|MULT)$/.test(s.name)) continue;      // 関数1本ものは対象外
    const m = html.match(s.findRe);
    if (!m) continue;
    const after = keysOf(s.to);
    const lost = [...keysOf(m[0])].filter(k => !after.has(k));
    if (lost.length) {
      lostAll.push(`${s.name}: ${lost.join(' ')}`);
      log(`  ! ${s.name} の差し替えで消える鍵 ${lost.length} 個: ${lost.join(' ')}`);
    }
  }
  if (lostAll.length) {
    throw new Error('語彙表の差し替えで鍵が失われます。そのままだと画面に内部語'
      + '（"+1 keeneye." の類）が並びます。lexicon.mjs を原文の表に合わせてください。\n  '
      + lostAll.join('\n  '));
  }

  // --- 飲み込みすぎの検出 -----------------------------------------------
  // 差し替えの正規表現は「宣言の頭から、それらしい終端まで」で書いてある。
  // 終端の当て方を間違えると隣の宣言まで巻き込むが、置換後の構文検査に
  // 出るまで気づけない。当たった範囲に別の宣言が入っていたら、その場で止める。
  for (const s of subs) {
    const m = html.match(s.findRe);
    if (!m) continue;
    const inner = m[0].slice(('const ' + s.name).length);
    const other = inner.match(/\bconst\s+[A-Z][A-Z0-9_]*\s*=/);
    if (other) {
      throw new Error(`${s.name} の差し替え範囲が広すぎます。`
        + `隣の宣言（${other[0]}）まで飲み込んでいます。\n`
        + '  終端の当て方（改行が CRLF かどうか等）を確かめてください。');
    }
  }

  let done = 0;
  for (const s of subs) {
    // 「置換後に変化がない」＝失敗、ではない。すでに同じ文言のこともある。
    // 当たったかどうかは、正規表現が一致したかで見る。
    if (!s.findRe.test(html)) { log('  ! 語彙表の差し替え先が見つからない: ' + s.name); continue; }
    html = html.replace(s.findRe, s.to);
    done++;
  }
  log(`  語彙表を日本語の語順で書き直し: ${done}/${subs.length} 表`);
  return html;
}
