extends SceneTree

# Usage (preferred):
#   godot --headless --path demo --script scripts/export_map.gd -- \
#     --generator brogue|blobber --seed N --depth N [--levels N] --out PATH.map
#
# Legacy positional form (brogue only):
#   godot --headless --path demo --script scripts/export_map.gd -- SEED DEPTH [LEVELS] OUT.map
#
# Generates N dungeons (seeds SEED..SEED+N-1, depths DEPTH..DEPTH+N-1 for
# brogue; single seed multi-level for blobber), stacks them vertically, and
# writes one Standard Quake .map file with typed FuncGodot entities for
# water, lava, stairs, doors, and a player spawn.
#
# Chasms on non-bottom levels are real holes — no floor brush, and the
# level below has its ceiling drilled out at the same (x,y) so the shaft
# stays open all the way down until it hits a non-chasm floor. Chasms on
# the bottom level get a local pit floor at CHASM_TOP so you don't fall
# into the void.

const TILE_SIZE := 64
const HEIGHT := 128
const WALL_THICKNESS := 64
const TEXTURE := "__TB_empty"

const FLOOR_TOP := 0
const WATER_TOP := -32
const LAVA_TOP := -32
const CHASM_TOP := -128
const PIT_BOTTOM := CHASM_TOP - WALL_THICKNESS   # = -192
const WALL_TOP := HEIGHT + WALL_THICKNESS        # = 192

# Per-level Z offset. Each level's brushes get translated by
# -level_index * LEVEL_SPACING. Sized so level N+1's ceiling top sits at
# or below level N's PIT_BOTTOM — no overlap, no z-fighting.
const LEVEL_SPACING := 384

# 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

# liquid_t
const L_WATER := 1
const L_LAVA  := 2
const L_CHASM := 3

enum Kind { EMPTY, WALL, FLOOR, WATER, LAVA, CHASM }

func _init() -> void:
	var parsed := _parse_args(OS.get_cmdline_user_args())
	if parsed.is_empty():
		quit(1)
		return
	var generator: String = parsed["generator"]
	var seed_value: int = parsed["seed"]
	var depth: int = parsed["depth"]
	var levels: int = parsed["levels"]
	var out_path: String = parsed["out"]

	if levels < 1:
		push_error("levels must be >= 1")
		quit(1)
		return

	var grids: Array = _generate_grids(generator, seed_value, depth, levels)
	if grids.is_empty():
		push_error("generator '%s' returned no grids" % generator)
		quit(1)
		return

	var f := FileAccess.open(out_path, FileAccess.WRITE)
	if f == null:
		push_error("cannot open %s for write" % out_path)
		quit(1)
		return

	var stats := _write_map(f, grids, seed_value, depth, generator)
	f.close()
	print("wrote %s — generator=%s levels=%d brushes=%d entities=%d" % [
		out_path, generator, levels, stats["brushes"], stats["entities"],
	])
	quit(0)

func _parse_args(args: PackedStringArray) -> Dictionary:
	# Accept either --flag form or legacy positional form.
	if args.size() >= 1 and args[0].begins_with("--"):
		return _parse_flag_args(args)
	return _parse_positional_args(args)

func _parse_flag_args(args: PackedStringArray) -> Dictionary:
	var out := {
		"generator": "brogue",
		"seed": 0,
		"depth": 1,
		"levels": 1,
		"out": "",
	}
	var seen_seed := false
	var seen_depth := false
	var seen_out := false
	var i := 0
	while i < args.size():
		var a: String = args[i]
		match a:
			"--generator":
				i += 1
				out["generator"] = args[i] if i < args.size() else ""
			"--seed":
				i += 1
				if i < args.size():
					out["seed"] = int(args[i]); seen_seed = true
			"--depth":
				i += 1
				if i < args.size():
					out["depth"] = int(args[i]); seen_depth = true
			"--levels":
				i += 1
				if i < args.size():
					out["levels"] = int(args[i])
			"--out":
				i += 1
				if i < args.size():
					out["out"] = args[i]; seen_out = true
			_:
				push_error("unknown arg: %s" % a)
				return {}
		i += 1
	if not seen_seed or not seen_depth or not seen_out:
		push_error("usage: --generator brogue|blobber --seed N --depth N [--levels N] --out PATH")
		return {}
	if out["generator"] != "brogue" and out["generator"] != "blobber":
		push_error("--generator must be 'brogue' or 'blobber'")
		return {}
	return out

func _parse_positional_args(args: PackedStringArray) -> Dictionary:
	match args.size():
		3:
			return {
				"generator": "brogue",
				"seed": int(args[0]),
				"depth": int(args[1]),
				"levels": 1,
				"out": args[2],
			}
		4:
			return {
				"generator": "brogue",
				"seed": int(args[0]),
				"depth": int(args[1]),
				"levels": int(args[2]),
				"out": args[3],
			}
		_:
			push_error("usage: --generator brogue|blobber --seed N --depth N [--levels N] --out PATH (or: SEED DEPTH [LEVELS] OUT.map)")
			return {}

func _generate_grids(generator: String, seed_value: int, depth: int, levels: int) -> Array:
	match generator:
		"brogue":
			var out: Array = []
			for k in range(levels):
				var gen := BrogueGen.new()
				out.append(gen.generate(seed_value + k, depth + k))
				gen.free()
			return out
		"blobber":
			var gen := BrogueGen.new()
			var slices_v: Variant = gen.generate_2d_slices(seed_value, levels, depth)
			gen.free()
			if slices_v == null:
				return []
			var slices: Array = slices_v
			return slices
	return []

func _write_map(f: FileAccess, grids: Array, seed_value: int, depth: int, generator: String) -> Dictionary:
	f.store_string("// Game: FuncGodot\n")
	f.store_string("// Format: Valve\n")
	f.store_string("// Generated by brogue-genesis — generator: %s, seed: %d, depth: %d, levels: %d\n" % [
		generator, seed_value, depth, grids.size(),
	])

	var stats := {"brushes": 0, "entities": 0}
	_emit_worldspawn(f, grids, stats)
	_emit_liquid_entities(f, grids, stats)
	_emit_point_entities(f, grids, stats)
	return stats

# --- Worldspawn: static geometry only (floors/walls/ceilings/bridges/stairs floors). ---
func _emit_worldspawn(f: FileAccess, grids: Array, stats: Dictionary) -> void:
	f.store_string("{\n")
	f.store_string("\"classname\" \"worldspawn\"\n")

	var w: int = grids[0]["width"]
	var h: int = grids[0]["height"]
	var chasm_above := PackedByteArray()
	chasm_above.resize(w * h)

	for k in range(grids.size()):
		var grid: Dictionary = grids[k]
		var is_bottom := (k == grids.size() - 1)
		var z_offset := -k * LEVEL_SPACING
		stats["brushes"] += _write_level_worldspawn(f, grid, z_offset, chasm_above, is_bottom)
		_propagate_chasms(grid, chasm_above)

	f.store_string("}\n")

func _write_level_worldspawn(f: FileAccess, grid: Dictionary, z_off: int,
				chasm_above: PackedByteArray, is_bottom: bool) -> int:
	var w: int = grid["width"]
	var h: int = grid["height"]
	var terrain: PackedByteArray = grid["terrain"]
	var liquid: PackedByteArray = grid["liquid"]

	var count := 0
	var ts := TILE_SIZE

	# Pass 1 — floors / walls / chasm pit-floors, row-merged.
	# Water & lava are skipped here; they are emitted as entities.
	for gy in range(h):
		var run_start := 0
		var run_kind := _kind(terrain, liquid, w, 0, gy)
		for gx in range(1, w + 1):
			var cur: int = Kind.EMPTY
			if gx < w:
				cur = _kind(terrain, liquid, w, gx, gy)
			if cur == run_kind and gx < w:
				continue
			var x0 := run_start * ts
			var x1 := gx * ts
			var y0 := gy * ts
			var y1 := (gy + 1) * ts
			match run_kind:
				Kind.WALL:
					count += _emit_box(f, x0, y0, z_off + PIT_BOTTOM,
										x1, y1, z_off + WALL_TOP)
				Kind.FLOOR:
					count += _emit_box(f, x0, y0, z_off + PIT_BOTTOM,
										x1, y1, z_off + FLOOR_TOP)
				Kind.CHASM:
					# Non-bottom chasms are real holes — no floor. Bottom
					# chasms get a local pit floor so the player doesn't
					# fall into empty void.
					if is_bottom:
						count += _emit_box(f, x0, y0, z_off + PIT_BOTTOM,
											x1, y1, z_off + CHASM_TOP)
				_:
					pass
			run_start = gx
			run_kind = cur

	# Pass 2 — ceilings, row-merged by "is ceiling present here?". A
	# ceiling is present if the cell has floor-like terrain AND no level
	# above has drilled a chasm through it.
	for gy in range(h):
		var run_start := 0
		var run_has := _has_ceiling(terrain, chasm_above, w, 0, gy)
		for gx in range(1, w + 1):
			var cur := false
			if gx < w:
				cur = _has_ceiling(terrain, chasm_above, w, gx, gy)
			if cur == run_has and gx < w:
				continue
			if run_has:
				var x0 := run_start * ts
				var x1 := gx * ts
				var y0 := gy * ts
				var y1 := (gy + 1) * ts
				count += _emit_box(f, x0, y0, z_off + HEIGHT,
									x1, y1, z_off + WALL_TOP)
			run_start = gx
			run_has = cur

	return count

# --- Liquid entities: one func_water / func_lava per row-run, per level. ---
func _emit_liquid_entities(f: FileAccess, grids: Array, stats: Dictionary) -> void:
	for k in range(grids.size()):
		var grid: Dictionary = grids[k]
		var z_off := -k * LEVEL_SPACING
		var counts := _write_level_liquid_entities(f, grid, z_off)
		stats["brushes"] += counts["brushes"]
		stats["entities"] += counts["entities"]

func _write_level_liquid_entities(f: FileAccess, grid: Dictionary, z_off: int) -> Dictionary:
	var w: int = grid["width"]
	var h: int = grid["height"]
	var terrain: PackedByteArray = grid["terrain"]
	var liquid: PackedByteArray = grid["liquid"]

	var brushes := 0
	var entities := 0
	var ts := TILE_SIZE

	for gy in range(h):
		var run_start := 0
		var run_kind := _kind(terrain, liquid, w, 0, gy)
		for gx in range(1, w + 1):
			var cur: int = Kind.EMPTY
			if gx < w:
				cur = _kind(terrain, liquid, w, gx, gy)
			if cur == run_kind and gx < w:
				continue
			var x0 := run_start * ts
			var x1 := gx * ts
			var y0 := gy * ts
			var y1 := (gy + 1) * ts
			match run_kind:
				Kind.WATER:
					brushes += _emit_solid_entity_box(f, "func_water",
						x0, y0, z_off + PIT_BOTTOM, x1, y1, z_off + WATER_TOP)
					entities += 1
				Kind.LAVA:
					brushes += _emit_solid_entity_box(f, "func_lava",
						x0, y0, z_off + PIT_BOTTOM, x1, y1, z_off + LAVA_TOP)
					entities += 1
				_:
					pass
			run_start = gx
			run_kind = cur

	return {"brushes": brushes, "entities": entities}

# --- Point entities: stairs, doors, and a single player spawn. ---
func _emit_point_entities(f: FileAccess, grids: Array, stats: Dictionary) -> void:
	var player_start_origin: Vector3i = Vector3i(-1, -1, -1)

	for k in range(grids.size()):
		var grid: Dictionary = grids[k]
		var z_off := -k * LEVEL_SPACING
		var w: int = grid["width"]
		var h: int = grid["height"]
		var terrain: PackedByteArray = grid["terrain"]
		var ts := TILE_SIZE
		for gy in range(h):
			for gx in range(w):
				var idx := gy * w + gx
				var t: int = terrain[idx]
				var ox := gx * ts + ts / 2
				var oy := gy * ts + ts / 2
				var oz := z_off + FLOOR_TOP
				match t:
					T_STAIRS_UP:
						_emit_point_entity(f, "point_stair_up", Vector3i(ox, oy, oz))
						stats["entities"] += 1
						if k == 0 and player_start_origin.x < 0:
							player_start_origin = Vector3i(ox, oy, oz + 32)
					T_STAIRS_DOWN:
						_emit_point_entity(f, "point_stair_down", Vector3i(ox, oy, oz))
						stats["entities"] += 1
					T_DOOR:
						var angle := _door_angle(terrain, w, h, gx, gy)
						_emit_point_entity(f, "point_door", Vector3i(ox, oy, oz),
							{"angle": str(angle)})
						stats["entities"] += 1
					_:
						pass

	# Fallback: no stairs-up on level 0 — pick first floor cell.
	if player_start_origin.x < 0 and grids.size() > 0:
		var grid: Dictionary = grids[0]
		var w: int = grid["width"]
		var h: int = grid["height"]
		var terrain: PackedByteArray = grid["terrain"]
		var ts := TILE_SIZE
		for gy in range(h):
			for gx in range(w):
				var idx := gy * w + gx
				var t: int = terrain[idx]
				if t == T_FLOOR or t == T_CORRIDOR or t == T_BRIDGE:
					player_start_origin = Vector3i(
						gx * ts + ts / 2, gy * ts + ts / 2, FLOOR_TOP + 32)
					break
			if player_start_origin.x >= 0:
				break

	if player_start_origin.x >= 0:
		_emit_point_entity(f, "point_player_start", player_start_origin)
		stats["entities"] += 1

# Angle (0/90/180/270, Quake convention: 0=east) derived from which
# adjacent cell is open — door faces into the open corridor/room.
func _door_angle(terrain: PackedByteArray, w: int, _h: int, x: int, y: int) -> int:
	var east_open := x + 1 < w and _is_passable(terrain[y * w + (x + 1)])
	var west_open := x - 1 >= 0 and _is_passable(terrain[y * w + (x - 1)])
	if east_open or west_open:
		return 0  # door axis along E/W; value doesn't matter much for now
	return 90      # default to N/S axis

func _is_passable(t: int) -> bool:
	return t == T_FLOOR or t == T_CORRIDOR or t == T_BRIDGE \
		or t == T_DOOR or t == T_STAIRS_UP or t == T_STAIRS_DOWN

# Record any chasm cells at this level into the running mask so the NEXT
# level knows to drill its ceiling there.
func _propagate_chasms(grid: Dictionary, chasm_above: PackedByteArray) -> void:
	var w: int = grid["width"]
	var h: int = grid["height"]
	var terrain: PackedByteArray = grid["terrain"]
	var liquid: PackedByteArray = grid["liquid"]
	for gy in range(h):
		for gx in range(w):
			var idx := gy * w + gx
			if terrain[idx] == T_LIQUID and liquid[idx] == L_CHASM:
				chasm_above[idx] = 1

# Draw kind for floor/wall emission. Water and lava are distinct so the
# main switch can route them to separate entity emitters.
func _kind(terrain: PackedByteArray, liquid: PackedByteArray,
			w: int, x: int, y: int) -> int:
	var idx := y * w + x
	var t: int = terrain[idx]
	match t:
		T_NOTHING:  return Kind.EMPTY
		T_WALL:     return Kind.WALL
		T_LIQUID:
			var liq: int = liquid[idx]
			if liq == L_CHASM: return Kind.CHASM
			if liq == L_WATER: return Kind.WATER
			if liq == L_LAVA:  return Kind.LAVA
			return Kind.FLOOR
		_:          return Kind.FLOOR

# Ceiling emitted when the cell is floor-like AND no chasm sits above it.
# Walls and empty cells don't get ceilings either (wall brush already
# reaches WALL_TOP; empty is empty).
func _has_ceiling(terrain: PackedByteArray, chasm_above: PackedByteArray,
					w: int, x: int, y: int) -> bool:
	var idx := y * w + x
	if chasm_above[idx] != 0:
		return false
	var t: int = terrain[idx]
	return t != T_NOTHING and t != T_WALL

# Standard Quake brush: 6 axis-aligned planes, 3 points per plane, inward
# normals per TrenchBroom convention. Mirrors libd's emit_solid_box.
func _emit_box(f: FileAccess, x0: int, y0: int, z0: int,
				x1: int, y1: int, z1: int) -> int:
	if x0 >= x1 or y0 >= y1 or z0 >= z1:
		return 0
	f.store_string("{\n")
	# Valve 220 texture axes per face normal (Quake convention).
	var ax_x := "[ 0 1 0 0 ] [ 0 0 -1 0 ]"
	var ax_y := "[ 1 0 0 0 ] [ 0 0 -1 0 ]"
	var ax_z := "[ 1 0 0 0 ] [ 0 -1 0 0 ]"
	_face(f, x0, y0, z0, x0, y1, z0, x0, y0, z1, ax_x)
	_face(f, x1, y0, z0, x1, y0, z1, x1, y1, z0, ax_x)
	_face(f, x0, y0, z1, x1, y0, z1, x0, y0, z0, ax_y)
	_face(f, x0, y1, z0, x1, y1, z0, x0, y1, z1, ax_y)
	_face(f, x0, y0, z0, x1, y0, z0, x0, y1, z0, ax_z)
	_face(f, x0, y0, z1, x0, y1, z1, x1, y0, z1, ax_z)
	f.store_string("}\n")
	return 1

# Solid entity wrapping a single axis-aligned box (one brush per entity).
func _emit_solid_entity_box(f: FileAccess, classname: String,
				x0: int, y0: int, z0: int, x1: int, y1: int, z1: int) -> int:
	if x0 >= x1 or y0 >= y1 or z0 >= z1:
		return 0
	f.store_string("{\n")
	f.store_string("\"classname\" \"%s\"\n" % classname)
	var emitted := _emit_box(f, x0, y0, z0, x1, y1, z1)
	f.store_string("}\n")
	return emitted

func _emit_point_entity(f: FileAccess, classname: String, origin: Vector3i,
				extra: Dictionary = {}) -> void:
	f.store_string("{\n")
	f.store_string("\"classname\" \"%s\"\n" % classname)
	f.store_string("\"origin\" \"%d %d %d\"\n" % [origin.x, origin.y, origin.z])
	for key in extra.keys():
		f.store_string("\"%s\" \"%s\"\n" % [key, extra[key]])
	f.store_string("}\n")

func _face(f: FileAccess, x1: int, y1: int, z1: int,
			x2: int, y2: int, z2: int,
			x3: int, y3: int, z3: int, axes: String) -> void:
	f.store_string("( %d %d %d ) ( %d %d %d ) ( %d %d %d ) %s %s 0 1 1\n" % [
		x1, y1, z1, x2, y2, z2, x3, y3, z3, TEXTURE, axes,
	])
