"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const lruCache_1 = require("../lruCache");
const spriteBitSet_1 = require("../spriteBitSet");
const terminal_1 = require("../ui/terminal");
const util_1 = require("../util");
const BaseForLines_1 = require("./BaseForLines");
const BitSet2 = require('bitset'); // tslint:disable-line:no-var-requires
const MAX_ITERATIONS_IN_LOOP = 350; // Set by the Random World Generation game
const LRU_CACHE_SIZE = 50; // 1000
exports.SIMPLE_DIRECTION_DIRECTIONS = [
    util_1.RULE_DIRECTION.RIGHT,
    util_1.RULE_DIRECTION.DOWN,
    util_1.RULE_DIRECTION.LEFT,
    util_1.RULE_DIRECTION.UP
];
class BracketPair {
    constructor(condition, action) {
        this.condition = condition;
        this.action = action;
    }
}
class ExtraPair extends BracketPair {
    constructor(condition, action, extra) {
        super(condition, action);
        this.extra = extra;
    }
}
class CellMutation {
    constructor(cell) {
        this.cell = cell;
    }
    hasCell() { return true; }
    getCell() { return this.cell; }
    getCommand() {
        throw new Error(`BUG: check hasCommand first`);
    }
}
class CommandMutation {
    constructor(command) {
        this.command = command;
    }
    getCommand() { return this.command; }
    hasCell() { return false; }
    getCell() {
        throw new Error(`BUG: check hasCell first`);
    }
}
// Converts `[ [1,2], [a,b] ]` to:
// `[ [1,a], [2,a], [1,b], [2,b] ]`
function buildPermutations(cells) {
    let tuples = [[]];
    for (const row of cells) {
        const newtuples = [];
        for (const valtoappend of row) {
            for (const tuple of tuples) {
                const newtuple = tuple.concat([valtoappend]);
                newtuples.push(newtuple);
            }
        }
        tuples = newtuples;
    }
    return tuples;
}
class SimpleRuleGroup extends BaseForLines_1.BaseForLines {
    constructor(source, isRandom, rules) {
        super(source);
        this.rules = rules;
        this.isRandom = isRandom;
    }
    hasMatches(level) {
        for (const rule of this.rules) {
            if (rule.hasMatches(level)) {
                return true;
            }
        }
        return false;
    }
    evaluate(level, onlyEvaluateFirstMatch) {
        let start;
        if (process.env.LOG_LEVEL === 'debug') {
            start = Date.now();
        }
        // Keep looping as long as one of the rules evaluated something
        const allMutations = [];
        let iteration;
        for (iteration = 0; iteration < MAX_ITERATIONS_IN_LOOP; iteration++) {
            if (process.env.NODE_ENV === 'development' && iteration === MAX_ITERATIONS_IN_LOOP - 10) {
                // Provide a breakpoint just before we run out of MAX_ITERATIONS_IN_LOOP
                // so that we can step through the evaluations.
                console.error(this.toString()); // tslint:disable-line:no-console
                console.error('BUG: Iterated too many times in startloop or + (rule group)'); // tslint:disable-line:no-console
                if (process.stdout) {
                    terminal_1.default.debugRenderScreen();
                }
                debugger; // tslint:disable-line:no-debugger
            }
            if (iteration === MAX_ITERATIONS_IN_LOOP - 1) {
                throw new Error(`BUG: Iterated too many times in startloop or + (rule group)\n${this.toString()}`);
            }
            if (this.isRandom) {
                // Randomly pick one of the rules. I wonder if it needs to be smart
                // It is important that it only be evaluated once (hence the returns)
                const evaluatableRules = this.rules.filter((r) => r.hasMatches(level));
                if (evaluatableRules.length === 0) {
                    return [];
                }
                else if (evaluatableRules.length === 1) {
                    const ret = evaluatableRules[0].evaluate(level, true /*only evaluate the 1st match because we are RANDOM and in a loop*/);
                    return ret;
                }
                else {
                    const randomIndex = util_1.nextRandom(evaluatableRules.length);
                    const rule = evaluatableRules[randomIndex];
                    const ret = rule.evaluate(level, true /*only evaluate the 1st match because we are RANDOM and in a loop*/);
                    return ret;
                }
            }
            else {
                let evaluatedSomething = false;
                for (const rule of this.rules) {
                    // Keep evaluating the rule until nothing changes
                    const ret = rule.evaluate(level, onlyEvaluateFirstMatch);
                    if (ret.length > 0) {
                        // filter because a Rule may have caused only command mutations
                        if (ret.filter((m) => m.hasCell()).length > 0) {
                            evaluatedSomething = true;
                        }
                        if (onlyEvaluateFirstMatch) {
                            return ret;
                        }
                        allMutations.push(ret);
                    }
                }
                if (!evaluatedSomething) {
                    break;
                }
            }
        }
        if (process.env.LOG_LEVEL === 'debug') {
            if (allMutations.length > 0) {
                if (start && (Date.now() - start) > 30 /*only show times for rules that took a long time*/) {
                    console.error(`Rule ${this.__getSourceLineAndColumn().lineNum} applied. ${iteration === 1 ? '' : `(x${iteration})`} [[${Date.now() - start}ms]]`); // tslint:disable-line:no-console
                }
                else {
                    console.error(`Rule ${this.__getSourceLineAndColumn().lineNum} applied. ${iteration === 1 ? '' : `(x${iteration})`}`); // tslint:disable-line:no-console
                }
            }
        }
        return util_1._flatten(allMutations);
        // let mutations = []
        // for (const rule of this._rules) {
        //     const ret = rule.evaluate()
        //     if (ret.length > 0) {
        //         mutations = mutations.concat(ret)
        //     }
        // }
        // return mutations
    }
    clearCaches() {
        for (const rule of this.rules) {
            rule.clearCaches();
        }
    }
    getChildRules() {
        return this.rules;
    }
    isLate() {
        // All rules in a group should be parked as late if any is marked as late
        return this.rules[0].isLate();
    }
    hasRigid() {
        for (const rule of this.rules) {
            if (rule.hasRigid()) {
                return true;
            }
        }
        return false;
    }
    addCellsToEmptyRules(cells) {
        for (const rule of this.rules) {
            rule.addCellsToEmptyRules(cells);
        }
    }
    toKey() {
        return this.rules.map((r) => r.toKey()).join('\n');
    }
}
exports.SimpleRuleGroup = SimpleRuleGroup;
class SimpleRuleLoop extends SimpleRuleGroup {
}
exports.SimpleRuleLoop = SimpleRuleLoop;
// This is a rule that has been expanded from `DOWN [ > player < cat RIGHT dog ] -> [ ^ crate ]` to:
// DOWN [ DOWN player UP cat RIGHT dog ] -> [ RIGHT crate ]
//
// And a more complicated example:
// DOWN [ > player LEFT cat HORIZONTAL dog < crate VERTICAL wall ] -> [ ^ crate  HORIZONTAL dog ]
//
// DOWN [ DOWN player LEFT cat LEFT dog UP crate UP wall ] -> [ right crate LEFT dog ]
// DOWN [ DOWN player LEFT cat LEFT dog UP crate DOWN wall ] -> [ right crate LEFT dog ]
// DOWN [ DOWN player LEFT cat RIGHT dog UP crate UP wall ] -> [ RIGHT crate RIGHT dog ]
// DOWN [ DOWN player LEFT cat RIGHT dog UP crate DOWN wall ] -> [ RIGHT crate RIGHT dog ]
class SimpleRule extends BaseForLines_1.BaseForLines {
    constructor(source, evaluationDirection, conditionBrackets, actionBrackets, commands, isLate, isRigid, debugFlag) {
        super(source);
        this.evaluationDirection = evaluationDirection;
        this.conditionBrackets = conditionBrackets;
        this.actionBrackets = actionBrackets;
        this.commands = commands;
        this._isLate = isLate;
        this.isRigid = isRigid;
        this.debugFlag = debugFlag;
        this.isSubscribedToCellChanges = false;
        if (actionBrackets.length > 0) {
            for (let index = 0; index < conditionBrackets.length; index++) {
                conditionBrackets[index].prepareAction(actionBrackets[index]);
            }
        }
    }
    toKey() {
        const dir = this.dependsOnDirection() ? this.evaluationDirection : '';
        const conditions = this.conditionBrackets.map((x) => x.toKey());
        const actions = this.actionBrackets.map((x) => x.toKey());
        const commands = this.commands.map((c) => c.toKey());
        return `{Late?${this._isLate}} {Rigid?${this.isRigid}}  ${dir} ${conditions} -> ${actions} ${commands.join(' ')} {debugger?${this.debugFlag}}`;
    }
    getChildRules() {
        return [];
    }
    subscribeToCellChanges() {
        if (!this.isSubscribedToCellChanges) {
            // Subscribe the bracket and neighbors to cell Changes (only the condition side)
            for (const bracket of this.conditionBrackets) {
                bracket.subscribeToNeighborChanges();
            }
            this.isSubscribedToCellChanges = true;
        }
    }
    clearCaches() {
        for (const bracket of this.conditionBrackets) {
            bracket.clearCaches();
        }
    }
    getMatches(level) {
        const allBracketsToProcess = [];
        for (let index = 0; index < this.conditionBrackets.length; index++) {
            const condition = this.conditionBrackets[index];
            const action = this.actionBrackets[index];
            const bracketMatches = condition.getMatches(level, action);
            if (bracketMatches.length === 0) {
                return [];
            }
            allBracketsToProcess.push(bracketMatches);
        }
        return allBracketsToProcess;
    }
    evaluate(level, onlyEvaluateFirstMatch) {
        // Verify that each condition bracket has matches
        // for (const condition of this.conditionBrackets) {
        //     if (!condition.hasFirstCells()) {
        //         if (process.env['NODE_ENV'] === 'development' && this.debugFlag === DEBUG_FLAG.BREAKPOINT_REMOVE) {
        //             // A "DEBUGGER_REMOVE" flag was set in the game so we are pausing here
        //             if (process.stdout) { TerminalUI.debugRenderScreen(); } debugger
        //         }
        //         return [] // Rule did not match, so nothing ran
        //     }
        // }
        // If a Rule cannot impact itself then the evaluation order does not matter.
        // We can vastly simplify the evaluation in that case
        let ret = [];
        if (process.env.NODE_ENV === 'development') {
            // A "DEBUGGER" flag was set in the game so we are pausing here
            if (process.env.LOG_LEVEL === 'debug' && process.stdout) {
                terminal_1.default.renderScreen(false);
            }
            if (this.debugFlag === util_1.DEBUG_FLAG.BREAKPOINT) {
                debugger; // tslint:disable-line:no-debugger
            }
        }
        const allBracketsToProcess = this.getMatches(level);
        if (allBracketsToProcess.length === 0) {
            return [];
        }
        // Some rules only contain commands.
        // If there are actionBrackets then evaluate them.
        // Get ready to Evaluate
        if (this.actionBrackets.length > 0) {
            const cellPermutations = buildPermutations(allBracketsToProcess);
            const allMutations = [];
            for (const permutation of cellPermutations) {
                let didAllBracketsStillMatch = true;
                const magicOrTiles = new Map();
                // Populate the magicOrTiles. This needs to be done first because of things like:
                // [ Player ] [ Color ] -> [ Player Color ] [ ]
                // Check that all Cells still match
                for (const x of permutation) {
                    if (!x.doesStillMatch()) {
                        didAllBracketsStillMatch = false;
                        break;
                    }
                    x.populateMagicOrTiles(magicOrTiles);
                }
                // break if the cells no longer match
                if (!didAllBracketsStillMatch) {
                    continue;
                }
                for (const x of permutation) {
                    if (!x.doesStillMatch()) {
                        // part of the rule not longer matches so stop. (Test if that is the correct behavior)
                        // E.g. [ Player ] [ Player ] [ ] -> [ ] [ ] [ SeeIfThisIsExecuted ]
                        continue;
                    }
                    allMutations.push(x.evaluate(magicOrTiles));
                    if (process.env.NODE_ENV === 'development') {
                        this.__incrementCoverage();
                    }
                }
                // Only evaluate once. This is a HACK since it always picks the 1st cell that matched rather than a RANDOM cell
                if (onlyEvaluateFirstMatch) {
                    break; // evaluate the subsequent brackets but do not continue evaluating cells
                }
            }
            ret = util_1._flatten(allMutations);
        }
        // Append any Commands that need to be evaluated (only if the rule was evaluated at least once)
        for (const command of this.commands) {
            ret.push(new CommandMutation(command));
        }
        return ret;
    }
    hasMatches(level) {
        for (let index = 0; index < this.conditionBrackets.length; index++) {
            const condition = this.conditionBrackets[index];
            const action = this.actionBrackets[index];
            if (!condition.hasMatches(level, action)) {
                return false;
            }
        }
        return true;
    }
    isLate() { return this._isLate; }
    hasRigid() { return this.isRigid; }
    canCollapseBecauseBracketsMatch(rule) {
        for (let index = 0; index < this.conditionBrackets.length; index++) {
            if (this.conditionBrackets[index] !== rule.conditionBrackets[index]) {
                return false;
            }
            // also ensure there is only one neighbor.
            // that way we can de-duplicate the rule
            if (this.conditionBrackets[index]._getAllNeighbors().length > 1) {
                return false;
            }
        }
        for (let index = 0; index < this.actionBrackets.length; index++) {
            if (this.actionBrackets[index] !== rule.actionBrackets[index]) {
                return false;
            }
        }
        return true;
    }
    addCellsToEmptyRules(cells) {
        for (const bracket of this.conditionBrackets) {
            bracket.addCellsToEmptyRules(cells);
        }
    }
    dependsOnDirection() {
        return !!(this.conditionBrackets.find((b) => b.dependsOnDirection()) || this.actionBrackets.find((b) => b.dependsOnDirection()));
    }
}
exports.SimpleRule = SimpleRule;
class SimpleTileWithModifier extends BaseForLines_1.BaseForLines {
    constructor(source, isNegated, isRandom, direction, tile, debugFlag) {
        super(source);
        this._isNegated = isNegated;
        this._isRandom = isRandom;
        this._direction = direction;
        this._tile = tile;
        this.neighbors = new Set();
        this._debugFlag = debugFlag;
        this.trickleCells = new Set();
    }
    toKey(ignoreDebugFlag) {
        const sprites = this._tile.getSprites().map((sprite) => sprite.getName()).sort();
        if (ignoreDebugFlag) {
            return `{-?${this._isNegated}} {#?${this._isRandom}} dir="${this._direction}" [${sprites.join(' ')}]`;
        }
        else {
            return `{-?${this._isNegated}} {#?${this._isRandom}} dir="${this._direction}" [${sprites.join(' ')}]{debugging?${!!this._debugFlag}}`;
        }
    }
    equals(t) {
        return this._isNegated === t._isNegated && this._tile.equals(t._tile) && this._direction === t._direction && this._isRandom === t._isRandom;
    }
    clearCaches() {
        // this._localCache.clear()
    }
    isNo() {
        return this._isNegated;
    }
    isRandom() {
        return this._isRandom;
    }
    getCollisionLayers() {
        const collisionLayers = new Set();
        for (const sprite of this._tile.getSprites()) {
            collisionLayers.add(sprite.getCollisionLayer());
        }
        return collisionLayers;
    }
    // This should only be called on Condition Brackets
    subscribeToCellChanges(neighbor) {
        this.neighbors.add(neighbor);
        this._tile.subscribeToCellChanges(this);
    }
    addCells(tile, sprite, cells, wantsToMove) {
        if (process.env.NODE_ENV === 'development' && this._debugFlag === util_1.DEBUG_FLAG.BREAKPOINT) {
            // Pause here because it was marked in the code
            if (process.stdout) {
                terminal_1.default.debugRenderScreen();
            }
            debugger; // tslint:disable-line:no-debugger
        }
        // Cells all have the same sprites, so if the 1st matches, they all do.
        // Also, we only need to check that the direction matches (optimization),
        // we do not need to re-check that the Tile matches
        let shouldAdd = true;
        if (!this._direction || wantsToMove === this._direction) {
            shouldAdd = !this.isNo();
        }
        else if (this._tile.isOr() && this.matchesFirstCell(cells, wantsToMove)) {
            // In OR tiles, one of the sprites may add/remove a direction but
            // another sprite may still have the direction
            // so we check by doing the long and expensive comparison above
            shouldAdd = !this.isNo();
        }
        else {
            shouldAdd = this.isNo();
        }
        if (shouldAdd) {
            for (const cell of cells) {
                this.trickleCells.add(cell);
            }
            // const cellsNotInCache = setDifference(new Set(cells), new Set(this._localCache.keys()))
            for (const neighbor of this.neighbors) {
                // neighbor.addCells(this, sprite, cellsNotInCache, wantsToMove)
                neighbor.addCells(this, sprite, cells, wantsToMove);
            }
        }
        else {
            for (const cell of cells) {
                this.trickleCells.delete(cell);
            }
            // const cellsInCache = setIntersection(new Set(cells), new Set(this._localCache.keys()))
            for (const neighbor of this.neighbors) {
                // neighbor.removeCells(this, sprite, cellsInCache)
                neighbor.removeCells(this, sprite, cells);
            }
        }
    }
    updateCells(sprite, cells, wantsToMove) {
        if (process.env.NODE_ENV === 'development' && this._debugFlag === util_1.DEBUG_FLAG.BREAKPOINT) {
            // Pause here because it was marked in the code
            if (process.stdout) {
                terminal_1.default.debugRenderScreen();
            }
            debugger; // tslint:disable-line:no-debugger
        }
        // Cells all have the same sprites, so if the 1st matches, they all do
        if (this.matchesFirstCell(cells, wantsToMove)) {
            for (const cell of cells) {
                this.trickleCells.add(cell);
            }
            if (wantsToMove) {
                for (const neighbor of this.neighbors) {
                    neighbor.updateCells(this, sprite, cells, wantsToMove);
                }
            }
        }
        else {
            for (const cell of cells) {
                this.trickleCells.delete(cell);
            }
            for (const neighbor of this.neighbors) {
                neighbor.removeCells(this, sprite, cells);
            }
        }
    }
    removeCells(tile, sprite, cells) {
        if (process.env.NODE_ENV === 'development' && this._debugFlag === util_1.DEBUG_FLAG.BREAKPOINT_REMOVE) {
            // Pause here because it was marked in the code
            if (process.stdout) {
                terminal_1.default.debugRenderScreen();
            }
            debugger; // tslint:disable-line:no-debugger
        }
        // Cells all have the same sprites, so if the 1st matches, they all do
        // OR Tiles need to be checked to see if the tile still matches.
        // Non-OR tiles can be safely removed
        let shouldAdd = false;
        if (this._tile.isOr()) {
            shouldAdd = this.matchesFirstCell(cells, null);
        }
        else {
            shouldAdd = this.isNo();
        }
        if (shouldAdd) {
            for (const cell of cells) {
                this.trickleCells.add(cell);
            }
            for (const neighbor of this.neighbors) {
                neighbor.addCells(this, sprite, cells, util_1.RULE_DIRECTION.STATIONARY);
            }
        }
        else {
            for (const cell of cells) {
                this.trickleCells.delete(cell);
            }
            for (const neighbor of this.neighbors) {
                neighbor.removeCells(this, sprite, cells);
            }
        }
    }
    hasCell(cell) {
        return this.trickleCells.has(cell);
    }
    matchesCellWantsToMove(cell, wantsToMove) {
        const hasTile = this._tile.hasCell(cell);
        const didMatch = this._isNegated !== (hasTile && (this._direction === wantsToMove || this._direction === null));
        if (didMatch) {
            return true;
        }
        else if (!this._direction) {
            return false;
        }
        else {
            // do the more expensive match
            for (const sprite of this._tile.getSpritesThatMatch(cell)) {
                if (this._direction === cell.getWantsToMove(sprite)) {
                    return true;
                }
            }
            return false;
        }
    }
    matchesFirstCell(cells, wantsToMove) {
        return this.matchesCellWantsToMove(cells[0], wantsToMove);
    }
}
exports.SimpleTileWithModifier = SimpleTileWithModifier;
class ISimpleBracket extends BaseForLines_1.BaseForLines {
    constructor(source, direction, allNeighbors, debugFlag) {
        super(source);
        this.direction = direction;
        this.debugFlag = debugFlag;
        this.allNeighbors = allNeighbors;
        this.firstCells = new Set();
    }
    _getAllNeighbors() {
        return this.allNeighbors;
    }
    hasMatches(level, actionBracket) {
        return this.getMatches(level, actionBracket).length > 0;
    }
}
exports.ISimpleBracket = ISimpleBracket;
class MatchedCellsForRule {
    constructor(cellsAndNeighbors) {
        this.cellsAndNeighbors = cellsAndNeighbors;
    }
    firstCell() {
        for (const { cell } of this.cellsAndNeighbors) {
            return cell;
        }
        throw new Error(`BUG: ? No cells were included in the match`);
    }
    lastCell() {
        let ret;
        for (const { cell } of this.cellsAndNeighbors) {
            ret = cell;
        }
        if (ret) {
            return ret;
        }
        throw new Error(`BUG: ? No cells were included in the match`);
    }
    doesStillMatch() {
        for (const { cell, condition } of this.cellsAndNeighbors) {
            if (!condition.matchesCellSimple(cell)) {
                return false;
            }
        }
        return true;
    }
    populateMagicOrTiles(magicOrTiles) {
        // populate the OR tiles in all neighbors first. Example:
        // [ | Player ] -> [ Player | ]
        for (const { cell, condition } of this.cellsAndNeighbors) {
            condition.populateMagicOrTiles(cell, magicOrTiles);
        }
    }
    evaluate(magicOrTiles) {
        const mutations = [];
        for (const { cell, condition, action } of this.cellsAndNeighbors) {
            if (!action) {
                throw new Error(`BUG: Should not have tried to evaluate something when there is no action`);
            }
            const mutation = condition.evaluate(action, cell, magicOrTiles);
            if (mutation) {
                mutations.push(mutation);
            }
        }
        return mutations;
    }
}
class SimpleBracket extends ISimpleBracket {
    constructor(source, direction, neighbors, debugFlag) {
        super(source, direction, neighbors, debugFlag);
        this.neighbors = neighbors;
        this.ellipsisBracketListeners = new Map();
        // Compute which sprites need to be in the Row/Column to check cells in that row/column (optimization)
        this.spritesPresentInRowOrColumn = this.neighbors[0].spritesPresent.union(this.neighbors.map((n) => n.spritesPresent));
        const anySprites = [];
        for (const neighbor of this.neighbors) {
            for (const a of neighbor.anySpritesPresent) {
                anySprites.push(a);
            }
        }
        this.anySpritesPresentInRowOrColumn = (new spriteBitSet_1.SpriteBitSet()).union(anySprites);
    }
    toKey(ignoreDebugFlag) {
        const dir = this.dependsOnDirection() ? this.direction : '';
        if (ignoreDebugFlag) {
            return `{${dir}[${this.neighbors.map((n) => n.toKey(ignoreDebugFlag)).join('|')}]}`;
        }
        else {
            return `{${dir}[${this.neighbors.map((n) => n.toKey(ignoreDebugFlag)).join('|')}]{debugging?${!!this.debugFlag}}}`;
        }
    }
    dependsOnDirection() {
        return this.neighbors.length > 1 || !!this.neighbors.find((n) => n.dependsOnDirection());
    }
    subscribeToNeighborChanges() {
        if (this.shouldUseOnDemandMethod()) {
            return;
        } // Skip. Do not subscribe to changes because we will use .getMatches() to find the matches
        this.neighbors.forEach((neighbor, index) => {
            neighbor.subscribeToTileChanges(this, index);
        });
    }
    addEllipsisBracket(bracket, token) {
        this.ellipsisBracketListeners.set(bracket, token);
    }
    clearCaches() {
        this.firstCells.clear();
        for (const neighbor of this.neighbors) {
            neighbor.clearCaches();
        }
    }
    getNeighbors() { return this.neighbors; }
    prepareAction(actionBracket) {
        const actionBracketSimple = actionBracket; // since we know the condition and action side need to match
        this.actionDebugFlag = actionBracketSimple.debugFlag;
        for (let index = 0; index < this.neighbors.length; index++) {
            const condition = this.neighbors[index];
            const action = actionBracketSimple.neighbors[index];
            condition.prepareAction(action);
        }
    }
    addCellsToEmptyRules(cells) {
        if (this.neighbors.length === 1) {
            if (this.neighbors[0]._tilesWithModifier.size === 0) {
                for (const cell of cells) {
                    this._addFirstCell(cell);
                }
            }
        }
    }
    _addFirstCell(firstCell) {
        if (process.env.NODE_ENV === 'development' && this.debugFlag === util_1.DEBUG_FLAG.BREAKPOINT) {
            // Pausing here because it was marked in the code
            if (process.stdout) {
                terminal_1.default.debugRenderScreen();
            }
            debugger; // tslint:disable-line:no-debugger
        }
        this.firstCells.add(firstCell);
        for (const [ellipsisBracket, token] of this.ellipsisBracketListeners) {
            ellipsisBracket.addFirstCell(this, firstCell, token);
        }
    }
    addCell(index, neighbor, t, sprite, cell, wantsToMove) {
        // check if downstream neighbors match
        if (!this.matchesDownstream(cell, index)) {
            // Try to remove the match if there is one
            const firstCell = this.getUpstream(cell, index);
            if (firstCell) {
                this._removeFirstCell(cell);
            }
            return;
        }
        // Loop Upstream
        // check the neighbors upstream of curCell
        const firstCellUp = this.matchesUpstream(cell, index);
        if (!firstCellUp) {
            // Try to remove the match if there is one
            const firstCellMatched = this.getUpstream(cell, index);
            if (firstCellMatched) {
                this._removeFirstCell(firstCellMatched);
            }
            return;
        }
        // Add to the set of firstNeighbors
        // We have a match. Add to the firstCells set.
        this._addFirstCell(firstCellUp);
    }
    removeCell(index, neighbor, t, sprite, cell) {
        // cell was removed
        // Loop Upstream
        const firstCell = this.getFirstCellToRemove(cell, index);
        // Bracket might not match for all directions (likely not), so we might not find a firstCell to remove
        // But that's OK.
        if (firstCell && this.firstCells.has(firstCell)) {
            this._removeFirstCell(firstCell);
        }
    }
    shouldUseOnDemandMethod() {
        // return true
        // return false
        // return this.neighbors.length === 1
        // return this.neighbors.length !== 1
        return process.env.PUZZLESCRIPT_METHOD === 'ondemand';
    }
    getMatchesByTrickling(level, actionBracket) {
        const matches = [];
        for (const firstCell of this.firstCells) {
            this.addToCellMatches(matches, firstCell, actionBracket);
        }
        return matches;
    }
    getMatchesByLooping(level, actionBracket) {
        const matches = [];
        // Naiive version:
        // for (const row of level.getCells()) {
        //     for (const cell of row) {
        //         checkCell(cell)
        //     }
        // }
        const cells = level.getCells();
        const rowCount = cells.length;
        const colCount = cells[0].length;
        switch (this.direction) {
            case util_1.RULE_DIRECTION.UP:
            case util_1.RULE_DIRECTION.DOWN:
                for (let colIndex = 0; colIndex < colCount; colIndex++) {
                    if (level.colContainsSprites(colIndex, this.spritesPresentInRowOrColumn, this.anySpritesPresentInRowOrColumn)) {
                        for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
                            this.addIfCellMatches(matches, level.getCell(rowIndex, colIndex), actionBracket);
                        }
                    }
                }
                break;
            case util_1.RULE_DIRECTION.LEFT:
            case util_1.RULE_DIRECTION.RIGHT:
                for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
                    if (level.rowContainsSprites(rowIndex, this.spritesPresentInRowOrColumn, this.anySpritesPresentInRowOrColumn)) {
                        for (let colIndex = 0; colIndex < colCount; colIndex++) {
                            this.addIfCellMatches(matches, level.getCell(rowIndex, colIndex), actionBracket);
                        }
                    }
                }
                break;
            default:
                throw new Error(`BUG: Unsupported Direction "${this.direction}"`);
        }
        return matches;
    }
    getMatches(level, actionBracket) {
        if (process.env.NODE_ENV === 'development' && this.debugFlag === util_1.DEBUG_FLAG.BREAKPOINT) {
            // A "DEBUGGER" flag was set in the game so we are pausing here
            if (process.stdout) {
                terminal_1.default.debugRenderScreen();
            }
            debugger; // tslint:disable-line:no-debugger
        }
        let matches;
        if (!this.shouldUseOnDemandMethod()) {
            matches = this.getMatchesByTrickling(level, actionBracket);
            if (process.env.VERIFY_MATCHES) {
                const loopingMatches = this.getMatchesByLooping(level, actionBracket);
                if (matches.length !== loopingMatches.length) {
                    debugger; // tslint:disable-line:no-debugger
                    this.getMatchesByLooping(level, actionBracket); // run again so we can step through
                    throw new Error(`Match lengths differ. Expected ${loopingMatches.length} but found ${matches.length}. \n${this.toString()}`);
                }
            }
        }
        else {
            matches = this.getMatchesByLooping(level, actionBracket);
        }
        return matches;
    }
    getFirstCells() {
        return this.firstCells;
    }
    _removeFirstCell(firstCell) {
        if (this.firstCells.has(firstCell)) {
            if (process.env.NODE_ENV === 'development' && this.debugFlag === util_1.DEBUG_FLAG.BREAKPOINT_REMOVE) {
                // Pausing here because it was marked in the code
                if (process.stdout) {
                    terminal_1.default.debugRenderScreen();
                }
                debugger; // tslint:disable-line:no-debugger
            }
            this.firstCells.delete(firstCell);
            for (const [ellipsisBracket, token] of this.ellipsisBracketListeners) {
                ellipsisBracket.removeFirstCell(this, firstCell, token);
            }
        }
    }
    matchesDownstream(cell, index) {
        // Check all the neighbors and add the firstNeighbor to the set of matches for this direction
        let matched = true;
        let curCell = cell;
        // Loop Downstream
        // check the neighbors downstream of curCell
        for (let x = index + 1; x < this.neighbors.length; x++) {
            curCell = curCell.getNeighbor(this.direction);
            // TODO: Convert the neighbor check into a method
            if (curCell && (this.neighbors[x]._tilesWithModifier.size === 0 || this.neighbors[x].matchesCellSimple(curCell))) {
                // keep going
            }
            else {
                matched = false;
                break;
            }
        }
        return matched;
    }
    getUpstream(cell, index) {
        let curCell = cell;
        for (let x = index - 1; x >= 0; x--) {
            curCell = curCell.getNeighbor(util_1.opposite(this.direction));
            if (curCell) {
                // keep going
            }
            else {
                return null;
            }
        }
        return curCell;
    }
    matchesUpstream(cell, index) {
        let matched = true;
        let curCell = cell;
        // check the neighbors upstream of curCell
        for (let x = index - 1; x >= 0; x--) {
            curCell = curCell.getNeighbor(util_1.opposite(this.direction));
            if (curCell && (this.neighbors[x]._tilesWithModifier.size === 0 || this.neighbors[x].matchesCellSimple(curCell))) {
                // keep going
            }
            else {
                matched = false;
                break;
            }
        }
        return matched ? curCell : null;
    }
    getFirstCellToRemove(cell, index) {
        // Loop Upstream
        // check the neighbors upstream of curCell
        let matched = true;
        let curCell = cell;
        // check the neighbors upstream of curCell
        for (let x = index - 1; x >= 0; x--) {
            curCell = curCell.getNeighbor(util_1.opposite(this.direction));
            if (curCell) {
                // keep going
            }
            else {
                matched = false;
                break;
            }
        }
        return matched ? curCell : null;
    }
    addToCellMatches(matches, cell, actionBracket) {
        const cellMatches = [];
        let curCell = cell;
        let didAllNeighborsMatch = true;
        for (let index = 0; index < this.neighbors.length; index++) {
            if (!curCell) {
                didAllNeighborsMatch = false;
                break;
            }
            const condition = this.neighbors[index];
            let action;
            // Some rules only contain a condition bracket and a command
            if (actionBracket) {
                action = actionBracket.neighbors[index];
            }
            const x = {
                cell: curCell,
                condition,
                action
            };
            cellMatches.push(x);
            curCell = curCell.getNeighbor(this.direction);
        }
        if (didAllNeighborsMatch) {
            matches.push(new MatchedCellsForRule(cellMatches));
        }
    }
    addIfCellMatches(matches, cell, actionBracket) {
        if (this.neighbors[0].matchesCellSimple(cell) && this.matchesDownstream(cell, 0)) {
            this.addToCellMatches(matches, cell, actionBracket);
        }
    }
}
exports.SimpleBracket = SimpleBracket;
var BEFORE_OR_AFTER;
(function (BEFORE_OR_AFTER) {
    BEFORE_OR_AFTER[BEFORE_OR_AFTER["BEFORE"] = 0] = "BEFORE";
    BEFORE_OR_AFTER[BEFORE_OR_AFTER["AFTER"] = 1] = "AFTER";
})(BEFORE_OR_AFTER || (BEFORE_OR_AFTER = {}));
class MultiMap {
    constructor() {
        this.map = new Map();
    }
    clear() {
        this.map.clear();
    }
    has(a, b) {
        const set = this.map.get(a);
        if (set) {
            return set.has(b);
        }
        return false;
    }
    getB(a) {
        return this.map.get(a);
    }
    add(a, b) {
        let set = this.map.get(a);
        if (!set) {
            set = new Set();
            this.map.set(a, set);
        }
        if (!set.has(b)) {
            set.add(b);
            return true;
        }
        return false;
    }
    deleteAllA(a) {
        this.map.delete(a);
    }
    deleteAllB(b) {
        const asRemoved = new Set();
        for (const [a, set] of this.map) {
            if (set.has(b)) {
                set.delete(b);
                if (set.size === 0) {
                    this.map.delete(a);
                    asRemoved.add(a);
                }
            }
        }
        return asRemoved;
    }
    delete(a, b) {
        const set = this.map.get(a);
        if (set) {
            if (!set.has(b)) {
                throw new Error(`BUG: Invariant error. Link did not exist so nothing to remove`);
            }
            set.delete(b);
        }
    }
    sizeA() {
        return this.map.size;
    }
    hasA(a) {
        return this.map.has(a);
    }
    hasB(b) {
        return !!this.getA(b);
    }
    getA(b) {
        const ret = new Set();
        for (const [a, set] of this.map) {
            if (set.has(b)) {
                ret.add(a);
            }
        }
        if (ret.size > 0) {
            return ret;
        }
        return undefined;
    }
    size() {
        let size = 0;
        for (const set of this.map.values()) {
            size += set.size;
        }
        return size;
    }
}
class SimpleEllipsisBracket extends ISimpleBracket {
    constructor(source, direction, beforeEllipsisNeighbors, afterEllipsisNeighbors, debugFlag) {
        super(source, direction, [...beforeEllipsisNeighbors, ...afterEllipsisNeighbors], debugFlag);
        this.beforeEllipsisBracket = new SimpleBracket(source, direction, beforeEllipsisNeighbors, debugFlag);
        this.afterEllipsisBracket = new SimpleBracket(source, direction, afterEllipsisNeighbors, debugFlag);
        this.linkages = new MultiMap();
    }
    subscribeToNeighborChanges() {
        this.beforeEllipsisBracket.subscribeToNeighborChanges();
        this.afterEllipsisBracket.subscribeToNeighborChanges();
        this.beforeEllipsisBracket.addEllipsisBracket(this, BEFORE_OR_AFTER.BEFORE);
        this.afterEllipsisBracket.addEllipsisBracket(this, BEFORE_OR_AFTER.AFTER);
    }
    toKey(ignoreDebugFlag) {
        return `[${this.direction} ${this.beforeEllipsisBracket.toKey(ignoreDebugFlag)} ... ${this.afterEllipsisBracket.toKey(ignoreDebugFlag)}]}`;
    }
    dependsOnDirection() {
        return true;
    }
    getNeighbors() {
        // throw new Error(`BUG: Should not be calling this method`)
        return []; // TODO: Implement me
    }
    clearCaches() {
        this.firstCells.clear();
        this.linkages.clear();
        this.beforeEllipsisBracket.clearCaches();
        this.afterEllipsisBracket.clearCaches();
    }
    prepareAction(action) {
        const actionBracket = action; // since we know the condition and action side need to match
        this.beforeEllipsisBracket.prepareAction(actionBracket.beforeEllipsisBracket);
        this.afterEllipsisBracket.prepareAction(actionBracket.afterEllipsisBracket);
    }
    addCellsToEmptyRules(cells) {
        this.beforeEllipsisBracket.addCellsToEmptyRules(cells);
        this.afterEllipsisBracket.addCellsToEmptyRules(cells);
    }
    addCell(index, neighbor, t, sprite, cell, wantsToMove) {
        throw new Error(`BUG: We should not be subscribed to these events`);
    }
    removeCell(index, neighbor, t, sprite, cell) {
        throw new Error(`BUG: We should not be subscribed to these events`);
    }
    addFirstCell(bracket, firstCell, token) {
        // // check to see if the new cell is in line with any firstCells in the other bracket. If so, we have a match!
        // let firstBeforeCells
        // let firstAfterCells
        // if (bracket == this.beforeEllipsisBracket) {
        //     firstBeforeCells = new Set([firstCell])
        //     // search for a matching afterCell
        //     firstAfterCells = this.findMatching(firstCell, this.direction, this.afterEllipsisBracket)
        // } else if (bracket === this.afterEllipsisBracket) {
        //     firstAfterCells = new Set([firstCell])
        //     // search for a matching beforeCell
        //     firstBeforeCells = this.findMatching(firstCell, opposite(this.direction), this.beforeEllipsisBracket)
        // } else {
        //     throw new Error(`BUG: Bracket should only ever be the before-ellipsis or after-ellipsis one`)
        // }
        // for (const firstBeforeCell of firstBeforeCells) {
        //     for (const firstAfterCell of firstAfterCells) {
        //         this.checkInvariants()
        //         // Check if we need to actually change anything first. Becauase the !doesEvaluationOrderMatter case
        //         // keeps iterating on the set of firstCells but if they keep flipping then it's a problem because it
        //         // runs in an infinite loop
        //         // Delete any mapping that may have existed before
        //         if (this.linkages.has(firstBeforeCell, firstAfterCell)) {
        //             // nothing to do. we already have those entries
        //         } else {
        //             this.linkages.add(firstBeforeCell, firstAfterCell)
        //             this.firstCells.add(firstBeforeCell)
        //         }
        //         this.checkInvariants()
        //     }
        // }
    }
    removeFirstCell(bracket, firstCell, token) {
        // Figure out the 1st cell for us and remove it (by maybe looking at the matching bracket)
        this.checkInvariants();
        if (bracket === this.beforeEllipsisBracket) {
            this.linkages.deleteAllA(firstCell);
            if (this.firstCells.has(firstCell)) {
                this.firstCells.delete(firstCell);
            }
            else {
                // console.warn('Removing firstCell but it has already been removed')
                // console.warn(this.toString())
            }
        }
        else if (bracket === this.afterEllipsisBracket) {
            const beforeCellsRemoved = this.linkages.deleteAllB(firstCell);
            for (const b of beforeCellsRemoved) {
                this.firstCells.delete(b);
            }
        }
        else {
            throw new Error(`BUG: Bracket should only ever be the before-ellipsis or after-ellipsis one`);
        }
        this.checkInvariants();
    }
    // private findMatching(cell: Cell, direction: RULE_DIRECTION, inBracket: SimpleBracket) {
    //     const matches = new Set()
    //     for (const inBracketCell of inBracket.getFirstCells()) {
    //         switch (direction) {
    //             case RULE_DIRECTION.UP:
    //                 if (cell.colIndex === inBracketCell.colIndex && cell.rowIndex > inBracketCell.rowIndex) {
    //                     matches.add(inBracketCell)
    //                 }
    //                 break
    //             case RULE_DIRECTION.DOWN:
    //                 if (cell.colIndex === inBracketCell.colIndex && cell.rowIndex < inBracketCell.rowIndex) {
    //                     matches.add(inBracketCell)
    //                 }
    //                 break
    //             case RULE_DIRECTION.LEFT:
    //                 if (cell.colIndex > inBracketCell.colIndex && cell.rowIndex === inBracketCell.rowIndex) {
    //                     matches.add(inBracketCell)
    //                 }
    //                 break
    //             case RULE_DIRECTION.RIGHT:
    //                 if (cell.colIndex < inBracketCell.colIndex && cell.rowIndex === inBracketCell.rowIndex) {
    //                     matches.add(inBracketCell)
    //                 }
    //                 break
    //             default:
    //                 throw new Error(`BUG: Invalid direction`)
    //         }
    //     }
    //     return matches
    // }
    getMatches(level, actionBracket) {
        const ret = [];
        let beforeMatches;
        let afterMatches;
        if (actionBracket) {
            beforeMatches = this.beforeEllipsisBracket.getMatches(level, actionBracket.beforeEllipsisBracket);
            afterMatches = this.afterEllipsisBracket.getMatches(level, actionBracket.afterEllipsisBracket);
        }
        else {
            beforeMatches = this.beforeEllipsisBracket.getMatches(level, null);
            afterMatches = this.afterEllipsisBracket.getMatches(level, null);
        }
        const beforeMatchesByIndex = new MultiMap();
        if (beforeMatches.length === 0 || afterMatches.length === 0) {
            return [];
        }
        switch (this.direction) {
            case util_1.RULE_DIRECTION.UP:
            case util_1.RULE_DIRECTION.DOWN:
                for (const beforeMatch of beforeMatches) {
                    beforeMatchesByIndex.add(beforeMatch.lastCell().colIndex, beforeMatch);
                }
                for (const afterMatch of afterMatches) {
                    const { colIndex, rowIndex } = afterMatch.firstCell();
                    for (const beforeMatch of beforeMatchesByIndex.getB(colIndex) || []) {
                        // check if the afterMatch matches it.
                        // If so, remove the beforeMatch and include the whole match
                        const { rowIndex: beforeRowIndex } = beforeMatch.lastCell();
                        const isAfter = (this.direction === util_1.RULE_DIRECTION.DOWN) ? beforeRowIndex < rowIndex : rowIndex < beforeRowIndex;
                        if (isAfter) {
                            ret.push(new MatchedCellsForRule([...beforeMatch.cellsAndNeighbors].concat([...afterMatch.cellsAndNeighbors])));
                            // beforeMatchesByIndex.delete(colIndex, beforeMatch)
                        }
                    }
                }
                break;
            case util_1.RULE_DIRECTION.LEFT:
            case util_1.RULE_DIRECTION.RIGHT:
                for (const beforeMatch of beforeMatches) {
                    beforeMatchesByIndex.add(beforeMatch.lastCell().rowIndex, beforeMatch);
                }
                for (const afterMatch of afterMatches) {
                    const { rowIndex, colIndex } = afterMatch.firstCell();
                    for (const beforeMatch of beforeMatchesByIndex.getB(rowIndex) || []) {
                        // check if the afterMatch matches it.
                        // If so, remove the beforeMatch and include the whole match
                        const { colIndex: beforeColIndex } = beforeMatch.lastCell();
                        const isAfter = (this.direction === util_1.RULE_DIRECTION.RIGHT) ? beforeColIndex < colIndex : colIndex < beforeColIndex;
                        if (isAfter) {
                            ret.push(new MatchedCellsForRule([...beforeMatch.cellsAndNeighbors].concat([...afterMatch.cellsAndNeighbors])));
                            // beforeMatchesByIndex.delete(rowIndex, beforeMatch)
                        }
                    }
                }
                break;
            default:
                throw new Error(`BUG: Invalid direction ${this.direction}`);
        }
        return ret;
    }
    checkInvariants() {
        if (this.firstCells.size !== this.linkages.sizeA()) {
            throw new Error(`BUG: Invariant violation`);
        }
    }
}
exports.SimpleEllipsisBracket = SimpleEllipsisBracket;
class ReplaceTile {
    constructor(collisionLayer, actionTileWithModifier, mightNotFindConditionButThatIsOk, conditionSpritesToRemove, newDirection) {
        if (!collisionLayer) {
            throw new Error('BUG: collisionLayer is not set');
        }
        this.collisionLayer = collisionLayer;
        this.actionTileWithModifier = actionTileWithModifier;
        this.mightNotFindConditionButThatIsOk = mightNotFindConditionButThatIsOk;
        this.conditionSpritesToRemove = conditionSpritesToRemove;
        this.newDirection = newDirection;
    }
    replace(cell, magicOrTiles, orTilesRemoved) {
        let didActuallyChange = false;
        // Check if we are adding or removing....
        if (this.actionTileWithModifier) {
            // adding
            let sprites;
            // if RANDOM is set then pick a random sprite to add
            if (this.actionTileWithModifier.isRandom()) {
                const spritesToChoose = this.actionTileWithModifier._tile.getSprites();
                const rnd = util_1.nextRandom(spritesToChoose.length);
                sprites = [spritesToChoose[rnd]];
            }
            else if (this.actionTileWithModifier._tile.isOr()) {
                // There is no sprite of this type already in the cell. It's in the magicOrTiles
                const s = magicOrTiles.get(this.actionTileWithModifier._tile);
                if (!s) {
                    throw new Error(`BUG: Magic OR tile not found\n${this.actionTileWithModifier.toString()}`);
                }
                sprites = s;
            }
            else {
                sprites = this.actionTileWithModifier._tile.getSprites();
            }
            for (const sprite of sprites) {
                const c = sprite.getCollisionLayer();
                const wantsToMove = this.newDirection || cell.getCollisionLayerWantsToMove(c);
                let added;
                if (cell.hasSprite(sprite)) {
                    if (!wantsToMove) {
                        throw new Error(`BUG: Invariant violation. if the sprite exists then wantsToMove must also exist (at least it would be STATIONARY)`);
                    }
                    added = cell.updateSprite(sprite, wantsToMove);
                }
                else {
                    // preserve the wantsToMove if the sprite is in the same collision layer
                    added = cell.addSprite(sprite, wantsToMove);
                }
                didActuallyChange = didActuallyChange || added;
            }
        }
        else {
            // removing
            const tile = cell.getSpriteByCollisionLayer(this.collisionLayer);
            if (!tile && this.mightNotFindConditionButThatIsOk) {
                // this occurs when there is just a -> [ NO Color ] on the action side (remove color if it exists)
                return { actuallyDidChange: false };
            }
            if (!tile) {
                throw new Error(`BUG: No tile found`);
            }
            if (this.conditionSpritesToRemove) {
                // For OR tiles we need to only remove one of the sprites, not ALL of the sprites
                if (this.conditionSpritesToRemove._tile.isOr()) {
                    if (!orTilesRemoved.has(this.conditionSpritesToRemove._tile)) {
                        // only remove the sprites in the cell that match the condition... not all the sprites in a collisionLayer
                        const cellSprites = tile.getSprites();
                        for (const conditionSpriteToRemove of this.conditionSpritesToRemove._tile.getSprites()) {
                            if (cellSprites.indexOf(conditionSpriteToRemove) >= 0) {
                                const removed = cell.removeSprite(conditionSpriteToRemove);
                                didActuallyChange = didActuallyChange || removed;
                                if (removed) {
                                    orTilesRemoved.add(this.conditionSpritesToRemove._tile);
                                }
                                break;
                            }
                        }
                    }
                }
                else {
                    // only remove the sprites in the cell that match the condition... not all the sprites in a collisionLayer
                    const conditionSpritesToRemove = new Set(this.conditionSpritesToRemove._tile.getSprites());
                    for (const sprite of tile.getSprites()) {
                        if (conditionSpritesToRemove.has(sprite)) {
                            const removed = cell.removeSprite(sprite);
                            didActuallyChange = didActuallyChange || removed;
                        }
                    }
                }
            }
            else {
                throw new Error(`BUG: Not implemented (just commented out)`);
                // // remove all sprites
                // for (const sprite of tile.getSprites()) {
                //     const removed = cell.removeSprite(sprite)
                //     didActuallyChange = didActuallyChange || removed
                // }
            }
        }
        // return the oldSprite for UNDO
        return {
            didActuallyChange
        };
    }
}
class ReplaceDirection {
    constructor(collisionLayer, direction, mightNotFindConditionButThatIsOk) {
        if (!collisionLayer) {
            throw new Error('BUG: collisionLayer is not set');
        }
        this.collisionLayer = collisionLayer;
        this.direction = direction;
        this.mightNotFindConditionButThatIsOk = mightNotFindConditionButThatIsOk;
    }
    replace(cell) {
        let direction = this.direction;
        // It's OK if this sprite is not in the condition. This happens when an OR action tile has sprites that are in multiple collision layers
        if (this.mightNotFindConditionButThatIsOk && !cell.getSpriteByCollisionLayer(this.collisionLayer)) {
            return false;
        }
        // Pick a random direction
        if (this.direction === util_1.RULE_DIRECTION.RANDOMDIR) {
            // only set the direction if one has not already been set
            if (cell.getCollisionLayerWantsToMove(this.collisionLayer) === util_1.RULE_DIRECTION.STATIONARY) {
                switch (util_1.nextRandom(4)) {
                    case 0:
                        direction = util_1.RULE_DIRECTION.UP;
                        break;
                    case 1:
                        direction = util_1.RULE_DIRECTION.DOWN;
                        break;
                    case 2:
                        direction = util_1.RULE_DIRECTION.LEFT;
                        break;
                    case 3:
                        direction = util_1.RULE_DIRECTION.RIGHT;
                        break;
                    default:
                        throw new Error(`BUG: invalid random number chosen`);
                }
            }
            else {
                // a direction was already set
                return false;
            }
        }
        return cell.setWantsToMoveCollisionLayer(this.collisionLayer, direction);
    }
}
class SimpleNeighbor extends BaseForLines_1.BaseForLines {
    constructor(source, tilesWithModifier, debugFlag) {
        super(source);
        // this.alreadyReportedMismatch = false
        this._tilesWithModifier = tilesWithModifier;
        this.brackets = new Map();
        // this._localCellCache = new Map()
        this.debugFlag = debugFlag;
        this.staticCache = new Map();
        this.spritesPresent = new spriteBitSet_1.SpriteBitSet();
        this.spritesMissing = new spriteBitSet_1.SpriteBitSet();
        this.anySpritesPresent = new Set();
        this.spriteMovementsPresent = new Map();
        this.orTileMovementsPresent = new Map();
        this.trickleCells = new Set();
        this.lruCache = new lruCache_1.default(LRU_CACHE_SIZE);
        // Build up the cache BitSet for each collisionLayer
        this.cacheYesBitSets = new Map();
        this.cacheNoBitSets = new Map();
        this.cacheDirections = new Map();
        this.cacheMultiCollisionLayerTiles = new Set();
        const allTiles = [...tilesWithModifier];
        const noTiles = allTiles.filter((t) => t.isNo());
        const yesTiles = allTiles.filter((t) => !t.isNo());
        for (const t of yesTiles) {
            if (t._tile.hasSingleCollisionLayer()) {
                for (const sprite of t._tile.getSprites()) {
                    const c = sprite.getCollisionLayer();
                    if (t._direction) {
                        this.cacheDirections.set(c, t._direction);
                    }
                    let yesBitSet = this.cacheYesBitSets.get(c);
                    if (!yesBitSet) {
                        yesBitSet = new BitSet2();
                        this.cacheYesBitSets.set(c, yesBitSet);
                    }
                    yesBitSet.set(c.getBitSetIndexOf(sprite));
                }
            }
            else {
                this.cacheMultiCollisionLayerTiles.add(t);
            }
            if (t._tile.isOr()) {
                this.anySpritesPresent.add(new spriteBitSet_1.SpriteBitSet(t._tile.getSprites()));
                if (t._direction) {
                    this.orTileMovementsPresent.set(t._tile, t._direction);
                }
            }
            else {
                this.spritesPresent.addAll(t._tile.getSprites());
                for (const sprite of t._tile.getSprites()) {
                    if (t._direction) {
                        const prevDir = this.spriteMovementsPresent.get(sprite.getCollisionLayer());
                        if (prevDir && prevDir !== t._direction) {
                            throw new Error(`BUG??? prev=${prevDir} ${t._direction}`);
                        }
                        this.spriteMovementsPresent.set(sprite.getCollisionLayer(), t._direction);
                    }
                }
            }
        }
        for (const t of noTiles) {
            if (t._tile.hasSingleCollisionLayer()) {
                for (const sprite of t._tile.getSprites()) {
                    const c = sprite.getCollisionLayer();
                    if (t._direction) {
                        this.cacheDirections.set(c, t._direction);
                    }
                    let noBitSet = this.cacheNoBitSets.get(c);
                    if (!noBitSet) {
                        noBitSet = new BitSet2();
                        this.cacheNoBitSets.set(c, noBitSet);
                    }
                    noBitSet.set(c.getBitSetIndexOf(sprite));
                }
            }
            else {
                this.cacheMultiCollisionLayerTiles.add(t);
            }
            if (t._tile.isOr()) {
                // NO Color === NO Red NO Green NO Blue
                this.spritesMissing.addAll(t._tile.getSprites());
            }
            else {
                this.spritesMissing.addAll(t._tile.getSprites());
            }
        }
    }
    toKey(ignoreDebugFlag) {
        if (ignoreDebugFlag) {
            return `{${[...this._tilesWithModifier].map((t) => t.toKey(ignoreDebugFlag)).sort().join(' ')}}`;
        }
        else {
            return `{${[...this._tilesWithModifier].map((t) => t.toKey(ignoreDebugFlag)).sort().join(' ')} debugging?${!!this.debugFlag}}`;
        }
    }
    dependsOnDirection() {
        return !![...this._tilesWithModifier].find((t) => !!t._direction);
    }
    prepareAction(actionNeighbor) {
        if (process.env.NODE_ENV === 'development' && actionNeighbor.debugFlag === util_1.DEBUG_FLAG.BREAKPOINT) {
            // Pausing here because this breakpoint was marked in the game code
            debugger; // tslint:disable-line:no-debugger
        }
        if (this.staticCache.has(actionNeighbor)) {
            return;
        }
        // Compute the Mutators on-the-fly for now....
        const pairsByCollisionLayer = new Map();
        const orTiles = new Map();
        for (const t of this._tilesWithModifier) {
            if (t._tile.isOr() && !t._tile.hasSingleCollisionLayer()) {
                if (!t.isNo()) {
                    orTiles.set(t._tile, t);
                }
            }
            else {
                // AND Tiles can have multiple collisionLayers too...
                if (t._tile.hasSingleCollisionLayer()) {
                    const c = t._tile.getCollisionLayer();
                    if (!c) {
                        throw new Error(`BUG: Tile is not assigned to a collision layer\n${t._tile.toString()}`);
                    }
                    // If we have something like `[Player NO PlayerHold] -> ...` then keep the Player, not the PlayerHold
                    if (pairsByCollisionLayer.has(c)) {
                        // Determine whether to keep the 1st match or the current one.
                        // If the current one is a NO tile then definitely do not replace it.
                        // Maybe the correct thing to do is to always keep the 1st thing put in
                        // if (!t.isNo()) {
                        //     pairsByCollisionLayer.set(c, new ExtraPair<SimpleTileWithModifier>(t, null/*filled in later if there is an action*/, false/*okToIgnoreNonMatches*/))
                        // }
                    }
                    else {
                        pairsByCollisionLayer.set(c, new ExtraPair(t, null /*filled in later if there is an action*/, false /*okToIgnoreNonMatches*/));
                    }
                }
                else {
                    // loop over each collisionLayer
                    for (const sprite of t._tile.getSprites()) {
                        const c = sprite.getCollisionLayer();
                        if (!pairsByCollisionLayer.has(c)) {
                            // TODO: Should we ues the whole tileWithModifier or create a new one out of the sprite?
                            pairsByCollisionLayer.set(c, new ExtraPair(t, null /*filled in later if there is an action*/, false /*okToIgnoreNonMatches*/));
                        }
                    }
                }
            }
        }
        // First just pair up all the conditions and actions (keep the negations)
        // Then, remove all negations
        // Then, build the ReplaceTile and ReplaceDirections
        const unmatchedOrTiles = new Map(orTiles.entries());
        for (const t of actionNeighbor._tilesWithModifier) {
            if (t._tile.isOr() && !t._tile.hasSingleCollisionLayer()) {
                // OR tiles may belong to different collisionlayers so... it's complicated
                const orTile = orTiles.get(t._tile);
                if (orTile) {
                    unmatchedOrTiles.delete(t._tile);
                    // simple case. at most we just change direction
                    const conditionT = orTile;
                    if (conditionT._direction !== t._direction) {
                        for (const sprite of t._tile.getSprites()) {
                            const c = sprite.getCollisionLayer();
                            if (!pairsByCollisionLayer.has(c)) {
                                pairsByCollisionLayer.set(c, new ExtraPair(new SimpleTileWithModifier(conditionT.__source, conditionT._isNegated /*since the action side is a NO */, conditionT._isRandom /*isRandom*/, conditionT._direction, sprite, conditionT._debugFlag), new SimpleTileWithModifier(t.__source, t._isNegated /*since the action side is a NO */, t._isRandom /*isRandom*/, t._direction, sprite, t._debugFlag), true /*okToIgnoreNonMatches*/));
                            }
                        }
                    }
                }
                else {
                    if (t.isNo()) {
                        for (const sprite of t._tile.getSprites()) {
                            const c = sprite.getCollisionLayer();
                            if (!pairsByCollisionLayer.has(c)) {
                                const t2 = new SimpleTileWithModifier(t.__source, false /*since the action side is a NO */, t._isRandom /*isRandom*/, t._direction, t._tile, t._debugFlag);
                                pairsByCollisionLayer.set(c, new ExtraPair(t2, null, true /*okToIgnoreNonMatches*/));
                            }
                        }
                    }
                    else {
                        for (const sprite of t._tile.getSprites()) {
                            const c = sprite.getCollisionLayer();
                            if (!pairsByCollisionLayer.has(c)) {
                                const t2 = new SimpleTileWithModifier(t.__source, false /*since the action side is NOT? NO */, t._isRandom /*isRandom*/, t._direction, t._tile, t._debugFlag);
                                pairsByCollisionLayer.set(c, new ExtraPair(null, t2, true /*okToIgnoreNonMatches*/));
                            }
                        }
                    }
                }
            }
            else {
                for (const c of t.getCollisionLayers()) {
                    if (!c) {
                        throw new Error(`BUG: Tile is not assigned to a collision layer.\n${t._tile.toString()}`);
                    }
                    // if the condition is the same as the action then it's a no-op and we can remove the code
                    const p = pairsByCollisionLayer.get(c);
                    const conditionVersion = (p && p.condition) || null;
                    if (conditionVersion && conditionVersion.equals(t)) {
                        // condition and action are the same. No need to add a Pair
                        pairsByCollisionLayer.delete(c);
                    }
                    else {
                        if (t.isNo()) {
                            // set it to be null (removed)
                            if (p) {
                                // just leave the action side as null (so it's removed)
                                if (p.condition === t) {
                                    // remove if both the condition and action are the same
                                    pairsByCollisionLayer.delete(c);
                                }
                            }
                            else {
                                // we need to set the condition side to be the tile so that it is removed
                                // (it might not exist in the cell though but that's an optimization for later)
                                const t2 = new SimpleTileWithModifier(t.__source, false /*since the action side is a NO */, false /*isRandom*/, t._direction, t._tile, t._debugFlag);
                                pairsByCollisionLayer.set(c, new ExtraPair(t2, null, true /*okToIgnoreNonMatches*/));
                            }
                        }
                        else {
                            if (p) {
                                p.action = t;
                            }
                            else {
                                pairsByCollisionLayer.set(c, new ExtraPair(null, t, false /*okToIgnoreNonMatches*/));
                            }
                        }
                    }
                }
            }
        }
        // Any unmatched OR tiles need to be removed from the Cell
        if (unmatchedOrTiles.size > 0) {
            for (const t of unmatchedOrTiles.values()) {
                for (const sprite of t._tile.getSprites()) {
                    const c = sprite.getCollisionLayer();
                    if (!pairsByCollisionLayer.has(c)) {
                        const t2 = new SimpleTileWithModifier(t.__source, false /*since the action side is a NO */, false /*isRandom*/, t._direction, t._tile, t._debugFlag);
                        pairsByCollisionLayer.set(c, new ExtraPair(t2, null, true /*okToIgnoreNonMatches*/));
                    }
                }
            }
        }
        const replaceTiles = new Set();
        const replaceDirections = new Set();
        for (const [collisionLayer, { condition, action, extra }] of pairsByCollisionLayer.entries()) {
            if (condition && action) {
                if (condition !== action) { // Could be `[ TrolleyFull no CleanDishes] -> [TrolleyEmpty no CleanDishes ]`
                    let newDirection = null;
                    if (condition._direction !== action._direction || condition.isNo()) {
                        newDirection = action._direction || util_1.RULE_DIRECTION.STATIONARY;
                    }
                    if (!condition._tile.equals(action._tile) || condition.isNo()) {
                        replaceTiles.add(new ReplaceTile(collisionLayer, action, extra, null, newDirection));
                    }
                    else if (newDirection) {
                        replaceDirections.add(new ReplaceDirection(collisionLayer, newDirection, extra));
                    }
                }
            }
            else if (condition) {
                if (!condition.isNo()) {
                    replaceTiles.add(new ReplaceTile(collisionLayer, null, extra, condition, null));
                }
            }
            else if (action) {
                if (!action.isNo()) {
                    replaceTiles.add(new ReplaceTile(collisionLayer, action, extra, null, action._direction || util_1.RULE_DIRECTION.STATIONARY));
                }
            }
        }
        this.staticCache.set(actionNeighbor, { replaceTiles, replaceDirections });
    }
    evaluate(actionNeighbor, cell, magicOrTiles) {
        if (process.env.NODE_ENV === 'development' && actionNeighbor.debugFlag === util_1.DEBUG_FLAG.BREAKPOINT) {
            // Pausing here because this breakpoint was marked in the game code
            if (process.stdout) {
                terminal_1.default.debugRenderScreen();
            }
            debugger; // tslint:disable-line:no-debugger
        }
        let r = this.staticCache.get(actionNeighbor);
        if (!r) {
            this.prepareAction(actionNeighbor);
            r = this.staticCache.get(actionNeighbor);
        }
        if (!r) {
            throw new Error('BUG: Missing actionNeighbor. Should have been prepared before');
        }
        const { replaceTiles, replaceDirections } = r;
        let didChangeSprites = false;
        let didChangeDirection = false;
        const orTilesRemoved = new Set();
        for (const replaceTile of replaceTiles) {
            const { didActuallyChange } = replaceTile.replace(cell, magicOrTiles, orTilesRemoved);
            didChangeSprites = didChangeSprites || didActuallyChange || false;
        }
        for (const replaceDirection of replaceDirections) {
            const didActuallyChange = replaceDirection.replace(cell);
            didChangeDirection = didChangeDirection || didActuallyChange;
        }
        // TODO: Be better about recording when the cell actually updated
        if (didChangeSprites || didChangeDirection) {
            return new CellMutation(cell);
        }
        else {
            return null;
        }
    }
    clearCaches() {
        // this._localCellCache.clear()
        for (const t of this._tilesWithModifier) {
            t.clearCaches();
        }
    }
    // set this ahead of time becuase order does not matter when populating the magicOrTiles `[ > Player | Pill ] -> [ Pill OldPos | Player ]`
    populateMagicOrTiles(cell, magicOrTiles) {
        for (const t of this._tilesWithModifier) {
            if (!t.isNo() && t._tile.isOr()) {
                const sprites = util_1.setIntersection(new Set(t._tile.getSprites()), cell.getSpritesAsSet());
                magicOrTiles.set(t._tile, sprites);
            }
        }
    }
    subscribeToTileChanges(bracket, index) {
        // add the bracket and then subscribe the tiles
        let b = this.brackets.get(bracket);
        if (!b) {
            b = new Set();
            this.brackets.set(bracket, b);
        }
        b.add(index);
        this._tilesWithModifier.forEach((t) => {
            t.subscribeToCellChanges(this);
        });
    }
    matchesCellSimple(cell) {
        return this.matchesCell(cell, null, null);
    }
    addCells(t, sprite, cells, wantsToMove) {
        if (process.env.NODE_ENV === 'development' && this.debugFlag === util_1.DEBUG_FLAG.BREAKPOINT) {
            // Pausing here because it was marked in the code
            if (process.stdout) {
                terminal_1.default.debugRenderScreen();
            }
            debugger; // tslint:disable-line:no-debugger
        }
        for (const cell of cells) {
            if (this.matchesTiles(cell)) {
                this.trickleCells.add(cell);
                for (const [bracket, indexes] of this.brackets.entries()) {
                    for (const index of indexes) {
                        bracket.addCell(index, this, t, sprite, cell, wantsToMove);
                    }
                }
            }
            else if (this.trickleCells.has(cell)) {
                this.trickleCells.delete(cell);
                // adding the Cell causes the set of Tiles to no longer match.
                // If it previously matched, notify the bracket that it no longer matches
                // (and delete it from our cache)
                for (const [bracket, indexes] of this.brackets.entries()) {
                    for (const index of indexes) {
                        bracket.removeCell(index, this, t, sprite, cell);
                    }
                }
            }
        }
    }
    updateCells(t, sprite, cells, wantsToMove) {
        this.addCells(t, sprite, cells, wantsToMove);
    }
    removeCells(t, sprite, cells) {
        if (process.env.NODE_ENV === 'development' && this.debugFlag === util_1.DEBUG_FLAG.BREAKPOINT_REMOVE) {
            // Pausing here because it was marked in the code
            if (process.stdout) {
                terminal_1.default.debugRenderScreen();
            }
            debugger; // tslint:disable-line:no-debugger
        }
        for (const cell of cells) {
            // Check if the cell still matches. If not, remove it from upstream
            // It's a little funky if we have a NO tile. I _think_ we need to negate the
            // result of matchesCellWithout in that case but not completely sure
            if (!this.matchesTiles(cell)) {
                this.trickleCells.delete(cell);
                // remove it from upstream
                for (const [bracket, indexes] of this.brackets.entries()) {
                    for (const index of indexes) {
                        bracket.removeCell(index, this, t, sprite, cell);
                    }
                }
            }
        }
    }
    matchesCell(cell, tileWithModifier, wantsToMove) {
        const valueFn = () => {
            let doesMatch = cell.spriteBitSet.containsAll(this.spritesPresent) &&
                cell.spriteBitSet.containsNone(this.spritesMissing);
            if (doesMatch) {
                for (const anySpritesPresent of this.anySpritesPresent) {
                    doesMatch = doesMatch && cell.spriteBitSet.containsAny(anySpritesPresent);
                }
            }
            // Check CollisionLayers
            // TODO: Move this into the Cell definition
            if (doesMatch) {
                for (const [collisionLayer, direction] of this.spriteMovementsPresent) {
                    const cellDir = cell.getCollisionLayerWantsToMove(collisionLayer);
                    if (direction !== cellDir) {
                        doesMatch = false;
                    }
                }
            }
            if (doesMatch) {
                for (const [orTile, direction] of this.orTileMovementsPresent) {
                    if (orTile.hasSingleCollisionLayer()) {
                        const cellDir = cell.getCollisionLayerWantsToMove(orTile.getCollisionLayer());
                        if (direction !== cellDir) {
                            doesMatch = false;
                        }
                    }
                    else {
                        // find which sprite in the OR tile matched and get its direction
                        let foundSprite = false;
                        // the OR tile can match multiple sprites so make sure at least one matched (not all)
                        // e.g:
                        // Movable = Player OR Island
                        // Rule: [ LEFT Movable ]
                        // Cell: STATIONARY Player LEFT Island
                        let didMatch = false;
                        for (const sprite of orTile.getSprites()) {
                            if (cell.spriteBitSet.has(sprite)) {
                                foundSprite = true;
                                const cellDir = cell.getCollisionLayerWantsToMove(sprite.getCollisionLayer());
                                if (direction === cellDir) {
                                    didMatch = true;
                                }
                            }
                        }
                        doesMatch = doesMatch && didMatch;
                        if (!foundSprite) {
                            throw new Error(`BUG: Could not find sprite. One should have already been matched before`);
                        }
                    }
                }
            }
            return doesMatch;
        };
        return this.lruCache.get(`[${cell.toKey()}]`, valueFn);
    }
    matchesTiles(cell) {
        for (const t of this._tilesWithModifier) {
            if (!t.hasCell(cell)) {
                return false;
            }
        }
        return true;
    }
}
exports.SimpleNeighbor = SimpleNeighbor;
//# sourceMappingURL=rule.js.map