-- @author SolarEdge 2024 - 2025
local ZR_Moodles = require "ZomboRut/ZR_Moodles"

local BodyCount = {}

local function nowHour()
	local gt = getGameTime and getGameTime()
	return gt and gt:getWorldAgeHours() or 0
end

local function hasTrait(player, key)
	return ZR_Trait ~= nil and ZR_Trait.has ~= nil and ZR_Trait.has(player, key) == true
end

local LIMIT_BY_OPTION = { [1] = -1, [2] = 1, [3] = 2, [4] = 4 }
local DEFAULT_LIMIT_OPTION = 2

function BodyCount.resolveLimit(player)
	if player == nil then return LIMIT_BY_OPTION[DEFAULT_LIMIT_OPTION] end
	local sv  = SandboxVars and SandboxVars.ZomboRut
	local opt = sv and sv.banditLimit
	local limit = LIMIT_BY_OPTION[opt] or LIMIT_BY_OPTION[DEFAULT_LIMIT_OPTION]
	if limit ~= -1 and hasTrait(player, "swinger") then limit = limit * 2 end
	if hasTrait(player, "promiscuous") or hasTrait(player, "Rapist") then limit = -1 end
	local md = player:getModData()
	if md then md.slimitMax = limit end
	return limit
end

local MOODLE_NAME     = "BodyCount"
local MOODLE_NEUTRAL  = 0.5
local MOODLE_REACHED  = 0.4
local MOODLE_EXCEEDED = 0.1

local DEFAULT_MOODLE_HOURS = 12

ZR_Moodles.create(MOODLE_NAME)

local function moodleWindowHours()
	local v = SandboxVars and SandboxVars.ZomboRut and SandboxVars.ZomboRut.slimitMoodleHours
	if type(v) ~= "number" or v < 1 then return DEFAULT_MOODLE_HOURS end
	return v
end

local LIMIT_BARK_KEYS = {
	[MOODLE_REACHED] = {
		calm   = { "IGUI_ZR_BC_Enough" },
		defeat = { "IGUI_ZR_BC_Enough_Defeat1", "IGUI_ZR_BC_Enough_Defeat2" },
	},
	[MOODLE_EXCEEDED] = {
		calm   = { "IGUI_ZR_BC_TooMuch", "IGUI_ZR_BC_WishNot", "IGUI_ZR_BC_WhyLikeThis" },
		defeat = { "IGUI_ZR_BC_TooMuch_Defeat1", "IGUI_ZR_BC_TooMuch_Defeat2",
		           "IGUI_ZR_BC_TooMuch_Defeat3" },
	},
}

local RECOVERY_KEYS = { "IGUI_ZR_BC_Horny", "IGUI_ZR_BC_Intimacy" }

local _pendingBark = {}

local function sayLimitBark(target, tier)
	local barks = LIMIT_BARK_KEYS[tier]
	if not barks then return end
	local inDefeat = false
	pcall(function()
		inDefeat = (ZR_Surrender and ZR_Surrender.isActive
			and ZR_Surrender.isActive(target)) == true
	end)
	if inDefeat then
		target:Say(getText(barks.defeat[ZombRand(#barks.defeat) + 1]))
		return
	end
	local pending = _pendingBark[target]
	if pending ~= MOODLE_EXCEEDED then _pendingBark[target] = tier end
end

function BodyCount.flushPendingBark(player)
	if player == nil then return end
	local tier = _pendingBark[player]
	_pendingBark[player] = nil
	if tier == nil then return end
	local barks = LIMIT_BARK_KEYS[tier]
	if not barks then return end
	pcall(function() player:Say(getText(barks.calm[ZombRand(#barks.calm) + 1])) end)
end

local function readStamp(player)
	local md = player:getModData()
	local untilHour, tier = md.zr_slimitMoodleUntil, md.zr_slimitMoodleTier
	local wellFormed = type(untilHour) == "number"
		and (tier == MOODLE_REACHED or tier == MOODLE_EXCEEDED)
	if wellFormed and nowHour() < untilHour then return tier, false end
	md.zr_slimitMoodleUntil = nil
	md.zr_slimitMoodleTier  = nil
	return nil, wellFormed
end

local function sayRecoveryLine(player, quiet)
	if quiet then return end
	if player.isAsleep and player:isAsleep() then return end
	local inDefeat = false
	pcall(function()
		inDefeat = (ZR_Surrender and ZR_Surrender.isActive
			and ZR_Surrender.isActive(player)) == true
	end)
	if inDefeat then return end
	local key = RECOVERY_KEYS[ZombRand(#RECOVERY_KEYS) + 1]
	pcall(function() player:Say(getText(key)) end)
end

local function MoodleManager(player, quiet)
	local md = player:getModData()

	local limitIcon = getTexture(player:isFemale()
		and "media/ui/ZRArousal_FemaleLimit.png"
		or  "media/ui/ZRArousal_MaleLimit.png")
	if limitIcon then
		for lvl = 1, 4 do ZR_Moodles.setPicture(MOODLE_NAME, player, 2, lvl, limitIcon) end
	end

	if md.slimitMax == -1 then
		md.zr_slimitMoodleUntil = nil
		md.zr_slimitMoodleTier  = nil
		ZR_Moodles.set(MOODLE_NAME, player, MOODLE_NEUTRAL)
		return
	end

	local tier, expired = readStamp(player)
	if tier then
		ZR_Moodles.set(MOODLE_NAME, player, tier)
		return
	end
	ZR_Moodles.set(MOODLE_NAME, player, MOODLE_NEUTRAL)
	if expired then sayRecoveryLine(player, quiet) end
end

function BodyCount.Check(player, quiet)
	if player == nil then return end
	BodyCount.resolveLimit(player)
	MoodleManager(player, quiet);
end

local function decayWindowHours()
	local v = SandboxVars and SandboxVars.ZomboRut and SandboxVars.ZomboRut.banditRewardNew
	if v == 1 then return 6
	elseif v == 2 then return 12
	elseif v == 3 then return 18
	else return 24 end
end

local function banditKey(zombie)
	if zombie == nil then return nil end
	if not (zombie.getVariableBoolean and zombie:getVariableBoolean("Bandit")) then return nil end
	if not (BanditBrain and BanditBrain.Get) then return nil end
	local brain
	pcall(function() brain = BanditBrain.Get(zombie) end)
	if brain and brain.id ~= nil then return "b:" .. tostring(brain.id) end
	return nil
end

local function allocPartnerKey(player, zombie)
	local bkey = banditKey(zombie)
	if bkey then return bkey end
	local md = player:getModData()
	md.zr_partnerCounter = (md.zr_partnerCounter or 0) + 1
	return "z:" .. md.zr_partnerCounter
end

local function pruneByCount(player)
	local md = player:getModData()
	if type(md.byCount) ~= "table" then md.byCount = {} end
	local list   = md.byCount
	local now    = nowHour()
	local window = decayWindowHours()
	for i = #list, 1, -1 do
		local e = list[i]
		local keep = false
		if type(e) == "table" and type(e.key) == "string" then
			local h = type(e.hour) == "number" and e.hour or now
			e.hour = h
			if (now - h) < window then keep = true end
		end
		if not keep then table.remove(list, i) end
	end
	return list
end

function BodyCount.PruneBodyCount(player)
	if player == nil then return end
	pruneByCount(player)
end

function BodyCount.RunID(player, zombie)
	local bkey = banditKey(zombie)
	if not bkey then return true end
	local byCount = pruneByCount(player)
	for _, e in ipairs(byCount) do
		if e.key == bkey then return false end
	end
	return true
end

function BodyCount.NewID(player, zombie)
	local byCount = pruneByCount(player)
	local bkey = banditKey(zombie)
	if bkey then
		for _, e in ipairs(byCount) do
			if e.key == bkey then return false end
		end
	end
	local key = allocPartnerKey(player, zombie)
	byCount[#byCount + 1] = { key = key, hour = nowHour() }
	return true
end

function BodyCount.increaseCount(target, zombie)
	local md = target:getModData()
	local slimitMax = BodyCount.resolveLimit(target)

	local byCount = pruneByCount(target)

	local added = false

	if #byCount <= slimitMax then
		added = BodyCount.NewID(target, zombie);
	end

	if #byCount > slimitMax and not added then
		added = BodyCount.NewID(target, zombie);
	end

	if slimitMax == -1 then return end

	if added and #byCount >= slimitMax then
		local was  = readStamp(target)
		local tier = (#byCount > slimitMax) and MOODLE_EXCEEDED or MOODLE_REACHED
		if was == MOODLE_EXCEEDED then tier = MOODLE_EXCEEDED end
		md.zr_slimitMoodleTier  = tier
		md.zr_slimitMoodleUntil = nowHour() + moodleWindowHours()
		if tier ~= was then
			if tier == MOODLE_EXCEEDED then
				local stats = target:getStats()
				if stats then
					local unhappiness = stats:get(CharacterStat.UNHAPPINESS)
					sayLimitBark(target, MOODLE_EXCEEDED)
					stats:set(CharacterStat.UNHAPPINESS, unhappiness + 50)
				end
			else
				sayLimitBark(target, MOODLE_REACHED)
			end
		end
	end

	BodyCount.Check(target, true)
end

function BodyCount.resetForNewCharacter(player)
	if player == nil then return end
	local md = player:getModData(); if md == nil then return end
	md.byCount              = {}
	md.zr_partnerCounter    = nil
	md.zr_slimitMoodleUntil = nil
	md.zr_slimitMoodleTier  = nil
	md.slimitMax            = nil
	_pendingBark[player]    = nil
	BodyCount.Check(player, true)
end

return BodyCount