"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlayerCollectibleDetection = void 0;
const isaac_typescript_definitions_1 = require("isaac-typescript-definitions");
const cachedEnumValues_1 = require("../../../cachedEnumValues");
const ISCFeature_1 = require("../../../enums/ISCFeature");
const ModCallbackCustom_1 = require("../../../enums/ModCallbackCustom");
const array_1 = require("../../../functions/array");
const flag_1 = require("../../../functions/flag");
const playerDataStructures_1 = require("../../../functions/playerDataStructures");
const players_1 = require("../../../functions/players");
const sort_1 = require("../../../functions/sort");
const utils_1 = require("../../../functions/utils");
const DefaultMap_1 = require("../../DefaultMap");
const Feature_1 = require("../../private/Feature");
const v = {
    run: {
        playersCollectibleCount: new DefaultMap_1.DefaultMap(0),
        playersCollectibleMap: new DefaultMap_1.DefaultMap(() => new Map()),
        playersActiveItemMap: new DefaultMap_1.DefaultMap(() => new Map()),
    },
};
class PlayerCollectibleDetection extends Feature_1.Feature {
    v = v;
    postPlayerCollectibleAdded;
    postPlayerCollectibleRemoved;
    moddedElementSets;
    runInNFrames;
    constructor(postPlayerCollectibleAdded, postPlayerCollectibleRemoved, moddedElementSets, runInNFrames) {
        super();
        this.featuresUsed = [
            ISCFeature_1.ISCFeature.MODDED_ELEMENT_SETS,
            ISCFeature_1.ISCFeature.RUN_IN_N_FRAMES,
        ];
        this.callbacksUsed = [
            // 3
            [isaac_typescript_definitions_1.ModCallback.POST_USE_ITEM, this.postUseItemD4, [isaac_typescript_definitions_1.CollectibleType.D4]],
        ];
        this.customCallbacksUsed = [
            [ModCallbackCustom_1.ModCallbackCustom.ENTITY_TAKE_DMG_PLAYER, this.entityTakeDmgPlayer],
            [ModCallbackCustom_1.ModCallbackCustom.POST_ITEM_PICKUP, this.postItemPickup],
            [
                ModCallbackCustom_1.ModCallbackCustom.POST_PEFFECT_UPDATE_REORDERED,
                this.postPEffectUpdateReordered,
            ],
        ];
        this.postPlayerCollectibleAdded = postPlayerCollectibleAdded;
        this.postPlayerCollectibleRemoved = postPlayerCollectibleRemoved;
        this.moddedElementSets = moddedElementSets;
        this.runInNFrames = runInNFrames;
    }
    /**
     * This is called when the collectible count changes and in situations where the entire build is
     * rerolled.
     *
     * Since getting a new player collectible map is expensive, we want to only run this function when
     * necessary, and not on e.g. every frame. Unfortunately, this has the side effect of missing out
     * on collectible changes from mods that add and remove a collectible on the same frame.
     *
     * @param player The player to update.
     * @param numCollectiblesChanged Pass undefined for situations where the entire build was
     *                               rerolled.
     */
    updateCollectibleMapAndFire(player, numCollectiblesChanged) {
        const oldCollectibleMap = (0, playerDataStructures_1.defaultMapGetPlayer)(v.run.playersCollectibleMap, player);
        const newCollectibleMap = this.moddedElementSets.getPlayerCollectibleMap(player);
        (0, playerDataStructures_1.mapSetPlayer)(v.run.playersCollectibleMap, player, newCollectibleMap);
        const collectibleTypesSet = new Set([
            ...oldCollectibleMap.keys(),
            ...newCollectibleMap.keys(),
        ]);
        let numFired = 0;
        for (const collectibleType of collectibleTypesSet) {
            const oldNum = oldCollectibleMap.get(collectibleType) ?? 0;
            const newNum = newCollectibleMap.get(collectibleType) ?? 0;
            const difference = newNum - oldNum;
            const increased = difference > 0;
            const absoluteDifference = Math.abs(difference);
            (0, utils_1.repeat)(absoluteDifference, () => {
                if (increased) {
                    this.postPlayerCollectibleAdded.fire(player, collectibleType);
                }
                else {
                    this.postPlayerCollectibleRemoved.fire(player, collectibleType);
                }
                numFired++;
            });
            if (numFired === numCollectiblesChanged) {
                return;
            }
        }
    }
    // ModCallback.POST_USE_ITEM (3)
    // CollectibleType.D4 (284)
    postUseItemD4 = (_collectibleType, _rng, player) => {
        // This function is also triggered for:
        // - D100
        // - D Infinity copying D4 or D100
        // - 1-pip dice room
        // - 6-pip dice room
        // - Reverse Wheel of Fortune copying 1-pip or 6-pip dice room
        // - First getting Missing No.
        // - Arriving on a new floor with Missing No.
        // This function is not triggered for:
        // - Tainted Eden getting hit (this is explicitly handled elsewhere)
        // - Genesis (which is automatically handled by the collectibles being removed in the normal
        //   `POST_PLAYER_COLLECTIBLE_REMOVED` callback)
        this.updateCollectibleMapAndFire(player, undefined);
        return undefined;
    };
    /** We need to handle the case of Tainted Eden taking damage. */
    // ModCallbackCustom.ENTITY_TAKE_DMG_PLAYER
    entityTakeDmgPlayer = (player, _amount, damageFlags, _source, _countdownFrames) => {
        // Tainted Eden's mechanic does not apply if she e.g. uses Dull Razor.
        if ((0, flag_1.hasFlag)(damageFlags, isaac_typescript_definitions_1.DamageFlag.FAKE)) {
            return undefined;
        }
        const character = player.GetPlayerType();
        if (character !== isaac_typescript_definitions_1.PlayerType.EDEN_B) {
            return undefined;
        }
        // The items will only be rerolled after the damage is successfully applied.
        const entityPtr = EntityPtr(player);
        this.runInNFrames.runNextGameFrame(() => {
            const futurePlayer = (0, players_1.getPlayerFromPtr)(entityPtr);
            if (futurePlayer !== undefined) {
                this.updateCollectibleMapAndFire(player, undefined);
            }
        });
        return undefined;
    };
    /**
     * We need to handle TMTRAINER collectibles, since they do not cause the player's collectible
     * count to change.
     */
    // ModCallbackCustom.POST_ITEM_PICKUP
    postItemPickup = (player, pickingUpItem) => {
        if (pickingUpItem.itemType === isaac_typescript_definitions_1.ItemType.TRINKET ||
            pickingUpItem.itemType === isaac_typescript_definitions_1.ItemType.NULL) {
            return;
        }
        const newCollectibleCount = player.GetCollectibleCount();
        (0, playerDataStructures_1.mapSetPlayer)(v.run.playersCollectibleCount, player, newCollectibleCount);
        this.updateCollectibleMapAndFire(player, 1);
    };
    // ModCallbackCustom.POST_PEFFECT_UPDATE_REORDERED
    postPEffectUpdateReordered = (player) => {
        const oldCollectibleCount = (0, playerDataStructures_1.defaultMapGetPlayer)(v.run.playersCollectibleCount, player);
        const newCollectibleCount = player.GetCollectibleCount();
        (0, playerDataStructures_1.mapSetPlayer)(v.run.playersCollectibleCount, player, newCollectibleCount);
        const difference = newCollectibleCount - oldCollectibleCount;
        if (difference > 0) {
            this.updateCollectibleMapAndFire(player, difference);
        }
        else if (difference < 0) {
            this.updateCollectibleMapAndFire(player, difference * -1);
        }
        else if (difference === 0) {
            this.checkActiveItemsChanged(player);
        }
    };
    /**
     * Checking for collectible count will work to detect when a player swaps their active item for
     * another active item. This is because the collectible count will decrement by 1 when the item is
     * swapped onto the pedestal and the hold animation begins, and increment by 1 when the item is
     * dequeued and the hold animation ends.
     *
     * However, we also want to explicitly check for the case where a mod swaps in a custom active
     * collectible on the same frame, since doing so is cheap.
     */
    checkActiveItemsChanged(player) {
        const activeItemMap = (0, playerDataStructures_1.defaultMapGetPlayer)(v.run.playersActiveItemMap, player);
        const oldCollectibleTypes = [];
        const newCollectibleTypes = [];
        for (const activeSlot of cachedEnumValues_1.ACTIVE_SLOT_VALUES) {
            const oldCollectibleType = activeItemMap.get(activeSlot) ?? isaac_typescript_definitions_1.CollectibleType.NULL;
            const newCollectibleType = player.GetActiveItem(activeSlot);
            activeItemMap.set(activeSlot, newCollectibleType);
            oldCollectibleTypes.push(oldCollectibleType);
            newCollectibleTypes.push(newCollectibleType);
        }
        // For example, it is possible for the player to switch Schoolbag items, which will cause the
        // collectibles in the array to be the same, but in a different order. Thus, we sort both arrays
        // before comparing them.
        oldCollectibleTypes.sort(sort_1.sortNormal);
        newCollectibleTypes.sort(sort_1.sortNormal);
        if (!(0, array_1.arrayEquals)(oldCollectibleTypes, newCollectibleTypes)) {
            // One or more active items have changed (with the player's total collectible count remaining
            // the same).
            this.updateCollectibleMapAndFire(player, undefined);
        }
    }
}
exports.PlayerCollectibleDetection = PlayerCollectibleDetection;
