// --- 正規表現リテラルを見分ける ------------------------------------------
//
// これが無いと走査が同期を失う。1.6.3 でコードが難読化され、正規表現が
// 大量に現れるようになって表面化した。例えば
//     /["']/.test(x)
// の中の " を「文字列の始まり」と読んでしまうと、そこから先の位置が全部ずれ、
// コードの断片を文字列だと思って訳してしまう。実際
//     "…the Guild's own stores…"
// の途中から始まる偽の文字列が訳され、構文が壊れた。
//
// JS では / が「割り算」か「正規表現の始まり」かを、直前のトークンで決める。
// 直前が値（識別子・数値・閉じ括弧など）なら割り算、演算子やキーワードなら正規表現。
const WORD_BEFORE_REGEX =
  /(?:^|[^\w$.])(?:return|typeof|instanceof|in|of|new|delete|void|throw|case|do|else|yield|await)$/;

export function regexCanStart(text, i) {
  let j = i - 1;
  // 直前の空白とコメントを飛ばす
  for (;;) {
    while (j >= 0 && /\s/.test(text[j])) j--;
    if (j >= 1 && text[j] === '/' && text[j - 1] === '*') {
      const s = text.lastIndexOf('/*', j - 1);
      if (s < 0) break;
      j = s - 1; continue;
    }
    break;
  }
  if (j < 0) return true;
  const c = text[j];
  if (')]}'.includes(c)) return false;          // 値の終わり → 割り算
  if (/[\w$]/.test(c)) {
    // 識別子・数値の直後。ただしキーワードなら正規表現が来られる。
    let k = j;
    while (k >= 0 && /[\w$]/.test(text[k])) k--;
    return WORD_BEFORE_REGEX.test(text.slice(Math.max(0, k - 1), j + 1));
  }
  return true;                                   // 演算子・区切りの直後
}

// 正規表現リテラルの終わりを返す（開始の / の位置を渡す）。見分けがつかなければ -1。
export function skipRegex(text, i) {
  let j = i + 1, cls = false;
  const BS = String.fromCharCode(92);
  while (j < text.length) {
    const c = text[j];
    if (c === BS) { j += 2; continue; }
    if (c === '\n') return -1;                   // 正規表現は改行をまたげない
    if (cls) { if (c === ']') cls = false; j++; continue; }
    if (c === '[') { cls = true; j++; continue; }
    if (c === '/') { j++; while (j < text.length && /[a-z]/i.test(text[j])) j++; return j; }
    j++;
  }
  return -1;
}

// --- JS の文字列リテラルを、位置つきで正しく拾う --------------------------
// コメント・正規表現・テンプレート（${} の入れ子を含む）を飛ばしながら歩く。
// テンプレートの中の ${ } は JS の文脈なので、その中のリテラルも拾う。
export function scanJsStrings(text, base = 0) {
  const out = [];
  const BS = String.fromCharCode(92);
  // tplDepth: テンプレートの地の文にいるかどうかを積む
  const stack = [];      // {kind:'tpl'} | {kind:'expr', depth}
  let i = 0;
  const n = text.length;
  while (i < n) {
    const top = stack[stack.length - 1];
    const c = text[i];

    if (top && top.kind === 'tpl') {             // テンプレートの地の文
      if (c === BS) { i += 2; continue; }
      if (c === '$' && text[i + 1] === '{') { stack.push({ kind: 'expr', depth: 0 }); i += 2; continue; }
      if (c === '`') { stack.pop(); i++; continue; }
      i++; continue;
    }

    // JS の文脈
    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 === '/' && regexCanStart(text, i)) {
      const e = skipRegex(text, i);
      if (e > 0) { i = e; continue; }
    }
    if (c === '"' || c === "'") {
      const q = c; let j = i + 1, cooked = '', bad = false;
      while (j < n) {
        const d = text[j];
        if (d === BS) { cooked += text[j + 1]; j += 2; continue; }
        if (d === q) break;
        if (d === '\n') { bad = true; break; }
        cooked += d; j++;
      }
      if (bad || j >= n) { i++; continue; }
      out.push({ start: base + i, end: base + j + 1, quote: q, cooked });
      i = j + 1; continue;
    }
    if (c === '`') { stack.push({ kind: 'tpl' }); i++; continue; }
    if (top && top.kind === 'expr') {
      if (c === '{') { top.depth++; i++; continue; }
      if (c === '}') { if (top.depth === 0) { stack.pop(); i++; continue; } top.depth--; i++; continue; }
    }
    i++;
  }
  return out;
}

// テンプレートリテラルを、${} の入れ子を数えながら正しく切り出す。
//
// これが要るのは、素朴な走査（バッククォートからバッククォートまで）だと
//   `a ${x ? `b` : ""} c`
// のような入れ子で位置がずれ、穴の中の文字列リテラルが丸ごと見えなくなるため。
// 実際 `<span class="m">${esc(has ? petGrowthSay(p) : "not yet taken")}</span>`
// の "not yet taken" は、どの抽出器からも見えていなかった。
//
// 返すのは各テンプレートの
//   start / end   … バッククォートを含む全体
//   chunks[]      … 地の文（HTML が書かれている部分）の範囲
//   holes[]       … ${ } の中身の範囲（JS の式そのもの）
export function scanTemplates(text, base = 0) {
  const out = [];
  const stack = [];
  let i = 0;
  const n = text.length;
  while (i < n) {
    const top = stack[stack.length - 1];
    const c = text[i];

    if (!top || top.kind === 'expr') {
      // ふつうの JS の文脈
      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 === '/' && regexCanStart(text, i)) {
        const e = skipRegex(text, i);
        if (e > 0) { i = e; continue; }
      }
      if (c === '"' || c === "'") {
        let j = i + 1;
        while (j < n) { if (text[j] === '\\') { j += 2; continue; } if (text[j] === c || text[j] === '\n') break; j++; }
        i = j + 1; continue;
      }
      if (c === '`') { stack.push({ kind: 'tpl', start: i, chunks: [], holes: [], chunkStart: i + 1 }); i++; continue; }
      if (top) {
        if (c === '{') { top.depth++; i++; continue; }
        if (c === '}') {
          if (top.depth === 0) {
            stack.pop();
            const t = stack[stack.length - 1];   // 必ず tpl
            t.holes.push({ s: top.start, e: i });
            t.chunkStart = i + 1;
            i++; continue;
          }
          top.depth--; i++; continue;
        }
      }
      i++; continue;
    }

    // テンプレートの地の文
    if (c === '\\') { i += 2; continue; }
    if (c === '$' && text[i + 1] === '{') {
      top.chunks.push({ s: top.chunkStart, e: i });
      stack.push({ kind: 'expr', depth: 0, start: i + 2 });
      i += 2; continue;
    }
    if (c === '`') {
      top.chunks.push({ s: top.chunkStart, e: i });
      top.end = i + 1;
      stack.pop();
      out.push(top);
      i++; continue;
    }
    i++;
  }
  return out.map(t => ({
    start: base + t.start,
    end: base + (t.end ?? t.start),
    chunks: t.chunks.map(ch => ({ s: base + ch.s, e: base + ch.e })),
    holes: t.holes.map(h => ({ s: base + h.s, e: base + h.e })),
  }));
}

// 穴の中（= JS の式）にある文字列リテラルを、位置つきで拾う。
// テンプレートの地の文は対象外（そちらは extract4 が HTML として扱う）。
export function scanLiteralsInHoles(src, ranges) {
  const templates = [];
  for (const [a, b] of ranges) templates.push(...scanTemplates(src.slice(a, b), a));

  // 内側の穴が外側の穴に含まれることがあるので、範囲は素直に全部見る。
  // 同じ位置のリテラルは呼び出し側で重複排除する。
  const out = [];
  for (const t of templates) {
    for (const h of t.holes) {
      let i = h.s;
      while (i < h.e) {
        const c = src[i];
        if (c === '/' && src[i + 1] === '/') { const e = src.indexOf('\n', i); i = e < 0 || e > h.e ? h.e : e; continue; }
        if (c === '/' && src[i + 1] === '*') { const e = src.indexOf('*/', i + 2); i = e < 0 || e > h.e ? h.e : e + 2; continue; }
        if (c === '/' && regexCanStart(src, i)) {
          const e = skipRegex(src, i);
          if (e > 0 && e <= h.e) { i = e; continue; }
        }
        if (c === '`') { i++; continue; }           // 入れ子テンプレートは別途拾われる
        if (c === '"' || c === "'") {
          let j = i + 1, cooked = '', bad = false;
          while (j < h.e) {
            const d = src[j];
            if (d === '\\') { cooked += src[j + 1]; j += 2; continue; }
            if (d === c) break;
            if (d === '\n') { bad = true; break; }
            cooked += d; j++;
          }
          if (bad || j >= h.e) { i++; continue; }
          out.push({ start: i, end: j + 1, quote: c, cooked });
          i = j + 1; continue;
        }
        i++;
      }
    }
  }
  return out;
}
