local ____lualib = require("lualib_bundle")
local __TS__New = ____lualib.__TS__New
local __TS__ArrayFilter = ____lualib.__TS__ArrayFilter
local __TS__ArrayFind = ____lualib.__TS__ArrayFind
local ____exports = {}
local getPlayerIndexCollectibleType, DEFAULT_COLLECTIBLE_TYPE, EXCLUDED_CHARACTERS
local ____isaac_2Dtypescript_2Ddefinitions = require("isaac-typescript-definitions")
local BabySubType = ____isaac_2Dtypescript_2Ddefinitions.BabySubType
local CollectibleType = ____isaac_2Dtypescript_2Ddefinitions.CollectibleType
local PlayerType = ____isaac_2Dtypescript_2Ddefinitions.PlayerType
local PlayerVariant = ____isaac_2Dtypescript_2Ddefinitions.PlayerVariant
local ____cachedClasses = require("core.cachedClasses")
local game = ____cachedClasses.game
local ____ReadonlySet = require("types.ReadonlySet")
local ReadonlySet = ____ReadonlySet.ReadonlySet
--- Helper function to get every player with no restrictions, by using `Game.GetNumPlayers` and
-- `Isaac.GetPlayer`.
-- 
-- This function is almost never what you want to use. For most purposes, use the `getPlayers`
-- helper function instead to get a filtered list of players.
function ____exports.getAllPlayers(self)
    local numPlayers = game:GetNumPlayers()
    local players = {}
    do
        local i = 0
        while i < numPlayers do
            local player = Isaac.GetPlayer(i)
            players[#players + 1] = player
            i = i + 1
        end
    end
    return players
end
--- Mods often have to track variables relating to the player. In naive mods, information will only
-- be stored about the first player. However, in order to be robust, mods must handle up to 4
-- players playing at the same time. This means that information must be stored on a map data
-- structure. Finding a good index for these types of map data structures is difficult:
-- 
-- - We cannot use the index from `Isaac.GetPlayer(i)` since this fails in the case where there are
--   two players and the first player leaves the run.
-- - We cannot use `EntityPlayer.ControllerIndex` as an index because it fails in the case of Jacob
--   & Esau or Tainted Forgotten. It also fails in the case of a player changing their controls
--   mid-run.
-- - We cannot use `EntityPlayer.GetData().index` because it does not persist across saving and
--   continuing.
-- - We cannot use `GetPtrHash()` as an index because it does not persist across exiting and
--   relaunching the game.
-- - We cannot use `EntityPlayer.InitSeed` because it is not consistent with additional players
--   beyond the first.
-- 
-- Instead, we use the `EntityPlayer.GetCollectibleRNG` method with an arbitrary value of Sad Onion
-- (1). This works even if the player does not have any Sad Onions.
-- 
-- Note that by default, this returns the same index for both The Forgotten and The Soul. (Even
-- though they are technically different characters, they share the same inventory and `InitSeed`.)
-- If this is not desired, pass true for the `differentiateForgottenAndSoul` argument, and the RNG
-- of Spoon Bender (3) will be used for The Soul.
-- 
-- Also note that this index does not work in the `POST_PLAYER_INIT` function for players 2 through
-- 4. With that said, in almost all cases, you should be lazy-initializing your data structures in
-- other callbacks, so this should not be an issue.
function ____exports.getPlayerIndex(self, player, differentiateForgottenAndSoul)
    if differentiateForgottenAndSoul == nil then
        differentiateForgottenAndSoul = false
    end
    local playerToUse = player
    local isSubPlayer = player:IsSubPlayer()
    if isSubPlayer then
        local subPlayer = player
        local playerParent = ____exports.getSubPlayerParent(nil, subPlayer)
        if playerParent ~= nil then
            playerToUse = playerParent
        end
    end
    local collectibleType = getPlayerIndexCollectibleType(nil, player, differentiateForgottenAndSoul)
    local collectibleRNG = playerToUse:GetCollectibleRNG(collectibleType)
    local seed = collectibleRNG:GetSeed()
    return seed
end
function getPlayerIndexCollectibleType(self, player, differentiateForgottenAndSoul)
    local character = player:GetPlayerType()
    if character == PlayerType.SOUL then
        return differentiateForgottenAndSoul and CollectibleType.INNER_EYE or DEFAULT_COLLECTIBLE_TYPE
    end
    return DEFAULT_COLLECTIBLE_TYPE
end
--- This function always excludes players with a non-undefined parent, since they are not real
-- players (e.g. the Strawman Keeper).
-- 
-- If this is not desired, use the `getAllPlayers` helper function instead.
-- 
-- @param performCharacterExclusions Whether to exclude characters that are not directly controlled
-- by the player (i.e. Esau & Tainted Soul). Default is false.
function ____exports.getPlayers(self, performCharacterExclusions)
    if performCharacterExclusions == nil then
        performCharacterExclusions = false
    end
    local players = ____exports.getAllPlayers(nil)
    local nonChildPlayers = __TS__ArrayFilter(
        players,
        function(____, player) return not ____exports.isChildPlayer(nil, player) end
    )
    local nonChildPlayersFiltered = __TS__ArrayFilter(
        nonChildPlayers,
        function(____, player)
            local character = player:GetPlayerType()
            return not EXCLUDED_CHARACTERS:has(character)
        end
    )
    return performCharacterExclusions and nonChildPlayersFiltered or nonChildPlayers
end
--- Helper function to get a parent `EntityPlayer` object for a given `EntitySubPlayer` object. This
-- is useful because calling the `EntityPlayer.GetSubPlayer` method on a sub-player object will
-- return undefined.
function ____exports.getSubPlayerParent(self, subPlayer)
    local subPlayerPtrHash = GetPtrHash(subPlayer)
    local players = ____exports.getPlayers(nil)
    return __TS__ArrayFind(
        players,
        function(____, player)
            local thisPlayerSubPlayer = player:GetSubPlayer()
            if thisPlayerSubPlayer == nil then
                return false
            end
            local thisPlayerSubPlayerPtrHash = GetPtrHash(thisPlayerSubPlayer)
            return thisPlayerSubPlayerPtrHash == subPlayerPtrHash
        end
    )
end
--- Helper function to detect if a particular player is a "child" player, meaning that they have a
-- non-undefined `EntityPlayer.Parent` field. (For example, the Strawman Keeper.)
function ____exports.isChildPlayer(self, player)
    return player.Parent ~= nil
end
DEFAULT_COLLECTIBLE_TYPE = CollectibleType.SAD_ONION
EXCLUDED_CHARACTERS = __TS__New(ReadonlySet, {PlayerType.ESAU, PlayerType.SOUL_B})
--- Helper function to get all of the other players in the room besides the one provided. (This
-- includes "child" players.)
function ____exports.getOtherPlayers(self, player)
    local playerPtrHash = GetPtrHash(player)
    local players = ____exports.getAllPlayers(nil)
    return __TS__ArrayFilter(
        players,
        function(____, otherPlayer) return GetPtrHash(otherPlayer) ~= playerPtrHash end
    )
end
--- Helper function to get the corresponding `EntityPlayer` object that corresponds to a
-- `PlayerIndex`.
function ____exports.getPlayerFromIndex(self, playerIndex)
    local players = ____exports.getAllPlayers(nil)
    return __TS__ArrayFind(
        players,
        function(____, player) return ____exports.getPlayerIndex(nil, player) == playerIndex end
    )
end
--- Helper function to return the index of this player with respect to the output of the
-- `Isaac.GetPlayer` method.
-- 
-- Note that if you storing information about a player in a data structure, you never want to use
-- this index; use the `getPlayerIndex` function instead.
function ____exports.getPlayerIndexVanilla(self, playerToFind)
    local numPlayers = game:GetNumPlayers()
    local playerToFindHash = GetPtrHash(playerToFind)
    do
        local i = 0
        while i < numPlayers do
            local player = Isaac.GetPlayer(i)
            local playerHash = GetPtrHash(player)
            if playerHash == playerToFindHash then
                return i
            end
            i = i + 1
        end
    end
    return nil
end
--- Helper function to detect if a particular player is the Found Soul player provided by the
-- trinket.
function ____exports.isFoundSoul(self, player)
    return ____exports.isChildPlayer(nil, player) and player.Variant == PlayerVariant.COOP_BABY and player.SubType == BabySubType.FOUND_SOUL
end
return ____exports
