// 1.6.3 以降（難読化されたコード）向けの、語順の書き換え。
//
// 1.6.2 まではソースが整形されていたので、`function roundLine(` や
// `const MECH_ADD = {` といった名前を目印にできた。1.6.3 で minify され、
// 変数名が 1〜2 文字に潰れて全体が 1 行になったため、その目印は全部消えた。
//
// 残っているのは
//   ・文字列リテラル（原文そのもの）
//   ・オブジェクトのキー（data から引くので潰せない）
// の二つ。ここを足場にして、書き換えたい範囲を **原文の位置** で切り出し、
// patch.mjs の他の置換と同じ「オフセット指定の編集」として返す。
//
// こうすると
//   ・訳文適用の前後関係に依存しない（前は「訳した後の文字列」を探していて、
//     訳語を変えるたびに規則が静かに死んでいた）
//   ・重なりの排除も既存の仕組みに乗る
// という利点がある。
import { mapMinified } from './minimap.mjs';

// 使う場所を数えて、当たらなかったら報せる
const findOne = (src, needle, label, notes) => {
  const i = src.indexOf(needle);
  if (i < 0) { notes.push('見つからない: ' + label); return -1; }
  if (src.indexOf(needle, i + 1) >= 0) notes.push('複数あって曖昧: ' + label);
  return i;
};

export function structuralEdits(src, log = console.log) {
  const edits = [], protect = [], notes = [];
  const map = mapMinified(src);
  if (!map.ok) {
    notes.push(...map.notes);
    log('  ! 難読化コードの割り出しに失敗: ' + map.notes.join(' / '));
    return { edits, protect, notes, map };
  }

  const { sNum, sPct, mPct, fmtFig } = map;

  /* ---------- 1. 数値行（mechLine）を日本語で置き換える ---------------
   *
   * ゲームの注記いわく「数値を述べる行は、すべてこの一箇所から読まれる」。
   * 表そのものを差し替えると、表の中で参照している定数（PACT_FLOOR など）も
   * 潰された名前で書かねばならず、次の版でまた外れる。
   * そこで表は触らず、**引く側の関数を包む**。訳を用意していない鍵は
   * 元の実装に渡すので、取りこぼしても英語で出るだけで壊れない。
   */
  const JA = `
const __jaAdd = {
  critDmg:(v,f)=>"クリティカル威力が"+(f?"さらに":"")+${sNum}(v)+"%。",
  ward:(v,f)=>"防御が"+(f?"さらに":"")+${sNum}(v)+"。",
  carry:(v,f)=>"荷袋の枠が"+(f?"さらに":"")+${sNum}(v)+"。",
  carryLvl:(v,f)=>"レベルごとの荷袋の枠が"+(f?"さらに":"")+${sNum}(v)+"。",
  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}(100*v)+"% 増、報酬 "+${fmtFig}(100*v)+"% 増。",
  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)+"%。",
  stair:v=>"忘れられた階段を見つける確率 "+${sPct}(v)+"%（区間ごと）。",
  trapper:v=>"発見した罠を解体して売却、1 階につき "+${fmtFig}(35*v)+" ゴールド。",
  campcatch:()=>"野営がある状態での落下は、療養所行きではなく野営の消費で済む。",
  dreamnamed:()=>"見たい夢を書類で指定できる。",
  dreamkeep:()=>"正夢を見た長休息は野営を消費しない。",
  dozing:v=>"短休息で夢を見る確率 "+${sPct}(v)+"%。",
  dreaming:()=>"夢のあとの「快眠」は HP・MP +20%、持続時間は 2 倍。",
  longdark:()=>"夢の武器の出現率が 2 倍。",
  dreams:v=>"長休息ごとの夢の判定 "+${sNum}(v)+" 回。",
  /* 1.6.3 で増えた鍵。難読化で定数が畳み込まれた（PACT_FLOOR → 15 など）ので、
     以前は元の実装に委ねるしかなかったものも日本語で書けるようになった。 */
  debt:v=>"能力の消費 MP が 0 になり、代わりに最大 HP の "+${fmtFig}(100*v)+"%。下限 15%。",
  lightHour:v=>"毎分 "+${fmtFig}(100*v)+"%: 5 分間、ダメージ +50%、速度 +50%。",
  tollman:()=>"CHA の判定が DC 2 ぶん易しくなり、帳場の値が 5% 安くなる。",
  scholar:()=>"INT の判定が DC 2 ぶん易しくなり、恩恵の持続が 25% 延びる。",
  bellows:()=>"鍛冶の段が 50% 速く貯まり、代金が 10% 安くなる。",
  scaled:()=>"レベルごとに防御 +1、HP +2。"
};
const __jaMult = {
  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}(100*Math.abs(v-1))+"%。",
  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?"引き。":"増し。")
};
/* 装備の枠名。G.equip のキーそのものでセーブに入るので、値は英語のまま据え置き、
   画面に出すときだけ読み替える。出る場所が二つある（装備表と武具庫の絞り込み）。 */
const __jaSlot = s => ({"Main hand":"利き手","Off hand":"盾手",
  "Armour":"鎧","Accessory":"装身具"})[s] || s;
const __jaSchool = {sight:"索敵", crit:"クリティカル", travel:"移動", mend:"回復",
  undead:"アンデッド", rarity:"発見", ward:"防御", fury:"猛り"};
const __jaProcs = (school,v,f)=>(__jaSchool[school]||school)+"の能力の発動が"
  +(f?"さらに":"")+${mPct}(v)+"% "+(v>1?"多い。":"少ない。");
`;

  // mechLine の本体を、日本語を先に見る形に差し替える
  if (map.mechLineStart >= 0 && map.mechLineEnd) {
    const body = src.slice(map.mechLineStart, map.mechLineEnd);
    const sig = body.match(/^function ([\w$]+)\(([\w$]+),([\w$]+),([\w$]+)\)\{/);
    if (!sig) notes.push('mechLine の引数を読めません');
    else {
      const [, fn, k, v, f] = sig;
      const orig = body.replace(/^function [\w$]+\([\w$]+,[\w$]+,[\w$]+\)\{/, '').replace(/\}$/, '');
      edits.push({
        start: map.mechLineStart, end: map.mechLineEnd, kind: 'struct',
        text: JA + `function ${fn}(${k},${v},${f}){`
          + `if(0===${k}.indexOf("procs:"))return __jaProcs(${k}.slice(6),${v},${f});`
          + `if(__jaAdd[${k}])return __jaAdd[${k}](${v},${f});`
          + `if(__jaMult[${k}])return __jaMult[${k}](${v},${f});`
          + `/* 訳を用意していない鍵は元の英語に委ねる */ ${orig}}`,
      });
    }
  }
  // 表そのものは英語のまま据え置く（上の日本語が優先されるので画面には出ない）。
  // 断片だけ訳されると「一撃の作用炎15% 頻繁に。」になるため、触らせない。
  for (const t of ['MECH_ADD', 'WORLD_ADD', 'MECH_MULT', 'WORLD_MULT'])
    if (map[t]) protect.push([map[t].start, map[t].end]);
  for (const t of ['MECH_PROCS', 'WORLD_PROCS'])
    if (map[t]) protect.push([map[t].start, map[t].end]);

  /* ---------- 2. 戦闘ログの一行 --------------------------------------- */
  const cut = (fromNeedle, toNeedle, label, make) => {
    const a = findOne(src, fromNeedle, label + ' の始まり', notes);
    if (a < 0) return;
    const b = src.indexOf(toNeedle, a);
    if (b < 0) { notes.push('見つからない: ' + label + ' の終わり'); return; }
    const region = src.slice(a, b + toNeedle.length);
    const text = make(region);
    if (text == null) { notes.push('組み立てられない: ' + label); return; }
    edits.push({ start: a, end: b + toNeedle.length, kind: 'struct', text });
  };

  cut('if(n?d.push(plainWords?"you hit "', '+"</span>"}', '戦闘ログ', region => {
    // 潰された名前は毎回変わりうるので、この場で読み取る
    const m = region.match(/^if\((\w+)\?d\.push\(plainWords\?"you hit "\+(\w+)\+" for "\+\1:(\w+)\.cast\?(\w+)\?/);
    if (!m) return null;
    const [, you, name, f, crit] = m;
    const foe = (region.match(/else if\((\w+)\)d\.push\(plainWords\?"it hits you for "\+\1/) || [])[1];
    const rd = (region.match(/(\w+)\.surprise<0/) || [])[1];
    const head = (region.match(/const (\w+)=d\.join\(", "\)/) || [])[1];
    const tail = (region.match(/,(\w+)=plainWords\?"\. "/) || [])[1];
    if (!foe || !rd || !head || !tail) return null;
    return `if(${you})d.push(plainWords?${name}+"に "+${you}+" の打撃"`
      + `:${f}.cast?(${crit}?"送りが"+${name}+"を貫き、"+${you}:"送りが"+${name}+"を捉え、"+${you})`
      + `:(${crit}?"清き一撃が"+${name}+"を斬り開き、"+${you}:${name}+"に斬りつけ、"+${you}));`
      + `else if(${rd}.surprise<0&&!${rd}.you.length)d.push(plainWords?"不意を突かれ、この回は攻撃できない":"足が定まらぬうちに詰め寄られ、こちらの一撃は間に合わない");`
      + `else d.push(plainWords?"外した":${f}.cast?"送りは逸れた":"一撃は空を切った");`
      + `if(${rd}.held)d.push(plainWords?"相手は拘束され、この回は攻撃なし":"相手は縛めに掛かり、その一撃は費やされない");`
      + `else if(${rd}.surprise>0)d.push(plainWords?"相手は不意を突かれ、この回は攻撃なし":"相手はまだあなたを探して身を巡らせており、その一撃は費やされない");`
      + `else if(${foe})d.push(plainWords?"被弾 "+${foe}:"返す一撃を受けて "+${foe});`
      + `else if(${rd}.foe.some(e=>e.warded)){const w=Math.round(${rd}.foe.reduce((a,b)=>a+(b.warded||0),0));`
      + `d.push(plainWords?"被弾 "+w+"（MP で肩代わり）":"一撃は入ったが、護りの井戸がそれを飲んだ：マナ "+w)}`
      + `else ${rd}.foe.length&&d.push(plainWords?"相手は外した":"相手の一撃は空を切った");`
      + `const ${head}=d.join("、"),${tail}=plainWords?"。"+${name}+" "+Math.round(${rd}.fhp)+"/"+${f}.foeMax+"、あなた "+Math.floor(${rd}.hp)+"/"+hpTotal()+"。"`
      + `:"。残り "+Math.round(${rd}.fhp)+" / "+${f}.foeMax+"。";`
      + `return"<span data-helptitle='第"+${rd}.n+"回' data-help='"+h+"'>"+${head}+${tail}+"</span>"}`;
  });

  /* ---------- 3. 装備の枠名（表示だけ日本語に） ------------------------ */
  {
    const i = src.indexOf('<td class="kitslot">');
    if (i < 0) notes.push('見つからない: 装備の枠の升目');
    else {
      // テンプレートリテラルの中なので `${e}` の形で入っている
      const m = src.slice(i).match(/^<td class="kitslot">\$\{(\w+)\}<\/td>/);
      if (!m) notes.push('組み立てられない: 装備の枠の升目');
      else edits.push({
        start: i, end: i + m[0].length, kind: 'struct',
        text: '<td class="kitslot">${__jaSlot(' + m[1] + ')}</td>',
      });
    }
  }

  /* ---------- 3b. 武具庫の絞り込みに出る枠名 -------------------------- */
  {
    const i = src.indexOf('"armslotfilt"');
    if (i < 0) notes.push('見つからない: 武具庫の枠の絞り込み');
    else {
      const seg = src.slice(i, i + 400);
      const m = seg.match(/createTextNode\(" "\+(\w+)\)/);
      if (!m) notes.push('組み立てられない: 武具庫の枠の絞り込み');
      else edits.push({
        start: i + m.index, end: i + m.index + m[0].length, kind: 'struct',
        text: `createTextNode(" "+__jaSlot(${m[1]}))`,
      });
    }
  }

  /* ---------- 4. 保存からの経過 ---------------------------------------- */
  {
    const i = src.indexOf('"Saved "+(');
    if (i < 0) notes.push('見つからない: 保存からの経過');
    else {
      const m = src.slice(i).match(/^"Saved "\+\((\w+)<5\?"just now":\1<60\?\1\+"s ago":Math\.floor\(\1\/60\)\+"m ago"\)/);
      if (!m) notes.push('組み立てられない: 保存からの経過');
      else edits.push({
        start: i, end: i + m[0].length, kind: 'struct',
        text: `(${m[1]}<5?"たった今保存":${m[1]}<60?${m[1]}+"秒前に保存":Math.floor(${m[1]}/60)+"分前に保存")`,
      });
    }
  }

  /* ---------- 5. 名誉録の段位表示「0 of 20」 --------------------------- */
  {
    // s = e.max && e.max<=8 ? "■"… : e.max ? r+" of "+e.max : …
    const m = src.match(/(\w+)\.max\?(\w+)\+" of "\+\1\.max:/);
    if (!m) notes.push('見つからない: 名誉録の段位表示');
    else edits.push({
      start: m.index, end: m.index + m[0].length, kind: 'struct',
      text: `${m[1]}.max?${m[2]}+"／"+${m[1]}.max:`,
    });
  }

  /* ---------- 6. 複数形の s と不定冠詞 ---------------------------------
   * 日本語に複数形も不定冠詞もない。掲示板が「ゴブリンsを追い返す」
   * 「an ゴブリン」と書いていたのはこれ。minify 後は関数名が潰れているので、
   * 中身の形で当てる。
   */
  {
    const m = src.match(/\/s\$\/\.test\((\w+)\)\?\1\+"es":\1\+"s"/);
    if (!m) notes.push('見つからない: 複数形の s');
    else edits.push({ start: m.index, end: m.index + m[0].length, kind: 'struct', text: m[1] });
  }
  {
    let n = 0;
    for (const m of src.matchAll(/\/\^\[aeiou\]\/i\.test\(([\w.]+)\)\?"[Aa]n ":"[Aa] "/g)) {
      edits.push({ start: m.index, end: m.index + m[0].length, kind: 'struct', text: '""' });
      n++;
    }
    if (!n) notes.push('見つからない: 不定冠詞');
  }

  /* ---------- 7. 単複の出し分け -----------------------------------------
   * "3 choice"+(1===n?"":"s")+" waiting" が「3選択s待っている」になっていた。
   * 6 の複数形 s は関数 1 本で片付いたが、こちらは呼び出しごとに書かれている。
   * 形が決まっているので、まとめて空文字にする。
   */
  {
    let n = 0;
    for (const re of [/\(1===\w+\?"":"s"\)/g, /\(\w+>1\?"s":""\)/g, /\(1!==\w+\?"s":""\)/g]) {
      for (const m of src.matchAll(re)) {
        edits.push({ start: m.index, end: m.index + m[0].length, kind: 'struct', text: '""' });
        n++;
      }
    }
    if (!n) notes.push('見つからない: 単複の出し分け');
  }

  log(`  難読化コードの書き換え: ${edits.length} 箇所 / 触らない範囲 ${protect.length} 件`
    + (notes.length ? `\n  ! ${notes.join('\n  ! ')}` : ''));
  return { edits, protect, notes, map };
}
