"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const collisionLayer_1 = require("../models/collisionLayer");
const colors_1 = 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 ast = require("./astTypes");
// enum RULE_DIRECTION {
//     UP = 'UP',
//     DOWN = 'DOWN',
//     LEFT = 'LEFT',
//     RIGHT = 'RIGHT',
//     // These only apply to Tiles
//     ACTION = 'ACTION',
//     STATIONARY = 'STATIONARY',
//     RANDOMDIR = 'RANDOMDIR'
// }
var TILE_TYPE;
(function (TILE_TYPE) {
    TILE_TYPE["OR"] = "OR";
    TILE_TYPE["AND"] = "AND";
    TILE_TYPE["SIMPLE"] = "SIMPLE";
    TILE_TYPE["SPRITE"] = "SPRITE";
})(TILE_TYPE || (TILE_TYPE = {}));
function toRULE_DIRECTION(dir) {
    return dir;
}
class MapWithId {
    constructor(prefix) {
        this.counter = 0;
        this.prefix = prefix;
        this.idMap = new Map();
        this.jsonMap = new Map();
    }
    set(key, value, id) {
        if (!this.idMap.has(key)) {
            this.idMap.set(key, id || this.freshId());
        }
        this.jsonMap.set(key, value);
        return this.getId(key);
    }
    get(key) {
        const value = this.jsonMap.get(key);
        if (!value) {
            debugger;
            throw new Error(`BUG: Element has not been added to the set`); // tslint:disable-line:no-debugger
        }
        return value;
    }
    getId(key) {
        const value = this.idMap.get(key);
        if (!value) {
            debugger;
            throw new Error(`BUG: Element has not been added to the set`); // tslint:disable-line:no-debugger
        }
        return value;
    }
    toJson() {
        const ret = {};
        for (const [obj, id] of this.idMap) {
            const json = this.jsonMap.get(obj);
            if (!json) {
                debugger;
                throw new Error(`BUG: Could not find matching json representation for "${id}"`); // tslint:disable-line:no-debugger
            }
            ret[id] = json;
        }
        return ret;
    }
    freshId() {
        return `${this.prefix}-${this.counter++}`;
    }
}
class DefiniteMap extends Map {
    get(key) {
        const v = super.get(key);
        if (!v) {
            throw new Error(`ERROR: JSON is missing key "${key}". Should have already been added`);
        }
        return v;
    }
}
class Serializer {
    static fromJson(source, code) {
        // First, build up all of the lookup maps
        const colorMap = new DefiniteMap();
        const spritesMap = new DefiniteMap();
        const soundMap = new DefiniteMap();
        const collisionLayerMap = new DefiniteMap();
        const bracketMap = new DefiniteMap();
        const neighborsMap = new DefiniteMap();
        const tileWithModifierMap = new DefiniteMap();
        const tileMap = new DefiniteMap();
        const ruleMap = new DefiniteMap();
        const commandMap = new DefiniteMap();
        for (const [key, val] of Object.entries(source.colors)) {
            colorMap.set(key, new colors_1.HexColor({ code, sourceOffset: 0 }, val));
        }
        for (const [key, val] of Object.entries(source.sounds)) {
            const { _sourceOffset: sourceOffset, soundCode } = val;
            soundMap.set(key, new sound_1.GameSound({ code, sourceOffset }, soundCode));
        }
        for (const [key, val] of Object.entries(source.commands)) {
            const { _sourceOffset: sourceOffset } = val;
            switch (val.type) {
                case ast.COMMAND_TYPE.MESSAGE:
                    commandMap.set(key, new command_1.MessageCommand({ code, sourceOffset }, val.message));
                    break;
                case ast.COMMAND_TYPE.SFX:
                    commandMap.set(key, new command_1.SoundCommand({ code, sourceOffset }, soundMap.get(val.sound)));
                    break;
                case ast.COMMAND_TYPE.RESTART:
                    commandMap.set(key, new command_1.RestartCommand({ code, sourceOffset }));
                    break;
                case ast.COMMAND_TYPE.AGAIN:
                    commandMap.set(key, new command_1.AgainCommand({ code, sourceOffset }));
                    break;
                case ast.COMMAND_TYPE.CANCEL:
                    commandMap.set(key, new command_1.CancelCommand({ code, sourceOffset }));
                    break;
                case ast.COMMAND_TYPE.CHECKPOINT:
                    commandMap.set(key, new command_1.CheckpointCommand({ code, sourceOffset }));
                    break;
                case ast.COMMAND_TYPE.WIN:
                    commandMap.set(key, new command_1.WinCommand({ code, sourceOffset }));
                    break;
                default:
                    throw new Error(`ERROR: Unsupported command type`);
            }
        }
        const layers = new DefiniteMap();
        for (const [key] of Object.entries(source.collisionLayers)) {
            layers.set(key, []);
        }
        const transparent = new colors_1.TransparentColor({ code, sourceOffset: 0 });
        let spriteIndex = 0;
        for (const [key, val] of Object.entries(source.sprites)) {
            const { _sourceOffset: sourceOffset, name, pixels } = val;
            const sprite = new tile_1.GameSpritePixels({ code, sourceOffset }, name, null, pixels.map((row) => row.map((color) => {
                if (color) {
                    return colorMap.get(color) || transparent;
                }
                else {
                    return transparent;
                }
            })));
            // assign an index to each GameSprite
            sprite.allSpritesBitSetIndex = spriteIndex;
            spriteIndex++;
            spritesMap.set(key, sprite);
            layers.get(val.collisionLayer).push(sprite);
        }
        for (const [key, val] of Object.entries(source.tiles)) {
            const { _sourceOffset: sourceOffset } = val;
            let tile;
            switch (val.type) {
                case 'OR':
                    tile = new tile_1.GameLegendTileOr({ code, sourceOffset }, val.name, val.sprites.map((item) => spritesMap.get(item)));
                    break;
                case 'AND':
                    tile = new tile_1.GameLegendTileAnd({ code, sourceOffset }, val.name, val.sprites.map((item) => spritesMap.get(item)));
                    break;
                case 'SIMPLE':
                    tile = new tile_1.GameLegendTileSimple({ code, sourceOffset }, val.name, spritesMap.get(val.sprite));
                    break;
                case 'SPRITE':
                    tile = new tile_1.GameLegendTileSimple({ code, sourceOffset }, val.name, spritesMap.get(val.sprite));
                    break;
                default:
                    throw new Error(`ERROR: Unsupported tile type`);
            }
            tileMap.set(key, tile);
            // layers.get(val.collisionLayer).push(tile)
        }
        for (const [key, val] of Object.entries(source.collisionLayers)) {
            const { _sourceOffset: sourceOffset } = val;
            collisionLayerMap.set(key, new collisionLayer_1.CollisionLayer({ code, sourceOffset }, layers.get(key), () => null));
        }
        for (const [key, val] of Object.entries(source.tilesWithModifiers)) {
            const { _sourceOffset: sourceOffset } = val;
            tileWithModifierMap.set(key, new rule_1.SimpleTileWithModifier({ code, sourceOffset }, val.isNegated, val.isRandom, val.direction, tileMap.get(val.tile), val.debugFlag));
        }
        for (const [key, val] of Object.entries(source.neighbors)) {
            const { _sourceOffset: sourceOffset } = val;
            neighborsMap.set(key, new rule_1.SimpleNeighbor({ code, sourceOffset }, new Set(val.tileWithModifiers.map((item) => tileWithModifierMap.get(item))), val.debugFlag));
        }
        for (const [key, val] of Object.entries(source.brackets)) {
            const { _sourceOffset: sourceOffset } = val;
            switch (val.type) {
                case ast.BRACKET_TYPE.SIMPLE:
                    bracketMap.set(key, new rule_1.SimpleBracket({ code, sourceOffset }, val.direction, val.neighbors.map((item) => neighborsMap.get(item)), val.debugFlag));
                    break;
                case ast.BRACKET_TYPE.ELLIPSIS:
                    bracketMap.set(key, new rule_1.SimpleEllipsisBracket({ code, sourceOffset }, val.direction, val.beforeNeighbors.map((item) => neighborsMap.get(item)), val.afterNeighbors.map((item) => neighborsMap.get(item)), val.debugFlag));
                    break;
                default:
                    throw new Error(`ERROR: Unsupported bracket type`);
            }
        }
        for (const [key, val] of Object.entries(source.ruleDefinitions)) {
            const { _sourceOffset: sourceOffset } = val;
            switch (val.type) {
                case ast.RULE_TYPE.SIMPLE:
                    ruleMap.set(key, new rule_1.SimpleRule({ code, sourceOffset }, val.conditions.map((item) => bracketMap.get(item)), val.actions.map((item) => bracketMap.get(item)), val.commands.map((item) => commandMap.get(item)), val.isLate, val.isRigid, val.debugFlag));
                    break;
                case ast.RULE_TYPE.GROUP:
                    ruleMap.set(key, new rule_1.SimpleRuleGroup({ code, sourceOffset }, val.isRandom, val.rules.map((item) => ruleMap.get(item))));
                    break;
                case ast.RULE_TYPE.LOOP:
                    ruleMap.set(key, new rule_1.SimpleRuleLoop({ code, sourceOffset }, false /*TODO: Figure out if loops need isRandom*/, val.rules.map((item) => ruleMap.get(item))));
                    break;
                default:
                    throw new Error(`ERROR: Unsupported rule type`);
            }
        }
        const levels = source.levels.map((item) => {
            const { _sourceOffset: sourceOffset } = item;
            switch (item.type) {
                case ast.LEVEL_TYPE.MESSAGE:
                    return new level_1.MessageLevel({ code, sourceOffset }, item.message);
                case ast.LEVEL_TYPE.MAP:
                    return new level_1.LevelMap({ code, sourceOffset }, item.cells.map((row) => row.map((tile) => tileMap.get(tile))));
                default:
                    throw new Error(`ERROR: Invalid level type`);
            }
        });
        const winConditions = source.winConditions.map((item) => {
            const { _sourceOffset: sourceOffset } = item;
            switch (item.type) {
                case ast.WIN_CONDITION_TYPE.SIMPLE:
                    return new winCondition_1.WinConditionSimple({ code, sourceOffset }, item.qualifier, tileMap.get(item.tile));
                case ast.WIN_CONDITION_TYPE.ON:
                    return new winCondition_1.WinConditionOn({ code, sourceOffset }, item.qualifier, tileMap.get(item.tile), tileMap.get(item.onTile));
                default:
                    throw new Error(`ERROR: Unsupported Win Condition type`);
            }
        });
        const metadata = new metadata_1.GameMetadata();
        for (const [key, val] of Object.entries(source.metadata)) {
            if (val) {
                switch (key) {
                    case 'backgroundColor':
                    case 'textColor':
                        metadata._setValue(key, colorMap.get(val));
                        break;
                    case 'zoomScreen':
                    case 'flickScreen':
                        const { width, height } = val;
                        metadata._setValue(key, new metadata_1.Dimension(width, height));
                        break;
                    default:
                        metadata._setValue(key, val);
                }
            }
        }
        return new game_1.GameData({ code, sourceOffset: 0 }, source.title, metadata, [...spritesMap.values()], [...tileMap.values()], [...soundMap.values()], [...collisionLayerMap.values()], source.rules.map((item) => ruleMap.get(item)), winConditions, levels);
    }
    constructor(game) {
        this.game = game;
        this.colorsMap = new Map();
        this.spritesMap = new MapWithId('sprite');
        this.soundMap = new MapWithId('sound');
        this.collisionLayerMap = new MapWithId('collision');
        this.conditionsMap = new MapWithId('bracket');
        this.neighborsMap = new MapWithId('neighbor');
        this.tileWithModifierMap = new MapWithId('twm');
        this.tileMap = new MapWithId('tile');
        this.ruleMap = new MapWithId('rule');
        this.commandMap = new MapWithId('command');
        if (this.game.metadata.backgroundColor) {
            const hex = this.game.metadata.backgroundColor.toHex();
            this.colorsMap.set(hex, hex);
        }
        if (this.game.metadata.textColor) {
            const hex = this.game.metadata.textColor.toHex();
            this.colorsMap.set(hex, hex);
        }
        // Load up the colors and sprites
        this.game.collisionLayers.forEach((item) => this.buildCollisionLayer(item));
        this.game.sounds.forEach((item) => {
            this.soundMap.set(item, this.soundToJson(item));
        });
        this.game.objects.forEach((sprite) => {
            this.buildSprite(sprite);
        });
        this.orderedRules = this.game.rules.map((item) => this.recBuildRule(item));
        this.game.legends.forEach((tile) => {
            this.buildTile(tile);
        });
        this.levels = this.game.levels.map((item) => this.buildLevel(item));
        this.winConditions = this.game.winConditions.map((item) => {
            if (item instanceof winCondition_1.WinConditionOn) {
                const ret = {
                    _sourceOffset: item.__source.sourceOffset,
                    type: ast.WIN_CONDITION_TYPE.ON,
                    qualifier: item.qualifier,
                    tile: this.buildTile(item.tile),
                    onTile: this.buildTile(item.onTile)
                };
                return ret;
            }
            else {
                const ret = {
                    _sourceOffset: item.__source.sourceOffset,
                    type: ast.WIN_CONDITION_TYPE.SIMPLE,
                    qualifier: item.qualifier,
                    tile: this.buildTile(item.tile)
                };
                return ret;
            }
        });
    }
    buildCollisionLayer(item) {
        return this.collisionLayerMap.set(item, { _sourceOffset: item.__source.sourceOffset });
    }
    metadataToJson() {
        return {
            author: this.game.metadata.author,
            homepage: this.game.metadata.homepage,
            youtube: this.game.metadata.youtube,
            zoomscreen: this.game.metadata.zoomscreen,
            flickscreen: this.game.metadata.flickscreen,
            colorPalette: this.game.metadata.colorPalette,
            backgroundColor: this.game.metadata.backgroundColor ? this.buildColor(this.game.metadata.backgroundColor) : undefined,
            textColor: this.game.metadata.textColor ? this.buildColor(this.game.metadata.textColor) : undefined,
            realtimeInterval: this.game.metadata.realtimeInterval,
            keyRepeatInterval: this.game.metadata.keyRepeatInterval,
            againInterval: this.game.metadata.againInterval,
            noAction: this.game.metadata.noAction,
            noUndo: this.game.metadata.noUndo,
            runRulesOnLevelStart: this.game.metadata.runRulesOnLevelStart,
            noRepeatAction: this.game.metadata.noRepeatAction,
            throttleMovement: this.game.metadata.throttleMovement,
            noRestart: this.game.metadata.noRestart,
            requirePlayerMovement: this.game.metadata.requirePlayerMovement,
            verboseLogging: this.game.metadata.verboseLogging
        };
    }
    toJson() {
        const colors = {};
        for (const [key, value] of this.colorsMap) {
            colors[key] = value;
        }
        return {
            version: 1,
            title: this.game.title,
            metadata: this.metadataToJson(),
            colors,
            sounds: this.soundMap.toJson(),
            collisionLayers: this.collisionLayerMap.toJson(),
            commands: this.commandMap.toJson(),
            sprites: this.spritesMap.toJson(),
            tiles: this.tileMap.toJson(),
            tilesWithModifiers: this.tileWithModifierMap.toJson(),
            neighbors: this.neighborsMap.toJson(),
            brackets: this.conditionsMap.toJson(),
            ruleDefinitions: this.ruleMap.toJson(),
            winConditions: this.winConditions,
            rules: this.orderedRules,
            levels: this.levels
        };
    }
    buildLevel(level) {
        if (level instanceof level_1.LevelMap) {
            return {
                type: ast.LEVEL_TYPE.MAP,
                cells: level.getRows().map((row) => row.map((cell) => this.buildTile(cell))),
                _sourceOffset: level.__source.sourceOffset
            };
        }
        else if (level instanceof level_1.MessageLevel) {
            return {
                type: ast.LEVEL_TYPE.MESSAGE,
                message: level.getMessage(),
                _sourceOffset: level.__source.sourceOffset
            };
        }
        else {
            debugger;
            throw new Error(`BUG: Unsupported level subtype`); // tslint:disable-line:no-debugger
        }
    }
    recBuildRule(rule) {
        if (rule instanceof rule_1.SimpleRule) {
            return this.ruleMap.set(rule, {
                type: ast.RULE_TYPE.SIMPLE,
                directions: [],
                conditions: rule.conditionBrackets.map((item) => this.buildConditionBracket(item)),
                actions: rule.actionBrackets.map((item) => this.buildConditionBracket(item)),
                commands: rule.commands.map((item) => this.buildCommand(item)),
                isRandom: null,
                isLate: rule.isLate(),
                isRigid: rule.hasRigid(),
                _sourceOffset: rule.__source.sourceOffset,
                debugFlag: rule.debugFlag
            });
        }
        else if (rule instanceof rule_1.SimpleRuleGroup) {
            return this.ruleMap.set(rule, {
                type: ast.RULE_TYPE.GROUP,
                isRandom: rule.isRandom,
                rules: rule.getChildRules().map((item) => this.recBuildRule(item)),
                _sourceOffset: rule.__source.sourceOffset,
                debugFlag: undefined // TODO: Unhardcode me
            });
        }
        else if (rule instanceof rule_1.SimpleRuleLoop) {
            const x = {
                type: ast.RULE_TYPE.LOOP,
                // isRandom: rule.isRandom,
                rules: rule.getChildRules().map((item) => this.recBuildRule(item)),
                _sourceOffset: rule.__source.sourceOffset,
                debugFlag: undefined // TODO: unhardcode me
            };
            return this.ruleMap.set(rule, x);
        }
        else {
            debugger;
            throw new Error(`BUG: Unsupported rule type`); // tslint:disable-line:no-debugger
        }
    }
    buildCommand(command) {
        if (command instanceof command_1.MessageCommand) {
            return this.commandMap.set(command, {
                type: ast.COMMAND_TYPE.MESSAGE,
                message: command.getMessage(),
                _sourceOffset: command.__source.sourceOffset
            });
        }
        else if (command instanceof command_1.SoundCommand) {
            return this.commandMap.set(command, {
                type: ast.COMMAND_TYPE.SFX,
                sound: this.soundMap.getId(command.getSound()),
                _sourceOffset: command.__source.sourceOffset
            });
        }
        else if (command instanceof command_1.CheckpointCommand) {
            return this.commandMap.set(command, {
                type: ast.COMMAND_TYPE.CHECKPOINT,
                _sourceOffset: command.__source.sourceOffset
            });
        }
        else if (command instanceof command_1.RestartCommand) {
            return this.commandMap.set(command, {
                type: ast.COMMAND_TYPE.RESTART,
                _sourceOffset: command.__source.sourceOffset
            });
        }
        else if (command instanceof command_1.CancelCommand) {
            return this.commandMap.set(command, {
                type: ast.COMMAND_TYPE.CANCEL,
                _sourceOffset: command.__source.sourceOffset
            });
        }
        else if (command instanceof command_1.AgainCommand) {
            return this.commandMap.set(command, {
                type: ast.COMMAND_TYPE.AGAIN,
                _sourceOffset: command.__source.sourceOffset
            });
        }
        else if (command instanceof command_1.WinCommand) {
            return this.commandMap.set(command, {
                type: ast.COMMAND_TYPE.WIN,
                _sourceOffset: command.__source.sourceOffset
            });
        }
        else {
            debugger;
            throw new Error(`BUG: Unsupoprted command type`); // tslint:disable-line:no-debugger
        }
    }
    buildConditionBracket(bracket) {
        if (bracket instanceof rule_1.SimpleEllipsisBracket) {
            const b = bracket;
            const before = b.beforeEllipsisBracket.getNeighbors().map((item) => this.buildNeighbor(item)); // this.buildConditionBracket(b.beforeEllipsisBracket)
            const after = b.afterEllipsisBracket.getNeighbors().map((item) => this.buildNeighbor(item)); // this.buildConditionBracket(b.afterEllipsisBracket)
            return this.conditionsMap.set(bracket, {
                type: ast.BRACKET_TYPE.ELLIPSIS,
                direction: toRULE_DIRECTION(b.direction),
                beforeNeighbors: before,
                afterNeighbors: after,
                _sourceOffset: bracket.__source.sourceOffset,
                debugFlag: b.debugFlag
            });
        }
        else if (bracket instanceof rule_1.SimpleBracket) {
            return this.conditionsMap.set(bracket, {
                type: ast.BRACKET_TYPE.SIMPLE,
                direction: toRULE_DIRECTION(bracket.direction),
                neighbors: bracket.getNeighbors().map((item) => this.buildNeighbor(item)),
                _sourceOffset: bracket.__source.sourceOffset,
                debugFlag: bracket.debugFlag
            });
        }
        else {
            debugger;
            throw new Error(`BUG: Unsupported bracket type`); // tslint:disable-line:no-debugger
        }
    }
    buildNeighbor(neighbor) {
        return this.neighborsMap.set(neighbor, {
            tileWithModifiers: [...neighbor._tilesWithModifier].map((item) => this.buildTileWithModifier(item)),
            _sourceOffset: neighbor.__source.sourceOffset,
            debugFlag: null // TODO: Pull it out of the neighbor
        });
    }
    buildTileWithModifier(t) {
        return this.tileWithModifierMap.set(t, {
            direction: t._direction ? toRULE_DIRECTION(t._direction) : null,
            isNegated: t._isNegated,
            isRandom: t._isRandom,
            tile: this.buildTile(t._tile),
            _sourceOffset: t.__source.sourceOffset,
            debugFlag: t._debugFlag
        });
    }
    buildTile(tile) {
        if (tile instanceof tile_1.GameLegendTileOr) {
            return this.tileMap.set(tile, {
                type: TILE_TYPE.OR,
                name: tile.getName(),
                sprites: tile.getSprites().map((item) => this.buildSprite(item)),
                collisionLayers: tile.getCollisionLayers().map((item) => this.buildCollisionLayer(item)),
                _sourceOffset: tile.__source.sourceOffset
            });
        }
        else if (tile instanceof tile_1.GameLegendTileAnd) {
            return this.tileMap.set(tile, {
                type: TILE_TYPE.AND,
                name: tile.getName(),
                sprites: tile.getSprites().map((item) => this.buildSprite(item)),
                collisionLayers: tile.getCollisionLayers().map((item) => this.buildCollisionLayer(item)),
                _sourceOffset: tile.__source.sourceOffset
            });
        }
        else if (tile instanceof tile_1.GameLegendTileSimple) {
            return this.tileMap.set(tile, {
                type: TILE_TYPE.SIMPLE,
                name: tile.getName(),
                sprite: this.buildSprite(tile.getSprites()[0]),
                collisionLayers: tile.getCollisionLayers().map((item) => this.buildCollisionLayer(item)),
                _sourceOffset: tile.__source.sourceOffset
            });
        }
        else if (tile instanceof tile_1.GameSprite) {
            return this.tileMap.set(tile, {
                type: TILE_TYPE.SPRITE,
                name: tile.getName(),
                sprite: this.buildSprite(tile),
                collisionLayer: this.buildCollisionLayer(tile.getCollisionLayer()),
                _sourceOffset: tile.__source.sourceOffset
            });
        }
        else {
            debugger;
            throw new Error(`BUG: Invalid tile type`); // tslint:disable-line:no-debugger
        }
    }
    soundToJson(sound) {
        return {
            soundCode: sound.soundCode,
            _sourceOffset: sound.__source.sourceOffset
        };
    }
    buildSprite(sprite) {
        const { spriteHeight, spriteWidth } = this.game.getSpriteSize();
        return this.spritesMap.set(sprite, {
            name: sprite.getName(),
            collisionLayer: this.collisionLayerMap.getId(sprite.getCollisionLayer()),
            pixels: sprite.getPixels(spriteHeight, spriteWidth).map((row) => row.map((pixel) => {
                if (pixel.isTransparent()) {
                    return null;
                }
                else {
                    return this.buildColor(pixel);
                }
            })),
            _sourceOffset: sprite.__source.sourceOffset
        });
    }
    buildColor(color) {
        const hex = color.toHex();
        this.colorsMap.set(hex, hex);
        return hex;
    }
}
exports.default = Serializer;
//# sourceMappingURL=serializer.js.map