"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ItemPoolDetection = void 0;
const isaac_typescript_definitions_1 = require("isaac-typescript-definitions");
const cachedClasses_1 = require("../../../core/cachedClasses");
const decorators_1 = require("../../../decorators");
const ISCFeature_1 = require("../../../enums/ISCFeature");
const collectibleTag_1 = require("../../../functions/collectibleTag");
const playerCollectibles_1 = require("../../../functions/playerCollectibles");
const playerDataStructures_1 = require("../../../functions/playerDataStructures");
const playerIndex_1 = require("../../../functions/playerIndex");
const players_1 = require("../../../functions/players");
const utils_1 = require("../../../functions/utils");
const Feature_1 = require("../../private/Feature");
const COLLECTIBLE_TYPE_THAT_IS_NOT_IN_ANY_POOLS = isaac_typescript_definitions_1.CollectibleType.KEY_PIECE_1;
const COLLECTIBLES_THAT_AFFECT_ITEM_POOLS = [
    isaac_typescript_definitions_1.CollectibleType.CHAOS,
    isaac_typescript_definitions_1.CollectibleType.SACRED_ORB,
    isaac_typescript_definitions_1.CollectibleType.TMTRAINER, // 721
];
const TRINKETS_THAT_AFFECT_ITEM_POOLS = [isaac_typescript_definitions_1.TrinketType.NO];
class ItemPoolDetection extends Feature_1.Feature {
    moddedElementSets;
    /** @internal */
    constructor(moddedElementSets) {
        super();
        this.featuresUsed = [ISCFeature_1.ISCFeature.MODDED_ELEMENT_SETS];
        this.moddedElementSets = moddedElementSets;
    }
    /**
     * Helper function to get the remaining collectibles in a given item pool. This function is
     * expensive, so only use it in situations where the lag is acceptable.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.ITEM_POOL_DETECTION`.
     *
     * @public
     */
    getCollectiblesInItemPool(itemPoolType) {
        const collectibleArray = this.moddedElementSets.getCollectibleTypes();
        return collectibleArray.filter((collectibleType) => this.isCollectibleInItemPool(collectibleType, itemPoolType));
    }
    /**
     * Helper function to see if the given collectible is still present in the given item pool.
     *
     * If the collectible is non-offensive, any Tainted Losts will be temporarily changed to Isaac and
     * then changed back. (This is because Tainted Lost is not able to retrieve non-offensive
     * collectibles from item pools).
     *
     * Under the hood, this function works by using the `ItemPool.AddRoomBlacklist` method to
     * blacklist every collectible except for the one provided.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.ITEM_POOL_DETECTION`.
     *
     * @public
     */
    isCollectibleInItemPool(collectibleType, itemPoolType) {
        // We use a specific collectible which is known to not be in any pools as a default value. Thus,
        // we must explicitly handle this case.
        if (collectibleType === COLLECTIBLE_TYPE_THAT_IS_NOT_IN_ANY_POOLS) {
            return false;
        }
        // On Tainted Lost, it is impossible to retrieve non-offensive collectibles from pools, so we
        // temporarily change the character to Isaac.
        const taintedLosts = (0, players_1.getPlayersOfType)(isaac_typescript_definitions_1.PlayerType.LOST_B);
        const isOffensive = (0, collectibleTag_1.collectibleHasTag)(collectibleType, isaac_typescript_definitions_1.ItemConfigTag.OFFENSIVE);
        let changedPlayerTypes = false;
        if (!isOffensive) {
            changedPlayerTypes = true;
            for (const player of taintedLosts) {
                player.ChangePlayerType(isaac_typescript_definitions_1.PlayerType.ISAAC);
            }
        }
        const { removedItemsMap, removedTrinketsMap } = removeItemsAndTrinketsThatAffectItemPools();
        // Blacklist every collectible in the game except for the provided collectible.
        const itemPool = cachedClasses_1.game.GetItemPool();
        itemPool.ResetRoomBlacklist();
        for (const collectibleTypeInSet of this.moddedElementSets.getCollectibleTypes()) {
            if (collectibleTypeInSet !== collectibleType) {
                itemPool.AddRoomBlacklist(collectibleTypeInSet);
            }
        }
        // Get a collectible from the pool and see if it is the intended collectible. (We can use any
        // arbitrary value as the seed since it should not influence the result.)
        const seed = 1;
        const retrievedCollectibleType = itemPool.GetCollectible(itemPoolType, false, seed, COLLECTIBLE_TYPE_THAT_IS_NOT_IN_ANY_POOLS);
        const collectibleUnlocked = retrievedCollectibleType === collectibleType;
        // Reset the blacklist
        itemPool.ResetRoomBlacklist();
        restoreItemsAndTrinketsThatAffectItemPools(removedItemsMap, removedTrinketsMap);
        // Change any players back to Tainted Lost, if necessary.
        if (changedPlayerTypes) {
            for (const player of taintedLosts) {
                player.ChangePlayerType(isaac_typescript_definitions_1.PlayerType.LOST_B);
            }
        }
        return collectibleUnlocked;
    }
    /**
     * Helper function to see if the given collectible is unlocked on the current save file. This
     * requires providing the corresponding item pool that the collectible is normally located in.
     *
     * - If any player currently has the collectible, then it is assumed to be unlocked. (This is
     *   because in almost all cases, when a collectible is added to a player's inventory, it is
     *   subsequently removed from all pools.)
     * - If the collectible is located in more than one item pool, then any item pool can be provided.
     * - If the collectible is not located in any item pools, then this function will always return
     *   false.
     * - If the collectible is non-offensive, any Tainted Losts will be temporarily changed to Isaac
     *   and then changed back. (This is because Tainted Lost is not able to retrieve non-offensive
     *   collectibles from item pools).
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.ITEM_POOL_DETECTION`.
     *
     * @public
     */
    isCollectibleUnlocked(collectibleType, itemPoolType) {
        if ((0, playerCollectibles_1.anyPlayerHasCollectible)(collectibleType)) {
            return true;
        }
        return this.isCollectibleInItemPool(collectibleType, itemPoolType);
    }
}
exports.ItemPoolDetection = ItemPoolDetection;
__decorate([
    decorators_1.Exported
], ItemPoolDetection.prototype, "getCollectiblesInItemPool", null);
__decorate([
    decorators_1.Exported
], ItemPoolDetection.prototype, "isCollectibleInItemPool", null);
__decorate([
    decorators_1.Exported
], ItemPoolDetection.prototype, "isCollectibleUnlocked", null);
/**
 * Before checking the item pools, remove any collectibles or trinkets that would affect the
 * retrieved collectible types.
 */
function removeItemsAndTrinketsThatAffectItemPools() {
    const removedItemsMap = new Map();
    const removedTrinketsMap = new Map();
    for (const player of (0, playerIndex_1.getAllPlayers)()) {
        const removedItems = [];
        for (const itemToRemove of COLLECTIBLES_THAT_AFFECT_ITEM_POOLS) {
            // We need to include non-real collectibles (like Lilith's Incubus), so we omit the second
            // argument.
            const numCollectibles = player.GetCollectibleNum(itemToRemove);
            (0, utils_1.repeat)(numCollectibles, () => {
                player.RemoveCollectible(itemToRemove);
                removedItems.push(itemToRemove);
            });
        }
        (0, playerDataStructures_1.mapSetPlayer)(removedItemsMap, player, removedItems);
        const removedTrinkets = [];
        for (const trinketToRemove of TRINKETS_THAT_AFFECT_ITEM_POOLS) {
            if (player.HasTrinket(trinketToRemove)) {
                const numTrinkets = player.GetTrinketMultiplier(trinketToRemove);
                (0, utils_1.repeat)(numTrinkets, () => {
                    player.TryRemoveTrinket(trinketToRemove);
                    removedTrinkets.push(trinketToRemove);
                });
            }
        }
        (0, playerDataStructures_1.mapSetPlayer)(removedTrinketsMap, player, removedTrinkets);
    }
    return { removedItemsMap, removedTrinketsMap };
}
function restoreItemsAndTrinketsThatAffectItemPools(removedItemsMap, removedTrinketsMap) {
    for (const player of (0, playerIndex_1.getAllPlayers)()) {
        const removedItems = (0, playerDataStructures_1.mapGetPlayer)(removedItemsMap, player);
        if (removedItems !== undefined) {
            for (const collectibleType of removedItems) {
                player.AddCollectible(collectibleType, 0, false); // Prevent Chaos from spawning pickups
            }
        }
        const removedTrinkets = (0, playerDataStructures_1.mapGetPlayer)(removedTrinketsMap, player);
        if (removedTrinkets !== undefined) {
            for (const trinketType of removedTrinkets) {
                player.AddTrinket(trinketType, false);
            }
        }
    }
}
