local ____lualib = require("lualib_bundle")
local __TS__ArrayMap = ____lualib.__TS__ArrayMap
local __TS__ObjectAssign = ____lualib.__TS__ObjectAssign
local __TS__ArrayEntries = ____lualib.__TS__ArrayEntries
local __TS__Iterator = ____lualib.__TS__Iterator
local ____exports = {}
local ____isaac_2Dtypescript_2Ddefinitions = require("isaac-typescript-definitions")
local CollectibleType = ____isaac_2Dtypescript_2Ddefinitions.CollectibleType
local HeartSubType = ____isaac_2Dtypescript_2Ddefinitions.HeartSubType
local PlayerType = ____isaac_2Dtypescript_2Ddefinitions.PlayerType
local TrinketType = ____isaac_2Dtypescript_2Ddefinitions.TrinketType
local ____constants = require("core.constants")
local MAX_PLAYER_HEART_CONTAINERS = ____constants.MAX_PLAYER_HEART_CONTAINERS
local ____HealthType = require("enums.HealthType")
local HealthType = ____HealthType.HealthType
local ____bitwise = require("functions.bitwise")
local countSetBits = ____bitwise.countSetBits
local getKBitOfN = ____bitwise.getKBitOfN
local getNumBitsOfN = ____bitwise.getNumBitsOfN
local ____characters = require("functions.characters")
local getCharacterMaxHeartContainers = ____characters.getCharacterMaxHeartContainers
local ____charge = require("functions.charge")
local getTotalCharge = ____charge.getTotalCharge
local ____playerCollectibles = require("functions.playerCollectibles")
local getActiveItemSlots = ____playerCollectibles.getActiveItemSlots
local setActiveItem = ____playerCollectibles.setActiveItem
local ____players = require("functions.players")
local isCharacter = ____players.isCharacter
local isKeeper = ____players.isKeeper
local ____utils = require("functions.utils")
local ____repeat = ____utils["repeat"]
--- Returns the number of black hearts that the player has, excluding any soul hearts. For example,
-- if the player has one full black heart, one full soul heart, and one half black heart, this
-- function returns 3.
-- 
-- This is different from the `EntityPlayer.GetBlackHearts` method, since that returns a bitmask.
function ____exports.getPlayerBlackHearts(self, player)
    local blackHeartsBitmask = player:GetBlackHearts()
    local blackHeartBits = countSetBits(nil, blackHeartsBitmask)
    return blackHeartBits * 2
end
--- Returns the number of red hearts that the player has, excluding any rotten hearts. For example,
-- if the player has one full black heart, one full soul heart, and one half black heart, this
-- function returns 3.
-- 
-- This is different from the `EntityPlayer.GetHearts` method, since that returns a value that
-- includes rotten hearts.
function ____exports.getPlayerHearts(self, player)
    local rottenHearts = player:GetRottenHearts()
    local hearts = player:GetHearts()
    return hearts - rottenHearts * 2
end
--- Returns the maximum heart containers that the provided player can have. Normally, this is 12, but
-- it can change depending on the character (e.g. Keeper) and other things (e.g. Mother's Kiss).
-- This function does not account for Broken Hearts; use the `getPlayerAvailableHeartSlots` helper
-- function for that.
function ____exports.getPlayerMaxHeartContainers(self, player)
    local character = player:GetPlayerType()
    local characterMaxHeartContainers = getCharacterMaxHeartContainers(nil, character)
    if character == PlayerType.MAGDALENE and player:HasCollectible(CollectibleType.BIRTHRIGHT) then
        local extraMaxHeartContainersFromBirthright = 6
        return characterMaxHeartContainers + extraMaxHeartContainersFromBirthright
    end
    if isKeeper(nil, player) then
        local numMothersKisses = player:GetTrinketMultiplier(TrinketType.MOTHERS_KISS)
        local hasGreedsGullet = player:HasCollectible(CollectibleType.GREEDS_GULLET)
        local coins = player:GetNumCoins()
        local greedsGulletCoinContainers = hasGreedsGullet and math.floor(coins / 25) or 0
        return characterMaxHeartContainers + numMothersKisses + greedsGulletCoinContainers
    end
    return characterMaxHeartContainers
end
--- Returns the number of soul hearts that the player has, excluding any black hearts. For example,
-- if the player has one full black heart, one full soul heart, and one half black heart, this
-- function returns 2.
-- 
-- This is different from the `EntityPlayer.GetSoulHearts` method, since that returns the combined
-- number of soul hearts and black hearts.
function ____exports.getPlayerSoulHearts(self, player)
    local soulHearts = player:GetSoulHearts()
    local blackHearts = ____exports.getPlayerBlackHearts(nil, player)
    return soulHearts - blackHearts
end
function ____exports.removeAllPlayerHealth(self, player)
    local goldenHearts = player:GetGoldenHearts()
    local eternalHearts = player:GetEternalHearts()
    local boneHearts = player:GetBoneHearts()
    local brokenHearts = player:GetBrokenHearts()
    player:AddGoldenHearts(goldenHearts * -1)
    player:AddEternalHearts(eternalHearts * -1)
    player:AddBoneHearts(boneHearts * -1)
    player:AddBrokenHearts(brokenHearts * -1)
    player:AddMaxHearts(MAX_PLAYER_HEART_CONTAINERS * -2, true)
    player:AddSoulHearts(MAX_PLAYER_HEART_CONTAINERS * -2)
    if isCharacter(nil, player, PlayerType.SOUL) then
        local forgotten = player:GetSubPlayer()
        if forgotten ~= nil then
            local forgottenBoneHearts = forgotten:GetBoneHearts()
            forgotten:AddBoneHearts(forgottenBoneHearts * -1)
        end
    end
end
--- Helper function to set a player's health to a specific state. You can use this in combination
-- with the `getPlayerHealth` function to restore the player's health back to a certain
-- configuration at a later time.
-- 
-- Based on the `REVEL.LoadHealth` function in the Revelations mod.
function ____exports.setPlayerHealth(self, player, playerHealth)
    local character = player:GetPlayerType()
    local subPlayer = player:GetSubPlayer()
    local alabasterBoxDescriptions = {}
    local alabasterBoxActiveSlots = getActiveItemSlots(nil, player, CollectibleType.ALABASTER_BOX)
    for ____, activeSlot in ipairs(alabasterBoxActiveSlots) do
        local totalCharge = getTotalCharge(nil, player, activeSlot)
        setActiveItem(nil, player, CollectibleType.NULL, activeSlot)
        alabasterBoxDescriptions[#alabasterBoxDescriptions + 1] = {activeSlot = activeSlot, totalCharge = totalCharge}
    end
    ____exports.removeAllPlayerHealth(nil, player)
    if character == PlayerType.SOUL and subPlayer ~= nil then
        subPlayer:AddMaxHearts(playerHealth.maxHearts, false)
    else
        player:AddMaxHearts(playerHealth.maxHearts, false)
    end
    player:AddEternalHearts(playerHealth.eternalHearts)
    local soulHeartsRemaining = playerHealth.soulHearts
    for ____, ____value in __TS__Iterator(__TS__ArrayEntries(playerHealth.soulHeartTypes)) do
        local i = ____value[1]
        local soulHeartType = ____value[2]
        local isHalf = playerHealth.soulHearts + playerHealth.boneHearts * 2 < (i + 1) * 2
        local addAmount = 2
        if isHalf or soulHeartType == HeartSubType.BONE or soulHeartsRemaining < 2 then
            addAmount = 1
        end
        repeat
            local ____switch70 = soulHeartType
            local ____cond70 = ____switch70 == HeartSubType.SOUL
            if ____cond70 then
                do
                    player:AddSoulHearts(addAmount)
                    soulHeartsRemaining = soulHeartsRemaining - addAmount
                    break
                end
            end
            ____cond70 = ____cond70 or ____switch70 == HeartSubType.BLACK
            if ____cond70 then
                do
                    player:AddBlackHearts(addAmount)
                    soulHeartsRemaining = soulHeartsRemaining - addAmount
                    break
                end
            end
            ____cond70 = ____cond70 or ____switch70 == HeartSubType.BONE
            if ____cond70 then
                do
                    player:AddBoneHearts(addAmount)
                    break
                end
            end
        until true
    end
    player:AddRottenHearts(playerHealth.rottenHearts * 2)
    if character == PlayerType.MAGDALENE_B then
        ____repeat(
            nil,
            playerHealth.hearts,
            function()
                if player:HasFullHearts() then
                    return
                end
                local hearts = player:GetHearts()
                local maxHearts = player:GetMaxHearts()
                if hearts == maxHearts - 1 then
                    player:AddHearts(1)
                    return
                end
                player:AddHearts(1)
                player:AddHearts(-1)
            end
        )
    else
        player:AddHearts(playerHealth.hearts)
    end
    player:AddGoldenHearts(playerHealth.goldenHearts)
    player:AddBrokenHearts(playerHealth.brokenHearts)
    if character == PlayerType.BETHANY then
        player:SetSoulCharge(playerHealth.soulCharges)
    elseif character == PlayerType.BETHANY_B then
        player:SetBloodCharge(playerHealth.bloodCharges)
    end
    for ____, ____value in ipairs(alabasterBoxDescriptions) do
        local activeSlot = ____value.activeSlot
        local totalCharge = ____value.totalCharge
        setActiveItem(
            nil,
            player,
            CollectibleType.ALABASTER_BOX,
            activeSlot,
            totalCharge
        )
    end
end
function ____exports.addPlayerHealthType(self, player, healthType, numHearts)
    repeat
        local ____switch3 = healthType
        local ____cond3 = ____switch3 == HealthType.RED
        if ____cond3 then
            do
                player:AddHearts(numHearts)
                break
            end
        end
        ____cond3 = ____cond3 or ____switch3 == HealthType.SOUL
        if ____cond3 then
            do
                player:AddSoulHearts(numHearts)
                break
            end
        end
        ____cond3 = ____cond3 or ____switch3 == HealthType.ETERNAL
        if ____cond3 then
            do
                player:AddEternalHearts(numHearts)
                break
            end
        end
        ____cond3 = ____cond3 or ____switch3 == HealthType.BLACK
        if ____cond3 then
            do
                player:AddBlackHearts(numHearts)
                break
            end
        end
        ____cond3 = ____cond3 or ____switch3 == HealthType.GOLDEN
        if ____cond3 then
            do
                player:AddGoldenHearts(numHearts)
                break
            end
        end
        ____cond3 = ____cond3 or ____switch3 == HealthType.BONE
        if ____cond3 then
            do
                player:AddBoneHearts(numHearts)
                break
            end
        end
        ____cond3 = ____cond3 or ____switch3 == HealthType.ROTTEN
        if ____cond3 then
            do
                player:AddRottenHearts(numHearts)
                break
            end
        end
        ____cond3 = ____cond3 or ____switch3 == HealthType.BROKEN
        if ____cond3 then
            do
                player:AddBrokenHearts(numHearts)
                break
            end
        end
        ____cond3 = ____cond3 or ____switch3 == HealthType.MAX_HEARTS
        if ____cond3 then
            do
                player:AddMaxHearts(numHearts, false)
                break
            end
        end
    until true
end
--- Helper function to see if the provided player can pick up an eternal heart. (If a player already
-- has an eternal heart and full heart containers, they are not able to pick up any additional
-- eternal hearts.)
-- 
-- This function's name matches the existing `EntityPlayer` methods.
function ____exports.canPickEternalHearts(self, player)
    local eternalHearts = player:GetEternalHearts()
    local maxHearts = player:GetMaxHearts()
    local heartLimit = player:GetHeartLimit()
    return eternalHearts == 0 or maxHearts ~= heartLimit
end
--- Returns whether all of the player's soul-heart-type hearts are black hearts.
-- 
-- Note that this function does not consider red heart containers.
-- 
-- For example:
-- 
-- - If the player has one black heart, this function would return true.
-- - If the player has one soul heart and two black hearts, this function would return false.
-- - If the player has no black hearts, this function will return false.
-- - If the player has one red heart container and three black hearts, this function would return
--   true.
function ____exports.doesPlayerHaveAllBlackHearts(self, player)
    local soulHearts = ____exports.getPlayerSoulHearts(nil, player)
    local blackHearts = ____exports.getPlayerBlackHearts(nil, player)
    return blackHearts > 0 and soulHearts == 0
end
--- Returns whether all of the player's soul-heart-type hearts are soul hearts.
-- 
-- Note that this function does not consider red heart containers.
-- 
-- For example:
-- 
-- - If the player has two soul hearts and one black heart, this function would return false.
-- - If the player has no soul hearts, this function will return false.
-- - If the player has one red heart container and three soul hearts, this function would return
--   true.
function ____exports.doesPlayerHaveAllSoulHearts(self, player)
    local soulHearts = ____exports.getPlayerSoulHearts(nil, player)
    local blackHearts = ____exports.getPlayerBlackHearts(nil, player)
    return soulHearts > 0 and blackHearts == 0
end
--- Returns the number of slots that the player has remaining for new heart containers, accounting
-- for broken hearts. For example, if the player is Judas and has 1 red heart containers and 2 full
-- soul hearts and 3 broken hearts, then this function would return 6 (i.e. 12 - 1 - 2 - 3).
function ____exports.getPlayerAvailableHeartSlots(self, player)
    local maxHeartContainers = ____exports.getPlayerMaxHeartContainers(nil, player)
    local effectiveMaxHearts = player:GetEffectiveMaxHearts()
    local normalAndBoneHeartContainers = effectiveMaxHearts / 2
    local soulHearts = player:GetSoulHearts()
    local soulHeartContainers = math.ceil(soulHearts / 2)
    local totalHeartContainers = normalAndBoneHeartContainers + soulHeartContainers
    local brokenHearts = player:GetBrokenHearts()
    local totalOccupiedHeartSlots = totalHeartContainers + brokenHearts
    return maxHeartContainers - totalOccupiedHeartSlots
end
--- Helper function to get an object representing the player's health. You can use this in
-- combination with the `setPlayerHealth` function to restore the player's health back to a certain
-- configuration at a later time.
-- 
-- This is based on the `REVEL.StoreHealth` function in the Revelations mod.
function ____exports.getPlayerHealth(self, player)
    local character = player:GetPlayerType()
    local maxHearts = player:GetMaxHearts()
    local hearts = ____exports.getPlayerHearts(nil, player)
    local soulHearts = player:GetSoulHearts()
    local boneHearts = player:GetBoneHearts()
    local goldenHearts = player:GetGoldenHearts()
    local eternalHearts = player:GetEternalHearts()
    local rottenHearts = player:GetRottenHearts()
    local brokenHearts = player:GetBrokenHearts()
    local subPlayer = player:GetSubPlayer()
    local soulCharges = player:GetEffectiveSoulCharge()
    local bloodCharges = player:GetEffectiveBloodCharge()
    if character == PlayerType.FORGOTTEN and subPlayer ~= nil then
        maxHearts = boneHearts * 2
        boneHearts = 0
        soulHearts = subPlayer:GetSoulHearts()
    elseif character == PlayerType.SOUL and subPlayer ~= nil then
        maxHearts = subPlayer:GetBoneHearts() * 2
        hearts = subPlayer:GetHearts()
    end
    local extraHearts = math.ceil(soulHearts / 2) + boneHearts
    local currentSoulHeart = 0
    local soulHeartTypes = {}
    do
        local i = 0
        while i < extraHearts do
            local isBoneHeart = player:IsBoneHeart(i)
            if character == PlayerType.FORGOTTEN and subPlayer ~= nil then
                isBoneHeart = subPlayer:IsBoneHeart(i)
            end
            if isBoneHeart then
                soulHeartTypes[#soulHeartTypes + 1] = HeartSubType.BONE
            else
                local isBlackHeart = player:IsBlackHeart(currentSoulHeart + 1)
                if character == PlayerType.FORGOTTEN and subPlayer ~= nil then
                    isBlackHeart = subPlayer:IsBlackHeart(currentSoulHeart + 1)
                end
                if isBlackHeart then
                    soulHeartTypes[#soulHeartTypes + 1] = HeartSubType.BLACK
                else
                    soulHeartTypes[#soulHeartTypes + 1] = HeartSubType.SOUL
                end
                currentSoulHeart = currentSoulHeart + 2
            end
            i = i + 1
        end
    end
    return {
        maxHearts = maxHearts,
        hearts = hearts,
        eternalHearts = eternalHearts,
        soulHearts = soulHearts,
        boneHearts = boneHearts,
        goldenHearts = goldenHearts,
        rottenHearts = rottenHearts,
        brokenHearts = brokenHearts,
        soulCharges = soulCharges,
        bloodCharges = bloodCharges,
        soulHeartTypes = soulHeartTypes
    }
end
function ____exports.getPlayerHealthType(self, player, healthType)
    repeat
        local ____switch29 = healthType
        local ____cond29 = ____switch29 == HealthType.RED
        if ____cond29 then
            do
                return ____exports.getPlayerHearts(nil, player)
            end
        end
        ____cond29 = ____cond29 or ____switch29 == HealthType.SOUL
        if ____cond29 then
            do
                return ____exports.getPlayerSoulHearts(nil, player)
            end
        end
        ____cond29 = ____cond29 or ____switch29 == HealthType.ETERNAL
        if ____cond29 then
            do
                return player:GetEternalHearts()
            end
        end
        ____cond29 = ____cond29 or ____switch29 == HealthType.BLACK
        if ____cond29 then
            do
                return ____exports.getPlayerBlackHearts(nil, player)
            end
        end
        ____cond29 = ____cond29 or ____switch29 == HealthType.GOLDEN
        if ____cond29 then
            do
                return player:GetGoldenHearts()
            end
        end
        ____cond29 = ____cond29 or ____switch29 == HealthType.BONE
        if ____cond29 then
            do
                return player:GetBoneHearts()
            end
        end
        ____cond29 = ____cond29 or ____switch29 == HealthType.ROTTEN
        if ____cond29 then
            do
                return player:GetRottenHearts()
            end
        end
        ____cond29 = ____cond29 or ____switch29 == HealthType.BROKEN
        if ____cond29 then
            do
                return player:GetBrokenHearts()
            end
        end
        ____cond29 = ____cond29 or ____switch29 == HealthType.MAX_HEARTS
        if ____cond29 then
            do
                return player:GetMaxHearts()
            end
        end
    until true
end
--- Helper function that returns the type of the rightmost heart. This does not including golden
-- hearts or broken hearts, since they cannot be damaged directly.
function ____exports.getPlayerLastHeart(self, player)
    local hearts = player:GetHearts()
    local effectiveMaxHearts = player:GetEffectiveMaxHearts()
    local soulHearts = player:GetSoulHearts()
    local blackHearts = player:GetBlackHearts()
    local eternalHearts = player:GetEternalHearts()
    local boneHearts = player:GetBoneHearts()
    local rottenHearts = player:GetRottenHearts()
    local soulHeartSlots = soulHearts / 2
    local lastHeartIndex = boneHearts + soulHeartSlots - 1
    local isLastHeartBone = player:IsBoneHeart(lastHeartIndex)
    if isLastHeartBone then
        local isLastContainerEmpty = hearts <= effectiveMaxHearts - 2
        if isLastContainerEmpty then
            return HealthType.BONE
        end
        if rottenHearts > 0 then
            return HealthType.ROTTEN
        end
        if eternalHearts > 0 then
            return HealthType.ETERNAL
        end
        return HealthType.RED
    end
    if soulHearts > 0 then
        local numBits = getNumBitsOfN(nil, blackHearts)
        local finalBit = getKBitOfN(nil, numBits - 1, blackHearts)
        local isBlack = finalBit == 1
        if isBlack then
            return HealthType.BLACK
        end
        return HealthType.SOUL
    end
    if eternalHearts > 0 then
        return HealthType.ETERNAL
    end
    if rottenHearts > 0 then
        return HealthType.ROTTEN
    end
    return HealthType.RED
end
--- Helper function to determine how many heart containers that Tainted Magdalene has that will not
-- be automatically depleted over time. By default, this is 2, but this function will return 4 so
-- that it is consistent with the `player.GetHearts` and `player.GetMaxHearts` methods.
-- 
-- If Tainted Magdalene has Birthright, she will gained an additional non-temporary heart container.
-- 
-- This function does not validate whether the provided player is Tainted Magdalene; that should be
-- accomplished before invoking this function.
function ____exports.getTaintedMagdaleneNonTemporaryMaxHearts(self, player)
    local maxHearts = player:GetMaxHearts()
    local hasBirthright = player:HasCollectible(CollectibleType.BIRTHRIGHT)
    local maxNonTemporaryMaxHearts = hasBirthright and 6 or 4
    return math.min(maxHearts, maxNonTemporaryMaxHearts)
end
--- Returns a `PlayerHealth` object with all zeros.
function ____exports.newPlayerHealth(self)
    return {
        maxHearts = 0,
        hearts = 0,
        eternalHearts = 0,
        soulHearts = 0,
        boneHearts = 0,
        goldenHearts = 0,
        rottenHearts = 0,
        brokenHearts = 0,
        soulCharges = 0,
        bloodCharges = 0,
        soulHeartTypes = {}
    }
end
--- Helper function to remove all of a player's black hearts and add the corresponding amount of soul
-- hearts.
function ____exports.playerConvertBlackHeartsToSoulHearts(self, player)
    local playerHealth = ____exports.getPlayerHealth(nil, player)
    ____exports.removeAllPlayerHealth(nil, player)
    local newSoulHeartTypes = __TS__ArrayMap(
        playerHealth.soulHeartTypes,
        function(____, soulHeartType) return soulHeartType == HeartSubType.BLACK and HeartSubType.SOUL or soulHeartType end
    )
    local playerHealthWithSoulHearts = __TS__ObjectAssign({}, playerHealth, {soulHeartTypes = newSoulHeartTypes})
    ____exports.setPlayerHealth(nil, player, playerHealthWithSoulHearts)
end
--- Helper function to remove all of a player's soul hearts and add the corresponding amount of black
-- hearts.
function ____exports.playerConvertSoulHeartsToBlackHearts(self, player)
    local playerHealth = ____exports.getPlayerHealth(nil, player)
    ____exports.removeAllPlayerHealth(nil, player)
    local newSoulHeartTypes = __TS__ArrayMap(
        playerHealth.soulHeartTypes,
        function(____, soulHeartType) return soulHeartType == HeartSubType.SOUL and HeartSubType.BLACK or soulHeartType end
    )
    local playerHealthWithBlackHearts = __TS__ObjectAssign({}, playerHealth, {soulHeartTypes = newSoulHeartTypes})
    ____exports.setPlayerHealth(nil, player, playerHealthWithBlackHearts)
end
--- Helper function to see if the player is out of health.
-- 
-- Specifically, this function will return false if the player has 0 red hearts, 0 soul/black
-- hearts, and 0 bone hearts.
function ____exports.playerHasHealthLeft(self, player)
    local hearts = player:GetHearts()
    local soulHearts = player:GetSoulHearts()
    local boneHearts = player:GetBoneHearts()
    return hearts > 0 or soulHearts > 0 or boneHearts > 0
end
--- Helper function to see if a certain damage amount would deal "permanent" damage to Tainted
-- Magdalene.
-- 
-- Tainted Magdalene has "permanent" health and "temporary" health. When standing still and doing
-- nothing, all of Tainted Magdalene's temporary health will eventually go away.
-- 
-- Before using this function, it is expected that you check to see if the player is Tainted
-- Magdalene first, or else it will give a nonsensical result.
function ____exports.wouldDamageTaintedMagdaleneNonTemporaryHeartContainers(self, player, damageAmount)
    local soulHearts = player:GetSoulHearts()
    if soulHearts > 0 then
        return false
    end
    local boneHearts = player:GetBoneHearts()
    if boneHearts > 0 then
        return false
    end
    local hearts = player:GetHearts()
    local rottenHearts = player:GetRottenHearts()
    local effectiveDamageAmount = damageAmount + math.min(rottenHearts, damageAmount)
    local heartsAfterDamage = hearts - effectiveDamageAmount
    local nonTemporaryMaxHearts = ____exports.getTaintedMagdaleneNonTemporaryMaxHearts(nil, player)
    return heartsAfterDamage < nonTemporaryMaxHearts
end
return ____exports
