"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const nearley = require("nearley");
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 sound_1 = require("../models/sound");
const tile_1 = require("../models/tile");
const winCondition_1 = require("../models/winCondition");
const astRule_1 = require("./astRule");
const ast = require("./astTypes");
const compiledGrammar = require("./grammar");
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;
}
class ValidationMessage {
    constructor(source, level, message) {
        this.source = source;
        this.level = level;
        this.message = message;
    }
    toKey() {
        return `[${this.source.toString()}] [${this.level}] [${this.message}]`;
    }
}
exports.ValidationMessage = ValidationMessage;
var ValidationLevel;
(function (ValidationLevel) {
    ValidationLevel[ValidationLevel["ERROR"] = 0] = "ERROR";
    ValidationLevel[ValidationLevel["WARNING"] = 1] = "WARNING";
    ValidationLevel[ValidationLevel["INFO"] = 2] = "INFO";
})(ValidationLevel = exports.ValidationLevel || (exports.ValidationLevel = {}));
class AstBuilder {
    constructor(code) {
        this.code = code;
        this.tileCache = new Map();
        this.soundCache = new Map();
        this.validationMessages = new Map();
    }
    build(root) {
        this.validationMessages.clear(); // clear because we are parsing
        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));
        // 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 rules = root.rules.map((n) => this.buildRuleCollection(n));
        const winConditions = root.winConditions.map((n) => this.buildWinConditon(n));
        const levels = root.levels.map((n) => this.buildLevel(n));
        // assign an index to each GameSprite
        sprites.forEach((sprite, index) => {
            sprite.allSpritesBitSetIndex = index;
        });
        // 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((rule) => rule.simplify(ruleCache, bracketCache, neighborCache, tileCache));
        const gameData = new game_1.GameData(source, root.title, metadata, sprites, legendItems, sounds, collisionLayers, simpleRules, winConditions, levels);
        const validationMessages = this.getValidationMessages();
        return { gameData, validationMessages };
    }
    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 {
                        this.addValidationMessage(source, ValidationLevel.WARNING, `ERROR: Invalid color name ${node.value}`);
                        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);
        const addValidation = (msg) => {
            this.addValidationMessage(msg.source, msg.level, msg.message);
        };
        return new collisionLayer_1.CollisionLayer(source, node.tiles.map((n) => this.cacheGet(n)), addValidation);
    }
    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}`);
        }
    }
    buildRuleCollection(node) {
        const source = this.toSource(node);
        switch (node.type) {
            case ast.RULE_TYPE.LOOP:
                return new astRule_1.ASTRuleLoop(source, node.rules.map((n) => this.buildRuleCollection(n)), node.debugFlag);
            case ast.RULE_TYPE.GROUP:
                // Extra checks to make TypeScript happy
                if (node.rules[0]) {
                    const firstRule = node.rules[0];
                    const isRandom = firstRule.isRandom;
                    return new astRule_1.ASTRuleGroup(source, !!isRandom, node.rules.map((n) => this.buildRuleCollection(n)), node.debugFlag);
                }
                throw new Error(`BUG!!!!!!`);
            case ast.RULE_TYPE.SIMPLE:
                const commands = [...node.commands];
                return new astRule_1.ASTRule(source, node.directions, !!node.isRandom, node.isLate, node.isRigid, node.conditions.map((n) => this.buildBracket(n)), node.actions.map((n) => this.buildBracket(n)), removeNulls(commands.map((n) => this.buildCommand(n))), node.debugFlag);
            default:
                throw new Error(`Unsupported type ${node}`);
        }
    }
    buildBracket(node) {
        const source = this.toSource(node);
        switch (node.type) {
            case ast.BRACKET_TYPE.SIMPLE:
                return new astRule_1.ASTRuleBracket(source, node.neighbors.map((n) => this.buildNeighbor(n)), null, node.debugFlag);
            case ast.BRACKET_TYPE.ELLIPSIS:
                return new astRule_1.ASTRuleBracketEllipsis(source, node.beforeNeighbors.map((n) => this.buildNeighbor(n)), node.afterNeighbors.map((n) => this.buildNeighbor(n)), node.debugFlag);
            default:
                throw new Error(`Unsupported type ${node}`);
        }
    }
    buildNeighbor(node) {
        const source = this.toSource(node);
        return new astRule_1.ASTRuleBracketNeighbor(source, removeNulls(node.tileWithModifiers.map((n) => this.buildTileWithModifier(n))), node.debugFlag);
    }
    buildTileWithModifier(node) {
        const source = this.toSource(node);
        if (!this.cacheHas(node.tile)) {
            this.addValidationMessage(source, ValidationLevel.ERROR, `Could not find tile named ${node.tile}`);
            return null;
        }
        return new astRule_1.ASTTileWithModifier(source, node.direction, node.isNegated, node.isRandom, this.cacheGet(node.tile), node.debugFlag);
    }
    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)) {
                    this.addValidationMessage(source, ValidationLevel.ERROR, `Could not find sound named ${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}`);
        }
    }
    toSource(node) {
        return {
            code: this.code,
            sourceOffset: node._sourceOffset
        };
    }
    addValidationMessage(source, level, message) {
        const msg = new ValidationMessage(source, level, message);
        if (!this.validationMessages.has(msg.toKey())) {
            this.validationMessages.set(msg.toKey(), msg);
        }
    }
    getValidationMessages() {
        return [...this.validationMessages.values()];
    }
    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}`);
        }
    }
    cacheHas(name) {
        return this.tileCache.has(name.toLowerCase());
    }
    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());
    }
}
class Parser {
    constructor() {
        this.grammar = nearley.Grammar.fromCompiled(compiledGrammar);
    }
    parseToAST(code) {
        const parser = new nearley.Parser(this.grammar);
        parser.feed(code);
        parser.feed('\n');
        parser.finish();
        const results = parser.results;
        if (results.length === 1) {
            return results[0];
        }
        else if (results.length === 0) {
            throw new Error(`ERROR: Could not parse`);
        }
        else {
            throw new Error(`AMBIGUOUS: has ${results.length} results`);
        }
    }
    parse(code) {
        const node = this.parseToAST(code);
        const builder = new AstBuilder(code);
        const { gameData, validationMessages } = builder.build(node);
        return { data: gameData, validationMessages };
    }
}
exports.default = new Parser();
//# sourceMappingURL=parser.js.map