"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const colors_1 = require("../colors");
const collisionLayer_1 = require("../models/collisionLayer");
const colors_2 = require("../models/colors");
const command_1 = require("../models/command");
const game_1 = require("../models/game");
const level_1 = require("../models/level");
const metadata_1 = require("../models/metadata");
const rule_1 = require("../models/rule");
const sound_1 = require("../models/sound");
const tile_1 = require("../models/tile");
const winCondition_1 = require("../models/winCondition");
const util_1 = require("../util");
const ast = __importStar(require("./astTypes"));
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 removeNulls(ary) {
    // return ary.filter(a => !!a)
    // TypeScript-friendly version
    const ret = [];
    for (const item of ary) {
        if (item) {
            ret.push(item);
        }
    }
    return ret;
}
function cacheSetAndGet(cache, obj) {
    const key = obj.toKey();
    if (!cache.has(key)) {
        cache.set(key, obj);
    }
    return cache.get(key);
}
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 util_1.RULE_DIRECTION_RELATIVE.RELATIVE_RIGHT:
            currentDir += 0;
            break;
        case util_1.RULE_DIRECTION_RELATIVE.RELATIVE_UP:
            currentDir += 1;
            break;
        case util_1.RULE_DIRECTION_RELATIVE.RELATIVE_LEFT:
            currentDir += 2;
            break;
        case util_1.RULE_DIRECTION_RELATIVE.RELATIVE_DOWN:
            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}"`);
    }
}
class AstBuilder {
    constructor(code) {
        this.code = code;
        this.tileCache = new Map();
        this.soundCache = new Map();
    }
    build(root) {
        const source = this.toSource({ _sourceOffset: 0 });
        const metadata = new metadata_1.GameMetadata();
        root.metadata.forEach((pair) => {
            let value;
            if (typeof pair.value === 'object' && pair.value.type) {
                switch (pair.value.type) {
                    case ast.COLOR_TYPE.HEX3:
                    case ast.COLOR_TYPE.HEX6:
                    case ast.COLOR_TYPE.NAME:
                        {
                            const v = pair.value;
                            value = this.buildColor(v, metadata.colorPalette);
                        }
                        break;
                    case 'WIDTH_AND_HEIGHT':
                        {
                            const v = pair.value;
                            const v2 = v;
                            value = new metadata_1.Dimension(v2.width, v2.height);
                        }
                        break;
                    default:
                        throw new Error(`BUG: Invalid type at this point in time: ${pair.value}`);
                }
            }
            else {
                value = pair.value;
            }
            metadata._setValue(pair.type, value);
        });
        const sprites = root.sprites.map((n) => this.buildSprite(n, metadata.colorPalette));
        // assign an index to each GameSprite
        sprites.forEach((sprite, index) => {
            sprite.allSpritesBitSetIndex = index;
        });
        // Load the legend items up (used in Rules and Levels later on)
        const legendItems = root.legendItems.map((n) => this.buildLegendItem(n));
        const sounds = root.sounds.map((n) => this.buildSound(n));
        const collisionLayers = root.collisionLayers.map((n) => this.buildCollisionLayer(n));
        const simpleRules = this.buildSimpleRules(root.rules);
        const winConditions = root.winConditions.map((n) => this.buildWinConditon(n));
        const levels = root.levels.map((n) => this.buildLevel(n));
        return new game_1.GameData(source, root.title, metadata, sprites, legendItems, sounds, collisionLayers, simpleRules, winConditions, levels);
    }
    buildSprite(node, colorPalette) {
        let ret;
        if (node.type === ast.SPRITE_TYPE.WITH_PIXELS) {
            const source = this.toSource(node);
            const colors = node.colors.map((n) => this.buildColor(n, colorPalette));
            const pixels = node.pixels.map((row) => {
                return row.map((col) => {
                    if (col === '.') {
                        return new colors_2.TransparentColor(source);
                    }
                    else {
                        return colors[col] || new colors_2.TransparentColor(source);
                    }
                });
            }); // Pixel colors are 0-indexed.
            ret = new tile_1.GameSpritePixels(source, node.name, node.mapChar, pixels);
        }
        else {
            ret = new tile_1.GameSpriteSingleColor(this.toSource(node), node.name, node.mapChar, node.colors.map((n) => this.buildColor(n, colorPalette)));
        }
        this.cacheAdd(node.name, ret);
        if (node.mapChar) {
            this.cacheAdd(node.mapChar, ret);
        }
        return ret;
    }
    buildColor(node, colorPalette) {
        const source = this.toSource(node);
        const currentColorPalette = colorPalette || 'arnecolors';
        switch (node.type) {
            case ast.COLOR_TYPE.HEX6:
            case ast.COLOR_TYPE.HEX3:
                return new colors_2.HexColor(source, node.value);
            case ast.COLOR_TYPE.NAME:
                if (node.value.toUpperCase() === 'TRANSPARENT') {
                    return new colors_2.TransparentColor(source);
                }
                else {
                    // Look up the color
                    const hex = colors_1.lookupColorPalette(currentColorPalette, node.value);
                    if (hex) {
                        return new colors_2.HexColor(source, hex);
                    }
                    else {
                        return new colors_2.TransparentColor(source);
                    }
                }
            default:
                throw new Error(`Unsupported type ${node}`);
        }
    }
    buildLegendItem(node) {
        const source = this.toSource(node);
        switch (node.type) {
            case ast.TILE_TYPE.SIMPLE:
                if (!node.tile) {
                    throw new Error(`BUG!!!!!!`);
                }
                {
                    const ret = new tile_1.GameLegendTileSimple(source, node.name, this.cacheGet(node.tile));
                    this.cacheAdd(node.name, ret);
                    return ret;
                }
            case ast.TILE_TYPE.AND:
                if (!node.tiles) {
                    throw new Error(`BUG!!!!!!`);
                }
                {
                    const ret = new tile_1.GameLegendTileAnd(source, node.name, node.tiles.map((n) => this.cacheGet(n)));
                    this.cacheAdd(node.name, ret);
                    return ret;
                }
            case ast.TILE_TYPE.OR:
                if (!node.tiles) {
                    throw new Error(`BUG!!!!!!`);
                }
                {
                    const ret = new tile_1.GameLegendTileOr(source, node.name, node.tiles.map((n) => this.cacheGet(n)));
                    this.cacheAdd(node.name, ret);
                    return ret;
                }
            default:
                throw new Error(`Unsupported type ${node}`);
        }
    }
    buildCollisionLayer(node) {
        const source = this.toSource(node);
        return new collisionLayer_1.CollisionLayer(source, node.tiles.map((n) => this.cacheGet(n)));
    }
    buildSound(node) {
        const source = this.toSource(node);
        switch (node.type) {
            case 'SOUND_SFX':
                const ret = new sound_1.GameSound(source, node.soundCode);
                this.soundCacheAdd(node.soundEffect, ret);
                return ret;
            case 'SOUND_WHEN':
                return new sound_1.GameSoundSimpleEnum(source, node.when, node.soundCode);
            case 'SOUND_SPRITE_MOVE':
                return new sound_1.GameSoundMoveSimple(source, this.cacheGet(node.sprite), node.soundCode);
            case 'SOUND_SPRITE_DIRECTION':
                return new sound_1.GameSoundMoveDirection(source, this.cacheGet(node.sprite), node.spriteDirection, node.soundCode);
            case 'SOUND_SPRITE_EVENT':
                return new sound_1.GameSoundNormal(source, this.cacheGet(node.sprite), node.spriteEvent, node.soundCode);
            default:
                throw new Error(`Unsupported type ${node}`);
        }
    }
    buildCommand(node) {
        const source = this.toSource(node);
        switch (node.type) {
            case ast.COMMAND_TYPE.MESSAGE:
                return new command_1.MessageCommand(source, node.message);
            case ast.COMMAND_TYPE.SFX:
                if (!this.soundCacheHas(node.sound)) {
                    return null;
                }
                return new command_1.SoundCommand(source, this.soundCacheGet(node.sound));
            case ast.COMMAND_TYPE.CANCEL:
                return new command_1.CancelCommand(source);
            case ast.COMMAND_TYPE.AGAIN:
                return new command_1.AgainCommand(source);
            case ast.COMMAND_TYPE.WIN:
                return new command_1.WinCommand(source);
            case ast.COMMAND_TYPE.RESTART:
                return new command_1.RestartCommand(source);
            case ast.COMMAND_TYPE.CHECKPOINT:
                return new command_1.CheckpointCommand(source);
            default:
                throw new Error(`Unsupported type ${node}`);
        }
    }
    buildWinConditon(node) {
        const source = this.toSource(node);
        switch (node.type) {
            case ast.WIN_CONDITION_TYPE.ON:
                return new winCondition_1.WinConditionOn(source, node.qualifier, this.cacheGet(node.tile), this.cacheGet(node.onTile));
            case ast.WIN_CONDITION_TYPE.SIMPLE:
                return new winCondition_1.WinConditionSimple(source, node.qualifier, this.cacheGet(node.tile));
            default:
                throw new Error(`Unsupported type ${node}`);
        }
    }
    buildLevel(node) {
        const source = this.toSource(node);
        switch (node.type) {
            case ast.LEVEL_TYPE.MESSAGE:
                return new level_1.MessageLevel(source, node.message);
            case ast.LEVEL_TYPE.MAP:
                return new level_1.LevelMap(source, node.cells.map((row) => row.map((cell) => this.cacheGet(cell))));
            default:
                throw new Error(`Unsupported type ${node}`);
        }
    }
    buildSimpleRules(rules) {
        // Simplify the rules by de-duplicating them
        const ruleCache = new Map();
        const bracketCache = new Map();
        const neighborCache = new Map();
        const tileCache = new Map();
        const simpleRules = rules.map((n) => this.simplifyRule(n, ruleCache, bracketCache, neighborCache, tileCache));
        return simpleRules;
    }
    simplifyRule(node, ruleCache, bracketCache, neighborCache, tileCache) {
        const source = this.toSource(node);
        switch (node.type) {
            case ast.RULE_TYPE.LOOP:
                {
                    const subRules = node.rules.map((n) => this.simplifyRule(n, ruleCache, bracketCache, neighborCache, tileCache));
                    return new rule_1.SimpleRuleLoop(source, false /*isRandom*/, subRules);
                }
            case ast.RULE_TYPE.GROUP:
                // Extra checks to make TypeScript happy
                if (node.rules[0]) {
                    const firstRule = node.rules[0];
                    const isRandom = !!firstRule.isRandom;
                    const subRules = node.rules.map((n) => this.simplifyRule(n, ruleCache, bracketCache, neighborCache, tileCache));
                    // if (rules.length === 1) {
                    //     return rules[0]
                    // }
                    return new rule_1.SimpleRuleGroup(source, isRandom, subRules);
                }
                throw new Error(`BUG!!!!!!`);
            case ast.RULE_TYPE.SIMPLE:
                {
                    /**
                     * Expands this Rule into multiple `SimpleRule` objects.
                     *
                     * For example, `HORIZONTAL [ > player ] -> [ < crate ]` gets expanded to the following `SimpleRule`s:
                     *
                     * -  `LEFT  [ LEFT  player ] -> [ RIGHT crate ]`
                     * -  `RIGHT [ RIGHT player ] -> [ LEFT  crate ]`
                     *
                     * The `SimpleRule`s only have absolute directions
                     * rather than the relative ones specified in the original game code.
                     */
                    const isRandom = !!node.isRandom;
                    const conditions = node.conditions;
                    const actions = node.actions;
                    // 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`);
                    }
                    const simpleRules = this.rule_convertToMultiple(node).map((r) => this.rule_toSimple(r, 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 || (!node.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(source, isRandom, [simpleRules[0]]);
                    }
                    else {
                        for (const rule of simpleRules) {
                            rule.subscribeToCellChanges();
                        }
                        return new rule_1.SimpleRuleGroup(source, isRandom, simpleRules);
                    }
                }
            default:
                throw new Error(`Unsupported type ${node}`);
        }
    }
    rule_toSimple(node, ruleCache, bracketCache, neighborCache, tileCache) {
        const source = this.toSource(node);
        const directions = this.rule_getDirectionModifiers(node);
        const commands = removeNulls(node.commands.map((n) => this.buildCommand(n)));
        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 = node.conditions.map((x) => this.bracket_toSimple(x, directions[0], ruleCache, bracketCache, neighborCache, tileCache));
        const actionBrackets = node.actions.map((x) => this.bracket_toSimple(x, 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(source, conditionBrackets, actionBrackets, commands, node.isLate, node.isRigid, node.debugFlag));
    }
    rule_convertToMultiple(node) {
        let rulesToConvert = [];
        let convertedRules = [];
        for (const direction of this.rule_getDirectionModifiers(node)) {
            const expandedDirection = this.rule_clone(node, 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 = this.rule_getDirectionModifiers(rule)[0];
                if (this.rule_getDirectionModifiers(rule).length !== 1) {
                    throw new Error(`BUG: should have already expanded the rule to only contian one direction`);
                }
                for (const [nameToExpand, variations] of expandModifiers) {
                    if (this.rule_hasModifier(rule, nameToExpand)) {
                        for (const variation of variations) {
                            convertedRules.push(this.rule_clone(rule, 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 (this.rule_hasModifier(rule, nameToExpand)) {
                                for (const variation of variations) {
                                    convertedRules.push(this.rule_clone(rule, 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;
    }
    rule_clone(node, direction, nameToExpand, newName) {
        const conditions = node.conditions.map((bracket) => this.bracket_clone(bracket, direction, nameToExpand, newName));
        const actions = node.actions.map((bracket) => this.bracket_clone(bracket, 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}"`);
        }
        return Object.assign({}, node, { directions: [directionModifier], conditions, actions });
    }
    rule_getDirectionModifiers(node) {
        // Convert HORIZONTAL and VERTICAL to 2:
        if (node.directions.indexOf(ast.RULE_MODIFIER.HORIZONTAL) >= 0) {
            return [util_1.RULE_DIRECTION.LEFT, util_1.RULE_DIRECTION.RIGHT];
        }
        if (node.directions.indexOf(ast.RULE_MODIFIER.VERTICAL) >= 0) {
            return [util_1.RULE_DIRECTION.UP, util_1.RULE_DIRECTION.DOWN];
        }
        const directions = node.directions.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;
        }
    }
    rule_hasModifier(node, modifier) {
        for (const bracket of node.conditions) {
            for (const neighbor of this.rule_getAllBracketNeighbors(bracket)) {
                for (const t of neighbor.tileWithModifiers) {
                    if (t.direction === modifier) { // HACK: cast to string
                        return true;
                    }
                }
            }
        }
        return false;
    }
    rule_getAllBracketNeighbors(node) {
        switch (node.type) {
            case ast.BRACKET_TYPE.SIMPLE:
                return node.neighbors;
            case ast.BRACKET_TYPE.ELLIPSIS:
                return [...node.beforeNeighbors, ...node.afterNeighbors];
            default:
                throw new Error(`Unsupported type ${node}`);
        }
    }
    bracket_clone(node, direction, nameToExpand, newName) {
        switch (node.type) {
            case ast.BRACKET_TYPE.SIMPLE:
                const neighbors = node.neighbors.map((n) => this.neighbor_clone(n, direction, nameToExpand, newName));
                return Object.assign({}, node, { neighbors });
            case ast.BRACKET_TYPE.ELLIPSIS:
                const beforeNeighbors = node.beforeNeighbors.map((n) => this.neighbor_clone(n, direction, nameToExpand, newName));
                const afterNeighbors = node.afterNeighbors.map((n) => this.neighbor_clone(n, direction, nameToExpand, newName));
                return Object.assign({}, node, { beforeNeighbors, afterNeighbors });
            default:
                throw new Error(`Unsupported type ${node}`);
        }
    }
    neighbor_clone(node, direction, nameToExpand, newName) {
        return Object.assign({}, node, { tileWithModifiers: node.tileWithModifiers.map((t) => this.tile_clone(t, direction, nameToExpand, newName)) });
    }
    tile_clone(node, direction, nameToExpand, newName) {
        switch (node.direction) {
            case util_1.RULE_DIRECTION_RELATIVE.RELATIVE_UP:
            case util_1.RULE_DIRECTION_RELATIVE.RELATIVE_DOWN:
            case util_1.RULE_DIRECTION_RELATIVE.RELATIVE_LEFT:
            case util_1.RULE_DIRECTION_RELATIVE.RELATIVE_RIGHT:
                const modifier = relativeDirectionToAbsolute(direction, node.direction);
                return Object.assign({}, node, { direction: modifier });
            case nameToExpand:
                return Object.assign({}, node, { direction: newName });
            case util_1.RULE_DIRECTION.UP:
            case util_1.RULE_DIRECTION.DOWN:
            case util_1.RULE_DIRECTION.LEFT:
            case util_1.RULE_DIRECTION.RIGHT:
            case util_1.RULE_DIRECTION.ACTION:
            case util_1.RULE_DIRECTION.STATIONARY:
            case util_1.RULE_DIRECTION.RANDOMDIR:
            case null:
            case undefined:
                return node;
            default:
                return node; // throw new Error(`BUG: Unsupported tile direction ${JSON.stringify(node)}`)
        }
    }
    bracket_toSimple(node, direction, ruleCache, bracketCache, neighborCache, tileCache) {
        const source = this.toSource(node);
        switch (node.type) {
            case ast.BRACKET_TYPE.SIMPLE:
                const neighbors = node.neighbors.map((x) => this.neighbor_toSimple(x, neighborCache, tileCache));
                return cacheSetAndGet(bracketCache, new rule_1.SimpleBracket(source, direction, neighbors, node.debugFlag));
            case ast.BRACKET_TYPE.ELLIPSIS:
                const beforeEllipsis = node.beforeNeighbors.map((x) => this.neighbor_toSimple(x, neighborCache, tileCache));
                const afterEllipsis = node.afterNeighbors.map((x) => this.neighbor_toSimple(x, neighborCache, tileCache));
                return cacheSetAndGet(bracketCache, new rule_1.SimpleEllipsisBracket(source, direction, beforeEllipsis, afterEllipsis, node.debugFlag));
            default:
                throw new Error(`Unsupported type ${node}`);
        }
    }
    neighbor_toSimple(node, neighborCache, tileCache) {
        const source = this.toSource(node);
        // 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 node.tileWithModifiers) {
            if (!tilesMap.has(t.tile)) {
                tilesMap.set(t.tile, t);
            }
        }
        const tileWithModifiers = [...tilesMap.values()];
        const simpleTilesWithModifier = new Set(removeNulls(tileWithModifiers.map((x) => this.tile_toSimple(x, tileCache))));
        return cacheSetAndGet(neighborCache, new rule_1.SimpleNeighbor(source, simpleTilesWithModifier, node.debugFlag));
    }
    tile_toSimple(node, tileCache) {
        const source = this.toSource(node);
        // Some games mistakenly use SFX# in a bracket when the SFX should be in the commands list after the brackets
        if (!this.cacheHas(node.tile)) {
            return null;
        }
        const tile = this.cacheGet(node.tile);
        let direction;
        switch (node.direction) {
            case util_1.RULE_DIRECTION_RELATIVE.RELATIVE_UP:
            case util_1.RULE_DIRECTION_RELATIVE.RELATIVE_DOWN:
            case util_1.RULE_DIRECTION_RELATIVE.RELATIVE_LEFT:
            case util_1.RULE_DIRECTION_RELATIVE.RELATIVE_RIGHT:
                throw new Error(`BUG: Relative directions should have been resolved by now`);
            default:
                direction = node.direction || null; // could be undefined (causes problems when evaluating)
        }
        return cacheSetAndGet(tileCache, new rule_1.SimpleTileWithModifier(source, node.isNegated, node.isRandom, direction, tile, node.debugFlag));
    }
    toSource(node) {
        return {
            code: this.code,
            sourceOffset: node._sourceOffset
        };
    }
    cacheHas(name) {
        return this.tileCache.has(name.toLowerCase());
    }
    cacheAdd(name, value) {
        if (this.tileCache.has(name.toLowerCase())) {
            throw new Error(`BUG??? duplicate definition of ${name}`);
        }
        this.tileCache.set(name.toLowerCase(), value);
    }
    cacheGet(name) {
        const value = this.tileCache.get(name.toLowerCase());
        if (value) {
            return value;
        }
        else {
            throw new Error(`BUG: Could not find tile named ${name}`);
        }
    }
    soundCacheAdd(name, value) {
        if (this.soundCache.has(name.toLowerCase())) {
            throw new Error(`BUG??? duplicate definition of ${name}`);
        }
        this.soundCache.set(name.toLowerCase(), value);
    }
    soundCacheGet(name) {
        const value = this.soundCache.get(name.toLowerCase());
        if (value) {
            return value;
        }
        else {
            throw new Error(`BUG: Could not find sound named ${name}`);
        }
    }
    soundCacheHas(name) {
        return this.soundCache.has(name.toLowerCase());
    }
}
exports.AstBuilder = AstBuilder;
//# sourceMappingURL=astBuilder.js.map