local LocalPlayerController = require("TOC/Controllers/LocalPlayerController")
local DataController = require("TOC/Controllers/DataController")

local CachedDataHandler = require("TOC/Handlers/CachedDataHandler")
local CommonMethods = require("TOC/CommonMethods")
local StaticData = require("TOC/StaticData")

local OverridenMethodsArchive = require("TOC/OverridenMethodsArchive")
-----------------
---@class LimitActionsController
local LimitActionsController = {}


--* DISABLE WEARING CERTAIN ITEMS WHEN NO LIMB

function LimitActionsController.CheckLimbFeasibility(limbName)
    local dcInst = DataController.GetInstance()
    local isFeasible = not dcInst:getIsCut(limbName) or dcInst:getIsProstEquipped(limbName)
    --TOC_DEBUG.print("isFeasible="..tostring(isFeasible))
    return isFeasible
end

---@param obj any
---@param wrappedFunc function
---@param item InventoryItem
---@return boolean
function LimitActionsController.WrapClothingAction(obj, wrappedFunc, item)
    local isEquippable = wrappedFunc(obj)
    if not isEquippable then return isEquippable end

    local itemBodyLoc = item:getBodyLocation()

    local limbToCheck = StaticData.AFFECTED_BODYLOCS_TO_LIMBS_IND_STR[itemBodyLoc]
    if LimitActionsController.CheckLimbFeasibility(limbToCheck) then return isEquippable else return false end
end



-- We need to override when the player changes key binds manually to be sure that TOC changes are re-applied
local og_MainOptions_apply = MainOptions.apply
function MainOptions:apply(closeAfter)
    og_MainOptions_apply(self, closeAfter)
    CachedDataHandler.OverrideBothHandsFeasibility()
end


--------------------------------------------
--* TIMED ACTIONS 
--* We want to be able to modify how long actions are gonna take,
--* depending on amputation status and kind of action. Also, when the
--* player has not completely cicatrized their own wounds, and try to do any action with
--* a prosthesis on, that can trigger random bleeds.

local function CheckHandFeasibility(limbName)
    TOC_DEBUG.print("Checking hand feasibility: " .. limbName)
    local dcInst = DataController.GetInstance()

    local isFeasible = not dcInst:getIsCut(limbName) or dcInst:getIsProstEquipped(limbName)
    TOC_DEBUG.print("isFeasible: " .. tostring(isFeasible))
    return isFeasible
end


--* Time to perform actions overrides
local og_ISBaseTimedAction_adjustMaxTime = ISBaseTimedAction.adjustMaxTime
--- Adjust time
---@diagnostic disable-next-line: duplicate-set-field
function ISBaseTimedAction:adjustMaxTime(maxTime)
    local time = og_ISBaseTimedAction_adjustMaxTime(self, maxTime)
    --TOC_DEBUG.print("Running override for adjustMaxTime")
    -- Exceptions handling, if we find that parameter then we just use the original time
    local actionsQueue = ISTimedActionQueue.getTimedActionQueue(getPlayer())

    if actionsQueue and actionsQueue.current and actionsQueue.current.skipTOC then
        TOC_DEBUG.print("Should skip TOC stuff")
        return time
    end

    -- Action is valid, check if we have any cut limb and then modify maxTime
    local dcInst = DataController.GetInstance()
    if time ~= -1 and dcInst and dcInst:getIsAnyLimbCut() then
        --TOC_DEBUG.print("Overriding adjustMaxTime")
        local pl = getPlayer()
        local amputatedLimbs = CachedDataHandler.GetAmputatedLimbs(pl:getUsername())

        for k, _ in pairs(amputatedLimbs) do
            local limbName = k
            local perkAmp = Perks["Side_" .. CommonMethods.GetSide(limbName)]
            local perkLevel = pl:getPerkLevel(perkAmp)

            if dcInst:getIsProstEquipped(limbName) then
                -- TODO We should separate this in multiple perks, since this is gonna be a generic familiarity and could make no actual sense
                local perkProst = Perks["ProstFamiliarity"]
                perkLevel = perkLevel + pl:getPerkLevel(perkProst)
            end

            local perkLevelScaled
            if perkLevel ~= 0 then perkLevelScaled = perkLevel / 10 else perkLevelScaled = 0 end
            TOC_DEBUG.print("Perk Level: " .. tostring(perkLevel))
            TOC_DEBUG.print("OG time: " .. tostring(time))

            -- Modified Time shouldn't EVER be lower compared to the og one.
            local modifiedTime = time * (StaticData.LIMBS_TIME_MULTIPLIER_IND_NUM[limbName] - perkLevelScaled)

            if modifiedTime >= time then
                time = modifiedTime
            end

            --TOC_DEBUG.print("Modified time: " .. tostring(time))
        end

    end

    --TOC_DEBUG.print("New time with amputations: " .. tostring(time))
    return time
end

--* Random bleeding during cicatrization + Perks leveling override
local og_ISBaseTimedAction_perform = ISBaseTimedAction.perform
--- After each action, level up perks
---@diagnostic disable-next-line: duplicate-set-field
function ISBaseTimedAction:perform()
    og_ISBaseTimedAction_perform(self)


    --TOC_DEBUG.print("Running ISBaseTimedAction.perform override")
    --TOC_DEBUG.print("max time: " .. tostring(self.maxTime))

    local dcInst = DataController.GetInstance()
    if not dcInst:getIsAnyLimbCut() or self.noExp then return end


    --* LEVELING   
    -- First check level of perks. if already at max, skip
    local amputatedLimbs = CachedDataHandler.GetAmputatedLimbs(LocalPlayerController.username)
    local xp = self.maxTime / 100
    -- TODO Exp should be added while doing the action, not after it's done

    -- Prevent xp from being negative and decreasing perks
    if xp < 0 then xp = 0 end
    for k, _ in pairs(amputatedLimbs) do
        local limbName = k

        -- We're checking for only "visible" amputations to prevent from having bleeds everywhere
        if dcInst:getIsCut(limbName) and dcInst:getIsVisible(limbName) then
            local side = CommonMethods.GetSide(limbName)

            local ampPerk = Perks["Side_" .. side]
            local ampPerkLevel = LocalPlayerController.playerObj:getPerkLevel(ampPerk)

            if ampPerkLevel < 10 then
                --TOC_DEBUG.print("Levelling")
                LocalPlayerController.playerObj:getXp():AddXP(ampPerk, xp)
            end


            -- Level up prosthesis perk
            if dcInst:getIsProstEquipped(limbName) then
                local prostPerk = Perks["ProstFamiliarity"]
                local prostPerkLevel = LocalPlayerController.playerObj:getPerkLevel(prostPerk)
                if prostPerkLevel < 10 then
                    LocalPlayerController.playerObj:getXp():AddXP(prostPerk, xp)
                end
            end

            -- Bleeding when not cicatrized
            if not dcInst:getIsCicatrized(limbName) and dcInst:getIsProstEquipped(limbName) then
                --TOC_DEBUG.print("Trying for bleed, player met the criteria")
                LocalPlayerController.TryRandomBleed(self.character, limbName)
            end
        end
    end
end

--* EQUIPPING ITEMS *--
-- Check wheter the player can equip items or not, for example dual wielding when you only have one
-- hand (and no prosthesis) should be disabled. Same thing for some werable items, like watches.

---@class ISEquipWeaponAction
---@field character IsoPlayer

--* Equipping items overrides *--
local og_ISEquipWeaponAction_isValid = ISEquipWeaponAction.isValid
---Add a condition to check the feasibility of having 2 handed weapons or if both arms are cut off
---@return boolean?
---@diagnostic disable-next-line: duplicate-set-field
function ISEquipWeaponAction:isValid()
    local isValid = og_ISEquipWeaponAction_isValid(self)
    if isValid then
        local isPrimaryHandValid = CachedDataHandler.GetHandFeasibility(StaticData.SIDES_IND_STR.R)
        local isSecondaryHandValid = CachedDataHandler.GetHandFeasibility(StaticData.SIDES_IND_STR.L)
        -- Both hands are cut off, so it's impossible to equip in any way

        --TOC_DEBUG.print("isPrimaryHandValid : " .. tostring(isPrimaryHandValid))
        --TOC_DEBUG.print("isSecondaryHandValid : " .. tostring(isSecondaryHandValid))

        if not isPrimaryHandValid and not isSecondaryHandValid then
            isValid = false
        end
    end
    return isValid
end

---A recreation of the original method, but with amputations in mind
function ISEquipWeaponAction:performWithAmputation()
    TOC_DEBUG.print("running ISEquipWeaponAction performWithAmputation")
    local hand = nil
    local otherHand = nil
    local getMethodFirst = nil
    local setMethodFirst = nil
    local getMethodSecond = nil
    local setMethodSecond = nil

    if self.primary then
        hand = StaticData.LIMBS_IND_STR.Hand_R
        otherHand = StaticData.LIMBS_IND_STR.Hand_L
        getMethodFirst = self.character.getSecondaryHandItem
        setMethodFirst = self.character.setSecondaryHandItem
        getMethodSecond = self.character.getPrimaryHandItem
        setMethodSecond = self.character.setPrimaryHandItem
    else
        hand = StaticData.LIMBS_IND_STR.Hand_L
        otherHand = StaticData.LIMBS_IND_STR.Hand_R
        getMethodFirst = self.character.getPrimaryHandItem
        setMethodFirst = self.character.setPrimaryHandItem
        getMethodSecond = self.character.getSecondaryHandItem
        setMethodSecond = self.character.setSecondaryHandItem
    end

    local isFirstValid = CheckHandFeasibility(hand)
    local isSecondValid = CheckHandFeasibility(otherHand)


    if not self.twoHands then
        if getMethodFirst(self.character) and getMethodFirst(self.character):isRequiresEquippedBothHands() then
            setMethodFirst(self.character, nil)
            -- if this weapon is already equiped in the 2nd hand, we remove it
        elseif (getMethodFirst(self.character) == self.item or getMethodFirst(self.character) == getMethodSecond(self.character)) then
            setMethodFirst(self.character, nil)
            -- if we are equipping a handgun and there is a weapon in the secondary hand we remove it
        elseif instanceof(self.item, "HandWeapon") and self.item:getSwingAnim() and self.item:getSwingAnim() == "Handgun" then
            if getMethodFirst(self.character) and instanceof(getMethodFirst(self.character), "HandWeapon") then
                setMethodFirst(self.character, nil)
            end
        else
            setMethodSecond(self.character, nil)
            -- TODO We should use the CachedData indexable instead of dcInst

            if isFirstValid then
                setMethodSecond(self.character, self.item)
                -- Check other HAND!
            elseif isSecondValid then
                setMethodFirst(self.character, self.item)
            end
        end
    else
        setMethodFirst(self.character, nil)
        setMethodSecond(self.character, nil)

        -- TOC_DEBUG.print("First Hand: " .. tostring(hand))
        -- --TOC_DEBUG.print("Prost Group: " .. tostring(prostGroup))
        -- TOC_DEBUG.print("Other Hand: " .. tostring(otherHand))
        -- --TOC_DEBUG.print("Other Prost Group: " .. tostring(otherProstGroup))

        -- TOC_DEBUG.print("isPrimaryHandValid: " .. tostring(isFirstValid))
        -- TOC_DEBUG.print("isSecondaryHandValid: " .. tostring(isSecondValid))


        if isFirstValid then
            setMethodSecond(self.character, self.item)
        end

        if isSecondValid then
            setMethodFirst(self.character, self.item)
        end
    end
end

local og_ISEquipWeaponAction_perform = ISEquipWeaponAction.perform
---@diagnostic disable-next-line: duplicate-set-field
function ISEquipWeaponAction:perform()
    og_ISEquipWeaponAction_perform(self)


    --if self.character == getPlayer() then
    local dcInst = DataController.GetInstance(self.character:getUsername())
    -- Just check it any limb has been cut. If not, we can just return from here
    if dcInst:getIsAnyLimbCut() then
        self:performWithAmputation()
    end

    --end
end

function ISInventoryPaneContextMenu.doEquipOption(context, playerObj, isWeapon, items, player)
    -- check if hands if not heavy damaged
    if (not playerObj:isPrimaryHandItem(isWeapon) or (playerObj:isPrimaryHandItem(isWeapon) and playerObj:isSecondaryHandItem(isWeapon))) and not getSpecificPlayer(player):getBodyDamage():getBodyPart(BodyPartType.Hand_R):isDeepWounded() and (getSpecificPlayer(player):getBodyDamage():getBodyPart(BodyPartType.Hand_R):getFractureTime() == 0 or getSpecificPlayer(player):getBodyDamage():getBodyPart(BodyPartType.Hand_R):getSplintFactor() > 0) then
        -- forbid reequipping skinned items to avoid multiple problems for now
        local add = true
        if playerObj:getSecondaryHandItem() == isWeapon and isWeapon:getScriptItem():getReplaceWhenUnequip() then
            add = false
        end
        if add then
            local equipOption = context:addOption(getText("ContextMenu_Equip_Primary"), items,
                ISInventoryPaneContextMenu.OnPrimaryWeapon, player)
            equipOption.notAvailable = not CheckHandFeasibility(StaticData.LIMBS_IND_STR.Hand_R)
        end
    end
    if (not playerObj:isSecondaryHandItem(isWeapon) or (playerObj:isPrimaryHandItem(isWeapon) and playerObj:isSecondaryHandItem(isWeapon))) and not getSpecificPlayer(player):getBodyDamage():getBodyPart(BodyPartType.Hand_L):isDeepWounded() and (getSpecificPlayer(player):getBodyDamage():getBodyPart(BodyPartType.Hand_L):getFractureTime() == 0 or getSpecificPlayer(player):getBodyDamage():getBodyPart(BodyPartType.Hand_L):getSplintFactor() > 0) then
        -- forbid reequipping skinned items to avoid multiple problems for now
        local add = true
        if playerObj:getPrimaryHandItem() == isWeapon and isWeapon:getScriptItem():getReplaceWhenUnequip() then
            add = false
        end
        if add then
            local equipOption = context:addOption(getText("ContextMenu_Equip_Secondary"), items,
                ISInventoryPaneContextMenu.OnSecondWeapon, player)

            equipOption.notAvailable = not CheckHandFeasibility(StaticData.LIMBS_IND_STR.Hand_L)
        end
    end
end

local noHandsImpossibleActions = {
    getText("ContextMenu_Add_escape_rope_sheet"),
    getText("ContextMenu_Add_escape_rope"),
    getText("ContextMenu_Remove_escape_rope"),
    getText("ContextMenu_Barricade"),
    getText("ContextMenu_Unbarricade"),
    getText("ContextMenu_MetalBarricade"),
    getText("ContextMenu_MetalBarBarricade"),
    getText("ContextMenu_Open_window"),
    getText("ContextMenu_Close_window"),
    getText("ContextMenu_PickupBrokenGlass"),
    getText("ContextMenu_Open_door"),
    getText("ContextMenu_Close_door"),

}


local og_ISWorldObjectContextMenu_createMenu = ISWorldObjectContextMenu.createMenu

---@param player integer
---@param worldobjects any
---@param x any
---@param y any
---@param test any
function ISWorldObjectContextMenu.createMenu(player, worldobjects, x, y, test)
    ---@type ISContextMenu
    local ogContext = og_ISWorldObjectContextMenu_createMenu(player, worldobjects, x, y, test)

    -- goddamn it, zomboid devs. ogContext could be a boolean...
    -- TBH, I don't really care about gamepad support, but all this method can break stuff. Let's just disable thisfor gamepad users.
    if type(ogContext) == "boolean" or type(ogContext) == "string" then
        return ogContext
    end


    -- The vanilla game doesn't count an item in the off hand as "equipped" for picking up glass. Let's fix that here
    local brokenGlassOption = ogContext:getOptionFromName(getText("ContextMenu_RemoveBrokenGlass"))

    if brokenGlassOption then
        local playerObj = getSpecificPlayer(player)
        if (CachedDataHandler.GetHandFeasibility(StaticData.SIDES_IND_STR.R) and playerObj:getPrimaryHandItem()) or
            (CachedDataHandler.GetHandFeasibility(StaticData.SIDES_IND_STR.L) and playerObj:getSecondaryHandItem())
        then
            brokenGlassOption.notAvailable = false
            brokenGlassOption.toolTip = nil     -- This is active only when you can't do the action.
        end
    end




    -- check if no hands, disable various interactions
    if not CachedDataHandler.GetBothHandsFeasibility() then
        TOC_DEBUG.print("NO hands :((")
        for i = 1, #noHandsImpossibleActions do
            local optionName = noHandsImpossibleActions[i]
            local option = ogContext:getOptionFromName(optionName)
            if option then
                option.notAvailable = true
            end
        end
    end
    return ogContext
end

---@diagnostic disable-next-line: duplicate-set-field
local og_ISWearClothing_isValid = ISWearClothing.isValid
function ISWearClothing:isValid()
    return LimitActionsController.WrapClothingAction(self, og_ISWearClothing_isValid, self.item)
end



local og_ISClothingExtraAction_isValid = OverridenMethodsArchive.Save("ISClothingExtraAction_isValid", ISClothingExtraAction.isValid)
---@diagnostic disable-next-line: duplicate-set-field
function ISClothingExtraAction:isValid()
    return LimitActionsController.WrapClothingAction(self, og_ISClothingExtraAction_isValid, InventoryItemFactory.CreateItem(self.extra))
end




--* Book exception for exp

local og_ISReadABook_perform = ISReadABook.perform
function ISReadABook:perform()
    self.noExp = true
    og_ISReadABook_perform(self)

end

return LimitActionsController