"use strict";

/**
 * Cp437Render — shared glyph/screen module (SPEC §7.1, §7.2, §12).
 * ScreenBuffer is DOM-free (unit-tested under Node); CanvasPainter and
 * FontLoader touch the DOM. One namespace object per file (§20).
 */
var Cp437Render = (function () {

    /** Standard 16-color VGA palette, exact RGB (§7.2). */
    var PALETTE = [
        "#000000", "#0000AA", "#00AA00", "#00AAAA",
        "#AA0000", "#AA00AA", "#AA5500", "#AAAAAA",
        "#555555", "#5555FF", "#55FF55", "#55FFFF",
        "#FF5555", "#FF55FF", "#FFFF55", "#FFFFFF"
    ];

    var COLS = 80, ROWS = 25, CELL_W = 8, CELL_H = 16;

    /** Pack a cell: ch (16 bits) | fg<<16 (4) | bg<<20 (4) | blink<<24 (1). */
    function pack(ch, fg, bg, blink) {
        return (ch & 0xFFFF) | ((fg & 0x0F) << 16) | ((bg & 0x0F) << 20) | (blink ? (1 << 24) : 0);
    }

    function unpack(cell) {
        return {
            ch: cell & 0xFFFF,
            fg: (cell >> 16) & 0x0F,
            bg: (cell >> 20) & 0x0F,
            blink: ((cell >> 24) & 1) === 1
        };
    }

    /**
     * The 80×25 character grid in a flat Uint32Array (§7.1).
     * @constructor
     */
    function ScreenBuffer(cols, rows) {
        this.cols = cols || COLS;
        this.rows = rows || ROWS;
        this.cells = new Uint32Array(this.cols * this.rows);
        this.clear(7, 0);
    }

    ScreenBuffer.prototype.idx = function (x, y) {
        return y * this.cols + x;
    };

    /** Set one cell (0-based x, y; ch is a Unicode codepoint). */
    ScreenBuffer.prototype.set = function (x, y, ch, fg, bg, blink) {
        if (x < 0 || y < 0 || x >= this.cols || y >= this.rows) { return; }
        this.cells[this.idx(x, y)] = pack(ch, fg, bg, blink);
    };

    /** Get one cell as {ch, fg, bg, blink}. */
    ScreenBuffer.prototype.get = function (x, y) {
        return unpack(this.cells[this.idx(x, y)]);
    };

    /** Convenience for tests/tools: the character at x,y as a string. */
    ScreenBuffer.prototype.charAt = function (x, y) {
        return String.fromCharCode(this.get(x, y).ch);
    };

    /** A row's text, right-trimmed (test/debug helper). */
    ScreenBuffer.prototype.rowText = function (y) {
        var s = "";
        for (var x = 0; x < this.cols; x++) { s += this.charAt(x, y); }
        return s.replace(/\s+$/, "");
    };

    ScreenBuffer.prototype.clear = function (fg, bg) {
        this.cells.fill(pack(0x20, fg, bg, false));
    };

    /** Scroll up one row; the freed bottom row is blanked with fg/bg. */
    ScreenBuffer.prototype.scrollUp = function (fg, bg) {
        this.cells.copyWithin(0, this.cols);
        var blank = pack(0x20, fg, bg, false);
        for (var i = (this.rows - 1) * this.cols; i < this.rows * this.cols; i++) {
            this.cells[i] = blank;
        }
    };

    /**
     * Scroll rows top..bottom (0-based, inclusive) up or down by n
     * (1.6.0, DECSTBM support): the chat screens scroll their window
     * instead of repainting nineteen rows per line. Freed rows blank
     * in fg/bg.
     */
    ScreenBuffer.prototype.scrollRegion = function (top, bottom, n, fg, bg) {
        top = Math.max(0, Math.min(this.rows - 1, top));
        bottom = Math.max(top, Math.min(this.rows - 1, bottom));
        var height = bottom - top + 1;
        var count = Math.min(Math.abs(n), height);
        if (count === 0) { return; }
        var blank = pack(0x20, fg, bg, false);
        var r;
        if (n > 0) { // up: rows move toward top, bottom rows blank
            this.cells.copyWithin(top * this.cols, (top + count) * this.cols, (bottom + 1) * this.cols);
            for (r = bottom - count + 1; r <= bottom; r++) {
                this.cells.fill(blank, r * this.cols, (r + 1) * this.cols);
            }
        } else {     // down: rows move toward bottom, top rows blank
            for (r = bottom; r >= top + count; r--) {
                this.cells.copyWithin(r * this.cols, (r - count) * this.cols, (r - count + 1) * this.cols);
            }
            for (r = top; r < top + count; r++) {
                this.cells.fill(blank, r * this.cols, (r + 1) * this.cols);
            }
        }
    };

    /** Fill a horizontal span [x0, x1] on row y with blanks in fg/bg. */
    ScreenBuffer.prototype.blankSpan = function (x0, x1, y, fg, bg) {
        for (var x = x0; x <= x1; x++) { this.set(x, y, 0x20, fg, bg, false); }
    };

    /**
     * Load the CP437 webfont with a 3s fallback to monospace (§7.1).
     * onReady(familyName) fires exactly once either way.
     */
    function loadFont(url, onReady) {
        var family = "Web437 IBM VGA";
        if (typeof FontFace === "undefined" || !document.fonts) {
            console.warn("ATDT: FontFace API unavailable; using monospace");
            onReady("monospace");
            return;
        }
        var done = false;
        var finish = function (fam) {
            if (!done) { done = true; onReady(fam); }
        };
        var face = new FontFace(family, "url(" + url + ")");
        face.load().then(function (loaded) {
            document.fonts.add(loaded);
            finish("\"" + family + "\"");
        }).catch(function () {
            console.warn("ATDT: CP437 webfont failed to load; using monospace");
            finish("monospace");
        });
        setTimeout(function () {
            if (!done) { console.warn("ATDT: CP437 webfont timed out (3s); using monospace"); }
            finish("monospace");
        }, 3000);
    }

    /**
     * Paints a ScreenBuffer onto a 640×400 canvas (§7.1). Glyphs draw at
     * integer positions with fillText; blink phase hides glyphs while their
     * backgrounds stay painted (§7.2).
     * @constructor
     */
    function CanvasPainter(canvas, buffer) {
        this.canvas = canvas;
        this.buffer = buffer;
        // No alpha:false here (1.7.4): an opaque canvas invites the
        // browser to render fillText with LCD subpixel antialiasing,
        // which fringes every glyph edge red on one side and blue on
        // the other -- invisible in running text, glaring on the
        // isolated blinking caret ("a tiny blue dot to the left and a
        // tiny red dot to the right"). A default (alpha) canvas gets
        // greyscale antialiasing; every cell paints its own background
        // anyway, so nothing ever shows through.
        this.g = canvas.getContext("2d");
        this.family = "monospace";
        this.blinkVisible = true;
        canvas.width = buffer.cols * CELL_W;
        canvas.height = buffer.rows * CELL_H;
        this.g.textBaseline = "bottom";
    }

    CanvasPainter.prototype.setFont = function (family) {
        this.family = family;
        this.repaint();
    };

    CanvasPainter.prototype.setBlinkPhase = function (visible) {
        if (this.blinkVisible === visible) { return; }
        this.blinkVisible = visible;
        // Repaint only blink cells (§18).
        for (var y = 0; y < this.buffer.rows; y++) {
            for (var x = 0; x < this.buffer.cols; x++) {
                if (this.buffer.get(x, y).blink) { this.drawCell(x, y); }
            }
        }
        // The hardware cursor blinks on the same clock (1.7.4).
        if (this.cursor) {
            this.drawCell(this.cursor.x, this.cursor.y);
            this.drawCursor();
        }
    };

    CanvasPainter.prototype.drawCell = function (x, y) {
        var cell = this.buffer.get(x, y);
        var g = this.g;
        g.fillStyle = PALETTE[cell.bg];
        g.fillRect(x * CELL_W, y * CELL_H, CELL_W, CELL_H);
        if (cell.blink && !this.blinkVisible) { return; }
        if (cell.ch === 0x20 || cell.ch === 0) { return; }
        g.fillStyle = PALETTE[cell.fg];
        g.font = CELL_H + "px " + this.family;
        // Re-set every draw: assigning canvas.width (editor resizes do)
        // silently resets the context to the default alphabetic baseline,
        // which pushed y/g descenders into the next row's cell where the
        // next fillRect ate them (1.5.5 bug report).
        g.textBaseline = "bottom";
        // Clipped to the cell (1.7.4): the webfont's antialiasing can
        // spill a pixel past the 8-wide advance, and a spill next to a
        // BLINKING cell survives the blink-phase repaint of that cell
        // alone -- the owner's "tiny dots either side of the cursor".
        // A glyph grid owes its neighbors nothing.
        g.save();
        g.beginPath();
        g.rect(x * CELL_W, y * CELL_H, CELL_W, CELL_H);
        g.clip();
        g.fillText(String.fromCharCode(cell.ch), x * CELL_W, (y + 1) * CELL_H);
        g.restore();
    };

    /**
     * The hardware cursor (1.7.4, owner): a blinking underscore bar at
     * the position the board's output left the cursor, hotkey screens
     * above all -- the way a real terminal sat at "Command:". Set to
     * {x, y} (cell coordinates) or null; it rides the same blink phase
     * as SGR-5 text and is drawn OVER the buffer, never into it.
     */
    CanvasPainter.prototype.drawCursor = function () {
        var c = this.cursor;
        if (!c || !this.blinkVisible) { return; }
        if (c.x < 0 || c.y < 0 || c.x >= this.buffer.cols || c.y >= this.buffer.rows) { return; }
        var cell = this.buffer.get(c.x, c.y);
        this.g.fillStyle = PALETTE[cell.fg === cell.bg ? 7 : cell.fg];
        this.g.fillRect(c.x * CELL_W, (c.y + 1) * CELL_H - 3, CELL_W, 2);
    };

    CanvasPainter.prototype.setCursor = function (pos) {
        var old = this.cursor;
        this.cursor = pos || null;
        // Erase the bar it may have left behind, paint it anew.
        if (old) { this.drawCell(old.x, old.y); }
        if (this.cursor) {
            this.drawCell(this.cursor.x, this.cursor.y);
            this.drawCursor();
        }
    };

    CanvasPainter.prototype.repaint = function () {
        for (var y = 0; y < this.buffer.rows; y++) {
            for (var x = 0; x < this.buffer.cols; x++) {
                this.drawCell(x, y);
            }
        }
        this.drawCursor();
    };

    return {
        PALETTE: PALETTE,
        COLS: COLS,
        ROWS: ROWS,
        CELL_W: CELL_W,
        CELL_H: CELL_H,
        pack: pack,
        unpack: unpack,
        ScreenBuffer: ScreenBuffer,
        CanvasPainter: CanvasPainter,
        loadFont: loadFont
    };
})();

if (typeof module !== "undefined" && module.exports) {
    module.exports = Cp437Render;
}
