require "MS/MS_StatusIndicator"

local isInitialized = false
local _isExcrementumModActive = nil

-- Detect whether the Excrementum mod is active.
local function detectExcrementumMod()
    if _isExcrementumModActive ~= nil then
        return _isExcrementumModActive
    end

    if rawget(_G, "Excrementum") ~= nil then
        _isExcrementumModActive = true
        return true
    end

    local activatedMods = getActivatedMods()
    if activatedMods then
        for i = 0, activatedMods:size() - 1 do
            local modId = string.lower(tostring(activatedMods:get(i) or ""))
            if modId == "excrementumb42" or modId == "excrementum" or modId == "excrementum41" or modId == "excrementum41old" then
                _isExcrementumModActive = true
                return true
            end
        end
    end

    _isExcrementumModActive = false
    return false
end

-- Return Excrementum modData for the current player.
local function getExcData(player)
    if not player then return nil end
    local modData = player:getModData()
    return modData and modData.exc or nil
end

local function clamp01(value)
    if not value or value ~= value then return 0 end
    if value < 0 then return 0 end
    if value > 1 then return 1 end
    return value
end

-- Compute stomach totals in the same format used by Excrementum: total, solids, liquids.
local function getStomachTotals(stomach)
    if not stomach then
        return 0, 0, 0
    end

    local solids = stomach.h or 0
    local liquids = stomach.w or 0

    for _, row in ipairs(stomach) do
        solids = solids + (row.h or 0)
        liquids = liquids + (row.w or 0)
    end

    return solids + liquids, solids, liquids
end

-- Excrementum effects are driven by the combined stomach volume (food + water),
-- including the extra chyme table that the mod also feeds into StomachTotalV.
local function getCombinedStomachTotals(excData)
    if not excData then
        return 0, 0, 0
    end

    local total, solids, liquids = 0, 0, 0

    local function addTotals(stomach)
        local blockTotal, blockSolids, blockLiquids = getStomachTotals(stomach)
        total = total + blockTotal
        solids = solids + blockSolids
        liquids = liquids + blockLiquids
    end

    addTotals(excData.st)
    addTotals(excData.ch)

    return total, solids, liquids
end

-- Read the configured Excrementum stomach limit.
local function getStomachVolumeLimit()
    local sandbox = rawget(_G, "SandboxVars_Excrementum")
    local volume = sandbox and sandbox.StomachVolume or 100
    return math.max(0.001, (volume or 100) * 0.01)
end

local function getStomachGreenThresholds()
    local stomachLimit = getStomachVolumeLimit()
    local minimumThreshold = math.min(stomachLimit, 0.31)
    local mediumThreshold = ((stomachLimit - minimumThreshold) * 0.5) + minimumThreshold
    return stomachLimit, minimumThreshold, mediumThreshold
end

local function buildStomachData(player)
    if not player or player:isDead() then
        return nil
    end

    local excData = getExcData(player)
    if not excData then
        return nil
    end

    local total, solids, liquids = getCombinedStomachTotals(excData)
    local stomachLimit, minimumThreshold, mediumThreshold = getStomachGreenThresholds()
    local actualRatio = total / stomachLimit
    -- Resolve the Excrementum global inline here to avoid local forward-reference issues on load/resume.
    local excGlobal = Excrementum ~= nil and Excrementum or (_G and _G.Excrementum or nil)

    return {
        total = total,
        solids = solids,
        liquids = liquids,
        stomachLimit = stomachLimit,
        minimumThreshold = minimumThreshold,
        mediumThreshold = mediumThreshold,
        actualRatio = actualRatio,
        visualRatio = clamp01(actualRatio),
        stomachPain = excGlobal and tonumber(excGlobal.StomachPain) or 0,
        greenStomach = excGlobal and tonumber(excGlobal.green_stomach) or 0,
    }
end

-- Reproduce the dynamic colon threshold used by Excrementum as closely as possible.
local function getColonThreshold(excData)
    if not excData or not excData.col then
        return 0.001
    end

    local sandbox = rawget(_G, "SandboxVars_Excrementum")
    local baseThreshold = sandbox and sandbox.ColonBaseThreshold or 100
    local threshold = (excData.col.visc or 1) * (baseThreshold or 100) * 0.01

    local excGlobal = rawget(_G, "Excrementum")
    local now = excGlobal and excGlobal.now or 0
    local colonMinutes = sandbox and sandbox.ColonMinutes or 0

    if excData.col.tf and now > 0 and colonMinutes > 0 then
        local delta = now - excData.col.tf
        if delta >= colonMinutes then
            threshold = threshold - (delta - colonMinutes) * 0.00166667
        end
    end

    return math.max(0.001, threshold)
end

-- Provide a global Excrementum state fallback for values computed outside player modData.
local function getExcrementumGlobal()
    -- Avoid rawget here: in some PZ/Kahlua environments it may not be available
    -- or may behave inconsistently when resolving mod globals during resume/load.
    if Excrementum ~= nil then
        return Excrementum
    end

    if _G and _G.Excrementum ~= nil then
        return _G.Excrementum
    end

    return nil
end

local function getSequenceLength(sequence)
    if not sequence then
        return 0
    end

    if QList and QList.getLength then
        local ok, result = pcall(QList.getLength, sequence)
        if ok and result then
            return tonumber(result) or 0
        end
    end

    if sequence.size then
        local ok, result = pcall(function()
            return sequence:size()
        end)
        if ok and result then
            return tonumber(result) or 0
        end
    end

    if type(sequence) == "table" then
        return #sequence
    end

    return 0
end

local function getExcrementumMoodLevelFromPain(pain)
    local numericPain = tonumber(pain) or 0
    if numericPain >= 90 then
        return 4
    elseif numericPain >= 60 then
        return 3
    elseif numericPain >= 30 then
        return 2
    elseif numericPain > 0 then
        return 1
    end

    return 0
end

local function buildColonData(player)
    if not player or player:isDead() then
        return nil
    end

    local excData = getExcData(player)
    if not excData or not excData.col then
        return nil
    end

    local excGlobal = getExcrementumGlobal()
    local fill = tonumber(excData.col.V) or 0
    local threshold = getColonThreshold(excData)
    local visc = tonumber(excData.col.visc) or 0
    local colonPain = excGlobal and tonumber(excGlobal.ColonPain) or 0
    local hasUrge = excData.col.td ~= nil
    local intestineCount = getSequenceLength(excData.int)
    local now = excGlobal and tonumber(excGlobal.now) or 0
    local urgeSince = hasUrge and tonumber(excData.col.td) or nil
    local urgeMinutes = nil
    if urgeSince and now and now >= urgeSince then
        urgeMinutes = math.max(0, now - urgeSince)
    end

    local canDefecate = fill > 0.1 and fill >= threshold
    local painStartMinutes = 240
    local panicHordeThreshold = 0.30
    local diarrheaRisk = "Off"
    local diarrheaMinutes = nil
    if hasUrge and visc < 0.3 then
        diarrheaMinutes = 21 + (80 * visc)
        if urgeMinutes and urgeMinutes >= diarrheaMinutes then
            diarrheaRisk = "High"
        else
            diarrheaRisk = "Possible"
        end
    end

    local moodleBranch = nil
    local moodleLevel = 0

    if colonPain > 0 then
        moodleBranch = "Bad"
        moodleLevel = getExcrementumMoodLevelFromPain(colonPain)
    elseif hasUrge then
        moodleBranch = "Good"
        moodleLevel = 3
    elseif intestineCount > 0 then
        moodleBranch = "Good"
        moodleLevel = 1
    end

    return {
        fill = fill,
        threshold = threshold,
        viscosity = visc,
        colonPain = colonPain,
        hasUrge = hasUrge,
        intestineCount = intestineCount,
        urgeMinutes = urgeMinutes,
        canDefecate = canDefecate,
        painStartMinutes = painStartMinutes,
        panicHordeThreshold = panicHordeThreshold,
        diarrheaRisk = diarrheaRisk,
        diarrheaMinutes = diarrheaMinutes,
        actualRatio = threshold > 0 and (fill / threshold) or 0,
        visualRatio = clamp01(threshold > 0 and (fill / threshold) or 0),
        moodleBranch = moodleBranch,
        moodleLevel = moodleLevel,
    }
end

local function buildUrineData(player)
    if not player or player:isDead() then
        return nil
    end

    local excData = getExcData(player)
    local excGlobal = getExcrementumGlobal()
    local urine = excData and tonumber(excData.urine) or nil
    if urine == nil then
        urine = excGlobal and tonumber(excGlobal.urine) or 0
    end
    local ms = excGlobal and tonumber(excGlobal.Ms) or 0.3
    local thresholdBad1 = 0.30
    local thresholdBad3 = 0.30 + ms
    local thresholdBad4 = thresholdBad3 + 0.15
    local urinePain = excGlobal and tonumber(excGlobal.UrinePain) or 0
    local moodleBranch = nil
    local moodleLevel = 0

    if urine >= thresholdBad4 then
        moodleBranch = "Bad"
        moodleLevel = 4
    elseif urine >= thresholdBad3 then
        moodleBranch = "Bad"
        moodleLevel = 3
    elseif urine >= thresholdBad1 then
        moodleBranch = "Bad"
        moodleLevel = 1
    elseif urine >= 0.20 then
        moodleBranch = "Good"
        moodleLevel = 1
    end

    return {
        urine = urine,
        ms = ms,
        thresholdGood1 = 0.20,
        thresholdBad1 = thresholdBad1,
        thresholdBad3 = thresholdBad3,
        thresholdBad4 = thresholdBad4,
        canUrinate = urine >= 0.20,
        urinePain = urinePain,
        visualRatio = clamp01(urine),
        moodleBranch = moodleBranch,
        moodleLevel = moodleLevel,
    }
end

local function countDirtyVehicleSeats(vehicle)
    if not vehicle then
        return 0
    end

    local count = 0
    for i = 0, vehicle:getPartCount() - 1 do
        local part = vehicle:getPartByIndex(i)
        if part and part:getCategory() == "seat" then
            local item = part:getInventoryItem()
            if item and item:hasModData() then
                local data = item:getModData()
                if data.feces or data.urine then
                    count = count + 1
                end
            end
        end
    end

    return count
end

local function buildSmellData(player)
    if not player or player:isDead() then
        return nil
    end

    local excData = getExcData(player)
    local excGlobal = getExcrementumGlobal()
    if not excData and not excGlobal then
        return nil
    end

    local inventory = player.getInventory and player:getInventory() or nil
    local soiledClothing = 0
    local soiledWipingItems = 0
    local fecesCarried = 0
    local dirtySeats = 0

    if inventory then
        local clothing = inventory:getAllCategory("Clothing")
        for i = 0, clothing:size() - 1 do
            local item = clothing:get(i)
            if item and item:hasModData() then
                local data = item:getModData()
                if data.feces or data.urine then
                    soiledClothing = soiledClothing + 1
                end
            end
        end

        local items = inventory:getItems()
        for i = 0, items:size() - 1 do
            local item = items:get(i)
            if item and item:hasModData() and excGlobal and excGlobal.ItemHasTag and excGlobal.ItemHasTag(item, "DryWiping") then
                local data = item:getModData()
                if data.feces or data.urine then
                    soiledWipingItems = soiledWipingItems + 1
                end
            end
        end

        if excGlobal and excGlobal.InvCountTag then
            local ok, count = pcall(excGlobal.InvCountTag, inventory, "Feces")
            if ok then
                fecesCarried = tonumber(count) or 0
            end
        end
    end

    dirtySeats = countDirtyVehicleSeats(player.getVehicle and player:getVehicle() or nil)

    local smell = excGlobal and tonumber(excGlobal.smell) or 0
    local roomSmell = excGlobal and tonumber(excGlobal.room_smell) or 0
    local bodySmell = excData and tonumber(excData.ass) or 0
    local moodleLevel = 0
    if smell >= 4 then
        moodleLevel = 4
    elseif smell >= 3 then
        moodleLevel = 3
    elseif smell >= 2 then
        moodleLevel = 2
    elseif smell > 0 then
        moodleLevel = 1
    end

    return {
        smell = smell,
        roomSmell = roomSmell,
        bodySmell = bodySmell,
        soiledClothing = soiledClothing,
        soiledWipingItems = soiledWipingItems,
        fecesCarried = fecesCarried,
        dirtySeats = dirtySeats,
        visualRatio = clamp01(smell / 4),
        moodleBranch = moodleLevel > 0 and "Bad" or nil,
        moodleLevel = moodleLevel,
    }
end

local function buildShameData(player)
    if not player or player:isDead() then
        return nil
    end

    local excGlobal = getExcrementumGlobal()
    if not excGlobal then
        return nil
    end

    local rawShame = tonumber(excGlobal.shame) or 0
    local negativeVisual = math.max(0, rawShame)
    local moodleBranch = nil
    local moodleLevel = 0

    if rawShame > 0 then
        moodleBranch = "Bad"
        if rawShame >= 4 then
            moodleLevel = 4
        elseif rawShame >= 3 then
            moodleLevel = 3
        elseif rawShame >= 2 then
            moodleLevel = 2
        elseif rawShame > 0 then
            moodleLevel = 1
        end
    elseif rawShame < 0 then
        moodleBranch = "Good"
        if rawShame <= -2 then
            moodleLevel = 2
        else
            moodleLevel = 1
        end
    end

    return {
        rawShame = rawShame,
        visualRatio = clamp01(negativeVisual / 4),
        moodleBranch = moodleBranch,
        moodleLevel = moodleLevel,
    }
end


-- ---------------------------------------------------------------------------------------- --
-- Excrementum Stomach (food)
-- ---------------------------------------------------------------------------------------- --
ISExcStomachStatusIndicator = MS_StatusIndicator:derive("ISExcStomachStatusIndicator")

function ISExcStomachStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Exc_Stomach"
    o.__type = "ISExcStomachStatusIndicator"
    o.indicatorColor = { r = 0.86, g = 0.63, b = 0.34 }
    return o
end

function ISExcStomachStatusIndicator:getExcrementumStomachData()
    return buildStomachData(self.player)
end

function ISExcStomachStatusIndicator:getValue()
    local data = self:getExcrementumStomachData()
    if not data then return 0 end
    return data.visualRatio or 0
end

function ISExcStomachStatusIndicator:getHoverText(value)
    local data = self:getExcrementumStomachData()
    if not data then
        return tostring(round((tonumber(value) or 0) * 100))
    end

    return tostring(round((tonumber(data.actualRatio) or 0) * 100))
end


-- ---------------------------------------------------------------------------------------- --
-- Excrementum Colon
-- ---------------------------------------------------------------------------------------- --
ISExcColonStatusIndicator = MS_StatusIndicator:derive("ISExcColonStatusIndicator")

function ISExcColonStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Exc_Colon"
    o.__type = "ISExcColonStatusIndicator"
    o.indicatorColor = { r = 0.68, g = 0.44, b = 0.24 }
    return o
end

function ISExcColonStatusIndicator:getExcrementumColonData()
    return buildColonData(self.player)
end

function ISExcColonStatusIndicator:getValue()
    local data = self:getExcrementumColonData()
    if not data then return 0 end
    return data.visualRatio or 0
end

function ISExcColonStatusIndicator:getHoverText(value)
    local data = self:getExcrementumColonData()
    if not data then
        return tostring(round((tonumber(value) or 0) * 100))
    end

    return tostring(round((tonumber(data.actualRatio) or 0) * 100))
end

-- ---------------------------------------------------------------------------------------- --
-- Excrementum Urine
-- ---------------------------------------------------------------------------------------- --
ISExcUrineStatusIndicator = MS_StatusIndicator:derive("ISExcUrineStatusIndicator")

function ISExcUrineStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Exc_Urine"
    o.__type = "ISExcUrineStatusIndicator"
    o.indicatorColor = { r = 0.95, g = 0.82, b = 0.22 }
    return o
end

function ISExcUrineStatusIndicator:getExcrementumUrineData()
    return buildUrineData(self.player)
end

function ISExcUrineStatusIndicator:getValue()
    local data = self:getExcrementumUrineData()
    if not data then return 0 end
    return data.visualRatio or 0
end

function ISExcUrineStatusIndicator:getHoverText(value)
    local data = self:getExcrementumUrineData()
    if not data then
        return tostring(round((tonumber(value) or 0) * 100))
    end

    return tostring(round((tonumber(data.urine) or 0) * 100))
end

-- ---------------------------------------------------------------------------------------- --
-- Excrementum Smell
-- ---------------------------------------------------------------------------------------- --
ISExcSmellStatusIndicator = MS_StatusIndicator:derive("ISExcSmellStatusIndicator")

function ISExcSmellStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Exc_Smell"
    o.__type = "ISExcSmellStatusIndicator"
    o.indicatorColor = { r = 0.54, g = 0.80, b = 0.38 }
    return o
end

function ISExcSmellStatusIndicator:getExcrementumSmellData()
    return buildSmellData(self.player)
end

function ISExcSmellStatusIndicator:getValue()
    local data = self:getExcrementumSmellData()
    if not data then return 0 end
    return data.visualRatio or 0
end

function ISExcSmellStatusIndicator:getHoverText(value)
    local data = self:getExcrementumSmellData()
    if not data then
        return tostring(round((tonumber(value) or 0) * 400) / 100)
    end

    return tostring(round((tonumber(data.smell) or 0) * 100) / 100)
end

-- ---------------------------------------------------------------------------------------- --
-- Excrementum Shame
-- ---------------------------------------------------------------------------------------- --
ISExcShameStatusIndicator = MS_StatusIndicator:derive("ISExcShameStatusIndicator")

function ISExcShameStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Exc_Shame"
    o.__type = "ISExcShameStatusIndicator"
    o.indicatorColor = { r = 0.84, g = 0.50, b = 0.76 }
    return o
end

function ISExcShameStatusIndicator:getExcrementumShameData()
    return buildShameData(self.player)
end

function ISExcShameStatusIndicator:getValue()
    local data = self:getExcrementumShameData()
    if not data then return 0 end
    return data.visualRatio or 0
end

function ISExcShameStatusIndicator:getHoverText(value)
    local data = self:getExcrementumShameData()
    if not data then
        return tostring(round((tonumber(value) or 0) * 400) / 100)
    end

    return tostring(round((tonumber(data.rawShame) or 0) * 100) / 100)
end

-- ---------------------------------------------------------------------------------------- --
-- integrate To ModernStatus
-- ---------------------------------------------------------------------------------------- --
local function integrateToModernStatus()
    if isInitialized then return end
    if not detectExcrementumMod() then return end

    -- Register readable indicator names for settings panels and the all-indicators panel.
    MS_IndicatorSettingsPanel.IndicatorNames["ISExcStomachStatusIndicator"] = "IGUI_ModernStatus_ExcStomach"
    MS_IndicatorSettingsPanel.IndicatorNames["ISExcColonStatusIndicator"] = "IGUI_ModernStatus_ExcColon"
    MS_IndicatorSettingsPanel.IndicatorNames["ISExcUrineStatusIndicator"] = "IGUI_ModernStatus_ExcUrine"
    MS_IndicatorSettingsPanel.IndicatorNames["ISExcSmellStatusIndicator"] = "IGUI_ModernStatus_ExcSmell"
    MS_IndicatorSettingsPanel.IndicatorNames["ISExcShameStatusIndicator"] = "IGUI_ModernStatus_ExcShame"

    if not StatusWidget._originalCreateUI then
        StatusWidget._originalCreateUI = StatusWidget.createUI
    end

    local oldCreateUI = StatusWidget.createUI
    StatusWidget.createUI = function(playerIndex, player)
        oldCreateUI(playerIndex, player)

        if not StatusWidget.indicators[playerIndex] then
            StatusWidget.indicators[playerIndex] = {}
        end

        if StatusWidget.indicators[playerIndex]["exc_stomach"] then
            return
        end

        local function createExtraIndicator(storageKey, indicatorType, classRef)
            local config = MSConfig.getIndicatorConfig(playerIndex, indicatorType)
            local x = config.position.x
            local y = config.position.y
            local width, height

            if config.style == "bar" then
                width = config.barSize.width
                height = config.barSize.height
            else
                width = config.circularSize
                height = config.circularSize
            end

            local indicator = classRef:new(x, y, width, height, player)
            indicator:initialise()
            indicator:addToUIManager()
            StatusWidget.indicators[playerIndex][storageKey] = indicator
        end

        createExtraIndicator("exc_stomach", "ISExcStomachStatusIndicator", ISExcStomachStatusIndicator)
        createExtraIndicator("exc_colon", "ISExcColonStatusIndicator", ISExcColonStatusIndicator)
        createExtraIndicator("exc_urine", "ISExcUrineStatusIndicator", ISExcUrineStatusIndicator)
        createExtraIndicator("exc_smell", "ISExcSmellStatusIndicator", ISExcSmellStatusIndicator)
        createExtraIndicator("exc_shame", "ISExcShameStatusIndicator", ISExcShameStatusIndicator)
    end

    if not MSConfig._originalCreateDefaultConfig_EXC then
        MSConfig._originalCreateDefaultConfig_EXC = MSConfig.createDefaultConfig

        MSConfig.createDefaultConfig = function(playerNum)
            local config = MSConfig._originalCreateDefaultConfig_EXC(playerNum)

            if detectExcrementumMod() then
                local function addIndicatorConfig(indicatorType, color)
                    if config.indicators[indicatorType] then
                        return
                    end

                    config.indicators[indicatorType] = MSConfig.getDefaultIndicatorConfig(indicatorType)
                    config.indicators[indicatorType].color = { r = color.r, g = color.g, b = color.b }
                end

                addIndicatorConfig("ISExcStomachStatusIndicator", { r = 0.86, g = 0.63, b = 0.34 })
                addIndicatorConfig("ISExcColonStatusIndicator", { r = 0.68, g = 0.44, b = 0.24 })
                addIndicatorConfig("ISExcUrineStatusIndicator", { r = 0.95, g = 0.82, b = 0.22 })
                addIndicatorConfig("ISExcSmellStatusIndicator", { r = 0.54, g = 0.80, b = 0.38 })
                addIndicatorConfig("ISExcShameStatusIndicator", { r = 0.84, g = 0.50, b = 0.76 })
            end

            return config
        end
    end

    isInitialized = true
end

Events.OnGameBoot.Add(integrateToModernStatus)

return {
    isActive = detectExcrementumMod
}
