extends Node
## Bakes the painted look of the map the player is on: the real scenery rendered once from straight above
## (orthographic, noon light, no fog / glow) into a 1024 px picture, then cached on disk by map_paint.gd
## (user://map_cache/<id>.png, redone only when the map's data or MapPaint.VERSION changes).
## Entity views (players, monsters, NPCs, drops), name labels and signs are hidden for that one frame.
## Headless runs and the automated tests skip it (they use the painted picture from map data).
## Child of the HUD; listens to main.map_changed.

const MapPaint = preload("res://scripts/map_paint.gd")
const EnvTime = preload("res://scripts/env_time.gd")
const WorldBuilder = preload("res://scripts/world_builder.gd")

const SIZE := 1024
## Camera height above the ground (above every tree / roof, below nothing).
const HEIGHT := 90.0

signal baked(id: String)

var main = null
var busy := false
var _queue: Array = []


func _ready() -> void:
	if main and main.has_signal("map_changed"):
		main.map_changed.connect(_on_map_changed)
		if str(main.map_id) != "":
			_on_map_changed(str(main.map_id))


func _exit_tree() -> void:
	MapPaint.clear()


static func can_bake() -> bool:
	if DisplayServer.get_name() == "headless":
		return false
	var a := OS.get_cmdline_user_args()
	for f in ["--selftest", "--envtest", "--tripotest", "--sfxtest"]:
		if a.has(f):
			return false
	for x in a:
		if x.begins_with("--nettest") or x.begins_with("--n3test"):
			return false
	return true


func _on_map_changed(id: String) -> void:
	# a painted picture right away (worker thread) so the map is never empty while the bake waits
	MapPaint.texture(id)
	if not can_bake() or MapPaint.has_baked(id) or id in _queue:
		return
	_queue.append(id)
	if not busy:
		_run()


func _run() -> void:
	busy = true
	while not _queue.is_empty():
		var id := str(_queue.pop_front())
		# let the new scenery settle (MultiMeshes, time of day, shader params)
		for i in 4:
			await get_tree().process_frame
		if main == null or str(main.map_id) != id or main.env_root == null:
			continue
		var img: Image = await bake_now()
		if img != null and str(main.map_id) == id:
			MapPaint.set_baked(id, img)
			baked.emit(id)
	busy = false


## Renders the loaded map from above and returns the picture (null when rendering is not possible).
func bake_now() -> Image:
	if DisplayServer.get_name() == "headless" or main == null or main.env_root == null:
		return null
	var hf := WorldBuilder.map_half()
	var sv := SubViewport.new()
	sv.size = Vector2i(SIZE, SIZE)
	sv.world_3d = get_viewport().world_3d
	sv.render_target_update_mode = SubViewport.UPDATE_ONCE
	sv.msaa_3d = Viewport.MSAA_4X
	sv.transparent_bg = false
	var cam := Camera3D.new()
	cam.projection = Camera3D.PROJECTION_ORTHOGONAL
	cam.size = hf * 2.0
	cam.near = 1.0
	cam.far = HEIGHT + 30.0
	cam.current = true
	sv.add_child(cam)
	var we: Environment = get_viewport().world_3d.environment
	if we:
		var e := we.duplicate() as Environment
		e.fog_enabled = false
		e.glow_enabled = false
		e.volumetric_fog_enabled = false
		e.background_mode = Environment.BG_COLOR
		e.background_color = Color(0.1, 0.08, 0.12)
		cam.environment = e
	add_child(sv)
	# +X right, +Z down on the picture (look straight down with -Z as "up")
	cam.look_at_from_position(Vector3(0, HEIGHT, 0), Vector3.ZERO, Vector3(0, 0, -1))
	# noon light, long shadow range, no people / labels for this one frame
	var tod = _find_tod(main.env_root)
	var hour0 := EnvTime.hour
	if tod:
		EnvTime.hour = 12.0
		tod.apply()
	var sun := main.env_root.get_node_or_null("Sun") as DirectionalLight3D
	var shadow0 := 0.0
	if sun:
		shadow0 = sun.directional_shadow_max_distance
		sun.directional_shadow_max_distance = HEIGHT + 40.0
	var hidden: Array = []
	for n in [main.map_root, main.player]:
		if n and n.visible:
			n.visible = false
			hidden.append(n)
	for l in main.env_root.find_children("*", "Label3D", true, false):
		if l.visible:
			l.visible = false
			hidden.append(l)
	await RenderingServer.frame_post_draw
	var img := sv.get_texture().get_image()
	for n in hidden:
		if is_instance_valid(n):
			n.visible = true
	if sun and is_instance_valid(sun):
		sun.directional_shadow_max_distance = shadow0
	if tod and is_instance_valid(tod):
		EnvTime.hour = hour0
		tod.apply()
	sv.queue_free()
	if img == null or img.is_empty():
		return null
	img.convert(Image.FORMAT_RGBA8)
	return img


static func _find_tod(root: Node):
	for c in root.get_children():
		if c is EnvTime:
			return c
	return null
