import { cellsToANSI, gridToANSI } from "../render/ansi.js";
import { flatten } from "../render/flatten.js";
import { sameStyle } from "../utils.js";
const ENTER_ALT_SCREEN = "\x1b[?1049h";
const EXIT_ALT_SCREEN = "\x1b[?1049l";
const HIDE_CURSOR = "\x1b[?25l";
const SHOW_CURSOR = "\x1b[?25h";
const CURSOR_HOME = "\x1b[H";
// Synchronized output (BSU/ESU, DCS 2026): tells supporting
// terminals to buffer drawing operations between BSU and ESU and
// apply them atomically. Eliminates the cell-by-cell repaint flicker
// otherwise visible during a full-screen render. Terminals without
// support (older xterm etc.) silently ignore the sequences.
// Spec: https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036
const BEGIN_SYNC = "\x1b[?2026h";
const END_SYNC = "\x1b[?2026l";
function moveTo(row, col) {
    return `\x1b[${row};${col}H`;
}
function sameCell(a, b) {
    return a.char === b.char && sameStyle(a, b);
}
function sameGridSize(a, b) {
    return a.length === b.length && a.every((row, idx) => row.length === b[idx].length);
}
function diffToANSI(prev, next) {
    const parts = [];
    for (let y = 0; y < next.length; y++) {
        const prevRow = prev[y];
        const nextRow = next[y];
        let x = 0;
        while (x < nextRow.length) {
            if (sameCell(prevRow[x], nextRow[x])) {
                x++;
                continue;
            }
            const start = x;
            const cells = [];
            while (x < nextRow.length && !sameCell(prevRow[x], nextRow[x])) {
                cells.push(nextRow[x]);
                x++;
            }
            parts.push(moveTo(y + 1, start + 1), cellsToANSI(cells));
        }
    }
    return parts.join("");
}
export class TerminalOutput {
    inAltScreen = false;
    previousGrid = null;
    synchronizedOutput;
    // Tracks whether our SIGTSTP listener is currently installed. We
    // remove it during the suspend handoff and re-install it on resume,
    // and SIGCONT can be delivered independently of our SIGTSTP path
    // (e.g. from a foreign SIGSTOP), so the resume path must be idempotent.
    sigtstpInstalled = false;
    exitHandler = () => this.destroy();
    sigintHandler = () => { this.destroy(); process.exit(130); };
    sigtermHandler = () => { this.destroy(); process.exit(143); };
    sigtstpHandler = () => {
        // Remove our handler before re-raising so the default handler can
        // actually suspend the process; otherwise we'd recurse into ourselves.
        this.suspend();
        if (this.sigtstpInstalled) {
            process.removeListener("SIGTSTP", this.sigtstpHandler);
            this.sigtstpInstalled = false;
        }
        process.kill(process.pid, "SIGTSTP");
    };
    sigcontHandler = () => {
        this.resume();
        // Re-install only if we don't currently have it registered.
        if (!this.sigtstpInstalled) {
            process.on("SIGTSTP", this.sigtstpHandler);
            this.sigtstpInstalled = true;
        }
    };
    constructor(opts = {}) {
        this.synchronizedOutput = opts.synchronizedOutput ?? true;
    }
    init() {
        process.stdout.write(ENTER_ALT_SCREEN + HIDE_CURSOR);
        this.inAltScreen = true;
        process.on("exit", this.exitHandler);
        process.on("SIGINT", this.sigintHandler);
        process.on("SIGTERM", this.sigtermHandler);
        process.on("SIGTSTP", this.sigtstpHandler);
        this.sigtstpInstalled = true;
        process.on("SIGCONT", this.sigcontHandler);
    }
    write(frame, _label) {
        if (!this.inAltScreen) {
            this.init();
        }
        const grid = flatten(frame, frame.width, frame.height);
        const ansi = this.previousGrid && sameGridSize(this.previousGrid, grid)
            ? diffToANSI(this.previousGrid, grid)
            : CURSOR_HOME + gridToANSI(grid);
        this.previousGrid = grid;
        if (ansi.length === 0)
            return;
        // Wrap the full-frame write in BSU/ESU so supporting terminals
        // apply the new frame atomically instead of streaming the
        // repaint cell-by-cell (which the eye perceives as flicker).
        process.stdout.write(this.synchronizedOutput ? BEGIN_SYNC + ansi + END_SYNC : ansi);
    }
    destroy() {
        if (this.inAltScreen) {
            process.stdout.write(SHOW_CURSOR + EXIT_ALT_SCREEN);
            this.inAltScreen = false;
        }
        this.previousGrid = null;
        this.removeSignalHandlers();
    }
    suspend() {
        if (this.inAltScreen) {
            process.stdout.write(SHOW_CURSOR + EXIT_ALT_SCREEN);
            this.inAltScreen = false;
        }
        this.previousGrid = null;
    }
    resume() {
        if (!this.inAltScreen) {
            process.stdout.write(ENTER_ALT_SCREEN + HIDE_CURSOR);
            this.inAltScreen = true;
        }
        this.previousGrid = null;
    }
    removeSignalHandlers() {
        process.removeListener("exit", this.exitHandler);
        process.removeListener("SIGINT", this.sigintHandler);
        process.removeListener("SIGTERM", this.sigtermHandler);
        process.removeListener("SIGTSTP", this.sigtstpHandler);
        process.removeListener("SIGCONT", this.sigcontHandler);
        this.sigtstpInstalled = false;
    }
}
