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

ZR_WalkToRandom = {}

function ZR_WalkToRandom.pickWalkableTile(player, radius, maxTries)
	maxTries = maxTries or 60
	local cell = getCell()
	if not cell then return nil end
	local px = math.floor(player:getX())
	local py = math.floor(player:getY())
	local pz = math.floor(player:getZ())
	local r2 = radius * radius
	for _ = 1, maxTries do
		local dx = ZombRand(2 * radius + 1) - radius
		local dy = ZombRand(2 * radius + 1) - radius
		if (dx ~= 0 or dy ~= 0) and (dx * dx + dy * dy) <= r2 then
			local sq = cell:getGridSquare(px + dx, py + dy, pz)
			if sq and sq:canStand() then return sq end
		end
	end
	return nil
end

function ZR_WalkToRandom.walkTo(player, square)
	if not (player and square) then return false end
	ISTimedActionQueue.add(ISWalkToTimedAction:new(player, square))
	return true
end

function ZR_WalkToRandom.start(player)
	if not player then return 0 end

	local s5  = ZR_WalkToRandom.pickWalkableTile(player, 5)
	local s10 = ZR_WalkToRandom.pickWalkableTile(player, 10)
	local n = 0

	if s5 then
		ZR_WalkToRandom.walkTo(player, s5)
		n = n + 1
	else
		DebugLog.log("[ZR WalkToRandom] no walkable tile found within 5")
	end

	if s10 then
		ZR_WalkToRandom.walkTo(player, s10)
		n = n + 1
	else
		DebugLog.log("[ZR WalkToRandom] no walkable tile found within 10")
	end

	return n
end

local function quantizeDir(fx, fy)
	fx = fx or 0; fy = fy or 0
	local ax, ay = math.abs(fx), math.abs(fy)
	local sx = (fx > 0) and 1 or ((fx < 0) and -1 or 0)
	local sy = (fy > 0) and 1 or ((fy < 0) and -1 or 0)
	if ax > ay * 2.414 then sy = 0 elseif ay > ax * 2.414 then sx = 0 end
	if sx == 0 and sy == 0 then sx = 1 end
	return sx, sy
end

function ZR_WalkToRandom.walkDir(player, fx, fy, tiles)
	if not player then return false end
	local cell = getCell()
	if not cell then return false end
	local sx, sy = quantizeDir(fx, fy)
	local px = math.floor(player:getX())
	local py = math.floor(player:getY())
	local pz = math.floor(player:getZ())
	local best
	for step = 1, (tiles or 3) do
		local sq = cell:getGridSquare(px + sx * step, py + sy * step, pz)
		if sq and sq:canStand() then best = sq else break end
	end
	if best then
		ZR_WalkToRandom.walkTo(player, best)
		return true
	end
	return false
end

function ZR_WalkToRandom.densestClusterSquare(player, searchR, tight, minCount)
	searchR  = searchR  or 25
	tight    = tight    or 5
	minCount = minCount or 5
	local cell = getCell()
	if not cell then return nil end
	local zList = cell:getZombieList()
	if not zList then return nil end
	local pz = math.floor(player:getZ())

	local grappled
	pcall(function()
		if player.getGrapplingTarget then grappled = player:getGrapplingTarget() end
	end)

	local near = {}
	for i = 0, zList:size() - 1 do
		local z = zList:get(i)
		if z and z ~= grappled and z:isAlive() and z:DistTo(player) <= searchR then
			near[#near + 1] = z
		end
	end
	if #near < minCount then return nil end

	local t2 = tight * tight
	local bestZ, bestCount = nil, 0
	for a = 1, #near do
		local za = near[a]
		local ax, ay = za:getX(), za:getY()
		local c = 0
		for b = 1, #near do
			local zb = near[b]
			local dx, dy = zb:getX() - ax, zb:getY() - ay
			if dx * dx + dy * dy <= t2 then c = c + 1 end
		end
		if c > bestCount then bestCount, bestZ = c, za end
	end
	if bestCount < minCount or not bestZ then return nil end

	local cx, cy = bestZ:getX(), bestZ:getY()
	local sumx, sumy, cnt = 0, 0, 0
	for b = 1, #near do
		local zb = near[b]
		local dx, dy = zb:getX() - cx, zb:getY() - cy
		if dx * dx + dy * dy <= t2 then
			sumx = sumx + zb:getX(); sumy = sumy + zb:getY(); cnt = cnt + 1
		end
	end
	if cnt == 0 then return nil end
	local tx, ty = math.floor(sumx / cnt), math.floor(sumy / cnt)

	local sq = cell:getGridSquare(tx, ty, pz)
	if sq and sq:canStand() then return sq end
	for ring = 1, 3 do
		for ddx = -ring, ring do
			for ddy = -ring, ring do
				if math.abs(ddx) == ring or math.abs(ddy) == ring then
					local s = cell:getGridSquare(tx + ddx, ty + ddy, pz)
					if s and s:canStand() then return s end
				end
			end
		end
	end
	return nil
end

function ZR_WalkToRandom.walkToCluster(player, searchR, tight, minCount)
	local sq = ZR_WalkToRandom.densestClusterSquare(player, searchR, tight, minCount)
	if sq then
		ZR_WalkToRandom.walkTo(player, sq)
		return true
	end
	return false
end

function ZR_WalkToRandom.pickDragDestination(player, fx, fy, dirTiles, searchR, tight, minCount)
	if not player then return nil end
	local sq = ZR_WalkToRandom.densestClusterSquare(player, searchR, tight, minCount)
	if sq then return sq, "cluster" end
	local cell = getCell()
	if not cell then return nil end
	local sx, sy = quantizeDir(fx, fy)
	local px = math.floor(player:getX())
	local py = math.floor(player:getY())
	local pz = math.floor(player:getZ())
	local best
	for step = 1, (dirTiles or 3) do
		local s = cell:getGridSquare(px + sx * step, py + sy * step, pz)
		if s and s:canStand() then best = s else break end
	end
	if best then return best, "heading" end
	return nil
end

local function dragStepClear(cell, fromSq, fx, fy, tx, ty, tz)
	if not (cell and fromSq) then return nil end
	local to = cell:getGridSquare(tx, ty, tz)
	if not to or not to:canStand() then return nil end
	if fromSq:isBlockedTo(to) then return nil end
	if fx == tx or fy == ty then
		if fromSq:isHoppableTo(to) then return nil end
	else
		local b1 = cell:getGridSquare(tx, fy, tz)
		local b2 = cell:getGridSquare(fx, ty, tz)
		local leg1 = b1 and b1:canStand()
			and not fromSq:isBlockedTo(b1)  and not b1:isBlockedTo(to)
			and not fromSq:isHoppableTo(b1) and not b1:isHoppableTo(to)
		local leg2 = b2 and b2:canStand()
			and not fromSq:isBlockedTo(b2)  and not b2:isBlockedTo(to)
			and not fromSq:isHoppableTo(b2) and not b2:isHoppableTo(to)
		if not (leg1 or leg2) then return nil end
	end
	return to
end

function ZR_WalkToRandom.dragRay(ox, oy, oz, sx, sy, maxTiles)
	local cell = getCell()
	if not cell then return nil end
	local curSq = cell:getGridSquare(ox, oy, oz)
	if not curSq then return nil end
	local bestSq, bx, by, n
	local cfx, cfy = ox, oy
	for step = 1, (maxTiles or 6) do
		local nx, ny = cfx + sx, cfy + sy
		local nsq = dragStepClear(cell, curSq, cfx, cfy, nx, ny, oz)
		if not nsq then break end
		bestSq, bx, by, n = nsq, nx, ny, step
		curSq, cfx, cfy = nsq, nx, ny
	end
	if n then return bestSq, bx, by, n end
	return nil
end

local DRAG_DIRS = {
	{ 1, 0}, { 1, 1}, { 0, 1}, {-1, 1},
	{-1, 0}, {-1,-1}, { 0,-1}, { 1,-1},
}
local function dragDirIndex(sx, sy)
	for i = 1, #DRAG_DIRS do
		if DRAG_DIRS[i][1] == sx and DRAG_DIRS[i][2] == sy then return i end
	end
	return 1
end

function ZR_WalkToRandom.pickDragLeg(ox, oy, oz, desiredSx, desiredSy, maxTiles)
	local qsx, qsy = quantizeDir(desiredSx, desiredSy)
	local base = dragDirIndex(qsx, qsy)
	local order = { 0, 1, -1, 2, -2, 3, -3, 4 }
	for _, off in ipairs(order) do
		local idx = ((base - 1 + off) % 8 + 8) % 8 + 1
		local d = DRAG_DIRS[idx]
		local sq, dx, dy, n = ZR_WalkToRandom.dragRay(ox, oy, oz, d[1], d[2], maxTiles)
		if sq then return sq, dx, dy, d[1], d[2], n end
	end
	return nil
end

local function isDragHusk(player, z)
	if not z then return true end
	local gt
	pcall(function() if player.getGrapplingTarget then gt = player:getGrapplingTarget() end end)
	if z == gt then return true end
	local husk = false
	pcall(function()
		local md = z.getModData and z:getModData()
		if md and md._zrHusk == true then husk = true end
	end)
	return husk
end

function ZR_WalkToRandom.pickDragTarget(player, searchR, tight, minCount, wideR)
	searchR  = searchR  or 25
	tight    = tight    or 5
	minCount = minCount or 5
	wideR    = wideR    or (searchR * 2)
	local cell = getCell()
	if not cell then return nil, nil, "none" end
	local zList = cell:getZombieList()
	if not zList then return nil, nil, "none" end

	local near = {}
	local wideBest, wideBestDist = nil, nil
	for i = 0, zList:size() - 1 do
		local z = zList:get(i)
		if z and z:isAlive() and not isDragHusk(player, z) then
			local d = z:DistTo(player)
			if d <= searchR then
				near[#near + 1] = z
			elseif d <= wideR and (not wideBestDist or d < wideBestDist) then
				wideBest, wideBestDist = z, d
			end
		end
	end
	if #near == 0 then
		if wideBest then
			local tx, ty = wideBest:getX(), wideBest:getY()
			DebugLog.log(string.format(
				"[ZR WalkToRandom] pickDragTarget: none within %d -> lone nearest @ (%.1f,%.1f) dist=%.1f (wideR=%d)",
				searchR, tx, ty, wideBestDist, wideR))
			return tx, ty, "lone"
		end
		DebugLog.log("[ZR WalkToRandom] pickDragTarget: no non-husk zombies within "
			.. tostring(wideR) .. " -> none")
		return nil, nil, "none"
	end

	local t2 = tight * tight
	local bestZ, bestCount = near[1], 0
	for a = 1, #near do
		local za = near[a]
		local ax, ay = za:getX(), za:getY()
		local c = 0
		for b = 1, #near do
			local zb = near[b]
			local dx, dy = zb:getX() - ax, zb:getY() - ay
			if dx * dx + dy * dy <= t2 then c = c + 1 end
		end
		if c > bestCount then bestCount, bestZ = c, za end
	end

	local cx, cy = bestZ:getX(), bestZ:getY()
	local sumx, sumy, cnt = 0, 0, 0
	for b = 1, #near do
		local zb = near[b]
		local dx, dy = zb:getX() - cx, zb:getY() - cy
		if dx * dx + dy * dy <= t2 then
			sumx = sumx + zb:getX(); sumy = sumy + zb:getY(); cnt = cnt + 1
		end
	end
	local tx = (cnt > 0) and (sumx / cnt) or cx
	local ty = (cnt > 0) and (sumy / cnt) or cy
	local kind = (bestCount >= minCount) and "cluster" or "loose"
	DebugLog.log(string.format(
		"[ZR WalkToRandom] pickDragTarget: near=%d densest=%d (min=%d, tight=%d) -> %s @ (%.1f,%.1f) dist=%.1f",
		#near, bestCount, minCount, tight, kind, tx, ty,
		math.sqrt((tx - player:getX())^2 + (ty - player:getY())^2)))
	return tx, ty, kind
end

return ZR_WalkToRandom
