"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const eventemitter2_1 = require("eventemitter2");
const command_1 = require("./models/command");
const spriteBitSet_1 = require("./spriteBitSet");
const util_1 = require("./util");
/**
 * The state of sprites in one position of the current level being played.
 *
 * This stores all the sprites and which direction those sprites want to move.
 *
 * The [[TerminalUI]] uses this object to render and the [[GameEngine]] uses this to maintain the state
 * of one position of the current level.
 */
class Cell {
    constructor(level, sprites, rowIndex, colIndex) {
        this.level = level;
        this.rowIndex = rowIndex;
        this.colIndex = colIndex;
        this.state = new Map();
        this.cacheCollisionLayers = [];
        this.spriteBitSet = new spriteBitSet_1.SpriteBitSet(sprites);
        this.cachedKeyValue = null;
        for (const sprite of sprites) {
            this._setWantsToMove(sprite, util_1.RULE_DIRECTION.STATIONARY);
        }
    }
    _setWantsToMove(sprite, wantsToMove) {
        const collisionLayer = sprite.getCollisionLayer();
        const { wantsToMove: cellWantsToMove, sprite: cellSprite } = this.getStateForCollisionLayer(collisionLayer);
        const didActuallyChangeDir = cellWantsToMove !== wantsToMove;
        const didActuallyChangeSprite = cellSprite !== sprite;
        // replace the sprite in the bitSet
        if (cellSprite !== sprite) {
            if (cellSprite) {
                throw new Error(`BUG: Should have already been removed?`);
                // this.spriteBitSet.remove(cellSprite)
            }
            this.spriteBitSet.add(sprite);
        }
        this._setState(collisionLayer, sprite, wantsToMove);
        // call replaceSprite only **after** we updated the Cell
        if (cellSprite !== sprite) {
            this.replaceSpriteInLevel(cellSprite, sprite);
        }
        return didActuallyChangeSprite || didActuallyChangeDir;
    }
    _deleteWantsToMove(sprite) {
        // There may be other sprites in the same ... oh wait, no that's not possible.
        const collisionLayer = sprite.getCollisionLayer();
        const cellSprite = this.getSpriteByCollisionLayer(collisionLayer);
        const didActuallyChange = !!cellSprite;
        if (cellSprite) {
            this.spriteBitSet.remove(cellSprite);
        }
        this._setState(collisionLayer, null, null); // delete the entry
        return didActuallyChange;
    }
    setWantsToMoveCollisionLayer(collisionLayer, wantsToMove) {
        // Check that there is a sprite for this collision layer
        const { sprite, wantsToMove: cellWantsToMove } = this.getStateForCollisionLayer(collisionLayer);
        if (!sprite) {
            throw new Error(`BUG: No sprite for collision layer. Cannot set direction.\n${collisionLayer.toString()}`);
        }
        const didActuallyChange = cellWantsToMove !== wantsToMove;
        this._setState(collisionLayer, sprite, wantsToMove);
        sprite.updateCell(this, wantsToMove);
        return didActuallyChange;
    }
    getSpriteByCollisionLayer(collisionLayer) {
        const { sprite } = this.getStateForCollisionLayer(collisionLayer);
        return sprite || null;
    }
    getCollisionLayers() {
        // return [...this._state.keys()]
        //     .sort((c1, c2) => c1.id - c2.id)
        return this.cacheCollisionLayers;
    }
    getSprites() {
        // Just pull out the sprite, not the wantsToMoveDir
        const sprites = [];
        const collisionLayers = this.getCollisionLayers();
        for (const collisionLayer of collisionLayers) {
            const sprite = this.getSpriteByCollisionLayer(collisionLayer);
            if (sprite) {
                sprites.push(sprite);
            }
        }
        return sprites.reverse(); // reversed so we render sprites properly
    }
    getSpritesAsSet() {
        // Just pull out the sprite, not the wantsToMoveDir
        const sprites = new Set();
        for (const { sprite } of this.state.values()) {
            sprites.add(sprite);
        }
        return sprites;
    }
    getSpriteAndWantsToMoves() {
        // Just pull out the sprite, not the wantsToMoveDir
        // Retur na new set so we can mutate it later
        const map = new Map();
        for (const collisionLayer of this.getCollisionLayers()) {
            const { sprite, wantsToMove } = this.getStateForCollisionLayer(collisionLayer);
            map.set(sprite, wantsToMove);
        }
        return map;
    }
    getCollisionLayerWantsToMove(collisionLayer) {
        const { wantsToMove } = this.getStateForCollisionLayer(collisionLayer);
        return wantsToMove || null;
    }
    hasSprite(sprite) {
        const cellSprite = this.getSpriteByCollisionLayer(sprite.getCollisionLayer());
        return sprite === cellSprite;
    }
    getNeighbor(direction) {
        switch (direction) {
            case util_1.RULE_DIRECTION.UP:
                return this.getRelativeNeighbor(-1, 0);
            case util_1.RULE_DIRECTION.DOWN:
                return this.getRelativeNeighbor(1, 0);
            case util_1.RULE_DIRECTION.LEFT:
                return this.getRelativeNeighbor(0, -1);
            case util_1.RULE_DIRECTION.RIGHT:
                return this.getRelativeNeighbor(0, 1);
            default:
                throw new Error(`BUG: Unsupported direction "${direction}"`);
        }
    }
    getWantsToMove(sprite) {
        return this.getCollisionLayerWantsToMove(sprite.getCollisionLayer());
    }
    hasCollisionWithSprite(otherSprite) {
        return !!this.getCollisionLayerWantsToMove(otherSprite.getCollisionLayer());
    }
    clearWantsToMove(sprite) {
        this._setWantsToMove(sprite, util_1.RULE_DIRECTION.STATIONARY);
        sprite.updateCell(this, util_1.RULE_DIRECTION.STATIONARY);
    }
    addSprite(sprite, wantsToMove) {
        let didActuallyChange = false;
        // If we already have a sprite in that collision layer then we need to remove it
        const prevSprite = this.getSpriteByCollisionLayer(sprite.getCollisionLayer());
        const prevWantsToMove = this.getCollisionLayerWantsToMove(sprite.getCollisionLayer());
        if (prevSprite && prevSprite !== sprite) {
            this.removeSprite(prevSprite);
        }
        if (wantsToMove) {
            didActuallyChange = this._setWantsToMove(sprite, wantsToMove);
        }
        else if (!this.hasSprite(sprite)) {
            wantsToMove = prevWantsToMove || util_1.RULE_DIRECTION.STATIONARY; // try to preserve the wantsToMove
            didActuallyChange = this._setWantsToMove(sprite, wantsToMove);
        }
        sprite.addCell(this, wantsToMove);
        return didActuallyChange;
    }
    updateSprite(sprite, wantsToMove) {
        // Copy/pasta from addSprite except it calls updateCell
        let didActuallyChange = false;
        // If we already have a sprite in that collision layer then we need to remove it
        const prevSprite = this.getSpriteByCollisionLayer(sprite.getCollisionLayer());
        if (prevSprite !== sprite) {
            throw new Error(`BUG: Should not be trying to update the direction of a sprite that is not in the cell`);
        }
        if (wantsToMove) {
            didActuallyChange = this._setWantsToMove(sprite, wantsToMove);
        }
        else if (!this.hasSprite(sprite)) {
            throw new Error(`BUG: sprite should already be in the cell since we are updating it`);
        }
        sprite.updateCell(this, wantsToMove);
        return didActuallyChange;
    }
    removeSprite(sprite) {
        const didActuallyChange = this._deleteWantsToMove(sprite);
        sprite.removeCell(this);
        return didActuallyChange;
    }
    toString() {
        return `Cell [${this.rowIndex}][${this.colIndex}] ${[...this.getSpriteAndWantsToMoves().entries()].map(([sprite, wantsToMove]) => `${wantsToMove} ${sprite.getName()}`).join(' ')}`;
    }
    toKey() {
        if (!this.cachedKeyValue) {
            this.cachedKeyValue = [...this.state.values()].map(({ sprite, wantsToMove }) => `${wantsToMove} ${sprite.getName()}`).join(' ');
            // this.cachedKeyValue = [...this.getSpriteAndWantsToMoves().entries()].map(([sprite, wantsToMove]) => `${wantsToMove} ${sprite.getName()}`).join(' ')
        }
        return this.cachedKeyValue;
    }
    toSnapshot() {
        return this.getSpritesAsSet();
    }
    fromSnapshot(newSprites) {
        const currentSprites = this.getSpritesAsSet();
        const spritesToRemove = util_1.setDifference(currentSprites, newSprites);
        const spritesToAdd = util_1.setDifference(newSprites, currentSprites);
        // Remove Sprites
        this.removeSprites(spritesToRemove);
        // Add Sprites
        this.addSprites(spritesToAdd);
    }
    // This method is replaced by LetterCells (because they are not boud to a level)
    replaceSpriteInLevel(cellSprite, newSprite) {
        this.getLevel().replaceSprite(this, cellSprite, newSprite);
    }
    _setState(collisionLayer, sprite, wantsToMove) {
        let needsToUpdateCache;
        if (sprite) {
            needsToUpdateCache = this.cacheCollisionLayers.indexOf(collisionLayer) < 0;
            this.state.set(collisionLayer, { wantsToMove, sprite });
        }
        else {
            this.state.delete(collisionLayer);
            needsToUpdateCache = true;
        }
        if (needsToUpdateCache) {
            // Update the collisionLayer Cache
            this.cacheCollisionLayers = [...this.state.keys()]
                .sort((c1, c2) => c1.id - c2.id);
        }
        this.invalidateKey();
    }
    getLevel() {
        if (!this.level) {
            throw new Error(`BUG: we need an engine Level in order to find neighbors. It is optional for letters in messages`);
        }
        return this.level;
    }
    getStateForCollisionLayer(collisionLayer) {
        const state = this.state.get(collisionLayer);
        if (!state) {
            return { wantsToMove: null, sprite: null };
        }
        return state;
    }
    getRelativeNeighbor(y, x) {
        return this.getLevel().getCellOrNull(this.rowIndex + y, this.colIndex + x);
    }
    removeSprites(sprites) {
        for (const sprite of sprites) {
            this.removeSprite(sprite);
        }
    }
    addSprites(sprites) {
        for (const sprite of sprites) {
            this.addSprite(sprite, null);
        }
    }
    invalidateKey() {
        this.cachedKeyValue = null;
    }
}
exports.Cell = Cell;
class Level {
    constructor() {
        this.rowCache = [];
        this.colCache = [];
    }
    setCells(cells) {
        this.cells = cells;
    }
    getCells() {
        if (!this.cells) {
            throw new Error(`BUG: Should have called setCells() first`);
        }
        return this.cells;
    }
    getCellOrNull(rowIndex, colIndex) {
        const row = this.getCells()[rowIndex];
        if (row) {
            return row[colIndex];
        }
        return null;
    }
    getCell(rowIndex, colIndex) {
        // Skip error checks for performance
        return this.getCells()[rowIndex][colIndex];
    }
    replaceSprite(cell, oldSprite, newSprite) {
        // When a new Cell is instantiated it will call this method but `this.cells` is not defined yet
        if (this.cells) {
            // Invalidate the row/column cache. It will be rebuilt when requested
            this.rowCache[cell.rowIndex] = null;
            this.colCache[cell.colIndex] = null;
        }
    }
    rowContainsSprites(rowIndex, spritesPresent, anySpritesPresent) {
        let cache = this.rowCache[rowIndex];
        if (!cache) {
            cache = this.computeRowCache(rowIndex);
            this.rowCache[rowIndex] = cache;
        }
        return cache.containsAll(spritesPresent) && anySpritesPresent.isEmpty() ? true : cache.containsAny(anySpritesPresent);
    }
    colContainsSprites(colIndex, sprites, anySpritesPresent) {
        let cache = this.colCache[colIndex];
        if (!cache) {
            cache = this.computeColCache(colIndex);
            this.colCache[colIndex] = cache;
        }
        return cache.containsAll(sprites) && anySpritesPresent.isEmpty() ? true : cache.containsAny(anySpritesPresent);
    }
    computeRowCache(rowIndex) {
        const cols = this.getCells()[0].length;
        const bitSets = [];
        for (let index = 0; index < cols; index++) {
            bitSets.push(this.getCell(rowIndex, index).spriteBitSet);
        }
        return (new spriteBitSet_1.SpriteBitSet()).union(bitSets);
    }
    computeColCache(colIndex) {
        const rows = this.getCells().length;
        const bitSets = [];
        for (let index = 0; index < rows; index++) {
            bitSets.push(this.getCell(index, colIndex).spriteBitSet);
        }
        return (new spriteBitSet_1.SpriteBitSet()).union(bitSets);
    }
}
exports.Level = Level;
/**
 * Internal class that ise used to maintain the state of a level.
 *
 * This should not be called directly. Instead, use [[GameEngine]] .
 */
class LevelEngine extends eventemitter2_1.EventEmitter2 {
    constructor(gameData) {
        super();
        this.gameData = gameData;
        this.hasAgainThatNeedsToRun = false;
        this.undoStack = [];
    }
    setLevel(levelNum) {
        this.undoStack = [];
        this.gameData.clearCaches();
        const levelData = this.gameData.levels[levelNum];
        if (!levelData) {
            throw new Error(`Invalid levelNum: ${levelNum}`);
        }
        if (process.env.NODE_ENV === 'development') {
            levelData.__incrementCoverage();
        }
        util_1.resetRandomSeed();
        const levelSprites = levelData.getRows().map((row) => {
            return row.map((col) => {
                const sprites = new Set(col.getSprites());
                const backgroundSprite = this.gameData.getMagicBackgroundSprite();
                if (backgroundSprite) {
                    sprites.add(backgroundSprite);
                }
                return sprites;
            });
        });
        // Clone the board because we will be modifying it
        this._setLevel(levelSprites);
        this.takeSnapshot(this.createSnapshot());
        // Return the cells so the UI can listen to when they change
        return this.getCells();
    }
    setMessageLevel(sprites) {
        this.tempOldLevel = this.currentLevel;
        this._setLevel(sprites);
    }
    restoreFromMessageLevel() {
        this.currentLevel = this.tempOldLevel;
        this.tempOldLevel = null;
        // this.setLevel(this.tempOldLevel)
    }
    getCurrentLevel() {
        if (this.currentLevel) {
            return this.currentLevel;
        }
        else {
            throw new Error(`BUG: There is no current level. Maybe it is a message level or maybe setLevel was never called`);
        }
    }
    toSnapshot() {
        return this.getCurrentLevel().getCells().map((row) => {
            return row.map((cell) => {
                const ret = [];
                cell.getSpriteAndWantsToMoves().forEach((wantsToMove, sprite) => {
                    ret.push(`${wantsToMove} ${sprite.getName()}`);
                });
                return ret;
            });
        });
    }
    tick() {
        if (process.env.LOG_LEVEL === 'debug') {
            console.error(``); // tslint:disable-line:no-console
        }
        if (this.hasAgainThatNeedsToRun) {
            // run the AGAIN rules
            this.hasAgainThatNeedsToRun = false; // let the .tick() make it true
        }
        const ret = this.tickNormal();
        // TODO: Handle the commands like RESTART, CANCEL, WIN at this point
        let soundToPlay = null;
        let messageToShow = null;
        let hasWinCommand = false;
        let hasRestart = false;
        for (const command of ret.commands) {
            switch (command.getType()) {
                case command_1.COMMAND_TYPE.RESTART:
                    hasRestart = true;
                    break;
                case command_1.COMMAND_TYPE.SFX:
                    soundToPlay = command.getSound();
                    break;
                case command_1.COMMAND_TYPE.MESSAGE:
                    this.hasAgainThatNeedsToRun = false; // make sure we won't be waiting on another tick
                    messageToShow = command.getMessage();
                    break;
                case command_1.COMMAND_TYPE.WIN:
                    hasWinCommand = true;
                    break;
                default:
                // console.error(`BUG: Unsupported command "${command.getType()}"`) // tslint:disable-line:no-console
            }
        }
        if (process.env.LOG_LEVEL === 'debug') {
            console.error(`checking win condition.`); // tslint:disable-line:no-console
        }
        if (this.hasAgainThatNeedsToRun) {
            if (process.env.LOG_LEVEL === 'debug') {
                console.error(`AGAIN command executed, with changes detected - will execute another turn.`); // tslint:disable-line:no-console
            }
        }
        return {
            changedCells: new Set(ret.changedCells.keys()),
            soundToPlay,
            messageToShow,
            hasRestart,
            isWinning: hasWinCommand || this.isWinning()
        };
    }
    hasAgain() {
        return this.hasAgainThatNeedsToRun;
    }
    press(direction) {
        // Should disable keypresses if `AGAIN` is running.
        // It is commented because the didSpritesChange logic is not correct.
        // a rule might add a sprite, and then another rule might remove a sprite.
        // We need to compare the set of sprites before and after ALL rules ran.
        // This will likely be implemented as part of UNDO or CHECKPOINT.
        // if (!this.hasAgain()) {
        this.pendingPlayerWantsToMove = direction;
        // }
    }
    pressRestart() {
        // Add the initial checkpoint to the top (rather than clearing the stack)
        // so the player can still "UNDO" after pressing "RESTART"
        const snapshot = this.undoStack[0];
        this.undoStack.push(snapshot);
        this.applySnapshot(snapshot);
    }
    pressUndo() {
        const snapshot = this.undoStack.pop();
        if (snapshot && this.undoStack.length > 0) { // the 0th entry is the initial load of the level
            this.applySnapshot(snapshot);
        }
        else if (snapshot) {
            // oops, put the snapshot back on the stack
            this.undoStack.push(snapshot);
        }
    }
    _setLevel(levelSprites) {
        const level = new Level();
        this.currentLevel = level;
        const spriteCells = levelSprites.map((row, rowIndex) => {
            return row.map((sprites, colIndex) => {
                const backgroundSprite = this.gameData.getMagicBackgroundSprite();
                if (backgroundSprite) {
                    sprites.add(backgroundSprite);
                }
                return new Cell(level, sprites, rowIndex, colIndex);
            });
        });
        level.setCells(spriteCells);
        // link up all the cells. Loop over all the sprites
        // in case they are NO tiles (so the cell is included)
        const batchCells = new Map();
        function spriteSetToKey(sprites) {
            const key = [];
            for (const spriteName of [...sprites].map((sprite) => sprite.getName()).sort()) {
                key.push(spriteName);
            }
            return key.join(' ');
        }
        const allCells = this.getCells();
        // But first, fill up any empty condition brackets with ALL THE CELLS
        for (const rule of this.gameData.rules) {
            rule.addCellsToEmptyRules(allCells);
        }
        for (const cell of allCells) {
            const key = spriteSetToKey(cell.getSpritesAsSet());
            let batch = batchCells.get(key);
            if (!batch) {
                batch = [];
                batchCells.set(key, batch);
            }
            batch.push(cell);
        }
        // Print progress while loading up the Cells
        let i = 0;
        for (const [key, cells] of batchCells) {
            if ((batchCells.size > 100 && i % 10 === 0) || cells.length > 100) {
                this.emit('loading-cells', {
                    cellStart: i,
                    cellEnd: i + cells.length,
                    cellTotal: allCells.length,
                    key
                });
            }
            // All Cells contain the same set of sprites so just pull out the 1st one
            for (const sprite of this.gameData.objects) {
                const cellSprites = cells[0].getSpritesAsSet();
                const hasSprite = cellSprites.has(sprite);
                if (hasSprite || sprite.hasNegationTileWithModifier()) {
                    if (hasSprite) {
                        sprite.addCells(sprite, cells, util_1.RULE_DIRECTION.STATIONARY);
                    }
                    else {
                        sprite.removeCells(sprite, cells);
                    }
                }
            }
            i += cells.length;
        }
        return level;
    }
    getCells() {
        return util_1._flatten(this.getCurrentLevel().getCells());
    }
    tickUpdateCells() {
        if (process.env.LOG_LEVEL === 'debug') {
            console.error(`applying rules`); // tslint:disable-line:no-console
        }
        return this._tickUpdateCells(this.gameData.rules.filter((r) => !r.isLate()));
    }
    tickUpdateCellsLate() {
        if (process.env.LOG_LEVEL === 'debug') {
            console.error(`applying late rules`); // tslint:disable-line:no-console
        }
        return this._tickUpdateCells(this.gameData.rules.filter((r) => r.isLate()));
    }
    _tickUpdateCells(rules) {
        const changedMutations = new Set();
        const evaluatedRules = [];
        if (!this.currentLevel) {
            throw new Error(`BUG: Level Cells do not exist yet`);
        }
        for (const rule of rules) {
            const cellMutations = rule.evaluate(this.currentLevel, false /*evaluate all rules*/);
            if (cellMutations.length > 0) {
                evaluatedRules.push(rule);
            }
            for (const mutation of cellMutations) {
                changedMutations.add(mutation);
            }
            // if (process.env['LOG_LEVEL'] === 'debug') {
            //     if (rule.timesRan && rule.totalTimeMs) {
            //         const avg = rule.totalTimeMs // Math.round(rule.totalTimeMs / rule.timesRan)
            //         if (avg > 100) {
            //             console.error(`Took:${avg}ms (${cellMutations.length} changed) ${rule.toString()}`) // tslint:disable-line:no-console
            //         }
            //     }
            //     // if (cellMutations.length > 0) {
            //     //     console.error(`Took:${rule.totalTimeMs}ms (${cellMutations.length} changed) ${rule.toString()}`) // tslint:disable-line:no-console
            //     // }
            // }
        }
        // We may have mutated the same cell 4 times (e.g. [Player]->[>Player]) so consolidate
        const changedCells = new Set();
        const commands = new Set();
        // let didSomeSpriteChange = false
        for (const mutation of changedMutations) {
            // if (mutation.getDidSpritesChange()) {
            //     didSomeSpriteChange = true
            // }
            if (mutation.hasCell()) {
                changedCells.add(mutation.getCell());
            }
            else {
                commands.add(mutation.getCommand());
            }
            // if (!changedCells.has(mutation.cell)) {
            //     changedCells.set(mutation.cell, mutation.didSpritesChange)
            // }
        }
        return { evaluatedRules, changedCells, commands /*, didSomeSpriteChange: didSomeSpriteChange*/ };
    }
    tickMoveSprites(changedCells) {
        const movedCells = new Set();
        // Loop over all the cells, see if a Rule matches, apply the transition, and notify that cells changed
        let somethingChanged;
        do {
            somethingChanged = false;
            for (const cell of changedCells) {
                for (const [sprite, wantsToMove] of cell.getSpriteAndWantsToMoves()) {
                    switch (wantsToMove) {
                        case util_1.RULE_DIRECTION.STATIONARY:
                            // nothing to do
                            break;
                        case util_1.RULE_DIRECTION.ACTION:
                            // just clear the wantsToMove flag
                            somethingChanged = true;
                            cell.clearWantsToMove(sprite);
                            break;
                        case util_1.RULE_DIRECTION.UP:
                        case util_1.RULE_DIRECTION.DOWN:
                        case util_1.RULE_DIRECTION.LEFT:
                        case util_1.RULE_DIRECTION.RIGHT:
                            const neighbor = cell.getNeighbor(wantsToMove);
                            // Make sure
                            if (neighbor && !neighbor.hasCollisionWithSprite(sprite)) {
                                cell.removeSprite(sprite);
                                neighbor.addSprite(sprite, util_1.RULE_DIRECTION.STATIONARY);
                                movedCells.add(neighbor);
                                movedCells.add(cell);
                                somethingChanged = true;
                                // Don't delete until we are sure none of the sprites want to move
                                // changedCells.delete(cell)
                            }
                            else {
                                // Clear the wantsToMove flag LATER if we hit a wall (a sprite in the same collisionLayer) or are at the end of the map
                                // We do this later because we are looping as long as something changed
                                // cell.clearWantsToMove(sprite)
                            }
                            break;
                        default:
                            throw new Error(`BUG: wantsToMove should have been handled earlier`);
                    }
                }
            }
        } while (somethingChanged);
        // Clear the wantsToMove from all remaining cells
        for (const cell of changedCells) {
            for (const [sprite] of cell.getSpriteAndWantsToMoves()) {
                cell.clearWantsToMove(sprite);
            }
        }
        return movedCells;
    }
    tickNormal() {
        let changedCellMutations = new Set();
        const initialSnapshot = this.createSnapshot();
        if (this.pendingPlayerWantsToMove) {
            this.takeSnapshot(initialSnapshot);
            if (process.env.LOG_LEVEL === 'debug') {
                console.error(`=======================`); // tslint:disable-line:no-console
                console.error(`Turn starts with input of ${this.pendingPlayerWantsToMove.toLowerCase()}.`); // tslint:disable-line:no-console
            }
            const t = this.gameData.getPlayer();
            for (const cell of t.getCellsThatMatch()) {
                for (const sprite of t.getSpritesThatMatch(cell)) {
                    cell.updateSprite(sprite, this.pendingPlayerWantsToMove);
                    changedCellMutations.add(cell);
                }
            }
            this.pendingPlayerWantsToMove = null;
        }
        else {
            if (process.env.LOG_LEVEL === 'debug') {
                console.error(`Turn starts with no input.`); // tslint:disable-line:no-console
            }
        }
        const { changedCells: changedCellMutations2, evaluatedRules, commands } = this.tickUpdateCells();
        changedCellMutations = util_1.setAddAll(changedCellMutations, changedCellMutations2);
        // Continue evaluating again rules only when some sprites have changed
        // The didSpritesChange logic is not correct.
        // a rule might add a sprite, and then another rule might remove a sprite.
        // We need to compare the set of sprites before and after ALL rules ran.
        // This will likely be implemented as part of UNDO or CHECKPOINT.
        const movedCells = this.tickMoveSprites(new Set(changedCellMutations.keys()));
        const { changedCells: changedCellsLate, evaluatedRules: evaluatedRulesLate, commands: commandsLate } = this.tickUpdateCellsLate();
        const allCommands = [...commands, ...commandsLate];
        const didCancel = !!allCommands.filter((c) => c.getType() === command_1.COMMAND_TYPE.CANCEL)[0];
        if (didCancel) {
            this.hasAgainThatNeedsToRun = false;
            if (this.undoStack.length > 0) {
                this.applySnapshot(this.undoStack[this.undoStack.length - 1]);
            }
            return {
                changedCells: new Set(),
                commands: new Set(),
                evaluatedRules
            };
        }
        const didCheckpoint = !!allCommands.find((c) => c.getType() === command_1.COMMAND_TYPE.CHECKPOINT);
        if (didCheckpoint) {
            this.undoStack = [];
            this.takeSnapshot(this.createSnapshot());
        }
        // set this only if we did not CANCEL and if some cell changed
        const changedCells = util_1.setAddAll(util_1.setAddAll(changedCellMutations, changedCellsLate), movedCells);
        if (allCommands.find((c) => c.getType() === command_1.COMMAND_TYPE.AGAIN)) {
            // Compare all the cells to the top of the undo stack. If it does not differ
            this.hasAgainThatNeedsToRun = this.doSnapshotsDiffer(initialSnapshot, this.createSnapshot());
        }
        return {
            changedCells,
            evaluatedRules: evaluatedRules.concat(evaluatedRulesLate),
            commands: allCommands
        };
    }
    isWinning() {
        let conditionsSatisfied = this.gameData.winConditions.length > 0; // true
        this.gameData.winConditions.forEach((winCondition) => {
            if (!winCondition.isSatisfied(this.getCells())) {
                conditionsSatisfied = false;
            }
        });
        return conditionsSatisfied;
    }
    // Used for UNDO and RESTART
    createSnapshot() {
        return this.getCurrentLevel().getCells().map((row) => row.map((cell) => cell.toSnapshot()));
    }
    takeSnapshot(snapshot) {
        this.undoStack.push(snapshot);
    }
    applySnapshot(snpashot) {
        const cells = this.getCurrentLevel().getCells();
        for (let rowIndex = 0; rowIndex < cells.length; rowIndex++) {
            const row = cells[rowIndex];
            const snapshotRow = snpashot[rowIndex];
            for (let colIndex = 0; colIndex < row.length; colIndex++) {
                const cell = row[colIndex];
                const state = snapshotRow[colIndex];
                cell.fromSnapshot(state);
            }
        }
    }
    doSnapshotsDiffer(snapshot1, snapshot2) {
        for (let rowIndex = 0; rowIndex < snapshot1.length; rowIndex++) {
            for (let colIndex = 0; colIndex < snapshot1[0].length; colIndex++) {
                const sprites1 = snapshot1[rowIndex][colIndex];
                const sprites2 = snapshot2[rowIndex][colIndex];
                if (!util_1.setEquals(sprites1, sprites2)) {
                    return true;
                }
            }
        }
        return false;
    }
}
exports.LevelEngine = LevelEngine;
/**
 * Maintains the state of the game. Here is an example flow:
 *
 * ```js
 * const engine = new GameEngine(gameData)
 * engine.setLevel(0)
 * engine.pressRight()
 * engine.tick()
 * engine.tick()
 * engine.pressUp()
 * engine.tick()
 * engine.pressUndo()
 * engine.tick()
 * ```
 */
class GameEngine {
    constructor(gameData) {
        this.isFirstTick = true;
        this.currentLevelNum = -1234567;
        this.messageShownAndWaitingForActionPress = false;
        this.levelEngine = new LevelEngine(gameData);
        this.messageShownAndWaitingForActionPress = false;
    }
    on(eventName, handler) {
        this.levelEngine.on(eventName, handler);
    }
    getGameData() {
        return this.levelEngine.gameData;
    }
    getCurrentLevelCells() {
        return this.levelEngine.getCurrentLevel().getCells();
    }
    getCurrentLevel() {
        return this.getGameData().levels[this.getCurrentLevelNum()];
    }
    getCurrentLevelNum() {
        return this.currentLevelNum;
    }
    hasAgain() {
        return this.levelEngine.hasAgain();
    }
    setLevel(levelNum) {
        this.messageShownAndWaitingForActionPress = false;
        this.levelEngine.hasAgainThatNeedsToRun = false; // clear this so the user can press "X"
        if (this.getGameData().levels[levelNum].isMap()) {
            this.isFirstTick = true;
            this.levelEngine.setLevel(levelNum);
        }
        else {
            // TODO: no need to set the levelEngine when the current level is a Message
        }
        this.currentLevelNum = levelNum;
    }
    tick() {
        // When the current level is a Message, wait until the user presses ACTION
        if (!this.getCurrentLevel().isMap()) {
            // Wait until the user presses "X" (ACTION)
            let didWinGameInMessage = false;
            let didLevelChange = false;
            if (this.levelEngine.pendingPlayerWantsToMove === util_1.RULE_DIRECTION.ACTION) {
                didLevelChange = true;
                if (this.currentLevelNum === this.levelEngine.gameData.levels.length - 1) {
                    didWinGameInMessage = true;
                }
                else {
                    this.setLevel(this.currentLevelNum + 1);
                }
            }
            // clear any keys that were pressed
            this.levelEngine.pendingPlayerWantsToMove = null;
            return {
                changedCells: new Set(),
                soundToPlay: null,
                messageToShow: null,
                didWinGame: didWinGameInMessage,
                didLevelChange,
                wasAgainTick: false
            };
        }
        const hasAgain = this.levelEngine.hasAgain();
        if (this.levelEngine.gameData.metadata.runRulesOnLevelStart && this.isFirstTick) {
            // don't cancel early
        }
        else if (!hasAgain && !(this.levelEngine.gameData.metadata.realtimeInterval || this.levelEngine.pendingPlayerWantsToMove)) {
            // check if the `require_player_movement` flag is set in the game
            return {
                changedCells: new Set(),
                soundToPlay: null,
                messageToShow: null,
                didWinGame: false,
                didLevelChange: false,
                wasAgainTick: false
            };
        }
        // If we are showing a message then wait until ACTION is pressed
        if (this.messageShownAndWaitingForActionPress) {
            if (this.levelEngine.pendingPlayerWantsToMove === util_1.RULE_DIRECTION.ACTION) {
                // render all the cells because we are currently rendering a Message
                this.messageShownAndWaitingForActionPress = false;
                this.levelEngine.pendingPlayerWantsToMove = null;
                return {
                    changedCells: new Set(util_1._flatten(this.getCurrentLevelCells())),
                    soundToPlay: null,
                    messageToShow: null,
                    didWinGame: false,
                    didLevelChange: false,
                    wasAgainTick: false
                };
            }
            else {
                // Keep waiting until ACTION is pressed
                return {
                    changedCells: new Set(),
                    soundToPlay: null,
                    messageToShow: null,
                    didWinGame: false,
                    didLevelChange: false,
                    wasAgainTick: false
                };
            }
        }
        const { changedCells, soundToPlay, messageToShow, isWinning, hasRestart } = this.levelEngine.tick();
        this.isFirstTick = false;
        if (hasRestart) {
            this.pressRestart();
            return {
                changedCells: new Set(util_1._flatten(this.getCurrentLevelCells())),
                soundToPlay: null,
                messageToShow: null,
                didWinGame: false,
                didLevelChange: false,
                wasAgainTick: false
            };
        }
        let didWinGame = false;
        if (isWinning) {
            if (this.currentLevelNum === this.levelEngine.gameData.levels.length - 1) {
                didWinGame = true;
            }
            else {
                this.setLevel(this.currentLevelNum + 1);
            }
        }
        if (messageToShow) {
            this.messageShownAndWaitingForActionPress = true;
        }
        return {
            changedCells,
            soundToPlay,
            messageToShow,
            didWinGame,
            didLevelChange: isWinning,
            wasAgainTick: hasAgain
        };
    }
    press(direction) {
        return this.levelEngine.press(direction);
    }
    pressUp() {
        this.levelEngine.press(util_1.RULE_DIRECTION.UP);
    }
    pressDown() {
        this.levelEngine.press(util_1.RULE_DIRECTION.DOWN);
    }
    pressLeft() {
        this.levelEngine.press(util_1.RULE_DIRECTION.LEFT);
    }
    pressRight() {
        this.levelEngine.press(util_1.RULE_DIRECTION.RIGHT);
    }
    pressAction() {
        this.levelEngine.press(util_1.RULE_DIRECTION.ACTION);
    }
    pressRestart() {
        this.isFirstTick = true;
        this.levelEngine.pressRestart();
    }
    pressUndo() {
        this.messageShownAndWaitingForActionPress = false;
        this.levelEngine.pressUndo();
    }
    // Pixels and Sprites
    getSpriteSize() {
        return this.getGameData().getSpriteSize();
    }
    saveSnapshotToJSON() {
        return this.getCurrentLevelCells().map((row) => row.map((cell) => [...cell.toSnapshot()].map((s) => s.getName())));
    }
    loadSnapshotFromJSON(json) {
        json.forEach((rowSave, rowIndex) => {
            rowSave.forEach((cellSave, colIndex) => {
                const cell = this.levelEngine.getCurrentLevel().getCell(rowIndex, colIndex);
                const spritesToHave = cellSave.map((spriteName) => {
                    const sprite = this.getGameData()._getSpriteByName(spriteName);
                    if (sprite) {
                        return sprite;
                    }
                    else {
                        throw new Error(`BUG: Could not find sprite to add named ${spriteName}`);
                    }
                });
                cell.fromSnapshot(new Set(spritesToHave));
            });
        });
    }
    setMessageLevel(sprites) {
        this.levelEngine.setMessageLevel(sprites);
    }
    restoreFromMessageLevel() {
        this.levelEngine.restoreFromMessageLevel();
    }
}
exports.GameEngine = GameEngine;
//# sourceMappingURL=engine.js.map