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

ZR_StruggleModes = ZR_StruggleModes or {}

local DIRS = { "Forward", "Left", "Backward", "Right" }

local DIR_EDGE = {
	Forward  = { 0, -1 },
	Backward = { 0,  1 },
	Left     = { -1, 0 },
	Right    = {  1, 0 },
}

local function randDir()
	return DIRS[ZombRand(#DIRS) + 1]
end

local function randDirNot(prev)
	if not prev then return randDir() end
	local d = randDir()
	local guard = 0
	while d == prev and guard < 8 do
		d = randDir(); guard = guard + 1
	end
	return d
end

local function firstPressed(run)
	for i = 1, #DIRS do
		if run.input.pressed[DIRS[i]] then return DIRS[i] end
	end
	return nil
end

local keyNameCache = {}
local function keyName(binding)
	local cached = keyNameCache[binding]
	if cached then return cached end
	local n
	pcall(function()
		n = Keyboard.getKeyName(getCore():getKey(binding))
	end)
	if not n or n == "" then n = binding end
	keyNameCache[binding] = n
	return n
end

ZR_StruggleModes.keyName = keyName

ZR_StruggleModes.DIRS     = DIRS
ZR_StruggleModes.DIR_EDGE = DIR_EDGE

local M1 = {
	id   = 1,
	name = "BREAK THE GRIP",
	hint = "Press the movement keys in the order shown",
	stages = {
		{ len = 2, perMs = 1250, lives = 3 },
		{ len = 2, perMs = 1150, lives = 3 },
		{ len = 3, perMs = 1050, lives = 2 },
		{ len = 3, perMs = 1000, lives = 2 },
	},
}

function M1.startStage(run, sd)
	local s = run.s
	s.seq = {}
	local prev
	for i = 1, sd.len do
		prev = randDirNot(prev)
		s.seq[i] = prev
	end
	s.idx      = 1
	s.lives    = sd.lives
	s.perMs    = math.max(650, sd.perMs * run.diff.timeMul)
	s.deadline = run.nowMs + s.perMs
	s.badFlash = 0
end

function M1.tick(run, sd, nowMs)
	local s = run.s

	local want = s.seq[s.idx]
	local hit  = run.input.pressed[want] and want or firstPressed(run)
	if hit then
		if hit == want then
			s.idx = s.idx + 1
			if s.idx > #s.seq then return "clear" end
			s.deadline = nowMs + s.perMs
		else
			s.lives    = s.lives - 1
			s.badFlash = nowMs + 180
			s.deadline = math.max(nowMs + 90, s.deadline - 120)
			if s.lives < 0 then return "fail" end
		end
	end

	if nowMs >= s.deadline then return "fail" end
	return nil
end

function M1.draw(ui, run, sd)
	local s = run.s
	if not s.seq then return end

	local n = #s.seq
	local box, gap = 50, 8
	local totalW = n * box + (n - 1) * gap
	local x0 = (ui.panelW - totalW) * 0.5
	local y0 = 8

	for i = 1, n do
		local x    = x0 + (i - 1) * (box + gap)
		local done = i < s.idx
		local cur  = i == s.idx

		ui:box(x, y0, box, box, done and 0.22 or (cur and 0.55 or 0.34),
		       0.04, 0.04, 0.04)

		local r, g, b = 1, 1, 1
		if done then r, g, b = 0.35, 0.95, 0.45 end
		if cur and run.nowMs < (s.badFlash or 0) then r, g, b = 1.0, 0.3, 0.3 end
		local a = done and 0.5 or (cur and 1.0 or 0.55)

		local edge = DIR_EDGE[s.seq[i]]
		local t = 6
		local cx, cy = x + box * 0.5, y0 + box * 0.5
		local bw = (edge[1] ~= 0) and t or (box - 24)
		local bh = (edge[2] ~= 0) and t or (box - 24)
		ui:box(cx + edge[1] * (box * 0.5 - t) - bw * 0.5,
		       cy + edge[2] * (box * 0.5 - t) - bh * 0.5,
		       bw, bh, a, r, g, b)
		ui:text(keyName(s.seq[i]), cx, cy - 10, r, g, b, a, true, UIFont.Medium)

		if cur then
			local left = math.max(0, (s.deadline - run.nowMs) / s.perMs)
			ui:box(x, y0 + box + 3, box * left, 4, 0.95, 1.0, 0.75, 0.2)
		end
	end
end

local M2 = {
	id   = 2,
	name = "BREAKLINE",
	hint = "Tap any movement key while the marker is in the GREEN",
	stages = {
		{ sweepMs = 1900, zoneW = 0.42, hits = 1, clockMs = 3000 },
		{ sweepMs = 1750, zoneW = 0.38, hits = 1, clockMs = 2800 },
		{ sweepMs = 1600, zoneW = 0.34, hits = 1, clockMs = 2600 },
		{ sweepMs = 1400, zoneW = 0.32, hits = 2, clockMs = 3600 },
	},
}

local MIN_APPROACH_MS = 400

local function rollZone(w, sweepMs)
	local half = w * 0.5
	local lo = math.max(half + 0.06, half + MIN_APPROACH_MS / math.max(1, sweepMs))
	local hi = 1.0 - half - 0.06
	if hi <= lo then return math.min(1.0 - half, lo) end
	return lo + (ZombRand(1000) / 1000) * (hi - lo)
end

function M2.startStage(run, sd)
	local s = run.s
	s.sweepMs    = sd.sweepMs * run.diff.timeMul
	s.zoneW      = math.min(0.48, sd.zoneW * run.diff.widthMul)
	s.zoneW      = math.max(s.zoneW, math.min(0.48, 220 / s.sweepMs))
	s.zoneC      = rollZone(s.zoneW, s.sweepMs)
	s.hitsNeeded = sd.hits
	s.hits       = 0
	s.endMs      = run.nowMs + sd.clockMs * run.diff.timeMul
	s.lockUntil  = 0
	s.flash      = 0
	s.flashGood  = false
end

local function sweepPos(elapsed, sweepMs)
	local phase = (elapsed % (sweepMs * 2)) / sweepMs
	if phase <= 1 then return phase end
	return 2 - phase
end

function M2.tick(run, sd, nowMs)
	local s = run.s
	s.pos = sweepPos(nowMs - run.stageStartedMs, s.sweepMs)

	if nowMs >= s.endMs then return "fail" end

	if nowMs < s.lockUntil then return nil end

	if run.input.anyPressed then
		if math.abs(s.pos - s.zoneC) <= s.zoneW * 0.5 then
			s.hits = s.hits + 1
			s.flash, s.flashGood = nowMs + 200, true
			if s.hits >= s.hitsNeeded then return "clear" end
			s.zoneC = rollZone(s.zoneW, s.sweepMs)
		else
			s.lockUntil = nowMs + 420
			s.flash, s.flashGood = nowMs + 200, false
		end
	end
	return nil
end

function M2.draw(ui, run, sd)
	local s = run.s
	local trackW = ui.panelW - 40
	local x0, y0, h = 20, 26, 18

	ui:box(x0, y0, trackW, h, 0.5, 0.05, 0.05, 0.05)

	local zx = x0 + (s.zoneC - s.zoneW * 0.5) * trackW
	ui:box(zx, y0, s.zoneW * trackW, h, 0.5, 0.25, 0.95, 0.35)

	local locked = run.nowMs < (s.lockUntil or 0)
	local mx = x0 + (s.pos or 0) * trackW
	ui:box(mx - 2, y0 - 3, 4, h + 6, locked and 0.35 or 1.0, 1, 1, 1)

	if s.hitsNeeded > 1 then
		for i = 1, s.hitsNeeded do
			local on = i <= s.hits
			ui:box(x0 + (i - 1) * 12, y0 + h + 5, 8, 4,
			       on and 0.95 or 0.30, on and 0.35 or 0.8, on and 0.95 or 0.8, 0.45)
		end
	end

	local left = math.max(0, (s.endMs - run.nowMs) / (sd.clockMs * run.diff.timeMul))
	ui:box(x0, y0 + h + 12, trackW * left, 3, 0.8, 0.95, 0.30, 0.25)

	if run.nowMs < (s.flash or 0) then
		local g = s.flashGood
		ui:box(x0, y0, trackW, h, 0.30, g and 0.3 or 1.0, g and 1.0 or 0.3, 0.3)
	end
end

local M3 = {
	id   = 3,
	name = "PANIC PULSE",
	hint = "Mash any movement key - keep the bar inside the BLUE band",
	stages = {
		{ clockMs = 2100, lo = 2.5, hi = 7.0, needMs = 400 },
		{ clockMs = 2100, lo = 2.8, hi = 7.4, needMs = 450 },
		{ clockMs = 2200, lo = 3.2, hi = 7.8, needMs = 500 },
		{ clockMs = 2200, lo = 3.6, hi = 8.2, needMs = 550 },
	},
}

local PULSE_MAX_HZ  = 10.0
local PULSE_MIN_GAP = 55

function M3.startStage(run, sd)
	local s = run.s
	local mid  = (sd.lo + sd.hi) * 0.5
	local half = (sd.hi - sd.lo) * 0.5 * run.diff.widthMul
	s.lo       = math.max(sd.lo * 0.75, mid - half)
	s.hi       = mid + half
	s.needMs   = sd.needMs * run.diff.timeMul
	s.inBand   = 0
	s.presses  = 0
	s.endMs    = run.nowMs + sd.clockMs * run.diff.timeMul
	local seedMs = 1000 / (s.lo * 0.5)
	s.iv       = { seedMs, seedMs, seedMs }
	s.lastMs   = run.nowMs
	s.rate     = 0
end

function M3.tick(run, sd, nowMs, dtMs)
	local s = run.s

	if run.input.anyPressed then
		local gap = nowMs - s.lastMs
		if gap >= PULSE_MIN_GAP then
			s.iv[3], s.iv[2], s.iv[1] = s.iv[2], s.iv[1], gap
			s.lastMs  = nowMs
			s.presses = s.presses + 1
		end
	end

	local open = nowMs - s.lastMs
	local n    = math.min(s.presses or 0, 3)
	local sum  = math.max(s.iv[1], open)
	local cnt  = 1
	for k = 2, n do
		sum = sum + s.iv[k]
		cnt = cnt + 1
	end
	local avg = sum / cnt
	s.rate = (avg > 0) and (1000 / avg) or 0

	if s.presses > 0 and s.rate >= s.lo and s.rate <= s.hi then
		s.inBand = s.inBand + dtMs
		if s.inBand >= s.needMs then return "clear" end
	end

	if nowMs >= s.endMs then return "fail" end
	return nil
end

function M3.draw(ui, run, sd)
	local s = run.s
	local barW = ui.panelW - 40
	local x0, y0, h = 20, 26, 18

	ui:box(x0, y0, barW, h, 0.55, 0.05, 0.05, 0.05)

	local bx = x0 + (s.lo / PULSE_MAX_HZ) * barW
	local bw = ((s.hi - s.lo) / PULSE_MAX_HZ) * barW
	ui:box(bx, y0, bw, h, 0.35, 0.30, 1.0, 0.35)

	local frac = math.min(1, (s.rate or 0) / PULSE_MAX_HZ)
	local r, g, b = 1.0, 0.72, 0.20
	if s.rate > s.hi then r, g, b = 1.0, 0.25, 0.22
	elseif s.rate >= s.lo then r, g, b = 0.35, 0.95, 0.45 end
	ui:box(x0 + 2, y0 + 2, (barW - 4) * frac, h - 4, 0.92, r, g, b)

	ui:box(bx, y0 - 2, 2, h + 4, 0.9, 1, 1, 1)
	ui:box(bx + bw, y0 - 2, 2, h + 4, 0.9, 1, 1, 1)

	local p = math.min(1, s.inBand / s.needMs)
	ui:box(x0, y0 + h + 5, barW * p, 4, 0.9, 0.35, 0.95, 0.45)

	local left = math.max(0, (s.endMs - run.nowMs) / (sd.clockMs * run.diff.timeMul))
	ui:box(x0, y0 + h + 12, barW * left, 3, 0.8, 0.95, 0.30, 0.25)
end

ZR_StruggleModes.byId = { M1, M2, M3 }

function ZR_StruggleModes.get(id)
	return ZR_StruggleModes.byId[id or 1] or ZR_StruggleModes.byId[1]
end

function ZR_StruggleModes.count()
	return #ZR_StruggleModes.byId
end

return ZR_StruggleModes
