"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.PickupIndexCreation = 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 entities_1 = require("../../../functions/entities");
const frames_1 = require("../../../functions/frames");
const roomData_1 = require("../../../functions/roomData");
const stage_1 = require("../../../functions/stage");
const vector_1 = require("../../../functions/vector");
const DefaultMap_1 = require("../../DefaultMap");
const Feature_1 = require("../../private/Feature");
const v = {
    run: {
        /** Is incremented before assignment. Thus, the first pickup will have an index of 1. */
        pickupCounter: 0,
        pickupDataTreasureRooms: new Map(),
        pickupDataBossRooms: new Map(),
    },
    level: {
        /** Indexed by room list index. */
        pickupData: new DefaultMap_1.DefaultMap(() => new Map()),
    },
    room: {
        pickupIndexes: new Map(),
    },
};
class PickupIndexCreation extends Feature_1.Feature {
    /** @internal */
    v = v;
    roomHistory;
    saveDataManager;
    /** @internal */
    constructor(roomHistory, saveDataManager) {
        super();
        this.featuresUsed = [ISCFeature_1.ISCFeature.ROOM_HISTORY, ISCFeature_1.ISCFeature.SAVE_DATA_MANAGER];
        this.callbacksUsed = [
            // 34
            [isaac_typescript_definitions_1.ModCallback.POST_PICKUP_INIT, this.postPickupInit],
            // 67
            [
                isaac_typescript_definitions_1.ModCallback.POST_ENTITY_REMOVE,
                this.postEntityRemovePickup,
                [isaac_typescript_definitions_1.EntityType.PICKUP],
            ],
        ];
        this.roomHistory = roomHistory;
        this.saveDataManager = saveDataManager;
    }
    // ModCallback.POST_PICKUP_INIT (34)
    postPickupInit = (pickup) => {
        this.setPickupIndex(pickup);
    };
    setPickupIndex(pickup) {
        const ptrHash = GetPtrHash(pickup);
        // In certain situations, pickups can be morphed, which will trigger the `POST_PICKUP_INIT`
        // callback but should not incur a new pickup counter. (For example, the collectible rotation
        // with Tainted Isaac.) For these situations, we will already be tracking an index for this
        // pointer hash.
        if (v.room.pickupIndexes.has(ptrHash)) {
            return;
        }
        // First, handle the special case of re-entering a room with a previously tracked pickup. If we
        // find a match in the level pickup data, we will use the pickup index from the match.
        const pickupIndexFromLevelData = this.getPickupIndexFromPreviousData(pickup);
        const room = cachedClasses_1.game.GetRoom();
        const isFirstVisit = room.IsFirstVisit();
        if (pickupIndexFromLevelData !== undefined &&
            !isFirstVisit &&
            (0, frames_1.onOrBeforeRoomFrame)(0)) {
            v.room.pickupIndexes.set(ptrHash, pickupIndexFromLevelData);
            return;
        }
        // This is a brand new pickup that we have not previously seen on this run.
        v.run.pickupCounter++;
        v.room.pickupIndexes.set(ptrHash, v.run.pickupCounter);
    }
    getPickupIndexFromPreviousData(pickup) {
        const roomListIndex = (0, roomData_1.getRoomListIndex)();
        const pickupDescriptions = v.level.pickupData.getAndSetDefault(roomListIndex);
        let pickupIndex = getStoredPickupIndex(pickup, pickupDescriptions);
        if (pickupIndex === undefined) {
            pickupIndex = this.getPostAscentPickupIndex(pickup);
        }
        return pickupIndex;
    }
    // ModCallback.POST_ENTITY_REMOVE (67)
    // EntityType.PICKUP (5)
    postEntityRemovePickup = (entity) => {
        this.checkDespawningFromPlayerLeavingRoom(entity);
    };
    checkDespawningFromPlayerLeavingRoom(entity) {
        const ptrHash = GetPtrHash(entity);
        const pickupIndex = v.room.pickupIndexes.get(ptrHash);
        if (pickupIndex === undefined) {
            return;
        }
        if (!this.roomHistory.isLeavingRoom()) {
            return;
        }
        this.trackDespawningPickupMetadata(entity, pickupIndex);
    }
    /**
     * This is a pickup that is despawning because the player is in the process of leaving the room.
     * Keep track of the metadata for later.
     */
    trackDespawningPickupMetadata(entity, pickupIndex) {
        // The "latest" room description is really the previous room, because the `POST_NEW_ROOM`
        // callback has not fired yet.
        const previousRoomDescription = this.roomHistory.getLatestRoomDescription();
        if (previousRoomDescription === undefined) {
            return;
        }
        const previousRoomListIndex = previousRoomDescription.roomListIndex;
        const pickupDescriptions = v.level.pickupData.getAndSetDefault(previousRoomListIndex);
        const pickupDescription = {
            position: entity.Position,
            initSeed: entity.InitSeed,
        };
        pickupDescriptions.set(pickupIndex, pickupDescription);
        const pickupDataMapForCurrentRoom = this.getPickupDataMapForCurrentRoom();
        if (pickupDataMapForCurrentRoom !== undefined) {
            pickupDataMapForCurrentRoom.set(pickupIndex, pickupDescription);
        }
        // Since the `POST_ENTITY_REMOVE` callback fires after the `PRE_GAME_EXIT` callback, we need to
        // explicitly save data again if the player is in the process of saving and quitting the run.
        if (this.saveDataManager.saveDataManagerInMenu()) {
            this.saveDataManager.saveDataManagerSave();
        }
    }
    /**
     * If the despawning pickup was in a Treasure Room or Boss Room, then it is possible that the
     * pickup could re-appear during The Ascent. If this is the case, we store the metadata on a
     * separate map to reference later.
     */
    getPickupDataMapForCurrentRoom() {
        if ((0, stage_1.onAscent)()) {
            return undefined;
        }
        const room = cachedClasses_1.game.GetRoom();
        const roomType = room.GetType();
        switch (roomType) {
            case isaac_typescript_definitions_1.RoomType.TREASURE: {
                return v.run.pickupDataTreasureRooms;
            }
            case isaac_typescript_definitions_1.RoomType.BOSS: {
                return v.run.pickupDataBossRooms;
            }
            default: {
                return undefined;
            }
        }
    }
    getPostAscentPickupIndex(pickup) {
        // If we have not found the pickup index yet, we might be re-entering a post-Ascent Treasure
        // Room or Boss Room.
        if (!(0, stage_1.onAscent)()) {
            return undefined;
        }
        const room = cachedClasses_1.game.GetRoom();
        const roomType = room.GetType();
        switch (roomType) {
            case isaac_typescript_definitions_1.RoomType.TREASURE: {
                return getStoredPickupIndex(pickup, v.run.pickupDataTreasureRooms);
            }
            case isaac_typescript_definitions_1.RoomType.BOSS: {
                return getStoredPickupIndex(pickup, v.run.pickupDataBossRooms);
            }
            default: {
                return undefined;
            }
        }
    }
    /**
     * Mods often have to track variables relating to a pickups. Finding an index for these kinds of
     * data structures is difficult, since pickups are respawned every time a player re-enters a room,
     * so the `PtrHash` will change.
     *
     * Use this function to get a unique index for a pickup to use in these data structures.
     *
     * Specifically, `PickupIndex` is a number that represents the spawn order of the pickup on the
     * current run. For example, the first pickup spawned will have an index of 1, the second one will
     * have an index of 2, and so on.
     *
     * Tracking pickups requires stateful tracking, so using pickup indexes requires an upgraded mod.
     *
     * Note that the pickup index will not change:
     * - When a pickup is rolled with e.g. D6 or D20.
     * - When an item is "rotated" via e.g. Tainted Isaac's mechanic.
     *
     * In order to use this function, you must upgrade your mod with
     * `ISCFeature.PICKUP_INDEX_CREATION`.
     */
    getPickupIndex(pickup) {
        const ptrHash = GetPtrHash(pickup);
        const pickupIndexInitial = v.room.pickupIndexes.get(ptrHash);
        if (pickupIndexInitial !== undefined) {
            return pickupIndexInitial;
        }
        this.setPickupIndex(pickup);
        const pickupIndex = v.room.pickupIndexes.get(ptrHash);
        if (pickupIndex !== undefined) {
            return pickupIndex;
        }
        const entityID = (0, entities_1.getEntityID)(pickup);
        error(`Failed to generate a new pickup index for pickup: ${entityID}`);
    }
}
exports.PickupIndexCreation = PickupIndexCreation;
__decorate([
    decorators_1.Exported
], PickupIndexCreation.prototype, "getPickupIndex", null);
function getStoredPickupIndex(pickup, pickupDescriptions) {
    for (const [pickupIndex, pickupDescription] of pickupDescriptions) {
        if ((0, vector_1.vectorEquals)(pickupDescription.position, pickup.Position) &&
            pickupDescription.initSeed === pickup.InitSeed) {
            return pickupIndex;
        }
    }
    return undefined;
}
