/**
 * ============================================================
 *  src/ui/liveViewport.ts —— LiveArea 有界多行 live 区的宽度安全工具（纯函数）
 * ------------------------------------------------------------
 *  ★ 两条硬不变量（根治 Windows 终端"思维链刷屏"，缺一不可）：
 *
 *    Ink 默认 render 在主屏 scrollback 上用 log-update 维护**底部 live 区**：
 *    每帧 `eraseLines(上一帧 \n 数)`（光标上移逐行清）再重写。两个坑都会让上移清除"漏擦"：
 *
 *    ┌─ INV-1 每行物理宽度 < 终端宽 ──────────────────────────────┐
 *    │ stock ink `log-update.js` 用 `output.split('\n').length` 记账，    │
 *    │ **完全不补偿满宽行被终端自动折行**。若某行物理宽触到终端最后一列，   │
 *    │ 终端自己折一行 → 实际物理行 > ink 记的 \n 数 → eraseLines 擦少 →   │
 *    │ 它上面的行每帧 strand 进 scrollback = 刷屏。                       │
 *    │ ∴ live 区每一行都按**我们自己的** displayWidth（CJK/全角/emoji=2，  │
 *    │   宁宽不窄）预钳到 ≤ cols - LIVE_RIGHT_MARGIN 再交给 ink。不依赖    │
 *    │   ink 的字宽测量 → 终端永不折行 → \n 记账 == 物理行 → 精确擦除。     │
 *    └────────────────────────────────────────────────────────┘
 *    ┌─ INV-2 live 区总高度 < 终端行数 ──────────────────────────┐
 *    │ `ink.js` onRender：一旦 outputHeight >= stdout.rows 会**全屏        │
 *    │ clearTerminal 重写**（含整个 static），灾难性。∴ 流式内容预览必须   │
 *    │ 有界（取尾部最近 N 行，N 自适应终端高度，见 LiveArea.contentMaxLines）。│
 *    └────────────────────────────────────────────────────────┘
 *
 *    完整思维链 / 完整答案 / 完整工具结果一律走 `<Static>`（write-once，永不
 *    参与 log-update → 天然 strand-proof、高度不受限）。live 区只放**有界**的过程预览。
 *
 *  ★ 中文/emoji 宽度（关键，别退化成按码点切）：
 *    CJK / 全角 / emoji 1 码点 = 2 列。宽度必须按 charWidth 累加，不能按 length，
 *    否则这些字符实际占 2× 列宽被算窄、钳不住 → 终端折行 → INV-1 失守、刷屏复发。
 * ============================================================
 */
/** live 行右侧安全余量（列）：钳到 cols - 此值，吃掉光标在"恰好满宽"处的自动折行边界。 */
export const LIVE_RIGHT_MARGIN = 2;
/** 剥离 ANSI/CSI 控制序列（含 ESC 引导符）。流式文本理论上没有，做防御性清理。 */
const ANSI_RE = new RegExp('\\u001b\\[[0-9;?]*[ -/]*[@-~]', 'g');
/**
 * 单个 Unicode 码点的终端显示宽度：CJK / 全角 / emoji = 2，其余 = 1。
 *
 * 设计目标是做终端实际宽度的**上界估计**（宁可算宽、不可算窄）：算宽只会让行
 * 提前一两列截断（无害），算窄则会让钳制失效、终端折行（= 刷屏复发，INV-1 失守）。
 *
 * 覆盖：
 *   - 常见 East Asian Ambiguous 标点（…—–·“”‘’ 等）：CJK 终端按全角(2)渲染，
 *     这些是中文文本里最容易把行顶宽的"隐形 2 列"字符，统一按 2 计。
 *   - Emoji / 符号 / dingbat（💭✶ 等）：终端普遍按全角(2)渲染。注意 charWidth 是
 *     逐码点求和，ZWJ/变体序列会被算成各组件之和（偏大 = 安全方向）。
 *   - Hangul Jamo / CJK 部首笔画符号标点 / 假名 / CJK 统一 / 谚文音节 /
 *     兼容表意 / 竖排标点 / 全角 ASCII 与符号 / CJK 扩展 B+（星平面）。
 */
export function charWidth(cp) {
    // 常见 ambiguous-width 标点：CJK 终端按 2 列渲染，统一上界为 2 防折行
    if (cp === 0x00b7 || // ·
        cp === 0x2013 || // –
        cp === 0x2014 || // —
        cp === 0x2018 || // ‘
        cp === 0x2019 || // ’
        cp === 0x201c || // “
        cp === 0x201d || // ”
        cp === 0x2026 || // …
        cp === 0x203b // ※
    ) {
        return 2;
    }
    // Emoji / 符号 / dingbat：终端普遍全角渲染（含 💭 与 spinner ✶✷✸✹✺）。上界=2，算宽无害。
    if ((cp >= 0x2600 && cp <= 0x27bf) || // Misc Symbols + Dingbats（含 ✶✷✸✹✺）
        (cp >= 0x2b00 && cp <= 0x2bff) || // Misc Symbols and Arrows
        (cp >= 0xfe00 && cp <= 0xfe0f) || // Variation Selectors（VS16 强制全角）
        (cp >= 0x1f000 && cp <= 0x1faff) // Emoji 主区（含 💭）
    ) {
        return 2;
    }
    if (cp >= 0x1100 &&
        (cp <= 0x115f ||
            (cp >= 0x2e80 && cp <= 0x303e) ||
            (cp >= 0x3041 && cp <= 0xa4cf) ||
            (cp >= 0xac00 && cp <= 0xd7a3) ||
            (cp >= 0xf900 && cp <= 0xfaff) ||
            (cp >= 0xfe10 && cp <= 0xfe6f) ||
            (cp >= 0xff00 && cp <= 0xff60) ||
            (cp >= 0xffe0 && cp <= 0xffe6) ||
            (cp >= 0x20000 && cp <= 0x3fffd))) {
        return 2;
    }
    return 1;
}
/** 一段文本的显示宽度（按 charWidth 累加；不破坏 surrogate pair）。 */
export function displayWidth(text) {
    let w = 0;
    for (const ch of text) {
        w += charWidth(ch.codePointAt(0) ?? 0);
    }
    return w;
}
/**
 * 标准化流式文本：CRLF/CR → LF、剥离 ANSI、收敛尾部多余空行。
 */
export function normalizeLiveText(text) {
    return text
        .replace(/\r\n?/g, '\n')
        .replace(ANSI_RE, '')
        .replace(/\n+$/g, '');
}
/**
 * 取**前缀**：保留从头开始、显示宽度 ≤ maxWidth 的最长前缀（不破坏 surrogate pair）。
 * 用于状态行的单行宽度钳制（保住最重要的行首 glyph/阶段，多余的尾部丢弃）。
 */
export function clampWidthStart(text, maxWidth) {
    if (maxWidth <= 0)
        return '';
    let w = 0;
    let out = '';
    for (const ch of text) {
        const cw = charWidth(ch.codePointAt(0) ?? 0);
        if (w + cw > maxWidth)
            break;
        out += ch;
        w += cw;
    }
    return out;
}
/**
 * 把（可能多行的）流式文本硬换行成**显示行数组**，每行显示宽度 ≤ maxWidth（满足 INV-1）。
 *
 * 规则：
 *   - 先按原始 `\n` 切段落（保留空行 → 输出空字符串行）。
 *   - 段落内贪心累加：超过 maxWidth 时，优先在**最近一个空格**处断行（word-aware，
 *     英文不切断单词）；没有空格（如中文）则按字符硬断。
 *   - 全程按 charWidth（CJK/全角/emoji=2）计宽，绝不按 length。
 *
 * 这是"打字机多行"的核心：reasoning / 答案的长文本在这里被切成等宽窄行，
 * 再由 LiveArea 取尾部最近 N 行（满足 INV-2）。
 *
 * 导出供单测断言。
 */
export function wrapToDisplayLines(text, maxWidth) {
    if (maxWidth <= 0)
        return [];
    const normalized = normalizeLiveText(text);
    const out = [];
    for (const para of normalized.split('\n')) {
        if (para.length === 0) {
            out.push('');
            continue;
        }
        let line = '';
        let lineWidth = 0;
        let lastSpace = -1; // line 内最近一个空格的下标（断行机会）
        for (const ch of para) {
            const cw = charWidth(ch.codePointAt(0) ?? 0);
            // 超宽且当前行非空 → 先断行再放入 ch
            if (lineWidth + cw > maxWidth && line.length > 0) {
                if (lastSpace >= 0 && lastSpace < line.length - 1) {
                    // 在最近空格处断：head 不含该空格，余下部分（不含空格）顺延到下一行
                    const head = line.slice(0, lastSpace);
                    const rest = line.slice(lastSpace + 1);
                    out.push(head);
                    line = rest;
                    lineWidth = displayWidth(rest);
                }
                else {
                    out.push(line);
                    line = '';
                    lineWidth = 0;
                }
                lastSpace = -1;
            }
            line += ch;
            lineWidth += cw;
            if (ch === ' ')
                lastSpace = line.length - 1;
        }
        out.push(line);
    }
    return out;
}
