"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 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({ type: 'GAME', 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 'COLOR_HEX3':
                    case 'COLOR_HEX6':
                    case 'COLOR_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));
        const gameData = new game_1.GameData(source, root.title, metadata, sprites, legendItems, sounds, collisionLayers, rules, winConditions, levels);
        const validationMessages = this.getValidationMessages();
        return { gameData, validationMessages };
    }
    buildSprite(node, colorPalette) {
        let ret;
        if (node.pixels) {
            ret = new tile_1.GameSpritePixels(this.toSource(node), node.name, node.mapChar, node.colors.map((n) => this.buildColor(n, colorPalette)), node.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 'COLOR_HEX6':
            case 'COLOR_HEX3':
                return new colors_2.HexColor(source, node.value);
            case 'COLOR_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.type}`);
        }
    }
    buildLegendItem(node) {
        const source = this.toSource(node);
        switch (node.type) {
            case 'LEGEND_ITEM_SIMPLE':
                if (!node.value) {
                    throw new Error(`BUG!!!!!!`);
                }
                {
                    const ret = new tile_1.GameLegendTileSimple(source, node.name, this.cacheGet(node.value));
                    this.cacheAdd(node.name, ret);
                    return ret;
                }
            case 'LEGEND_ITEM_AND':
                if (!node.values) {
                    throw new Error(`BUG!!!!!!`);
                }
                {
                    const ret = new tile_1.GameLegendTileAnd(source, node.name, node.values.map((n) => this.cacheGet(n)));
                    this.cacheAdd(node.name, ret);
                    return ret;
                }
            case 'LEGEND_ITEM_OR':
                if (!node.values) {
                    throw new Error(`BUG!!!!!!`);
                }
                {
                    const ret = new tile_1.GameLegendTileOr(source, node.name, node.values.map((n) => this.cacheGet(n)));
                    this.cacheAdd(node.name, ret);
                    return ret;
                }
            default:
                throw new Error(`Unsupported type ${node.type}`);
        }
    }
    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 n = node;
                    const ret = new sound_1.GameSound(source, node.soundCode);
                    this.soundCacheAdd(n.sfx, ret);
                    return ret;
                }
            case 'SOUND_WHEN':
                {
                    const n = node;
                    return new sound_1.GameSoundSimpleEnum(source, n.when, node.soundCode);
                }
            case 'SOUND_SPRITE_MOVE':
                {
                    const n = node;
                    return new sound_1.GameSoundMoveSimple(source, this.cacheGet(n.sprite), node.soundCode);
                }
            case 'SOUND_SPRITE_DIRECTION':
                {
                    const n = node;
                    return new sound_1.GameSoundMoveDirection(source, this.cacheGet(n.sprite), n.spriteDirection, node.soundCode);
                }
            case 'SOUND_SPRITE_EVENT':
                {
                    const n = node;
                    return new sound_1.GameSoundNormal(source, this.cacheGet(n.sprite), n.spriteEvent, node.soundCode);
                }
            default:
                throw new Error(`Unsupported type ${node.type}`);
        }
    }
    buildRuleCollection(node) {
        const source = this.toSource(node);
        switch (node.type) {
            case 'RULE_LOOP':
                if (!node.rules) {
                    throw new Error(`BUG!!!!!!`);
                }
                return new astRule_1.ASTRuleLoop(source, node.rules.map((n) => this.buildRuleCollection(n)), node.debugFlag);
            case 'RULE_GROUP':
                if (!node.rules) {
                    throw new Error(`BUG!!!!!!`);
                }
                // Extra checks to make TypeScript happy
                if (node.rules[0]) {
                    const firstRule = node.rules[0];
                    const isRandom = firstRule.modifiers.indexOf(astRule_1.AST_RULE_MODIFIER.RANDOM) >= 0;
                    return new astRule_1.ASTRuleGroup(source, isRandom, node.rules.map((n) => this.buildRuleCollection(n)), node.debugFlag);
                }
                throw new Error(`BUG!!!!!!`);
            case 'RULE':
                const node2 = node;
                const commands = [...node2.commands];
                // TODO: Maybe do this step in the parser. No need for message to be a separate field
                if (node2.message) {
                    commands.push(node2.message);
                }
                return new astRule_1.ASTRule(source, node2.modifiers, node2.conditions.map((n) => this.buildBracket(n)), node2.actions.map((n) => this.buildBracket(n)), removeNulls(commands.map((n) => this.buildCommand(n))), node.debugFlag);
            default:
                throw new Error(`Unsupported type ${node.type}`);
        }
    }
    buildBracket(node) {
        const source = this.toSource(node);
        switch (node.type) {
            case 'BRACKET':
                {
                    const node2 = node;
                    return new astRule_1.ASTRuleBracket(source, node2.neighbors.map((n) => this.buildNeighbor(n)), null, node.debugFlag);
                }
            case 'ELLIPSIS_BRACKET':
                {
                    const node2 = node;
                    return new astRule_1.ASTRuleBracketEllipsis(source, node2.beforeNeighbors.map((n) => this.buildNeighbor(n)), node2.afterNeighbors.map((n) => this.buildNeighbor(n)), node.debugFlag);
                }
            default:
                throw new Error(`Unsupported type ${node.type}`);
        }
    }
    buildNeighbor(node) {
        const source = this.toSource(node);
        return new astRule_1.ASTRuleBracketNeighbor(source, removeNulls(node.tilesWithModifier.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.modifier, this.cacheGet(node.tile), node.debugFlag);
    }
    buildCommand(node) {
        const source = this.toSource(node);
        switch (node.type) {
            case 'COMMAND_MESSAGE':
                {
                    const n = node;
                    return new command_1.MessageCommand(source, n.message);
                }
            case 'COMMAND_SFX':
                {
                    const n = node;
                    if (!this.soundCacheHas(n.sfx)) {
                        this.addValidationMessage(source, ValidationLevel.ERROR, `Could not find sound named ${n.sfx}`);
                        return null;
                    }
                    return new command_1.SoundCommand(source, this.soundCacheGet(n.sfx));
                }
            case 'COMMAND_CANCEL':
                return new command_1.CancelCommand(source);
            case 'COMMAND_AGAIN':
                return new command_1.AgainCommand(source);
            case 'COMMAND_WIN':
                return new command_1.WinCommand(source);
            case 'COMMAND_RESTART':
                return new command_1.RestartCommand(source);
            case 'COMMAND_CHECKPOINT':
                return new command_1.CheckpointCommand(source);
            default:
                throw new Error(`Unsupported type ${node.type}`);
        }
    }
    buildWinConditon(node) {
        const source = this.toSource(node);
        switch (node.type) {
            case 'WINCONDITION_ON':
                {
                    const n = node;
                    return new winCondition_1.WinConditionOn(source, n.qualifier, this.cacheGet(n.sprite), this.cacheGet(n.onSprite));
                }
            case 'WINCONDITION_SIMPLE':
                {
                    const n = node;
                    return new winCondition_1.WinConditionSimple(source, n.qualifier, this.cacheGet(n.sprite));
                }
            default:
                throw new Error(`Unsupported type ${node.type}`);
        }
    }
    buildLevel(node) {
        const source = this.toSource(node);
        switch (node.type) {
            case 'LEVEL_MESSAGE':
                {
                    const n = node;
                    return new level_1.MessageLevel(source, n.message);
                }
            case 'LEVEL_MAP':
                {
                    const n = node;
                    return new level_1.LevelMap(source, n.rowData.map((row) => row.map((cell) => this.cacheGet(cell))));
                }
            default:
                throw new Error(`Unsupported type ${node.type}`);
        }
    }
    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