-- @author DoomTero 2026
-- @copyright 2026 DoomTero - ZomboRut original work. ref:e0808b60
-- @license Proprietary. Not licensed for reuse, redistribution or derivative works without written permission from DoomTero. Ask - permission is obtainable. See ZomboRut/LICENSE.txt

ZR_CumLayers = ZR_CumLayers or {}

local REGIONS = {
	{ key = "neck",  label = "Neck",  loc = "ZomboRut:Cum_Neck",  item = "Base.ZR_CumStain_Neck"  },
	{ key = "boobs", label = "Boobs", loc = "ZomboRut:Cum_Chest", item = "Base.ZR_CumStain_Chest" },
	{ key = "arms",  label = "Arms",  loc = "ZomboRut:Cum_Arms",  item = "Base.ZR_CumStain_Arms"  },
	{ key = "legs",  label = "Legs",  loc = "ZomboRut:Cum_Legs",  item = "Base.ZR_CumStain_Legs"  },
	{ key = "back",  label = "Back",  loc = "ZomboRut:Cum_Back",  item = "Base.ZR_CumStain_Back"  },
	{ key = "ass",   label = "Ass",   loc = "ZomboRut:Cum_Butt",  item = "Base.ZR_CumStain_Butt"  },
	{ key = "pussy", label = "Pussy", loc = "ZomboRut:Cum_Pussy", item = "Base.ZR_CumStain_Pussy" },
	{ key = "feet",  label = "Feet",  loc = "ZomboRut:Cum_Feet",  item = "Base.ZR_CumStain_Feet"  },
}

local FRAME_COUNT  = 48
local STEP_MS      = 210
local PAUSE_GAP_MS = 400

local function log(msg)
	DebugLog.log("[ZR CumLayers] " .. msg)
end

local function regionByKey(key)
	for _, r in ipairs(REGIONS) do
		if r.key == key then return r end
	end
	return nil
end

local function getLocation(region)
	local loc = ItemBodyLocation.get(ResourceLocation.of(region.loc))
	if not loc then log("ItemBodyLocation missing: " .. region.loc) end
	return loc
end

Events.OnGameBoot.Add(function()
	local group = BodyLocations.getGroup("Human")
	if not group then log("Human BodyLocationGroup missing") return end
	local wired = 0
	for _, region in ipairs(REGIONS) do
		local loc = getLocation(region)
		if loc then
			local bodyLoc = group:getOrCreateLocation(loc)
			if bodyLoc then
				bodyLoc:setMultiItem(true)
				group:moveLocationToIndex(loc, 0)
				wired = wired + 1
			end
		end
	end
	log(wired .. "/" .. #REGIONS .. " region slots wired into Human group")
end)

local anims = {}
local clock = nil

local function setFrameNoReset(item, frame)
	local vis = item and item:getVisual()
	if not vis then return false end
	vis:setBaseTexture(frame)
	return true
end

Events.OnPlayerUpdate.Add(function(player)
	if not clock or #anims == 0 then return end
	if player ~= getSpecificPlayer(0) then return end

	local now = getTimestampMs()
	local gap = now - clock.lastMs
	clock.lastMs = now
	if gap < 0 or gap > PAUSE_GAP_MS then gap = 0 end
	clock.accumMs = clock.accumMs + gap
	if clock.accumMs < STEP_MS then return end
	clock.accumMs = clock.accumMs - STEP_MS

	local touched = {}
	local alive = {}
	for _, a in ipairs(anims) do
		a.frame = a.frame + 1
		if a.frame >= FRAME_COUNT then
			setFrameNoReset(a.item, FRAME_COUNT - 1)
		else
			setFrameNoReset(a.item, a.frame)
			alive[#alive + 1] = a
		end
		touched[a.player] = true
	end
	anims = alive
	for p in pairs(touched) do p:resetModelNextFrame() end
	if #anims == 0 then
		clock = nil
		log("all animations done")
	end
end)

local function startAnim(player, item)
	setFrameNoReset(item, 0)
	player:resetModelNextFrame()
	anims[#anims + 1] = { player = player, item = item, frame = 0 }
	clock = clock or { accumMs = 0, lastMs = getTimestampMs() }
end

local PRELOAD_PER_TICK    = 24
local PRELOAD_PATIENCE_MS = 15000

local preload = nil

local function preloadPump()
	local pl = preload
	if not pl then return end
	if not pl.names then
		if getTimestampMs() - pl.t0 > PRELOAD_PATIENCE_MS then
			log("preload gave up waiting on ClothingItem assets")
			preload = nil
			Events.OnTick.Remove(preloadPump)
			return
		end
		local names = {}
		for _, region in ipairs(REGIONS) do
			local vis = pl.items[region.key] and pl.items[region.key]:getVisual()
			local ci = vis and vis:getClothingItem()
			if not (ci and ci:isReady()) then return end
			local bt = ci:getBaseTextures()
			for i = 0, bt:size() - 1 do names[#names + 1] = bt:get(i) end
		end
		pl.names = names
		pl.items = nil
	end
	for _ = 1, PRELOAD_PER_TICK do
		if pl.i > #pl.names then break end
		Texture.getSharedTexture("media/textures/" .. pl.names[pl.i] .. ".png", pl.flags)
		pl.i = pl.i + 1
	end
	if pl.i > #pl.names then
		log("preloaded " .. #pl.names .. " stain frame textures (flags=" .. pl.flags .. ")")
		preload = nil
		Events.OnTick.Remove(preloadPump)
	end
end

Events.OnGameStart.Add(function()
	if isClient() or isServer() then return end
	local items = {}
	for _, region in ipairs(REGIONS) do
		items[region.key] = instanceItem(region.item)
	end
	preload = {
		items = items, i = 1, t0 = getTimestampMs(),
		flags = (getCore():getOptionTextureCompression() and 4 or 0) + 128,
	}
	Events.OnTick.Add(preloadPump)
end)

local function applyStain(player, region)
	if not (player and region) then return false end
	local loc = getLocation(region)
	if not loc then return false end

	local item = player:getWornItems():getItem(loc)
	if not item then
		item = instanceItem(region.item)
		if not item then log("instanceItem failed: " .. region.item) return false end
		player:setWornItem(loc, item, false)
	end

	for i = #anims, 1, -1 do
		if anims[i].item == item then table.remove(anims, i) end
	end
	startAnim(player, item)
	return true
end

function ZR_CumLayers.clearAll(player)
	if not player then return 0 end
	local worn = player.getWornItems and player:getWornItems()
	if not worn then return 0 end

	for i = #anims, 1, -1 do
		if anims[i].player == player then table.remove(anims, i) end
	end
	if #anims == 0 then clock = nil end

	local n = 0
	for _, region in ipairs(REGIONS) do
		local loc = getLocation(region)
		if loc then
			local item = worn:getItem(loc)
			while item do
				worn:remove(item)
				n = n + 1
				item = worn:getItem(loc)
			end
		end
	end
	if n > 0 then
		player:resetModelNextFrame()
		log("cleared " .. n .. " stain item(s)")
	end
	return n
end

local ZomboRutScene = require "ZomboRut/ZomboRutScene"

local UTILITY_SCENE_HINTS = { "drag_subdue", "helddrag", "tackle_drag" }

local CHAINS = {
	front = { "pussy", "legs", "feet", "boobs", "butt", "back", "neck", "arms" },
	rear  = { "pussy", "butt", "back", "legs", "feet", "neck", "boobs", "arms" },
	oral  = { "neck", "boobs", "arms", "back", "legs", "butt", "pussy", "feet" },
	body  = { "boobs", "neck", "legs", "arms", "back", "butt", "pussy", "feet" },
}

local function chainForScene(sceneId, cfg)
	local id = string.lower(sceneId or "")
	for _, hint in ipairs(UTILITY_SCENE_HINTS) do
		if id:find(hint, 1, true) then return nil end
	end
	if (cfg and cfg.isOral) or id:find("bj", 1, true) or id:find("oral", 1, true)
		or id:find("facesit", 1, true) or id:find("blow", 1, true) then
		return CHAINS.oral, "oral"
	end
	if id:find("doggy", 1, true) or id:find("revlotus", 1, true)
		or id:find("behind", 1, true) then
		return CHAINS.rear, "rear"
	end
	if (cfg and cfg.pregnancyRisk) or id:find("missionary", 1, true)
		or id:find("cowgirl", 1, true) or id:find("lotus", 1, true)
		or id:find("pile", 1, true) then
		return CHAINS.front, "front"
	end
	if id:find("sex", 1, true) or id:find("defeat", 1, true) then
		return CHAINS.body, "body"
	end
	return nil
end

local function partnerIsMale(active)
	local body = active and active.body
	if body and body.isFemale then
		local ok, fem = pcall(function() return body:isFemale() end)
		if ok and type(fem) == "boolean" then return not fem end
	end
	local id = tostring(active and active.sceneId or "")
	local pair = id:match("_([MF][MF])$")
	if pair then return pair:sub(2, 2) == "M" end
	local legacy = id:match("_([MF])$")
	if legacy then return legacy == "F" end
	return false
end

local function tryApplySceneStain(player, active, when)
	if isClient and isClient() then return end
	local chain, kind = chainForScene(active.sceneId, active.cfg)
	if not chain then
		log(("scene '%s': not a stain scene - skipped"):format(tostring(active.sceneId)))
		return
	end
	if not partnerIsMale(active) then
		log(("scene '%s': partner not male - no stain"):format(tostring(active.sceneId)))
		return
	end
	local worn = player:getWornItems()
	for _, key in ipairs(chain) do
		local region = regionByKey(key)
		local loc = region and getLocation(region)
		if loc and not worn:getItem(loc) then
			if applyStain(player, region) then
				log(("scene '%s' (%s, %s): stain -> %s")
					:format(tostring(active.sceneId), kind, when, key))
			end
			return
		end
	end
	log(("scene '%s': all regions already stained - skipped"):format(tostring(active.sceneId)))
end

ZomboRutScene.registerSceneUpdateCallback(function(player, active)
	if not (active and active.ctx) or active.ctx.zrCumDone then return end
	if not active.outroActivated then return end
	local ticks = active.dragTicks or 0
	active.ctx.zrCumOutroStart = active.ctx.zrCumOutroStart or ticks
	local outroDur = (active.cfg and active.cfg.outroDuration) or 5
	if ticks < active.ctx.zrCumOutroStart + outroDur * 60 * 0.55 then return end
	active.ctx.zrCumDone = true
	tryApplySceneStain(player, active, "outro")
end)

ZomboRutScene.registerSceneEndCallback(function(player, active, result)
	if not (active and active.ctx) or active.ctx.zrCumDone then return end
	if result ~= "Released" or active.struggleEscaped then return end
	active.ctx.zrCumDone = true
	tryApplySceneStain(player, active, "end")
end)

ZR_CumTest = {}

function ZR_CumTest.play(key, playerNum)
	local region = regionByKey(key or "boobs")
	if not region then log("unknown region: " .. tostring(key)) return end
	local player = getSpecificPlayer(playerNum or 0)
	if not player then log("no player") return end
	if applyStain(player, region) then
		log("playing " .. region.key)
	end
end

function ZR_CumTest.playAll(playerNum)
	for _, region in ipairs(REGIONS) do
		ZR_CumTest.play(region.key, playerNum)
	end
end

function ZR_CumTest.frame(n, key, playerNum)
	local region = regionByKey(key or "boobs")
	if not region then return end
	local player = getSpecificPlayer(playerNum or 0)
	if not player then return end
	local loc = getLocation(region)
	local item = loc and player:getWornItems():getItem(loc)
	if not item then log("no worn stain for " .. region.key) return end
	for i = #anims, 1, -1 do
		if anims[i].item == item then table.remove(anims, i) end
	end
	setFrameNoReset(item, math.max(0, math.min(FRAME_COUNT - 1, (n or 1) - 1)))
	player:resetModelNextFrame()
end

function ZR_CumTest.clear(playerNum)
	ZR_CumLayers.clearAll(getSpecificPlayer(playerNum or 0))
end

local WASH_SELF = {
	"wash", "shower", "scrub", "sponge",
	"cleanyourself", "cleanself", "cleanbody",
}
local WASH_NOT_SELF = {
	"clothing", "clothes", "garment", "laundry", "washer", "dryer",
	"vehicle", "car", "dish", "window", "wall", "floor",
	"corpse", "blood", "rag", "weapon", "ashes",
}

local function looksLikeSelfWash(typeName)
	local t = string.lower(typeName)
	for _, bad in ipairs(WASH_NOT_SELF) do
		if t:find(bad, 1, true) then return false end
	end
	for _, good in ipairs(WASH_SELF) do
		if t:find(good, 1, true) then return true end
	end
	return false
end

local function washIsh(typeName)
	local t = string.lower(typeName)
	return t:find("wash", 1, true) or t:find("clean", 1, true)
		or t:find("shower", 1, true) or t:find("bath", 1, true)
end

local function installWashHook()
	if not ISBaseTimedAction or ISBaseTimedAction._zrCumWashPatched then return end
	ISBaseTimedAction._zrCumWashPatched = true

	local origPerform = ISBaseTimedAction.perform
	function ISBaseTimedAction:perform()
		local typeName = self.Type
		if type(typeName) == "string" then
			if looksLikeSelfWash(typeName) then
				local n = ZR_CumLayers.clearAll(self.character)
				if n > 0 then
					log(("washed (%s): removed %d stain(s)"):format(typeName, n))
				end
			elseif washIsh(typeName) then
				log(("wash-like action '%s' did NOT clear stains (not recognised as self-wash)")
					:format(typeName))
			end
		end
		return origPerform(self)
	end
	log("wash hook installed on ISBaseTimedAction.perform")
end

installWashHook()
Events.OnGameStart.Add(installWashHook)

local function onFillWorldObjectContextMenu(playerIndex, context, worldobjects, test)
	if test == true then return end
	if not (isDebugEnabled and isDebugEnabled()) then return end
	local playerObj = getSpecificPlayer(playerIndex)
	if not playerObj then return end
	local setIcon = require("ZomboRut/ContextMenu/ZR_MenuUtil").icon

	local root = context:addOption("[ZR DEBUG] Cum Stain", worldobjects, nil)
	setIcon(root, "Debug")
	local sub = ISContextMenu:getNew(context)
	context:addSubMenu(root, sub)

	for _, region in ipairs(REGIONS) do
		setIcon(sub:addOption("Add " .. region.label, worldobjects, function()
			ZR_CumTest.play(region.key, playerIndex)
		end), "Debug")
	end
	setIcon(sub:addOption("Add ALL", worldobjects, function()
		ZR_CumTest.playAll(playerIndex)
	end), "Debug")
	setIcon(sub:addOption("Clear all stains", worldobjects, function()
		ZR_CumTest.clear(playerIndex)
	end), "Remove")

end

Events.OnFillWorldObjectContextMenu.Add(onFillWorldObjectContextMenu)
