"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const BaseForLines_1 = require("../models/BaseForLines");
const rule_1 = require("../models/rule");
const util_1 = require("../util");
var AST_RULE_MODIFIER;
(function (AST_RULE_MODIFIER) {
    AST_RULE_MODIFIER["RANDOM"] = "RANDOM";
    AST_RULE_MODIFIER["UP"] = "UP";
    AST_RULE_MODIFIER["DOWN"] = "DOWN";
    AST_RULE_MODIFIER["LEFT"] = "LEFT";
    AST_RULE_MODIFIER["RIGHT"] = "RIGHT";
    AST_RULE_MODIFIER["VERTICAL"] = "VERTICAL";
    AST_RULE_MODIFIER["HORIZONTAL"] = "HORIZONTAL";
    AST_RULE_MODIFIER["ORTHOGONAL"] = "ORTHOGONAL";
    AST_RULE_MODIFIER["PERPENDICULAR"] = "PERPENDICULAR";
    AST_RULE_MODIFIER["PARALLEL"] = "PARALLEL";
    AST_RULE_MODIFIER["MOVING"] = "MOVING";
    AST_RULE_MODIFIER["LATE"] = "LATE";
    AST_RULE_MODIFIER["RIGID"] = "RIGID";
})(AST_RULE_MODIFIER = exports.AST_RULE_MODIFIER || (exports.AST_RULE_MODIFIER = {}));
const RULE_DIRECTION_LIST = [
    util_1.RULE_DIRECTION.UP,
    util_1.RULE_DIRECTION.DOWN,
    util_1.RULE_DIRECTION.LEFT,
    util_1.RULE_DIRECTION.RIGHT
];
const RULE_DIRECTION_SET = new Set(RULE_DIRECTION_LIST);
function cacheSetAndGet(cache, obj) {
    const key = obj.toKey();
    if (!cache.has(key)) {
        cache.set(key, obj);
    }
    return cache.get(key);
}
// Note: Directions inside a Bracket are relative to other dorections inside a bracket
// Example:
//
// Interpret `[ > player > cat | < dog ] -> [ < player | cat < dog ]` to:
// Interpret `[ ^ player v cat | v dog ] -> [ v player | cat v dog ]` to:
// UP    [ UP    player UP    cat | DOWN  dog ] -> [ DOWN  player | cat DOWN  dog ]
// DOWN  [ DOWN  player DOWN  cat | UP    dog ] -> [ UP    player | cat UP    dog ]
// LEFT  [ LEFT  player LEFT  cat | RIGHT dog ] -> [ RIGHT player | cat RIGHT dog ]
// RIGHT [ RIGHT player RIGHT cat | LEFT  dog ] -> [ LEFT  player | cat LEFT  dog ]
//
// Interpret `HORIZONTAL [ > player ] -> [ < crate ] to:
// LEFT  [ LEFT  player ] -> [ RIGHT crate ]
// RIGHT [ RIGHT player ] -> [ LEFT  crate ]
//
// Interpret `VERTICAL [ ^ player PARALLEL cat | PERPENDICULAR dog ] -> [ < crate |  dog ] to:
// UP    [ LEFT   player HORIZONTAL cat ] -> [ DOWN  crate | VERTICAL   dog ]
// DOWN  [ RIGHT  player HORIZONTAL cat ] -> [ UP    crate | VERTICAL   dog ]
// LEFT  [ DOWN   player VERTICAL   cat ] -> [ RIGHT crate | HORIZONTAL dog ]
// DOWN  [ RIGHT  player HORIZONTAL cat ] -> [ UP    crate | HORIZONTAL dog ]
//
// See https://www.puzzlescript.net/Documentation/executionorder.html
/**
 * Represents a rule as written in the game as well as the expanded intermediate version.
 *
 * See `.simplify()` for more details on the expanded intermediate version.
 */
class ASTRule extends BaseForLines_1.BaseForLines {
    constructor(source, modifiers, conditions, actions, commands, debugFlag) {
        super(source);
        this.modifiers = modifiers;
        this.commands = commands;
        this.brackets = conditions;
        this.actionBrackets = actions;
        this.debugFlag = debugFlag;
        // Check if valid
        if (conditions.length !== actions.length && actions.length !== 0) {
            throw new Error(`Left side has "${conditions.length}" conditions and right side has "${actions.length}" actions!`);
        }
        if (conditions.length === actions.length) {
            // do nothing
        }
        else if (actions.length !== 0) {
            throw new Error(`Invalid Rule. The number of brackets on the right must match the structure of the left hand side or be 0`);
        }
    }
    // toKey() {
    //     return `${this.modifiers.join(' ')} ${this.brackets.map(x => x.toKey())} -> ${this.actionBrackets.map(x => x.toKey())} ${this.commands.join(' ')} {debugger?${this.debugFlag}}`
    // }
    /**
     * Expands this Rule into multiple `ASTRule` objects and then converts each one into a `SimpleRule`.
     *
     * For example, `HORIZONTAL [ > player ] -> [ < crate ]` gets expanded to the following `ASTRule`s:
     *
     * -  `LEFT  [ LEFT  player ] -> [ RIGHT crate ]`
     * -  `RIGHT [ RIGHT player ] -> [ LEFT  crate ]`
     *
     * and then each one is converted into a `SimpleRule` which only has absolute directions
     * rather than the relative ones that were specified in the original game code.
     *
     * @param ruleCache A cache for de-duplicating rules (so fewer need to be updated when the game is played)
     * @param bracketCache A cache for de-duplicating brackets found in the game (so fewer need to be updated)
     * @param neighborCache A cache for de-duplicating neighbors found in the game (so fewer need to be updated)
     * @param tileCache A cache for de-duplicating tiles in the game (so fewer need to be updated)
     */
    simplify(ruleCache, bracketCache, neighborCache, tileCache) {
        const simpleRules = this.convertToMultiple().map((r) => r.toSimple(ruleCache, bracketCache, neighborCache, tileCache));
        // If the brackets are all the same object then that means we can just output 1 rule
        // (the brackets don't have any directions. Otherwise they would not have been
        // deduplicated as part of the .toKey() and cacheGetAndSet)
        const isDuplicate = simpleRules.length === 1 || (!this.isRandom() && simpleRules[1] && simpleRules[0].canCollapseBecauseBracketsMatch(simpleRules[1]));
        if (isDuplicate) {
            simpleRules[0].subscribeToCellChanges();
            // we still need it to be in a RuleGroup
            // so the Rule can be evaluated multiple times (not just once)
            return new rule_1.SimpleRuleGroup(this.__source, this.isRandom(), [simpleRules[0]]);
        }
        else {
            for (const rule of simpleRules) {
                rule.subscribeToCellChanges();
            }
            return new rule_1.SimpleRuleGroup(this.__source, this.isRandom(), simpleRules);
        }
    }
    isRandom() {
        return this.modifiers.indexOf(AST_RULE_MODIFIER.RANDOM) >= 0;
    }
    toSimple(ruleCache, bracketCache, neighborCache, tileCache) {
        const directions = this.getDirectionModifiers();
        if (directions.length !== 1) {
            throw new Error(`BUG: should have exactly 1 direction by now but found the following: "${directions}"`);
        }
        // Check if the condition matches the action. If so, we can simplify evaluation.
        const conditionBrackets = this.brackets.map((x) => x.toSimple(directions[0], ruleCache, bracketCache, neighborCache, tileCache));
        const actionBrackets = this.actionBrackets.map((x) => x.toSimple(directions[0], ruleCache, bracketCache, neighborCache, tileCache));
        for (let index = 0; index < conditionBrackets.length; index++) {
            const action = actionBrackets[index];
            // Skip rules with no action bracket `[ > Player ] -> CHECKPOINT`
            if (!action) {
                continue;
            }
        }
        return cacheSetAndGet(ruleCache, new rule_1.SimpleRule(this.__source, directions[0], conditionBrackets, actionBrackets, this.commands, this.isLate(), this.isRigid(), this.debugFlag));
    }
    convertToMultiple() {
        let rulesToConvert = [];
        let convertedRules = [];
        for (const direction of this.getDirectionModifiers()) {
            const expandedDirection = this.clone(direction, null, null);
            rulesToConvert.push(expandedDirection);
        }
        const expandModifiers = new Map();
        expandModifiers.set(AST_RULE_MODIFIER.HORIZONTAL, [util_1.RULE_DIRECTION.LEFT, util_1.RULE_DIRECTION.RIGHT]);
        expandModifiers.set(AST_RULE_MODIFIER.VERTICAL, [util_1.RULE_DIRECTION.UP, util_1.RULE_DIRECTION.DOWN]);
        expandModifiers.set(AST_RULE_MODIFIER.MOVING, [util_1.RULE_DIRECTION.UP, util_1.RULE_DIRECTION.DOWN, util_1.RULE_DIRECTION.LEFT, util_1.RULE_DIRECTION.RIGHT, util_1.RULE_DIRECTION.ACTION]);
        let didExpandRulesToConvert;
        do {
            didExpandRulesToConvert = false;
            for (const rule of rulesToConvert) {
                let didExpand = false;
                const direction = rule.getDirectionModifiers()[0];
                if (rule.getDirectionModifiers().length !== 1) {
                    throw new Error(`BUG: should have already expanded the rule to only contian one direction`);
                }
                for (const [nameToExpand, variations] of expandModifiers) {
                    if (rule.hasModifier(nameToExpand)) {
                        for (const variation of variations) {
                            convertedRules.push(rule.clone(direction, nameToExpand, variation));
                            didExpand = true;
                            didExpandRulesToConvert = true;
                        }
                    }
                }
                if (!didExpand) {
                    // Try expanding PARALLEL and ORTHOGONAL (since they depend on the rule direction)
                    let perpendiculars;
                    let parallels;
                    switch (direction) {
                        case util_1.RULE_DIRECTION.UP:
                        case util_1.RULE_DIRECTION.DOWN:
                            perpendiculars = [util_1.RULE_DIRECTION.LEFT, util_1.RULE_DIRECTION.RIGHT];
                            parallels = [util_1.RULE_DIRECTION.UP, util_1.RULE_DIRECTION.DOWN];
                            break;
                        case util_1.RULE_DIRECTION.LEFT:
                        case util_1.RULE_DIRECTION.RIGHT:
                            perpendiculars = [util_1.RULE_DIRECTION.UP, util_1.RULE_DIRECTION.DOWN];
                            parallels = [util_1.RULE_DIRECTION.LEFT, util_1.RULE_DIRECTION.RIGHT];
                            break;
                        default:
                            throw new Error(`BUG: There must be some direction`);
                    }
                    if (perpendiculars && parallels) {
                        const orthoParallels = [
                            { nameToExpand: AST_RULE_MODIFIER.ORTHOGONAL, variations: perpendiculars },
                            { nameToExpand: AST_RULE_MODIFIER.PERPENDICULAR, variations: perpendiculars },
                            { nameToExpand: AST_RULE_MODIFIER.PARALLEL, variations: parallels }
                        ];
                        for (const { nameToExpand, variations } of orthoParallels) {
                            if (rule.hasModifier(nameToExpand)) {
                                for (const variation of variations) {
                                    convertedRules.push(rule.clone(direction, nameToExpand, variation));
                                    didExpand = true;
                                    didExpandRulesToConvert = true;
                                }
                            }
                        }
                    }
                }
                // If nothing was expanded and this is the current rule
                // then just keep it
                if (!didExpand) {
                    convertedRules.push(rule);
                }
            }
            rulesToConvert = convertedRules;
            convertedRules = [];
        } while (didExpandRulesToConvert);
        return rulesToConvert;
    }
    clone(direction, nameToExpand, newName) {
        const conditionBrackets = this.brackets.map((bracket) => bracket.clone(direction, nameToExpand, newName));
        const actionBrackets = this.actionBrackets.map((bracket) => bracket.clone(direction, nameToExpand, newName));
        // retain LATE and RIGID but discard the rest of the modifiers
        let directionModifier;
        switch (direction) {
            case util_1.RULE_DIRECTION.UP:
                directionModifier = AST_RULE_MODIFIER.UP;
                break;
            case util_1.RULE_DIRECTION.DOWN:
                directionModifier = AST_RULE_MODIFIER.DOWN;
                break;
            case util_1.RULE_DIRECTION.LEFT:
                directionModifier = AST_RULE_MODIFIER.LEFT;
                break;
            case util_1.RULE_DIRECTION.RIGHT:
                directionModifier = AST_RULE_MODIFIER.RIGHT;
                break;
            default:
                throw new Error(`BUG: Invalid direction "${direction}"`);
        }
        const modifiers = [...util_1.setIntersection(new Set(this.modifiers), [AST_RULE_MODIFIER.LATE, AST_RULE_MODIFIER.RIGID, AST_RULE_MODIFIER.RANDOM])].concat([directionModifier]);
        return new ASTRule(this.__source, modifiers, conditionBrackets, actionBrackets, this.commands, this.debugFlag);
    }
    hasModifier(modifier) {
        for (const bracket of this.brackets) {
            for (const neighbor of bracket._getAllNeighbors()) {
                for (const t of neighbor.tilesWithModifier) {
                    if (t.modifier === modifier) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    getDirectionModifiers() {
        // Convert HORIZONTAL and VERTICAL to 2:
        if (this.modifiers.indexOf(AST_RULE_MODIFIER.HORIZONTAL) >= 0) {
            return [util_1.RULE_DIRECTION.LEFT, util_1.RULE_DIRECTION.RIGHT];
        }
        if (this.modifiers.indexOf(AST_RULE_MODIFIER.VERTICAL) >= 0) {
            return [util_1.RULE_DIRECTION.UP, util_1.RULE_DIRECTION.DOWN];
        }
        const directions = this.modifiers.filter((m) => RULE_DIRECTION_SET.has(m)).map((d) => {
            switch (d) {
                case AST_RULE_MODIFIER.UP:
                    return util_1.RULE_DIRECTION.UP;
                case AST_RULE_MODIFIER.DOWN:
                    return util_1.RULE_DIRECTION.DOWN;
                case AST_RULE_MODIFIER.LEFT:
                    return util_1.RULE_DIRECTION.LEFT;
                case AST_RULE_MODIFIER.RIGHT:
                    return util_1.RULE_DIRECTION.RIGHT;
                default:
                    throw new Error(`BUG: Invalid rule direction "${d}"`);
            }
        });
        if (directions.length === 0) {
            return RULE_DIRECTION_LIST;
        }
        else {
            return directions;
        }
    }
    isLate() {
        return this.modifiers.indexOf(AST_RULE_MODIFIER.LATE) >= 0;
    }
    isRigid() {
        return this.modifiers.indexOf(AST_RULE_MODIFIER.RIGID) >= 0;
    }
}
exports.ASTRule = ASTRule;
class ASTRuleBracket extends BaseForLines_1.BaseForLines {
    constructor(source, neighbors, hack, debugFlag) {
        super(source);
        this.neighbors = neighbors;
        this.debugFlag = debugFlag;
        // populate the cache
        this.firstCellsInEachDirection = new Map();
        for (const direction of rule_1.SIMPLE_DIRECTION_DIRECTIONS) {
            this.firstCellsInEachDirection.set(direction, new Set());
        }
        this.firstCellsInEachDirection.set(util_1.RULE_DIRECTION.ACTION, new Set());
    }
    toKey() {
        return this.neighbors.map((n) => n.toKey()).join('|');
    }
    clone(direction, nameToExpand, newName) {
        return new ASTRuleBracket(this.__source, this.neighbors.map((n) => n.clone(direction, nameToExpand, newName)), null, this.debugFlag);
    }
    toSimple(direction, ruleCache, bracketCache, neighborCache, tileCache) {
        return cacheSetAndGet(bracketCache, new rule_1.SimpleBracket(this.__source, direction, this.neighbors.map((x) => x.toSimple(ruleCache, bracketCache, neighborCache, tileCache)), this.debugFlag));
    }
    _getAllNeighbors() {
        return this.neighbors;
    }
}
exports.ASTRuleBracket = ASTRuleBracket;
class ASTRuleBracketEllipsis extends BaseForLines_1.BaseForLines {
    constructor(source, beforeEllipsisNeighbors, afterEllipsisNeighbors, debugFlag) {
        super(source);
        this.beforeEllipsisNeighbors = beforeEllipsisNeighbors;
        this.afterEllipsisNeighbors = afterEllipsisNeighbors;
        this.debugFlag = debugFlag;
    }
    toKey() {
        return `${this.beforeEllipsisNeighbors.map((n) => n.toKey()).join('|')} ... ${this.afterEllipsisNeighbors.map((n) => n.toKey()).join('|')}`;
    }
    clone(direction, nameToExpand, newName) {
        const beforeEllipsis = this.beforeEllipsisNeighbors.map((n) => n.clone(direction, nameToExpand, newName));
        const afterEllipsis = this.afterEllipsisNeighbors.map((n) => n.clone(direction, nameToExpand, newName));
        return new ASTRuleBracketEllipsis(this.__source, beforeEllipsis, afterEllipsis, this.debugFlag);
    }
    toSimple(direction, ruleCache, bracketCache, neighborCache, tileCache) {
        const beforeEllipsis = this.beforeEllipsisNeighbors.map((x) => x.toSimple(ruleCache, bracketCache, neighborCache, tileCache));
        const afterEllipsis = this.afterEllipsisNeighbors.map((x) => x.toSimple(ruleCache, bracketCache, neighborCache, tileCache));
        return cacheSetAndGet(bracketCache, new rule_1.SimpleEllipsisBracket(this.__source, direction, beforeEllipsis, afterEllipsis, this.debugFlag));
    }
    _getAllNeighbors() {
        return [...this.beforeEllipsisNeighbors, ...this.afterEllipsisNeighbors];
    }
}
exports.ASTRuleBracketEllipsis = ASTRuleBracketEllipsis;
class ASTRuleBracketNeighbor extends BaseForLines_1.BaseForLines {
    constructor(source, tilesWithModifier, debugFlag) {
        super(source);
        this.debugFlag = debugFlag;
        // Collapse duplicate tiles into one.
        // e.g. Cyber-Lasso has the following rule:
        // ... -> [ ElectricFloor Powered no ElectricFloor Claimed ]
        //
        // ElectricFloor occurs twice (one is negated)
        // We keep the first and remove the rest
        const tilesMap = new Map();
        for (const t of tilesWithModifier) {
            if (!tilesMap.has(t.tile)) {
                tilesMap.set(t.tile, t);
            }
        }
        this.tilesWithModifier = [...tilesMap.values()];
    }
    toKey() {
        return `{${this.tilesWithModifier.map((t) => t.toKey()).sort().join(' ')} debugging?${!!this.debugFlag}}`;
    }
    clone(direction, nameToExpand, newName) {
        return new ASTRuleBracketNeighbor(this.__source, this.tilesWithModifier.map((t) => t.clone(direction, nameToExpand, newName)), this.debugFlag);
    }
    toSimple(ruleCache, bracketCache, neighborCache, tileCache) {
        const simpleTilesWithModifier = new Set(this.tilesWithModifier.map((x) => x.toSimple(ruleCache, bracketCache, neighborCache, tileCache)));
        return cacheSetAndGet(neighborCache, new rule_1.SimpleNeighbor(this.__source, simpleTilesWithModifier, this.debugFlag));
    }
}
exports.ASTRuleBracketNeighbor = ASTRuleBracketNeighbor;
const M_NO = 'NO';
class ASTTileWithModifier extends BaseForLines_1.BaseForLines {
    constructor(source, modifier, tile, debugFlag) {
        super(source);
        this.modifier = modifier;
        this.tile = tile;
        this.debugFlag = debugFlag;
    }
    toKey() {
        return `${this.modifier || ''} ${this.tile ? this.tile.getSprites().map((sprite) => sprite.getName()) : '|||(notile)|||'}{debugging?${!!this.debugFlag}}`;
    }
    clone(direction, nameToExpand, newName) {
        switch (this.modifier) {
            case '>':
            case '<':
            case '^':
            case 'v':
                const modifier = relativeDirectionToAbsolute(direction, this.modifier);
                return new ASTTileWithModifier(this.__source, modifier, this.tile, this.debugFlag);
            case nameToExpand:
                return new ASTTileWithModifier(this.__source, newName, this.tile, this.debugFlag);
            default:
                return this;
        }
    }
    toSimple(ruleCache, bracketCache, neighborCache, tileCache) {
        let direction;
        switch (this.modifier) {
            case 'UP':
            case 'DOWN':
            case 'LEFT':
            case 'RIGHT':
                direction = util_1.RULE_DIRECTION[this.modifier];
                break;
            case 'STATIONARY':
                direction = util_1.RULE_DIRECTION.STATIONARY;
                break;
            case 'ACTION':
                direction = util_1.RULE_DIRECTION.ACTION;
                break;
            case 'RANDOMDIR':
                direction = util_1.RULE_DIRECTION.RANDOMDIR;
                break;
            default:
                direction = null;
        }
        return cacheSetAndGet(tileCache, new rule_1.SimpleTileWithModifier(this.__source, this.isNo(), this.isRandom(), direction, this.tile, this.debugFlag));
    }
    isNo() {
        return this.modifier === M_NO;
    }
    isRandom() {
        return this.modifier === AST_RULE_MODIFIER.RANDOM;
    }
}
exports.ASTTileWithModifier = ASTTileWithModifier;
// Extend RuleBracketNeighbor so that NeighborPair doesn't break
class ASTRuleBracketNeighborHack extends ASTRuleBracketNeighbor {
    // These should be addressed as we write the interpreter
    constructor(source, fields, debugFlag) {
        super(source, [], debugFlag);
        this.fields = fields;
    }
}
exports.ASTRuleBracketNeighborHack = ASTRuleBracketNeighborHack;
class AbstractRuleish extends BaseForLines_1.BaseForLines {
}
exports.AbstractRuleish = AbstractRuleish;
class ASTRuleLoop extends AbstractRuleish {
    constructor(source, rules, debugFlag) {
        super(source);
        this.rules = rules;
    }
    simplify(ruleCache, bracketCache, neighborCache, tileCache) {
        return new rule_1.SimpleRuleLoop(this.__source, false /*isRandom*/, this.rules.map((rule) => rule.simplify(ruleCache, bracketCache, neighborCache, tileCache)));
    }
}
exports.ASTRuleLoop = ASTRuleLoop;
class ASTRuleGroup extends BaseForLines_1.BaseForLines {
    constructor(source, isRandom, rules, debugFlag) {
        super(source);
        this.isRandom = isRandom;
        this.rules = rules;
    }
    // Yes. One propagates isRandom while the other does not
    simplify(ruleCache, bracketCache, neighborCache, tileCache) {
        const rules = this.rules.map((rule) => rule.simplify(ruleCache, bracketCache, neighborCache, tileCache));
        // if (rules.length === 1) {
        //     return rules[0]
        // }
        return new rule_1.SimpleRuleGroup(this.__source, this.isRandom, rules);
    }
}
exports.ASTRuleGroup = ASTRuleGroup;
function relativeDirectionToAbsolute(currentDirection, relativeModifier) {
    let currentDir;
    switch (currentDirection) {
        case util_1.RULE_DIRECTION.RIGHT:
            currentDir = 0;
            break;
        case util_1.RULE_DIRECTION.UP:
            currentDir = 1;
            break;
        case util_1.RULE_DIRECTION.LEFT:
            currentDir = 2;
            break;
        case util_1.RULE_DIRECTION.DOWN:
            currentDir = 3;
            break;
        default:
            throw new Error(`BUG! Invalid rule direction "${currentDirection}`);
    }
    switch (relativeModifier) {
        case '>':
            currentDir += 0;
            break;
        case '^':
            currentDir += 1;
            break;
        case '<':
            currentDir += 2;
            break;
        case 'v':
            currentDir += 3;
            break;
        default:
            throw new Error(`BUG! invalid relative direction "${relativeModifier}"`);
    }
    switch (currentDir % 4) {
        case 0:
            return util_1.RULE_DIRECTION.RIGHT;
        case 1:
            return util_1.RULE_DIRECTION.UP;
        case 2:
            return util_1.RULE_DIRECTION.LEFT;
        case 3:
            return util_1.RULE_DIRECTION.DOWN;
        default:
            throw new Error(`BUG! Incorrectly computed rule direction "${currentDirection}" "${relativeModifier}"`);
    }
}
exports.relativeDirectionToAbsolute = relativeDirectionToAbsolute;
//# sourceMappingURL=astRule.js.map