"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.State = void 0;
/**
 * Simulator State
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * Helper functions for serializing Battle instances to JSON and back.
 *
 * (You might also consider using input logs instead.)
 *
 * @license MIT
 */
const battle_1 = require("./battle");
const dex_1 = require("./dex");
const field_1 = require("./field");
const pokemon_1 = require("./pokemon");
const prng_1 = require("./prng");
const side_1 = require("./side");
// The simulator supports up to 24 different Pokemon on a team. Serialization
// uses letters instead of numbers to indicate indices/positions, but where
// the simulator only gives a position to active Pokemon, serialization
// uses letters for every Pokemon on a team. Active pokemon will still
// have the same letter as their position would indicate, but non-active
// team members are filled in with subsequent letters.
const POSITIONS = 'abcdefghijklmnopqrstuvwx';
// Certain fields are either redundant (transient caches, constants, duplicate
// information) or require special treatment. These sets contain the specific
// keys which we skip during default (de)serialization and (the keys which)
// need special treatment from these sets are then handled manually.
const BATTLE = new Set([
    'dex', 'gen', 'ruleTable', 'id', 'log', 'inherit', 'format', 'teamGenerator',
    'HIT_SUBSTITUTE', 'NOT_FAIL', 'FAIL', 'SILENT_FAIL', 'field', 'sides', 'prng', 'hints',
    'deserialized', 'queue', 'actions',
]);
const FIELD = new Set(['id', 'battle']);
const SIDE = new Set(['battle', 'team', 'pokemon', 'choice', 'activeRequest']);
const POKEMON = new Set([
    'side', 'battle', 'set', 'name', 'fullname', 'id',
    'happiness', 'level', 'pokeball', 'baseMoveSlots',
]);
const CHOICE = new Set(['switchIns']);
const ACTIVE_MOVE = new Set(['move']);
exports.State = new class {
    serializeBattle(battle) {
        const state = this.serialize(battle, BATTLE, battle);
        state.field = this.serializeField(battle.field);
        state.sides = new Array(battle.sides.length);
        for (const [i, side] of battle.sides.entries()) {
            state.sides[i] = this.serializeSide(side);
        }
        state.prng = battle.prng.getSeed();
        state.hints = Array.from(battle.hints);
        // We treat log specially because we only set it back on Battle after everything
        // else has been deserialized to avoid anything accidentally `add`-ing to it.
        state.log = battle.log;
        state.queue = this.serializeWithRefs(battle.queue.list, battle);
        state.formatid = battle.format.id;
        return state;
    }
    // Deserialization can only really be done on the root Battle object as
    // the leaf nodes like Side or Pokemon contain backreferences to Battle
    // but don't contain the information to fill it in because the cycles in
    // the graph have been serialized as references. Once deserialzized, the
    // Battle can then be restarted (and provided with a `send` function for
    // receiving updates).
    deserializeBattle(serialized) {
        const state = typeof serialized === 'string' ? JSON.parse(serialized) : serialized;
        const options = {
            formatid: state.formatid,
            seed: state.prngSeed,
            rated: state.rated,
            debug: state.debugMode,
            // We need to tell the Battle that we're creating that it's been
            // deserialized so that it allows us to populate it correctly and
            // doesn't attempt to start playing out until we're ready.
            deserialized: true,
            strictChoices: state.strictChoices,
        };
        for (const side of state.sides) {
            // When we instantiate the Battle again we need the pokemon to be in
            // the correct order they were in at the start of the Battle which was
            // serialized. See serializeSide below for an explanation about the
            // encoding format used deserializeSide for where we reorder the Side's
            // pokemon to match their ordering at the point of serialization.
            const team = side.team.split(side.team.length > 9 ? ',' : '');
            // @ts-expect-error index signature
            options[side.id] = {
                name: side.name,
                avatar: side.avatar,
                team: team.map((p) => side.pokemon[Number(p) - 1].set),
            };
        }
        // We create the Battle, allowing it to instantiate the Field/Side/Pokemon
        // objects for us. The objects it creates will be incorrect, but we descend
        // down through the fields and repopulate all of the objects with the
        // correct state afterwards.
        const battle = new battle_1.Battle(options);
        // Calling `new Battle(...)` means side.pokemon is ordered to match what it
        // was at the start of the battle (state.team), but we need to order the Pokemon
        // back in their correct order based on how the battle has progressed. We need
        // do to this before making any deserialization calls so that `fromRef` will
        // be correct.
        for (const [i, s] of state.sides.entries()) {
            const side = battle.sides[i];
            const ordered = new Array(side.pokemon.length);
            const team = s.team.split(s.team.length > 9 ? ',' : '');
            for (const [j, pos] of team.entries()) {
                ordered[Number(pos) - 1] = side.pokemon[j];
            }
            side.pokemon = ordered;
        }
        this.deserialize(state, battle, BATTLE, battle);
        this.deserializeField(state.field, battle.field);
        let activeRequests = false;
        for (const [i, side] of state.sides.entries()) {
            this.deserializeSide(side, battle.sides[i]);
            activeRequests = activeRequests || side.activeRequest === undefined;
        }
        // Since battle.getRequests depends on the state of each side we can't combine
        // this loop with the one above which deserializes the sides. We also only do this
        // if there are any active requests, not only to avoid have to recompute request
        // states we wouldnt be using, but also because battle.getRequests will mutate
        // state on occasion (eg. `pokemon.getMoves` sets `pokemon.trapped = true` if locked).
        if (activeRequests) {
            const requests = battle.getRequests(battle.requestState);
            for (const [i, side] of state.sides.entries()) {
                battle.sides[i].activeRequest = side.activeRequest === null ? null : requests[i];
            }
        }
        battle.prng = new prng_1.PRNG(state.prng);
        const queue = this.deserializeWithRefs(state.queue, battle);
        battle.queue.list = queue;
        battle.hints = new Set(state.hints);
        battle.log = state.log;
        return battle;
    }
    // Direct comparsions of serialized state will be flakey as the timestamp
    // protocol message |t:| can diverge between two different runs over the same state.
    // State must first be normalized before it is comparable.
    normalize(state) {
        state.log = this.normalizeLog(state.log);
        return state;
    }
    normalizeLog(log) {
        if (!log)
            return log;
        const normalized = (typeof log === 'string' ? log.split('\n') : log).map(line => line.startsWith(`|t:|`) ? `|t:|` : line);
        return (typeof log === 'string' ? normalized.join('\n') : normalized);
    }
    serializeField(field) {
        return this.serialize(field, FIELD, field.battle);
    }
    deserializeField(state, field) {
        this.deserialize(state, field, FIELD, field.battle);
    }
    serializeSide(side) {
        const state = this.serialize(side, SIDE, side.battle);
        state.pokemon = new Array(side.pokemon.length);
        const team = new Array(side.pokemon.length);
        for (const [i, pokemon] of side.pokemon.entries()) {
            state.pokemon[i] = this.serializePokemon(pokemon);
            team[side.team.indexOf(pokemon.set)] = i + 1;
        }
        // We encode the team such that it could be used as a valid `/team` command
        // during decoding to transform the current ordering of the serialized Side's
        // pokemon array into the original team ordering at the start of the battle.
        // This is *not* the same as the original `/team` command used to order the
        // pokemon in team preview, but this encoding results in the most intuitive
        // and readable debugging of the raw JSON, so we're willing to add a small
        // amount of complexity to the encoding/decoding process to accommodate this.
        state.team = team.join(team.length > 9 ? ',' : '');
        state.choice = this.serializeChoice(side.choice, side.battle);
        // If activeRequest is null we encode it as a tombstone indicator to ensure
        // that during serialization when we recompute the activeRequest we don't turn
        // `activeRequest = null` into  `activeRequest = { wait: true, ... }`.
        if (side.activeRequest === null)
            state.activeRequest = null;
        return state;
    }
    deserializeSide(state, side) {
        this.deserialize(state, side, SIDE, side.battle);
        for (const [i, pokemon] of state.pokemon.entries()) {
            this.deserializePokemon(pokemon, side.pokemon[i]);
        }
        this.deserializeChoice(state.choice, side.choice, side.battle);
    }
    serializePokemon(pokemon) {
        const state = this.serialize(pokemon, POKEMON, pokemon.battle);
        state.set = pokemon.set;
        // Only serialize the baseMoveSlots if they differ from moveSlots. We could get fancy and
        // only serialize the diff and its index but thats overkill for a pretty niche case anyway.
        if (pokemon.baseMoveSlots.length !== pokemon.moveSlots.length ||
            !pokemon.baseMoveSlots.every((ms, i) => ms === pokemon.moveSlots[i])) {
            state.baseMoveSlots = this.serializeWithRefs(pokemon.baseMoveSlots, pokemon.battle);
        }
        return state;
    }
    deserializePokemon(state, pokemon) {
        this.deserialize(state, pokemon, POKEMON, pokemon.battle);
        pokemon.set = state.set;
        // baseMoveSlots and moveSlots need to point to the same objects (ie. identity, not equality).
        // If we serialized the baseMoveSlots, replace any that match moveSlots to preserve the
        // identity relationship requirement.
        let baseMoveSlots;
        if (state.baseMoveSlots) {
            baseMoveSlots = this.deserializeWithRefs(state.baseMoveSlots, pokemon.battle);
            for (const [i, baseMoveSlot] of baseMoveSlots.entries()) {
                const moveSlot = pokemon.moveSlots[i];
                if (moveSlot.id === baseMoveSlot.id && !moveSlot.virtual) {
                    baseMoveSlots[i] = moveSlot;
                }
            }
        }
        else {
            baseMoveSlots = pokemon.moveSlots.slice();
        }
        pokemon.baseMoveSlots = baseMoveSlots;
        if (state.showCure === undefined)
            pokemon.showCure = undefined;
    }
    serializeChoice(choice, battle) {
        const state = this.serialize(choice, CHOICE, battle);
        state.switchIns = Array.from(choice.switchIns);
        return state;
    }
    deserializeChoice(state, choice, battle) {
        this.deserialize(state, choice, CHOICE, battle);
        choice.switchIns = new Set(state.switchIns);
    }
    // Simply looking for a 'hit' field to determine if an object is an ActiveMove or not seems
    // pretty fragile, but its no different than what the simulator is doing. We go further and
    // also check if the object has an 'id', as that's what we will intrepret as the Move.
    isActiveMove(obj) {
        return obj.hasOwnProperty('hit') && (obj.hasOwnProperty('id') || obj.hasOwnProperty('move'));
    }
    // ActiveMove is somewhat problematic (#5415) as it sometimes extends a Move and adds on
    // some mutable fields. We'd like to avoid displaying all the readonly fields of Move
    // (which in theory should not be changed by the ActiveMove...), so we collapse them
    // into a 'move: [Move:...]' reference.  If isActiveMove returns a false positive *and*
    // and object contains an 'id' field matching a Move *and* it contains fields with the
    // same name as said Move then we'll miss them during serialization and won't
    // deserialize properly. This is unlikely to be the case, and would probably indicate
    // a bug in the simulator if it ever happened, but if not, the isActiveMove check can
    // be extended.
    serializeActiveMove(move, battle) {
        const base = battle.dex.moves.get(move.id);
        const skip = new Set([...ACTIVE_MOVE]);
        for (const [key, value] of Object.entries(base)) {
            // This should really be a deepEquals check to see if anything on ActiveMove was
            // modified from the base Move, but that ends up being expensive and mostly unnecessary
            // as ActiveMove currently only mutates its simple fields (eg. `type`, `target`) anyway.
            // @ts-expect-error index signature
            if (typeof value === 'object' || move[key] === value)
                skip.add(key);
        }
        const state = this.serialize(move, skip, battle);
        state.move = `[Move:${move.id}]`;
        return state;
    }
    deserializeActiveMove(state, battle) {
        const move = battle.dex.getActiveMove(this.fromRef(state.move, battle));
        this.deserialize(state, move, ACTIVE_MOVE, battle);
        return move;
    }
    serializeWithRefs(obj, battle) {
        switch (typeof obj) {
            case 'function':
                return undefined; // elide functions
            case 'undefined':
            case 'boolean':
            case 'number':
            case 'string':
                return obj;
            case 'object':
                if (obj === null)
                    return null;
                if (Array.isArray(obj)) {
                    const arr = new Array(obj.length);
                    for (const [i, o] of obj.entries()) {
                        arr[i] = this.serializeWithRefs(o, battle);
                    }
                    return arr;
                }
                if (this.isActiveMove(obj))
                    return this.serializeActiveMove(obj, battle);
                if (this.isReferable(obj))
                    return this.toRef(obj);
                if (obj.constructor !== Object) {
                    // If we're getting this error, some 'special' field has been added to
                    // an object and we need to update the logic in this file to handle it.
                    // The most common case it that someone added a Set/Map which probably
                    // needs to be serialized as an Array/Object respectively - see how
                    // Battle 'hints' or Choice 'switchIns' are handled (and you will likely
                    // need to add the new field to the respective skip constant).
                    throw new TypeError(`Unsupported type ${obj.constructor.name}: ${obj}`);
                }
                const o = {};
                for (const [key, value] of Object.entries(obj)) {
                    o[key] = this.serializeWithRefs(value, battle);
                }
                return o;
            default:
                throw new TypeError(`Unexpected typeof === '${typeof obj}': ${obj}`);
        }
    }
    deserializeWithRefs(obj, battle) {
        switch (typeof obj) {
            case 'undefined':
            case 'boolean':
            case 'number':
                return obj;
            case 'string':
                return this.fromRef(obj, battle) || obj;
            case 'object':
                if (obj === null)
                    return null;
                if (Array.isArray(obj)) {
                    const arr = new Array(obj.length);
                    for (const [i, o] of obj.entries()) {
                        arr[i] = this.deserializeWithRefs(o, battle);
                    }
                    return arr;
                }
                if (this.isActiveMove(obj))
                    return this.deserializeActiveMove(obj, battle);
                const o = {};
                for (const [key, value] of Object.entries(obj)) {
                    o[key] = this.deserializeWithRefs(value, battle);
                }
                return o;
            case 'function': // lol wtf
            default:
                throw new TypeError(`Unexpected typeof === '${typeof obj}': ${obj}`);
        }
    }
    isReferable(obj) {
        // NOTE: see explanation on the declaration above for why this must be defined lazily.
        if (!this.REFERABLE) {
            this.REFERABLE = new Set([
                battle_1.Battle, field_1.Field, side_1.Side, pokemon_1.Pokemon, dex_1.Dex.Condition,
                dex_1.Dex.Ability, dex_1.Dex.Item, dex_1.Dex.Move, dex_1.Dex.Species,
            ]);
        }
        return this.REFERABLE.has(obj.constructor);
    }
    toRef(obj) {
        // Pokemon's 'id' is not only more verbose than a position, it also isn't guaranteed
        // to be uniquely identifying in custom games without Nickname/Species Clause.
        const id = obj instanceof pokemon_1.Pokemon ? `${obj.side.id}${POSITIONS[obj.position]}` : `${obj.id}`;
        return `[${obj.constructor.name}${id ? ':' : ''}${id}]`;
    }
    fromRef(ref, battle) {
        // References are sort of fragile - we're mostly just counting on there
        // being a low chance that some string field in a simulator object will not
        // 'look' like one. However, it also needs to match one of the Referable
        // class types to be decode, so we're probably OK. We could make the reference
        // markers more esoteric with additional sigils etc to avoid collisions, but
        // we're making a conscious decision to favor readability over robustness.
        if (!ref.startsWith('[') && !ref.endsWith(']'))
            return undefined;
        ref = ref.substring(1, ref.length - 1);
        // There's only one instance of these thus they don't need an id to differentiate.
        if (ref === 'Battle')
            return battle;
        if (ref === 'Field')
            return battle.field;
        const [type, id] = ref.split(':');
        switch (type) {
            case 'Side': return battle.sides[Number(id[1]) - 1];
            case 'Pokemon': return battle.sides[Number(id[1]) - 1].pokemon[POSITIONS.indexOf(id[2])];
            case 'Ability': return battle.dex.abilities.get(id);
            case 'Item': return battle.dex.items.get(id);
            case 'Move': return battle.dex.moves.get(id);
            case 'Condition': return battle.dex.conditions.get(id);
            case 'Species': return battle.dex.species.get(id);
            default: return undefined; // maybe we actually got unlucky and its a string
        }
    }
    serialize(obj, skip, battle) {
        const state = {};
        for (const [key, value] of Object.entries(obj)) {
            if (skip.has(key))
                continue;
            const val = this.serializeWithRefs(value, battle);
            // JSON.stringify will get rid of keys with undefined values anyway, but
            // we also do it here so that assert.deepEqual works on battle.toJSON().
            if (typeof val !== 'undefined')
                state[key] = val;
        }
        return state;
    }
    deserialize(state, obj, skip, battle) {
        for (const [key, value] of Object.entries(state)) {
            if (skip.has(key))
                continue;
            // @ts-expect-error index signature
            obj[key] = this.deserializeWithRefs(value, battle);
        }
    }
};
//# sourceMappingURL=state.js.map