"use strict";
/**
 * Copyright (c) 2020 The xterm.js authors. All rights reserved.
 * @license MIT
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ImageStorage = exports.CELL_SIZE_DEFAULT = void 0;
const ImageRenderer_1 = require("./ImageRenderer");
// fallback default cell size
exports.CELL_SIZE_DEFAULT = {
    width: 7,
    height: 14
};
/**
 * Extend extended attribute to also hold image tile information.
 *
 * Object definition is copied from base repo to fully mimick its behavior.
 * Image data is added as additional public properties `imageId` and `tileId`.
 */
class ExtendedAttrsImage {
    get ext() {
        if (this._urlId) {
            return ((this._ext & ~469762048 /* ExtFlags.UNDERLINE_STYLE */) |
                (this.underlineStyle << 26));
        }
        return this._ext;
    }
    set ext(value) { this._ext = value; }
    get underlineStyle() {
        // Always return the URL style if it has one
        if (this._urlId) {
            return 5 /* UnderlineStyle.DASHED */;
        }
        return (this._ext & 469762048 /* ExtFlags.UNDERLINE_STYLE */) >> 26;
    }
    set underlineStyle(value) {
        this._ext &= ~469762048 /* ExtFlags.UNDERLINE_STYLE */;
        this._ext |= (value << 26) & 469762048 /* ExtFlags.UNDERLINE_STYLE */;
    }
    get underlineColor() {
        return this._ext & (50331648 /* Attributes.CM_MASK */ | 16777215 /* Attributes.RGB_MASK */);
    }
    set underlineColor(value) {
        this._ext &= ~(50331648 /* Attributes.CM_MASK */ | 16777215 /* Attributes.RGB_MASK */);
        this._ext |= value & (50331648 /* Attributes.CM_MASK */ | 16777215 /* Attributes.RGB_MASK */);
    }
    get underlineVariantOffset() {
        const val = (this._ext & 3758096384 /* ExtFlags.VARIANT_OFFSET */) >> 29;
        if (val < 0) {
            return val ^ 0xFFFFFFF8;
        }
        return val;
    }
    set underlineVariantOffset(value) {
        this._ext &= ~3758096384 /* ExtFlags.VARIANT_OFFSET */;
        this._ext |= (value << 29) & 3758096384 /* ExtFlags.VARIANT_OFFSET */;
    }
    get urlId() {
        return this._urlId;
    }
    set urlId(value) {
        this._urlId = value;
    }
    constructor(ext = 0, urlId = 0, imageId = -1, tileId = -1) {
        this.imageId = imageId;
        this.tileId = tileId;
        this._ext = 0;
        this._urlId = 0;
        this._ext = ext;
        this._urlId = urlId;
    }
    clone() {
        /**
         * Technically we dont need a clone variant of ExtendedAttrsImage,
         * as we never clone a cell holding image data.
         * Note: Clone is only meant to be used by the InputHandler for
         * sticky attributes, which is never the case for image data.
         * We still provide a proper clone method to reflect the full ext attr
         * state in case there are future use cases for clone.
         */
        return new ExtendedAttrsImage(this._ext, this._urlId, this.imageId, this.tileId);
    }
    isEmpty() {
        return this.underlineStyle === 0 /* UnderlineStyle.NONE */ && this._urlId === 0 && this.imageId === -1;
    }
}
const EMPTY_ATTRS = new ExtendedAttrsImage();
/**
 * ImageStorage - extension of CoreTerminal:
 * - hold image data
 * - write/read image data to/from buffer
 *
 * TODO: image composition for overwrites
 */
class ImageStorage {
    constructor(_terminal, _renderer, _opts) {
        this._terminal = _terminal;
        this._renderer = _renderer;
        this._opts = _opts;
        // storage
        this._images = new Map();
        // last used id
        this._lastId = 0;
        // last evicted id
        this._lowestId = 0;
        // whether a full clear happened before
        this._fullyCleared = false;
        // whether render should do a full clear
        this._needsFullClear = false;
        // hard limit of stored pixels (fallback limit of 10 MB)
        this._pixelLimit = 2500000;
        try {
            this.setLimit(this._opts.storageLimit);
        }
        catch (e) {
            console.error(e.message);
            console.warn(`storageLimit is set to ${this.getLimit()} MB`);
        }
        this._viewportMetrics = {
            cols: this._terminal.cols,
            rows: this._terminal.rows
        };
    }
    dispose() {
        this.reset();
    }
    reset() {
        var _a;
        for (const spec of this._images.values()) {
            (_a = spec.marker) === null || _a === void 0 ? void 0 : _a.dispose();
        }
        // NOTE: marker.dispose above already calls ImageBitmap.close
        // therefore we can just wipe the map here
        this._images.clear();
        this._renderer.clearAll();
    }
    getLimit() {
        return this._pixelLimit * 4 / 1000000;
    }
    setLimit(value) {
        if (value < 0.5 || value > 1000) {
            throw RangeError('invalid storageLimit, should be at least 0.5 MB and not exceed 1G');
        }
        this._pixelLimit = (value / 4 * 1000000) >>> 0;
        this._evictOldest(0);
    }
    getUsage() {
        return this._getStoredPixels() * 4 / 1000000;
    }
    _getStoredPixels() {
        let storedPixels = 0;
        for (const spec of this._images.values()) {
            if (spec.orig) {
                storedPixels += spec.orig.width * spec.orig.height;
                if (spec.actual && spec.actual !== spec.orig) {
                    storedPixels += spec.actual.width * spec.actual.height;
                }
            }
        }
        return storedPixels;
    }
    _delImg(id) {
        const spec = this._images.get(id);
        this._images.delete(id);
        // FIXME: really ugly workaround to get bitmaps deallocated :(
        if (spec && window.ImageBitmap && spec.orig instanceof ImageBitmap) {
            spec.orig.close();
        }
    }
    /**
     * Wipe canvas and images on alternate buffer.
     */
    wipeAlternate() {
        var _a;
        // remove all alternate tagged images
        const zero = [];
        for (const [id, spec] of this._images.entries()) {
            if (spec.bufferType === 'alternate') {
                (_a = spec.marker) === null || _a === void 0 ? void 0 : _a.dispose();
                zero.push(id);
            }
        }
        for (const id of zero) {
            this._delImg(id);
        }
        // mark canvas to be wiped on next render
        this._needsFullClear = true;
        this._fullyCleared = false;
    }
    /**
     * Only advance text cursor.
     * This is an edge case from empty sixels carrying only a height but no pixels.
     * Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
     */
    advanceCursor(height) {
        if (this._opts.sixelScrolling) {
            let cellSize = this._renderer.cellSize;
            if (cellSize.width === -1 || cellSize.height === -1) {
                cellSize = exports.CELL_SIZE_DEFAULT;
            }
            const rows = Math.ceil(height / cellSize.height);
            for (let i = 1; i < rows; ++i) {
                this._terminal._core._inputHandler.lineFeed();
            }
        }
    }
    /**
     * Method to add an image to the storage.
     */
    addImage(img) {
        var _a;
        // never allow storage to exceed memory limit
        this._evictOldest(img.width * img.height);
        // calc rows x cols needed to display the image
        let cellSize = this._renderer.cellSize;
        if (cellSize.width === -1 || cellSize.height === -1) {
            cellSize = exports.CELL_SIZE_DEFAULT;
        }
        const cols = Math.ceil(img.width / cellSize.width);
        const rows = Math.ceil(img.height / cellSize.height);
        const imageId = ++this._lastId;
        const buffer = this._terminal._core.buffer;
        const termCols = this._terminal.cols;
        const termRows = this._terminal.rows;
        const originX = buffer.x;
        const originY = buffer.y;
        let offset = originX;
        let tileCount = 0;
        if (!this._opts.sixelScrolling) {
            buffer.x = 0;
            buffer.y = 0;
            offset = 0;
        }
        this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
        for (let row = 0; row < rows; ++row) {
            const line = buffer.lines.get(buffer.y + buffer.ybase);
            for (let col = 0; col < cols; ++col) {
                if (offset + col >= termCols)
                    break;
                this._writeToCell(line, offset + col, imageId, row * cols + col);
                tileCount++;
            }
            if (this._opts.sixelScrolling) {
                if (row < rows - 1)
                    this._terminal._core._inputHandler.lineFeed();
            }
            else {
                if (++buffer.y >= termRows)
                    break;
            }
            buffer.x = offset;
        }
        this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
        // cursor positioning modes
        if (this._opts.sixelScrolling) {
            buffer.x = offset;
        }
        else {
            buffer.x = originX;
            buffer.y = originY;
        }
        // deleted images with zero tile count
        const zero = [];
        for (const [id, spec] of this._images.entries()) {
            if (spec.tileCount < 1) {
                (_a = spec.marker) === null || _a === void 0 ? void 0 : _a.dispose();
                zero.push(id);
            }
        }
        for (const id of zero) {
            this._delImg(id);
        }
        // eviction marker:
        // delete the image when the marker gets disposed
        const endMarker = this._terminal.registerMarker(0);
        endMarker === null || endMarker === void 0 ? void 0 : endMarker.onDispose(() => {
            const spec = this._images.get(imageId);
            if (spec) {
                this._delImg(imageId);
            }
        });
        // since markers do not work on alternate for some reason,
        // we evict images here manually
        if (this._terminal.buffer.active.type === 'alternate') {
            this._evictOnAlternate();
        }
        // create storage entry
        const imgSpec = {
            orig: img,
            origCellSize: cellSize,
            actual: img,
            actualCellSize: Object.assign({}, cellSize), // clone needed, since later modified
            marker: endMarker || undefined,
            tileCount,
            bufferType: this._terminal.buffer.active.type
        };
        // finally add the image
        this._images.set(imageId, imgSpec);
    }
    /**
     * Render method. Collects buffer information and triggers
     * canvas updates.
     */
    // TODO: Should we move this to the ImageRenderer?
    render(range) {
        // setup image canvas in case we have none yet, but have images in store
        if (!this._renderer.canvas && this._images.size) {
            this._renderer.insertLayerToDom();
            // safety measure - in case we cannot spawn a canvas at all, just exit
            if (!this._renderer.canvas) {
                return;
            }
        }
        // rescale if needed
        this._renderer.rescaleCanvas();
        // exit early if we dont have any images to test for
        if (!this._images.size) {
            if (!this._fullyCleared) {
                this._renderer.clearAll();
                this._fullyCleared = true;
                this._needsFullClear = false;
            }
            if (this._renderer.canvas) {
                this._renderer.removeLayerFromDom();
            }
            return;
        }
        // buffer switches force a full clear
        if (this._needsFullClear) {
            this._renderer.clearAll();
            this._fullyCleared = true;
            this._needsFullClear = false;
        }
        const { start, end } = range;
        const buffer = this._terminal._core.buffer;
        const cols = this._terminal._core.cols;
        // clear drawing area
        this._renderer.clearLines(start, end);
        // walk all cells in viewport and draw tiles found
        for (let row = start; row <= end; ++row) {
            const line = buffer.lines.get(row + buffer.ydisp);
            if (!line)
                return;
            for (let col = 0; col < cols; ++col) {
                if (line.getBg(col) & 268435456 /* BgFlags.HAS_EXTENDED */) {
                    let e = line._extendedAttrs[col] || EMPTY_ATTRS;
                    const imageId = e.imageId;
                    if (imageId === undefined || imageId === -1) {
                        continue;
                    }
                    const imgSpec = this._images.get(imageId);
                    if (e.tileId !== -1) {
                        const startTile = e.tileId;
                        const startCol = col;
                        let count = 1;
                        /**
                         * merge tiles to the right into a single draw call, if:
                         * - not at end of line
                         * - cell has same image id
                         * - cell has consecutive tile id
                         */
                        while (++col < cols
                            && (line.getBg(col) & 268435456 /* BgFlags.HAS_EXTENDED */)
                            && (e = line._extendedAttrs[col] || EMPTY_ATTRS)
                            && (e.imageId === imageId)
                            && (e.tileId === startTile + count)) {
                            count++;
                        }
                        col--;
                        if (imgSpec) {
                            if (imgSpec.actual) {
                                this._renderer.draw(imgSpec, startTile, startCol, row, count);
                            }
                        }
                        else if (this._opts.showPlaceholder) {
                            this._renderer.drawPlaceholder(startCol, row, count);
                        }
                        this._fullyCleared = false;
                    }
                }
            }
        }
    }
    viewportResize(metrics) {
        var _a;
        // exit early if we have nothing in storage
        if (!this._images.size) {
            this._viewportMetrics = metrics;
            return;
        }
        // handle only viewport width enlargements, exit all other cases
        // TODO: needs patch for tile counter
        if (this._viewportMetrics.cols >= metrics.cols) {
            this._viewportMetrics = metrics;
            return;
        }
        // walk scrollbuffer at old col width to find all possible expansion matches
        const buffer = this._terminal._core.buffer;
        const rows = buffer.lines.length;
        const oldCol = this._viewportMetrics.cols - 1;
        for (let row = 0; row < rows; ++row) {
            const line = buffer.lines.get(row);
            if (line.getBg(oldCol) & 268435456 /* BgFlags.HAS_EXTENDED */) {
                const e = line._extendedAttrs[oldCol] || EMPTY_ATTRS;
                const imageId = e.imageId;
                if (imageId === undefined || imageId === -1) {
                    continue;
                }
                const imgSpec = this._images.get(imageId);
                if (!imgSpec) {
                    continue;
                }
                // found an image tile at oldCol, check if it qualifies for right exapansion
                const tilesPerRow = Math.ceil((((_a = imgSpec.actual) === null || _a === void 0 ? void 0 : _a.width) || 0) / imgSpec.actualCellSize.width);
                if ((e.tileId % tilesPerRow) + 1 >= tilesPerRow) {
                    continue;
                }
                // expand only if right side is empty (nothing got wrapped from below)
                let hasData = false;
                for (let rightCol = oldCol + 1; rightCol > metrics.cols; ++rightCol) {
                    if (line._data[rightCol * 3 /* Cell.SIZE */ + 0 /* Cell.CONTENT */] & 4194303 /* Content.HAS_CONTENT_MASK */) {
                        hasData = true;
                        break;
                    }
                }
                if (hasData) {
                    continue;
                }
                // do right expansion on terminal buffer
                const end = Math.min(metrics.cols, tilesPerRow - (e.tileId % tilesPerRow) + oldCol);
                let lastTile = e.tileId;
                for (let expandCol = oldCol + 1; expandCol < end; ++expandCol) {
                    this._writeToCell(line, expandCol, imageId, ++lastTile);
                    imgSpec.tileCount++;
                }
            }
        }
        // store new viewport metrics
        this._viewportMetrics = metrics;
    }
    /**
     * Retrieve original canvas at buffer position.
     */
    getImageAtBufferCell(x, y) {
        var _a, _b;
        const buffer = this._terminal._core.buffer;
        const line = buffer.lines.get(y);
        if (line && line.getBg(x) & 268435456 /* BgFlags.HAS_EXTENDED */) {
            const e = line._extendedAttrs[x] || EMPTY_ATTRS;
            if (e.imageId && e.imageId !== -1) {
                const orig = (_a = this._images.get(e.imageId)) === null || _a === void 0 ? void 0 : _a.orig;
                if (window.ImageBitmap && orig instanceof ImageBitmap) {
                    const canvas = ImageRenderer_1.ImageRenderer.createCanvas(window.document, orig.width, orig.height);
                    (_b = canvas.getContext('2d')) === null || _b === void 0 ? void 0 : _b.drawImage(orig, 0, 0, orig.width, orig.height);
                    return canvas;
                }
                return orig;
            }
        }
    }
    /**
     * Extract active single tile at buffer position.
     */
    extractTileAtBufferCell(x, y) {
        const buffer = this._terminal._core.buffer;
        const line = buffer.lines.get(y);
        if (line && line.getBg(x) & 268435456 /* BgFlags.HAS_EXTENDED */) {
            const e = line._extendedAttrs[x] || EMPTY_ATTRS;
            if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
                const spec = this._images.get(e.imageId);
                if (spec) {
                    return this._renderer.extractTile(spec, e.tileId);
                }
            }
        }
    }
    // TODO: Do we need some blob offloading tricks here to avoid early eviction?
    // also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size
    _evictOldest(room) {
        var _a;
        const used = this._getStoredPixels();
        let current = used;
        while (this._pixelLimit < current + room && this._images.size) {
            const spec = this._images.get(++this._lowestId);
            if (spec && spec.orig) {
                current -= spec.orig.width * spec.orig.height;
                if (spec.actual && spec.orig !== spec.actual) {
                    current -= spec.actual.width * spec.actual.height;
                }
                (_a = spec.marker) === null || _a === void 0 ? void 0 : _a.dispose();
                this._delImg(this._lowestId);
            }
        }
        return used - current;
    }
    _writeToCell(line, x, imageId, tileId) {
        if (line._data[x * 3 /* Cell.SIZE */ + 2 /* Cell.BG */] & 268435456 /* BgFlags.HAS_EXTENDED */) {
            const old = line._extendedAttrs[x];
            if (old) {
                if (old.imageId !== undefined) {
                    // found an old ExtendedAttrsImage, since we know that
                    // they are always isolated instances (single cell usage),
                    // we can re-use it and just update their id entries
                    const oldSpec = this._images.get(old.imageId);
                    if (oldSpec) {
                        // early eviction for in-viewport overwrites
                        oldSpec.tileCount--;
                    }
                    old.imageId = imageId;
                    old.tileId = tileId;
                    return;
                }
                // found a plain ExtendedAttrs instance, clone it to new entry
                line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
                return;
            }
        }
        // fall-through: always create new ExtendedAttrsImage entry
        line._data[x * 3 /* Cell.SIZE */ + 2 /* Cell.BG */] |= 268435456 /* BgFlags.HAS_EXTENDED */;
        line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
    }
    _evictOnAlternate() {
        var _a, _b;
        // nullify tile count of all images on alternate buffer
        for (const spec of this._images.values()) {
            if (spec.bufferType === 'alternate') {
                spec.tileCount = 0;
            }
        }
        // re-count tiles on whole buffer
        const buffer = this._terminal._core.buffer;
        for (let y = 0; y < this._terminal.rows; ++y) {
            const line = buffer.lines.get(y);
            if (!line) {
                continue;
            }
            for (let x = 0; x < this._terminal.cols; ++x) {
                if (line._data[x * 3 /* Cell.SIZE */ + 2 /* Cell.BG */] & 268435456 /* BgFlags.HAS_EXTENDED */) {
                    const imgId = (_a = line._extendedAttrs[x]) === null || _a === void 0 ? void 0 : _a.imageId;
                    if (imgId) {
                        const spec = this._images.get(imgId);
                        if (spec) {
                            spec.tileCount++;
                        }
                    }
                }
            }
        }
        // deleted images with zero tile count
        const zero = [];
        for (const [id, spec] of this._images.entries()) {
            if (spec.bufferType === 'alternate' && !spec.tileCount) {
                (_b = spec.marker) === null || _b === void 0 ? void 0 : _b.dispose();
                zero.push(id);
            }
        }
        for (const id of zero) {
            this._delImg(id);
        }
    }
}
exports.ImageStorage = ImageStorage;