import * as TSU from "@panyam/tsutils";
export class GridModel extends TSU.Events.EventEmitter {
    constructor() {
        super(...arguments);
        this.uuid = GridModel.idCounter++;
        this.lastUpdatedAt = 0;
        this.rows = [];
        this.rowAligns = new Map();
        this.colAligns = new Map();
    }
    debugValue() {
        const out = {
            rows: this.rows.map((r) => r.debugValue()),
            lastUpdatedAt: this.lastUpdatedAt,
        };
        return out;
    }
    get firstRow() {
        for (const gr of this.rows) {
            if (gr.numCells > 0)
                return gr.rowIndex;
        }
        return -1;
    }
    get firstCol() {
        let minCol = -1;
        for (const gr of this.rows) {
            const fc = gr.firstCol;
            if (fc >= 0) {
                if (minCol < 0 || fc < minCol) {
                    minCol = fc;
                }
            }
        }
        return minCol;
    }
    cellsInRow(row) {
        const out = [];
        const gr = this.rows[row];
        if (gr) {
            for (const cell of gr.cells) {
                if (cell === null || cell === void 0 ? void 0 : cell.value)
                    out.push(cell);
            }
        }
        return out;
    }
    cellsInCol(col) {
        const out = [];
        for (const gr of this.rows) {
            const cell = gr.cellAt(col);
            if (cell === null || cell === void 0 ? void 0 : cell.value)
                out.push(cell);
        }
        return out;
    }
    addRowAlign(align) {
        this.rowAligns.set(align.uuid, align);
    }
    addColAlign(align) {
        this.colAligns.set(align.uuid, align);
    }
    addRows(insertBefore = -1, numRows = 1) {
        if (insertBefore < 0) {
            insertBefore = this.rows.length;
        }
        let next = this.rows[insertBefore] || null;
        const prev = this.rows[insertBefore - 1] || null;
        for (let i = numRows - 1; i >= 0; i--) {
            const newRow = new GridRow(this, insertBefore + i);
            this.rows.splice(insertBefore, 0, newRow);
            if (next != null) {
                newRow.defaultRowAlign.addSuccessor(next.defaultRowAlign);
            }
            if (i == 0 && insertBefore > 0) {
                prev.defaultRowAlign.addSuccessor(newRow.defaultRowAlign);
            }
            next = newRow;
        }
        for (let i = insertBefore + numRows; i < this.rows.length; i++) {
            this.rows[i].rowIndex += numRows;
        }
        return this;
    }
    getRow(row) {
        if (row >= this.rows.length) {
            this.addRows(-1, 1 + row - this.rows.length);
        }
        return this.rows[row];
    }
    setValue(row, col, value, cellCreator) {
        var _a, _b;
        const grow = this.getRow(row);
        if (!cellCreator) {
            cellCreator = (row, col) => {
                return new GridCell(row, col);
            };
        }
        if (value == null) {
            const out = grow.clearCellAt(col);
            if (out != null) {
                (_a = this.eventHub) === null || _a === void 0 ? void 0 : _a.emit(GridCellEvent.CLEARED, this, {
                    loc: out.location,
                });
            }
            return out;
        }
        else {
            const cell = grow.cellAt(col, cellCreator);
            const oldValue = cell.value;
            (_b = this.eventHub) === null || _b === void 0 ? void 0 : _b.emit(GridCellEvent.UPDATED, this, {
                loc: cell.location,
                cell: cell,
                oldValue: cell.value,
            });
            cell.value = value;
            return oldValue;
        }
    }
    eventHubChanged() {
        console.log("Event Hub Changed for GridModel");
    }
}
GridModel.idCounter = 0;
export var GridCellEvent;
(function (GridCellEvent) {
    GridCellEvent["ADDED"] = "CellAdded";
    GridCellEvent["CLEARED"] = "CellCleared";
    GridCellEvent["REMOVED"] = "CellRemoved";
    GridCellEvent["UPDATED"] = "CellUpdated";
    GridCellEvent["MOVED"] = "CellMoved";
})(GridCellEvent || (GridCellEvent = {}));
export class GridCell {
    constructor(gridRow, colIndex, value = null) {
        this.gridRow = gridRow;
        this.colIndex = colIndex;
        this.value = value;
        this.uuid = GridCell.idCounter++;
        this.rowAlign = gridRow.defaultRowAlign;
    }
    get rowAlign() {
        return this._rowAlign;
    }
    set rowAlign(val) {
        val.addCell(this);
        this._rowAlign = val;
    }
    get colAlign() {
        return this._colAlign;
    }
    set colAlign(val) {
        val.addCell(this);
        this._colAlign = val;
    }
    get location() {
        return this.gridRow.rowIndex + ":" + this.colIndex;
    }
    get grid() {
        return this.gridRow.grid;
    }
    get rowIndex() {
        return this.gridRow.rowIndex;
    }
    debugValue() {
        const out = {
            r: this.gridRow.rowIndex,
            c: this.colIndex,
            value: this.value,
            y: this.rowAlign.coordOffset,
            h: this.rowAlign.maxLength,
        };
        if (this.colAlign) {
            out.x = this.colAlign.coordOffset;
            out.w = this.colAlign.maxLength;
        }
        return out;
    }
}
GridCell.idCounter = 0;
export class GridRow {
    constructor(grid, rowIndex) {
        this.grid = grid;
        this.rowIndex = rowIndex;
        this.cells = [];
        this.defaultRowAlign = new RowAlign();
        this.grid.addRowAlign(this.defaultRowAlign);
    }
    get firstCol() {
        var _a;
        for (let i = 0; i < this.cells.length; i++) {
            if ((_a = this.cells[i]) === null || _a === void 0 ? void 0 : _a.value) {
                return i;
            }
        }
        return -1;
    }
    get numCols() {
        return this.cells.length;
    }
    get numCells() {
        let i = 0;
        for (const cell of this.cells) {
            if (cell != null && cell.value != null)
                i++;
        }
        return i;
    }
    cellAt(col, creator) {
        let out = this.cells[col] || null;
        if (!out && creator) {
            this.cells[col] = out = creator(this, col);
            out.gridRow = this;
            out.colIndex = col;
            if (out.rowAlign) {
                this.grid.addRowAlign(out.rowAlign);
            }
            if (out.colAlign) {
                this.grid.addColAlign(out.colAlign);
            }
        }
        return out;
    }
    clearCellAt(col) {
        const out = this.cells[col] || null;
        if (out) {
            this.cells[col] = null;
        }
        return out;
    }
    debugValue() {
        return {
            r: this.rowIndex,
            cells: this.cells.filter((c) => c).map((c) => c === null || c === void 0 ? void 0 : c.debugValue()),
        };
    }
}
export class AlignedLine {
    constructor() {
        this.uuid = AlignedLine.idCounter++;
        this.needsLayout = false;
        this._coordOffset = 0;
        this._maxLength = 0;
        this.paddingBefore = 5;
        this.paddingAfter = 5;
        this.cells = [];
        this.prevLines = [];
        this.nextLines = [];
    }
    get coordOffset() {
        return this._coordOffset;
    }
    get maxLength() {
        return this._maxLength + this.paddingBefore + this.paddingAfter;
    }
    setMaxLength(length) {
        this._maxLength = length;
    }
    setPadding(before, after) {
        if (before >= 0) {
            this.paddingBefore = before;
        }
        if (after >= 0) {
            this.paddingAfter = after;
        }
    }
    addCell(cell) {
        if (this.beforeAddingCell(cell)) {
            this.cells.push(cell);
        }
        return this;
    }
    removeCell(cell) {
        if (this.beforeRemovingCell(cell)) {
            for (let i = 0; i < this.cells.length; i++) {
                if (this.cells[i].uuid == cell.uuid) {
                    this.cells.splice(i, 1);
                    break;
                }
            }
        }
        return this;
    }
    addSuccessor(next) {
        for (const c of this.nextLines) {
            if (c == next)
                return;
        }
        this.nextLines.push(next);
        next.prevLines.push(this);
    }
}
AlignedLine.idCounter = 0;
export class ColAlign extends AlignedLine {
    constructor() {
        super(...arguments);
        this.paddingBefore = 10;
        this.paddingAfter = 10;
    }
    setOffset(val) {
        this._coordOffset = val;
        for (const cell of this.cells) {
            if (cell.value) {
                const cellView = this.getCellView(cell);
                if (this._maxLength <= 0) {
                    this.evalMaxLength();
                }
                cellView.setBounds(val, null, this.maxLength, null, true);
            }
        }
    }
    evalMaxLength(changedCells = []) {
        this._maxLength = 0;
        for (const cell of this.cells) {
            if (cell.value) {
                const cellView = this.getCellView(cell);
                this._maxLength = Math.max(cellView.minSize.width, this._maxLength);
            }
        }
        return this._maxLength;
    }
    beforeAddingCell(cell) {
        if (cell.colAlign && cell.colAlign != this) {
            cell.colAlign.removeCell(cell);
        }
        return cell.colAlign != this;
    }
    beforeRemovingCell(cell) {
        return cell.colAlign == this;
    }
}
export class RowAlign extends AlignedLine {
    setOffset(val) {
        this._coordOffset = val;
        for (const cell of this.cells) {
            if (cell.value) {
                const cellView = this.getCellView(cell);
                if (this._maxLength <= 0) {
                    this.evalMaxLength();
                }
                cellView.setBounds(null, val, null, this.maxLength, true);
            }
        }
    }
    evalMaxLength(changedCells = []) {
        this._maxLength = 0;
        for (const cell of this.cells) {
            if (cell.value) {
                const cellView = this.getCellView(cell);
                this._maxLength = Math.max(cellView.minSize.height, this._maxLength);
            }
        }
        return this._maxLength;
    }
    beforeAddingCell(cell) {
        if (cell.rowAlign && cell.rowAlign != this) {
            cell.rowAlign.removeCell(cell);
        }
        return cell.rowAlign != this;
    }
    beforeRemovingCell(cell) {
        return cell.rowAlign == this;
    }
}
export class GridLayoutGroup {
    constructor() {
        this.gridModels = [];
        this.layoutChangeSubscribers = new Set();
        this.previousColumnWidths = new Map();
        this.previousRowHeights = new Map();
        this.eventHandler = (event) => {
            this.applyModelEvents(event.payload);
        };
    }
    onLayoutChange(callback) {
        this.layoutChangeSubscribers.add(callback);
        return () => {
            this.layoutChangeSubscribers.delete(callback);
        };
    }
    notifyLayoutChange(event) {
        for (const callback of this.layoutChangeSubscribers) {
            try {
                callback(event);
            }
            catch (e) {
                console.error("Error in layout change callback:", e);
            }
        }
    }
    get subscriberCount() {
        return this.layoutChangeSubscribers.size;
    }
    addGridModel(gridModel) {
        var _a;
        (_a = gridModel.eventHub) === null || _a === void 0 ? void 0 : _a.on(TSU.Events.EventHub.BATCH_EVENTS, this.eventHandler);
        this.gridModels.push(gridModel);
        return true;
    }
    startingRowAligns() {
        const out = [];
        const visited = {};
        for (const gm of this.gridModels) {
            for (const cell of gm.cellsInRow(gm.firstRow)) {
                if (cell.rowAlign && !visited[cell.rowAlign.uuid]) {
                    visited[cell.rowAlign.uuid] = true;
                    out.push(cell.rowAlign);
                }
            }
        }
        return out;
    }
    startingColAligns() {
        const out = [];
        const visited = {};
        for (const gm of this.gridModels) {
            for (const cell of gm.cellsInCol(gm.firstCol)) {
                if (cell.colAlign && !visited[cell.colAlign.uuid]) {
                    visited[cell.colAlign.uuid] = true;
                    out.push(cell.colAlign);
                }
            }
        }
        return out;
    }
    removeGridModel(gridModel) {
        var _a;
        (_a = gridModel.eventHub) === null || _a === void 0 ? void 0 : _a.removeOn(TSU.Events.EventHub.BATCH_EVENTS, this.eventHandler);
    }
    get startingRows() {
        return this.startingRowAligns();
    }
    get startingCols() {
        return this.startingColAligns();
    }
    refreshLayout(notify = true) {
        const changedRowAligns = {};
        const changedColAligns = {};
        for (const rowAlign of this.startingRowAligns()) {
            if (!(rowAlign.uuid in changedRowAligns)) {
                changedRowAligns[rowAlign.uuid] = {
                    align: rowAlign,
                    cells: [],
                };
            }
        }
        for (const colAlign of this.startingColAligns()) {
            if (!(colAlign.uuid in changedColAligns)) {
                changedColAligns[colAlign.uuid] = {
                    align: colAlign,
                    cells: [],
                };
            }
        }
        const rowHeightsChanged = this.doBfsLayout(this.startingRows, changedRowAligns, this.previousRowHeights);
        const columnWidthsChanged = this.doBfsLayout(this.startingCols, changedColAligns, this.previousColumnWidths);
        if (notify && this.layoutChangeSubscribers.size > 0) {
            this.notifyLayoutChange({
                affectedRowRange: null,
                affectedColRange: null,
                columnWidthsChanged,
                rowHeightsChanged,
                affectedGridModels: this.gridModels,
            });
        }
    }
    applyModelEvents(events) {
        const [changedRowAligns, changedColAligns, affectedGridModels] = this.changesForEvents(events);
        const hadRowChanges = Object.keys(changedRowAligns).length > 0;
        const hadColChanges = Object.keys(changedColAligns).length > 0;
        const rowHeightsChanged = this.doBfsLayout(this.startingRows, changedRowAligns, this.previousRowHeights);
        const columnWidthsChanged = this.doBfsLayout(this.startingCols, changedColAligns, this.previousColumnWidths);
        if (this.layoutChangeSubscribers.size > 0 && (hadRowChanges || hadColChanges)) {
            const affectedRowRange = this.calculateAffectedRowRange(changedRowAligns);
            const affectedColRange = this.calculateAffectedColRange(changedColAligns);
            this.notifyLayoutChange({
                affectedRowRange,
                affectedColRange,
                columnWidthsChanged,
                rowHeightsChanged,
                affectedGridModels: affectedGridModels,
            });
        }
    }
    calculateAffectedRowRange(changedRowAligns) {
        let minRow = Infinity;
        let maxRow = -Infinity;
        for (const alignId in changedRowAligns) {
            const { cells } = changedRowAligns[alignId];
            for (const cell of cells) {
                const rowIndex = cell.gridRow.rowIndex;
                minRow = Math.min(minRow, rowIndex);
                maxRow = Math.max(maxRow, rowIndex);
            }
        }
        if (minRow === Infinity)
            return null;
        return { start: minRow, end: maxRow };
    }
    calculateAffectedColRange(changedColAligns) {
        let minCol = Infinity;
        let maxCol = -Infinity;
        for (const alignId in changedColAligns) {
            const { cells } = changedColAligns[alignId];
            for (const cell of cells) {
                const colIndex = cell.colIndex;
                minCol = Math.min(minCol, colIndex);
                maxCol = Math.max(maxCol, colIndex);
            }
        }
        if (minCol === Infinity)
            return null;
        return { start: minCol, end: maxCol };
    }
    checkAndUpdateLength(align, previousMap) {
        const previous = previousMap.get(align.uuid);
        const current = align.maxLength;
        previousMap.set(align.uuid, current);
        return previous === undefined || previous !== current;
    }
    changesForEvents(events) {
        const cellVisited = {};
        const changedRowAligns = {};
        const changedColAligns = {};
        const affectedGridModelsSet = new Set();
        for (let i = events.length - 1; i >= 0; i--) {
            const event = events[i];
            const loc = event.payload.loc;
            if (cellVisited[loc])
                continue;
            cellVisited[loc] = true;
            const [row, col] = loc.split(":").map((x) => parseInt(x));
            const gridModel = event.source;
            affectedGridModelsSet.add(gridModel);
            const cell = gridModel.getRow(row).cellAt(col);
            if (cell) {
                if (!(cell.rowAlign.uuid in changedRowAligns)) {
                    changedRowAligns[cell.rowAlign.uuid] = {
                        align: cell.rowAlign,
                        cells: [],
                    };
                }
                changedRowAligns[cell.rowAlign.uuid]["cells"].push(cell);
                if (!(cell.colAlign.uuid in changedColAligns)) {
                    changedColAligns[cell.colAlign.uuid] = {
                        align: cell.colAlign,
                        cells: [],
                    };
                }
                changedColAligns[cell.colAlign.uuid]["cells"].push(cell);
            }
        }
        return [changedRowAligns, changedColAligns, Array.from(affectedGridModelsSet)];
    }
    ensureGetCellView(align) {
        if (!align.getCellView) {
            if (!this.getCellView) {
                return null;
            }
            align.getCellView = this.getCellView;
        }
        return align.getCellView;
    }
    doBfsLayout(startingLines, changedAligns, previousLengths) {
        if (!this.getCellView)
            return false;
        let anyDimensionChanged = false;
        for (const alignId in changedAligns) {
            const val = changedAligns[alignId];
            this.ensureGetCellView(val.align);
            val.align.evalMaxLength(val.cells);
            if (previousLengths && this.checkAndUpdateLength(val.align, previousLengths)) {
                anyDimensionChanged = true;
            }
        }
        let lineQueue = [];
        const visitedLines = {};
        for (const line of startingLines)
            lineQueue.push([null, line]);
        const lineOffsetChanged = {};
        while (lineQueue.length > 0) {
            const nextQueue = [];
            for (let i = 0; i < lineQueue.length; i++) {
                const [prevLineAlign, lineAlign] = lineQueue[i];
                visitedLines[lineAlign.uuid] = true;
                let newOffset = lineAlign.coordOffset;
                let lineChanged = lineAlign.uuid in changedAligns;
                if (prevLineAlign) {
                    if (lineOffsetChanged[prevLineAlign.uuid]) {
                        newOffset = prevLineAlign.coordOffset + prevLineAlign.maxLength;
                        lineChanged = true;
                    }
                }
                if (lineChanged) {
                    this.ensureGetCellView(lineAlign);
                    lineAlign.setOffset(newOffset);
                    lineOffsetChanged[lineAlign.uuid] = true;
                }
                for (const next of lineAlign.nextLines) {
                    if (!visitedLines[next.uuid]) {
                        nextQueue.push([lineAlign, next]);
                    }
                }
            }
            lineQueue = nextQueue;
        }
        return anyDimensionChanged;
    }
}
//# sourceMappingURL=grids.js.map