class_name BrogueFOV
extends RefCounted

# Symmetric recursive shadowcasting, tile-accurate FOV for 2D grids.
#
# Port of the standard 8-octant algorithm (Bjoern Bergstrom / Adam Milazzo's
# symmetric variant). In ~100 lines of pure GDScript.
#
# Properties:
# - Tile-accurate: every cell is either fully visible or not visible.
# - Symmetric: if A sees B, B sees A. Critical for fair AI awareness.
# - Circular radius clipping (euclidean distance).
# - Opaque = walls, T_NOTHING, and unbridged liquid (lava/chasm/brimstone).
#   Water is transparent (you can see across water).
#
# Usage:
#   var vis := BrogueFOV.compute(grid, Vector2i(39, 14), 8)
#   for y in grid["height"]:
#       for x in grid["width"]:
#           if vis[y * grid["width"] + x] == 1:
#               draw_cell(x, y)

# Must match src/gen/grid.h:terrain_t
const T_NOTHING   := 0
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

# Octant transforms: each octant maps (row, col) of the shadowcasting loop
# to grid deltas. 8 octants cover all directions symmetrically.
const OCTANTS := [
	Vector4i( 1,  0,  0,  1),
	Vector4i( 0,  1,  1,  0),
	Vector4i( 0, -1,  1,  0),
	Vector4i(-1,  0,  0,  1),
	Vector4i(-1,  0,  0, -1),
	Vector4i( 0, -1, -1,  0),
	Vector4i( 0,  1, -1,  0),
	Vector4i( 1,  0,  0, -1),
]

# Returns PackedByteArray of width*height, row-major, 1 = visible.
static func compute(grid: Dictionary, origin: Vector2i, radius: int) -> PackedByteArray:
	var w: int = grid["width"]
	var h: int = grid["height"]
	var out := PackedByteArray()
	out.resize(w * h)
	for i in range(w * h):
		out[i] = 0

	if origin.x < 0 or origin.y < 0 or origin.x >= w or origin.y >= h:
		return out

	# Origin is always visible.
	out[origin.y * w + origin.x] = 1

	var terrain: PackedByteArray = grid["terrain"]
	var liquid: PackedByteArray = grid["liquid"]

	for oct in OCTANTS:
		_cast_octant(out, terrain, liquid, origin, radius, w, h,
					 1, 1.0, 0.0, oct)
	return out

static func _is_opaque(terrain: PackedByteArray, liquid: PackedByteArray,
					   x: int, y: int, w: int) -> bool:
	var idx := y * w + x
	var t: int = terrain[idx]
	if t == T_WALL or t == T_NOTHING:
		return true
	if t == T_LIQUID:
		# Water is transparent; lava / chasm / brimstone are opaque.
		return liquid[idx] != L_WATER
	return false

static func _cast_octant(out: PackedByteArray,
						 terrain: PackedByteArray, liquid: PackedByteArray,
						 origin: Vector2i, radius: int, w: int, h: int,
						 row: int, start_slope: float, end_slope: float,
						 oct: Vector4i) -> void:
	if start_slope < end_slope:
		return
	var r2: int = radius * radius
	var new_start := start_slope
	var blocked := false
	for i in range(row, radius + 1):
		if blocked:
			break
		for dy in range(-i, 1):
			var dx := -i
			while dx <= 0:
				var tx: int = origin.x + dx * oct.x + dy * oct.y
				var ty: int = origin.y + dx * oct.z + dy * oct.w
				var l_slope := (dx - 0.5) / (dy + 0.5)
				var r_slope := (dx + 0.5) / (dy - 0.5)
				if start_slope < r_slope:
					dx += 1
					continue
				if end_slope > l_slope:
					break
				if tx >= 0 and tx < w and ty >= 0 and ty < h:
					if dx * dx + dy * dy <= r2:
						out[ty * w + tx] = 1
					if blocked:
						if _is_opaque(terrain, liquid, tx, ty, w):
							new_start = r_slope
						else:
							blocked = false
							start_slope = new_start
					else:
						if _is_opaque(terrain, liquid, tx, ty, w) and i < radius:
							blocked = true
							_cast_octant(out, terrain, liquid, origin, radius, w, h,
										 i + 1, start_slope, l_slope, oct)
							new_start = r_slope
				dx += 1
