"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.RunInNFrames = 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 array_1 = require("../../../functions/array");
const run_1 = require("../../../functions/run");
const Feature_1 = require("../../private/Feature");
const v = {
    run: {
        queuedGameFunctions: [],
        queuedRenderFunctions: [],
        intervalGameFunctions: [],
        intervalRenderFunctions: [],
    },
};
class RunInNFrames extends Feature_1.Feature {
    /** @internal */
    v = v;
    vConditionalFunc = () => false;
    roomHistory;
    /** @internal */
    constructor(roomHistory) {
        super();
        this.featuresUsed = [ISCFeature_1.ISCFeature.ROOM_HISTORY];
        this.callbacksUsed = [
            // 1
            [isaac_typescript_definitions_1.ModCallback.POST_UPDATE, this.postUpdate],
            // 2
            [isaac_typescript_definitions_1.ModCallback.POST_RENDER, this.postRender],
        ];
        this.roomHistory = roomHistory;
    }
    // ModCallback.POST_UPDATE (1)
    postUpdate = () => {
        const gameFrameCount = cachedClasses_1.game.GetFrameCount();
        const numRoomsEntered = this.roomHistory.getNumRoomsEntered();
        checkExecuteQueuedFunctions(v.run.queuedGameFunctions, gameFrameCount, numRoomsEntered);
        checkExecuteIntervalFunctions(v.run.intervalGameFunctions, gameFrameCount, numRoomsEntered);
    };
    // ModCallback.POST_RENDER (2)
    postRender = () => {
        const renderFrameCount = Isaac.GetFrameCount();
        const numRoomsEntered = this.roomHistory.getNumRoomsEntered();
        checkExecuteQueuedFunctions(v.run.queuedRenderFunctions, renderFrameCount, numRoomsEntered);
        checkExecuteIntervalFunctions(v.run.intervalRenderFunctions, renderFrameCount, numRoomsEntered);
    };
    /**
     * Helper function to restart on the next render frame. Useful because it is impossible to restart
     * the game inside of the `POST_NEW_ROOM`, `POST_NEW_LEVEL`, or `POST_GAME_STARTED` callbacks when
     * a run is first starting.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.RUN_IN_N_FRAMES`.
     *
     * @param character Optional. If specified, will restart the game as the specified character.
     * @public
     */
    restartNextRenderFrame(character) {
        this.runNextRenderFrame(() => {
            (0, run_1.restart)(character);
        });
    }
    /**
     * Supply a function to run N game frames from now in the `POST_UPDATE` callback.
     *
     * For a usage example, see the documentation for the `runNextGameFrame`, which is used in a
     * similar way.
     *
     * Note that this function will not handle saving and quitting. If a player saving and quitting
     * before the deferred function fires would cause a bug in your mod, then you should handle
     * deferred functions manually using serializable data.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.RUN_IN_N_FRAMES`.
     *
     * @param func The function to run.
     * @param numGameFrames The amount of game frames to wait before running the function.
     * @param cancelIfRoomChanges Optional. Whether to cancel running the function if a new room is
     *                            loaded in the interim. Default is false.
     * @public
     */
    runInNGameFrames(func, numGameFrames, cancelIfRoomChanges = false) {
        const gameFrameCount = cachedClasses_1.game.GetFrameCount();
        const numRoomsEntered = this.roomHistory.getNumRoomsEntered();
        const frameCountToFire = gameFrameCount + numGameFrames;
        const queuedFunction = {
            func,
            frameCountToFire,
            numRoomsEntered,
            cancelIfRoomChanges,
        };
        v.run.queuedGameFunctions.push(queuedFunction);
    }
    /**
     * Supply a function to run N render frames from now in the `POST_RENDER` callback.
     *
     * For a usage example, see the documentation for the `runNextGameFrame`, which is used in a
     * similar way.
     *
     * Note that this function will not handle saving and quitting. If a player saving and quitting
     * before the deferred function fires would cause a bug in your mod, then you should handle
     * deferred functions manually using serializable data.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.RUN_IN_N_FRAMES`.
     *
     * @param func The function to run.
     * @param numRenderFrames The amount of render frames to wait before running the function.
     * @param cancelIfRoomChanges Optional. Whether to cancel running the function if a new room is
     *                            loaded in the interim. Default is false.
     * @public
     */
    runInNRenderFrames(func, numRenderFrames, cancelIfRoomChanges = false) {
        const renderFrameCount = Isaac.GetFrameCount();
        const numRoomsEntered = this.roomHistory.getNumRoomsEntered();
        const frameCountToFire = renderFrameCount + numRenderFrames;
        const queuedFunction = {
            func,
            frameCountToFire,
            numRoomsEntered,
            cancelIfRoomChanges,
        };
        v.run.queuedRenderFunctions.push(queuedFunction);
    }
    /**
     * Supply a function to run on the next `POST_UPDATE` callback.
     *
     * For example:
     *
     * ```ts
     * const NUM_EXPLODER_EXPLOSIONS = 5;
     *
     * function useItemExploder(player: EntityPlayer) {
     *   playSound("exploderBegin");
     *   explode(player, NUM_EXPLODER_EXPLOSIONS);
     * }
     *
     * function explode(player: EntityPlayer, numFramesLeft: int) {
     *   Isaac.Explode(player, undefined, 1);
     *   numFramesLeft -= 1;
     *   if (numFramesLeft === 0) {
     *     runNextFrame(() => {
     *       explode(player, numFramesLeft);
     *     });
     *   }
     * }
     * ```
     *
     * Note that this function will not handle saving and quitting. If a player saving and quitting
     * before the deferred function fires would cause a bug in your mod, then you should handle
     * deferred functions manually using serializable data.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.RUN_IN_N_FRAMES`.
     *
     * @param func The function to run.
     * @param cancelIfRoomChanges Optional. Whether to cancel running the function if a new room is
     *                            loaded in the interim. Default is false.
     * @public
     */
    runNextGameFrame(func, cancelIfRoomChanges = false) {
        this.runInNGameFrames(func, 1, cancelIfRoomChanges);
    }
    /**
     * Supply a function to run on the next `POST_RENDER` callback.
     *
     * For a usage example, see the documentation for the `runNextGameFrame`, which is used in a
     * similar way.
     *
     * Note that this function will not handle saving and quitting.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.RUN_IN_N_FRAMES`.
     *
     * @param func The function to run.
     * @param cancelIfRoomChanges Optional. Whether to cancel running the function if a new room is
     *                            loaded in the interim. Default is false.
     * @public
     */
    runNextRenderFrame(func, cancelIfRoomChanges = false) {
        this.runInNRenderFrames(func, 1, cancelIfRoomChanges);
    }
    /**
     * Supply a function to be repeatedly run on an interval of N game frames in the `POST_UPDATE`
     * callback. The function will continue to be fired until `false` is returned from the function.
     *
     * This is similar to the `setInterval` vanilla JavaScript function, except there is no
     * corresponding `clearInterval` function. (Instead, the return value from the supplied function
     * is used to stop the interval.)
     *
     * Note that this function will not handle saving and quitting. You must manually restart any
     * intervals if the player saves and quits in the middle of a run.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.RUN_IN_N_FRAMES`.
     *
     * @param func The function to repeatedly run on an interval.
     * @param numGameFrames The amount of game frames to wait between each run.
     * @param runImmediately Whether to execute the function right now before waiting for the
     *                       interval.
     * @param cancelIfRoomChanges Optional. Whether to cancel running the function if a new room is
     *                            loaded in the interim. Default is false.
     * @public
     */
    setIntervalGameFrames(func, numGameFrames, runImmediately, cancelIfRoomChanges = false) {
        if (runImmediately) {
            const returnValue = func();
            if (!returnValue) {
                return;
            }
        }
        const gameFrameCount = cachedClasses_1.game.GetFrameCount();
        const numRoomsEntered = this.roomHistory.getNumRoomsEntered();
        const intervalFunction = {
            func,
            frameCountToFire: gameFrameCount + numGameFrames,
            numRoomsEntered,
            cancelIfRoomChanges,
            numIntervalFrames: numGameFrames,
        };
        v.run.intervalGameFunctions.push(intervalFunction);
    }
    /**
     * Supply a function to be repeatedly run on an interval of N render frames in the `POST_RENDER`
     * callback. The function will continue to be fired until `false` is returned from the function.
     *
     * This is similar to the `setInterval` vanilla JavaScript function, except there is no
     * corresponding `clearInterval` function. (Instead, the return value from the supplied function
     * is used to stop the interval.)
     *
     * Note that this function will not handle saving and quitting. You must manually restart any
     * intervals if the player saves and quits in the middle of a run.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.RUN_IN_N_FRAMES`.
     *
     * @param func The function to repeatedly run on an interval.
     * @param numRenderFrames The amount of game frames to wait between each run.
     * @param runImmediately Whether to execute the function right now before waiting for the
     *                       interval.
     * @param cancelIfRoomChanges Optional. Whether to cancel running the function if a new room is
     *                            loaded in the interim. Default is false.
     * @public
     */
    setIntervalRenderFrames(func, numRenderFrames, runImmediately, cancelIfRoomChanges = false) {
        if (runImmediately) {
            const returnValue = func();
            if (!returnValue) {
                return;
            }
        }
        const renderFrameCount = Isaac.GetFrameCount();
        const numRoomsEntered = this.roomHistory.getNumRoomsEntered();
        const intervalFunction = {
            func,
            frameCountToFire: renderFrameCount + numRenderFrames,
            numRoomsEntered,
            cancelIfRoomChanges,
            numIntervalFrames: numRenderFrames,
        };
        v.run.intervalRenderFunctions.push(intervalFunction);
    }
}
exports.RunInNFrames = RunInNFrames;
__decorate([
    decorators_1.Exported
], RunInNFrames.prototype, "restartNextRenderFrame", null);
__decorate([
    decorators_1.Exported
], RunInNFrames.prototype, "runInNGameFrames", null);
__decorate([
    decorators_1.Exported
], RunInNFrames.prototype, "runInNRenderFrames", null);
__decorate([
    decorators_1.Exported
], RunInNFrames.prototype, "runNextGameFrame", null);
__decorate([
    decorators_1.Exported
], RunInNFrames.prototype, "runNextRenderFrame", null);
__decorate([
    decorators_1.Exported
], RunInNFrames.prototype, "setIntervalGameFrames", null);
__decorate([
    decorators_1.Exported
], RunInNFrames.prototype, "setIntervalRenderFrames", null);
function checkExecuteQueuedFunctions(
// eslint-disable-next-line isaacscript/prefer-readonly-parameter-types
queuedFunctions, frameCount, newNumRoomsEntered) {
    const firingFunctions = queuedFunctions.filter(({ frameCountToFire }) => frameCount >= frameCountToFire);
    for (const firingFunction of firingFunctions) {
        const { func, cancelIfRoomChanges, numRoomsEntered } = firingFunction;
        if (!cancelIfRoomChanges || numRoomsEntered === newNumRoomsEntered) {
            func();
        }
        (0, array_1.arrayRemoveInPlace)(queuedFunctions, firingFunction);
    }
}
function checkExecuteIntervalFunctions(
// eslint-disable-next-line isaacscript/prefer-readonly-parameter-types
intervalFunctions, frameCount, newNumRoomsEntered) {
    const firingFunctions = intervalFunctions.filter(({ frameCountToFire }) => frameCount >= frameCountToFire);
    for (const firingFunction of firingFunctions) {
        const { func, cancelIfRoomChanges, numRoomsEntered, numIntervalFrames } = firingFunction;
        let returnValue = false;
        if (!cancelIfRoomChanges || numRoomsEntered === newNumRoomsEntered) {
            returnValue = func();
        }
        (0, array_1.arrayRemoveInPlace)(intervalFunctions, firingFunction);
        // Queue the next interval (as long as the function did not return false).
        if (returnValue) {
            const newIntervalFunction = {
                func,
                frameCountToFire: frameCount + numIntervalFrames,
                numRoomsEntered,
                cancelIfRoomChanges,
                numIntervalFrames,
            };
            intervalFunctions.push(newIntervalFunction);
        }
    }
}
