// 第2次抽出: 第1次で取りこぼした「表示される文字列」を、
// DOM id / CSSクラス / 内部キーを機械的に除いたうえで拾い直す。
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]));
// $("x") / getElementById("x") / classList.*("x") の引数
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, base) {
  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({ start: base + i, quote: q, cooked });
      i = j + 1; continue;
    }
    i++;
  }
  return out;
}
let strings = [];
for (const [a, b] of ranges) strings = strings.concat(scan(src.slice(a, b), a));

const decls = [];
{
  const re = /^(?:const|let|function)\s+([A-Za-z_$][\w$]*)/gm;
  let m;
  while ((m = re.exec(src))) decls.push([m.index, m[1]]);
}
const ownerOf = pos => {
  let lo = 0, hi = decls.length - 1, best = '(top)';
  while (lo <= hi) { const mid = (lo + hi) >> 1; if (decls[mid][0] <= pos) { best = decls[mid][1]; lo = mid + 1; } else hi = mid - 1; }
  return best;
};

// ---- 表示テキストらしさの判定 -----------------------------------------
function skipReason(s) {
  if (!/[A-Za-z]{2,}/.test(s)) return 'no-letters';
  if (s.startsWith('data:')) return 'data-uri';
  if (/<(path|circle|ellipse|polyline|polygon|rect|line )\b/.test(s)) return 'svg';
  if (/^[\sMLCQZAHVmlcqzahv0-9.,\-]+$/.test(s)) return 'svg-path';
  if (/^https?:|^mailto:/.test(s)) return 'url';
  if (/^lowmoor\./.test(s)) return 'storage-key';
  if (domIds.has(s) || domArgs.has(s)) return 'dom';
  if (cssClasses.has(s)) return 'css-class';
  if (/^[a-z][a-z0-9]*(-[a-z0-9]+)+$/.test(s)) return 'kebab-id';          // lg-good, opt-wire
  if (/^[a-z]+:[a-z]+$/.test(s)) return 'lever-key';                        // procs:sight
  if (/^[A-Za-z_$][\w$]*$/.test(s) && dotProps.has(s)) return 'property';
  if (/[{};]\s*[a-z-]+\s*:/.test(s) && !/[.!?]/.test(s)) return 'css-text'; // インラインCSS
  if (/^[\w.#\-\/ ]*$/.test(s) && !/\s[a-z]/.test(s) && s.length < 12) return 'token';
  // タグを剥がして、実際に読める語が残らないものは表示テキストではない
  const bare = s.replace(/<[^>]*>/g, '').replace(/\$\{[^}]*\}/g, '').trim();
  if (!/[A-Za-z]{3,}/.test(bare)) return 'markup-only';
  if (!/[a-z]{2}/.test(bare)) return 'no-words';
  return null;
}

const plain = [], tmpl = [];
const owners = new Map();
for (const st of strings) {
  const s = st.cooked;
  if (tm[s] !== undefined) continue;
  const why = skipReason(s);
  if (why) continue;
  const owner = ownerOf(st.start);
  const bucket = s.includes('${') ? tmpl : plain;
  if (!bucket.includes(s)) bucket.push(s);
  const key = owner;
  owners.set(key, (owners.get(key) || 0) + s.length);
}

const chars = a => a.reduce((x, s) => x + s.length, 0);
console.log('NEW plain :', plain.length, chars(plain), 'chars');
console.log('NEW tmpl  :', tmpl.length, chars(tmpl), 'chars');
console.log('total     :', chars(plain) + chars(tmpl));
console.log('\ntop owners:');
[...owners.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20)
  .forEach(([k, v]) => console.log('  ' + String(v).padStart(6), k));

plain.sort((a, b) => b.length - a.length);
tmpl.sort((a, b) => b.length - a.length);
fs.writeFileSync('out2-plain.json', JSON.stringify(plain, null, 1));
fs.writeFileSync('out2-tmpl.json', JSON.stringify(tmpl, null, 1));

/* 翻訳に回す分だけを別に書き出す。
 *
 * ここは以前その場かぎりのコマンドでやっていて、スクリプトに入っていなかった。
 * そのため 1.6.0 の更新で out2-plain-go.json が古いまま使われ、新しく増えた
 * ツールチップがまるごと未訳で残った（画面の日本語比率が 92% → 68% に落ちた）。
 * 絞り込みは抽出の一部なので、ここに置く。 */
const stripEdgeTags = s => s.replace(/^(\s*<\/[a-z]+>)+/i, '').replace(/(<[a-z][^>]*>\s*)+$/i, '');
const unbalanced = s => {
  const t = stripEdgeTags(s);
  return (t.match(/</g) || []).length !== (t.match(/>/g) || []).length;
};
const notWorth = s => s.length < 5 || unbalanced(s) || /^[a-z]+:[a-z]/.test(s);
const go = plain.filter(s => !notWorth(s));
fs.writeFileSync('out2-plain-go.json', JSON.stringify(go, null, 1));
console.log('go(翻訳へ):', go.length, go.reduce((a, s) => a + s.length, 0), 'chars');
console.log('\nplain samples:');
[0, 1, Math.floor(plain.length / 2), plain.length - 30, plain.length - 5, plain.length - 1]
  .filter(i => i >= 0 && i < plain.length)
  .forEach(i => console.log('  ' + JSON.stringify(plain[i].slice(0, 110))));
