// 第3次抽出: 取りこぼしの最終掃除。
//  - 単語1語の表示文字列（KILL_VERBS など）を拾う
//  - 先頭/末尾に片方だけのタグが付いた文字列を拾う（"Achievement earned: <b>" 等）
import fs from 'fs';

const src = fs.readFileSync('app/game/index.html', 'utf8');
const tm = JSON.parse(fs.readFileSync('tm.json', 'utf8'));
const ov = JSON.parse(fs.readFileSync('overrides.json', 'utf8'));
for (const k in ov) if (!k.startsWith('_')) tm[k] = ov[k];

const styleBlock = (src.match(/<style>([\s\S]*?)<\/style>/) || [, ''])[1];
const cssClasses = new Set([...styleBlock.matchAll(/\.([A-Za-z][\w-]*)/g)].map(m => m[1]));
const domIds = new Set([...src.matchAll(/\bid="([^"]+)"/g)].map(m => m[1]));
const dotProps = new Set([...src.matchAll(/\.([A-Za-z_$][\w$]*)/g)].map(m => m[1]));
const domArgs = new Set([
  ...[...src.matchAll(/\$\(\s*["']([^"']+)["']\s*\)/g)].map(m => m[1]),
  ...[...src.matchAll(/getElementById\(\s*["']([^"']+)["']/g)].map(m => m[1]),
  ...[...src.matchAll(/classList\.\w+\(\s*["']([^"']+)["']/g)].map(m => m[1]),
  ...[...src.matchAll(/querySelector(?:All)?\(\s*["']([^"']+)["']/g)].map(m => m[1]),
  ...[...src.matchAll(/(?:addEventListener|on)\(\s*["']([a-z]+)["']/g)].map(m => m[1]),
]);

const ranges = [];
{
  const re = /<script>([\s\S]*?)<\/script>/g;
  let m;
  while ((m = re.exec(src))) ranges.push([m.index + '<script>'.length, m.index + '<script>'.length + m[1].length]);
}
function scan(text) {
  const out = []; let i = 0; const n = text.length;
  while (i < n) {
    const c = text[i];
    if (c === '/' && text[i + 1] === '/') { const e = text.indexOf('\n', i); i = e < 0 ? n : e; continue; }
    if (c === '/' && text[i + 1] === '*') { const e = text.indexOf('*/', i + 2); i = e < 0 ? n : e + 2; continue; }
    if (c === '"' || c === "'" || c === '`') {
      const q = c; let j = i + 1, cooked = '', bad = false;
      while (j < n) {
        const d = text[j];
        if (d === '\\') { cooked += text[j + 1]; j += 2; continue; }
        if (d === q) break;
        if (q !== '`' && d === '\n') { bad = true; break; }
        cooked += d; j++;
      }
      if (bad || j >= n) { i++; continue; }
      out.push(cooked); i = j + 1; continue;
    }
    i++;
  }
  return out;
}
let strings = [];
for (const [a, b] of ranges) strings = strings.concat(scan(src.slice(a, b)));

// 前後にはみ出したタグを外して、訳す本体だけを取り出す
export function affix(s) {
  const pre = (s.match(/^(?:\s*<\/?[a-zA-Z][^>]*>)+/) || [''])[0];
  let rest = s.slice(pre.length);
  const suf = (rest.match(/(?:<\/?[a-zA-Z][^>]*>\s*)+$/) || [''])[0];
  rest = rest.slice(0, rest.length - suf.length);
  return { pre, core: rest, suf };
}

// 内部id: id:"..." / unlock("...") / tally("...") の実引数はゲームの保存キー。
const idLits = new Set([
  ...[...src.matchAll(/id\s*:\s*["']([^"']+)["']/g)].map(m => m[1]),
  ...[...src.matchAll(/(?:unlock|tally|tallyAdd|hasAch|achOn)\(\s*["']([^"']+)["']/g)].map(m => m[1]),
  ...[...src.matchAll(/key\s*:\s*["']([^"']+)["']/g)].map(m => m[1]),
]);

function skip(s) {
  if (idLits.has(s)) return true;
  if (s.startsWith('data:')) return true;
  if (/<(path|circle|ellipse|polyline|polygon|rect|line )\b/.test(s)) return true;
  if (/^[\sMLCQZAHVmlcqzahv0-9.,\-]+$/.test(s)) return true;
  if (/^https?:|^mailto:|^lowmoor\./.test(s)) return true;
  if (domIds.has(s) || domArgs.has(s) || cssClasses.has(s)) return true;
  if (/^[a-z][a-z0-9]*(-[a-z0-9]+)+$/.test(s)) return true;
  if (/^[a-z]+:[a-z]/.test(s)) return true;
  if (/^[A-Za-z_$][\w$]*$/.test(s) && dotProps.has(s)) return true;
  if (/[{};]\s*[a-z-]+\s*:/.test(s) && !/[.!?]/.test(s)) return true;
  return false;
}

const todo = new Map();   // core -> 代表の原文
for (const s of strings) {
  if (tm[s] !== undefined) continue;
  if (skip(s)) continue;
  const { pre, core, suf } = affix(s);
  const bare = core.replace(/<[^>]*>/g, '').replace(/\$\{[^}]*\}/g, '').trim();
  if (!/[A-Za-z]{3,}/.test(bare)) continue;        // 読める語がない
  if (core.includes('${')) continue;               // テンプレート式は別扱い
  if (/^[A-Z]{2,6}$/.test(bare)) continue;         // STR/CON など
  const words = (core.match(/[A-Za-z][A-Za-z'-]*/g) || []).length;
  if (words === 1 && core.trim().length < 4) continue;            // 3文字以下の1語はid臭い
  if (/^[a-z]{8,}$/.test(core.trim())) continue;                  // 区切りなしの長い小文字 = id
  if (/stroke-width|stroke-dasharray|viewBox|<canvas|<input|<label class|<i class/.test(core)) continue;
  if (!todo.has(s)) todo.set(s, { pre, core, suf });
}

const rows = [...todo.entries()];
const cores = [...new Set(rows.map(([, v]) => v.core))].filter(c => c.trim().length >= 3);
const chars = cores.reduce((a, s) => a + s.length, 0);
console.log('strings needing work :', rows.length);
console.log('distinct cores       :', cores.length, chars, 'chars');
fs.writeFileSync('out3-cores.json', JSON.stringify(cores.sort((a, b) => b.length - a.length), null, 1));
fs.writeFileSync('out3-map.json', JSON.stringify(Object.fromEntries(rows), null, 1));

/* 翻訳に回す分。extract2 と同じく、以前その場のコマンドでやっていた絞り込み。
 * 人名・地名はローマ字のままにしたいので落とす（訳すと不自然になる）。 */
const grab = re => { const m = src.match(re); return m ? [...m[0].matchAll(/"([^"]+)"/g)].map(x => x[1]) : []; };
const names = new Set([
  ...grab(/const FIRSTNAMES = \[[\s\S]*?\];/),
  ...grab(/const SURNAMES = \[[\s\S]*?\];/),
  ...grab(/const HIRELING_BEAST_NAMES = \[[\s\S]*?\];/),
]);
// JS や DOM の予約語・列挙値。訳すと動かなくなる。
const JSWORDS = new Set(['object', 'string', 'number', 'boolean', 'function', 'undefined', 'true', 'false',
  'none', 'auto', 'hidden', 'block', 'flex', 'grid', 'inline', 'absolute', 'relative', 'fixed', 'sticky',
  'click', 'change', 'input', 'keydown', 'keyup', 'pointerdown', 'pointerup', 'mousemove', 'wheel', 'resize',
  'scroll', 'load', 'error', 'beforeunload', 'pagehide', 'visibilitychange', 'Escape', 'Enter', 'Tab',
  'main', 'page', 'file', 'text', 'html', 'body', 'head', 'span', 'div', 'img', 'svg', 'path', 'canvas',
  'left', 'right', 'center', 'top', 'bottom', 'start', 'end', 'both', 'web', 'desktop', 'class']);

const goodCore = s => {
  const t = s.trim();
  if (/^[:.<>'"/_#]/.test(t)) return false;
  if (/^--/.test(t)) return false;                 // CSS 変数
  if (/\d/.test(t) && !/\s/.test(t)) return false; // kills1000 のような id
  if (/^[a-z][a-zA-Z]*[A-Z]/.test(t)) return false;// startSpell のような camelCase
  if (names.has(t) || JSWORDS.has(t)) return false;
  if (/stroke-width|<button|<canvas|width=|height=/.test(t)) return false;
  const w = (t.match(/[A-Za-z][A-Za-z'-]*/g) || []).length;
  if (w === 1 && t.length < 5) return false;
  return true;
};
const go3 = cores.filter(goodCore);
fs.writeFileSync('out3-go.json', JSON.stringify(go3, null, 1));
console.log('go(翻訳へ):', go3.length, go3.reduce((a, s) => a + s.length, 0), 'chars');
console.log('\nsamples:');
cores.slice(0, 6).concat(cores.slice(-14)).forEach(s => console.log('  ' + JSON.stringify(s.slice(0, 100))));
