class_name GridUtil
extends RefCounted

# Static helpers for the arcade game. Single 79×29 grid per level; the
# generator dict is the source of truth. World↔grid mapping uses 1-unit
# cells (matches demo_fps.gd GridMap cell_size).
#
# Pathfinding is a plain 4-connected BFS, tiny code, fast enough at this
# scale (<1 ms for a full grid flood on a laptop). For an arcade game we
# recompute per-enemy every ~200ms, not every frame.

const T_FLOOR       := 1
const T_WALL        := 2
const T_DOOR        := 3
const T_CORRIDOR    := 4
const T_LIQUID      := 5
const T_BRIDGE      := 6
const T_STAIRS_UP   := 7
const T_STAIRS_DOWN := 8

const L_WATER     := 1
const L_LAVA      := 2
const L_CHASM     := 3
const L_BRIMSTONE := 4

const CELL_SIZE := 1.0

# --- coordinate mapping ---

static func grid_to_world(g: Vector2i, y: float = 0.0) -> Vector3:
	return Vector3(float(g.x), y, float(g.y))

static func world_to_grid(w: Vector3) -> Vector2i:
	return Vector2i(int(floor(w.x + 0.5)), int(floor(w.z + 0.5)))

# --- passability ---

# A cell is passable if an enemy (or player, for pathfinding intent) can
# step on it. Corridors, floor, doors, bridges, stairs, water count.
# Walls, chasms, lava, brimstone do not.
static func is_passable(grid: Dictionary, pos: Vector2i) -> bool:
	var w: int = grid["width"]
	var h: int = grid["height"]
	if pos.x < 0 or pos.y < 0 or pos.x >= w or pos.y >= h:
		return false
	var idx := pos.y * w + pos.x
	var t: int = (grid["terrain"] as PackedByteArray)[idx]
	if t == T_LIQUID:
		var liq: int = (grid["liquid"] as PackedByteArray)[idx]
		return liq == L_WATER  # water walkable, lava/chasm/brimstone not
	return t == T_FLOOR or t == T_CORRIDOR or t == T_DOOR \
		or t == T_BRIDGE or t == T_STAIRS_UP or t == T_STAIRS_DOWN

# --- BFS pathfinding ---

# Returns the next grid step from `from` toward `to`, or from itself if no
# path exists or we're already there. 4-connected, uniform cost.
static func next_step_toward(grid: Dictionary, from: Vector2i, to: Vector2i) -> Vector2i:
	if from == to:
		return from
	if not is_passable(grid, from) or not is_passable(grid, to):
		return from

	var w: int = grid["width"]
	var h: int = grid["height"]
	var came_from := {}  # Vector2i → Vector2i
	came_from[from] = from
	var queue: Array[Vector2i] = [from]
	var found := false
	var dirs := [Vector2i(1,0), Vector2i(-1,0), Vector2i(0,1), Vector2i(0,-1)]

	while queue.size() > 0 and not found:
		var cur: Vector2i = queue.pop_front()
		for d in dirs:
			var nxt: Vector2i = cur + d
			if came_from.has(nxt): continue
			if not is_passable(grid, nxt): continue
			came_from[nxt] = cur
			if nxt == to:
				found = true
				break
			queue.push_back(nxt)

	if not found:
		return from

	# Walk back from to → find cell whose came_from is `from`
	var cur: Vector2i = to
	while came_from[cur] != from:
		cur = came_from[cur]
	return cur

# True if there's an unobstructed line from a → b for enemy LOS purposes.
# Walks the Bresenham ray; any non-passable cell (wall / chasm / lava)
# blocks sight. Doors are transparent for LOS.
static func has_los(grid: Dictionary, a: Vector2i, b: Vector2i) -> bool:
	var dx := absi(b.x - a.x)
	var dy := absi(b.y - a.y)
	var sx := 1 if a.x < b.x else -1
	var sy := 1 if a.y < b.y else -1
	var err := dx - dy
	var x := a.x
	var y := a.y
	while true:
		if Vector2i(x, y) == b: return true
		# Skip blocking check at start cell.
		if not (x == a.x and y == a.y):
			if not _transparent(grid, Vector2i(x, y)):
				return false
		var e2 := 2 * err
		if e2 > -dy:
			err -= dy
			x += sx
		if e2 < dx:
			err += dx
			y += sy
	return false

static func _transparent(grid: Dictionary, pos: Vector2i) -> bool:
	var w: int = grid["width"]
	var h: int = grid["height"]
	if pos.x < 0 or pos.y < 0 or pos.x >= w or pos.y >= h:
		return false
	var idx := pos.y * w + pos.x
	var t: int = (grid["terrain"] as PackedByteArray)[idx]
	# Walls block; everything else is transparent for LOS.
	return t != T_WALL and t != 0

# Pick a random passable cell in the given list of cells. Returns (-1,-1)
# if none are passable. Used by enemy wander target selection.
static func random_passable(grid: Dictionary, cells: Array, rng: RandomNumberGenerator) -> Vector2i:
	if cells.is_empty():
		return Vector2i(-1, -1)
	var shuffled := cells.duplicate()
	shuffled.shuffle()
	for c in shuffled:
		if is_passable(grid, c):
			return c
	return Vector2i(-1, -1)
