extends Node3D

# A disposable cartoon: no physics bodies, layout writes, damage or gameplay RNG.
# The restaurant saves before this starts; only its visual bodies are displaced.
signal finished

const WINDUP := 1.15
const RUN_TIME := 0.62
const AFTERMATH := 0.70
const LANE_RADIUS := 36.0
const HOLE_HALF_WIDTH := 64.0

var elapsed := 0.0
var wall_broken := false
var launched_count := 0
var _scene: Node2D
var _world: Node3D
var _hero: Node3D
var _camera: Camera3D
var _camera_transform := Transform3D.IDENTITY
var _scene_mode: ProcessMode
var _origin := Vector3.ZERO
var _lane_z := 0.0
var _exit_x := 0.0
var _wall: MeshInstance3D
var _wall_x := 0.0
var _originals: Dictionary = {}
var _hidden: Dictionary = {}
var _targets: Array[Dictionary] = []
var _flying: Array[Dictionary] = []
var _particles: Array[Dictionary] = []
var _random := RandomNumberGenerator.new()
var _puff_clock := 0.0
var _impact_age := -1.0
var _overlay: CanvasLayer
var _fade: ColorRect
var _restored := false
var _started := false

func begin(scene: Node2D) -> bool:
	name = "DungeonDeparture"
	process_mode = Node.PROCESS_MODE_PAUSABLE
	_world = get_parent() as Node3D
	_hero = Bridge3D._avatars.get(scene.get("player")) as Node3D
	if not is_instance_valid(_hero) or not is_instance_valid(_world):
		return false
	_scene = scene
	_scene_mode = scene.process_mode
	Stage.claim("dungeon_departure", Stage.PRIORITY_CEREMONY + 1)
	_camera = Bridge3D._camera
	if is_instance_valid(_camera):
		_camera_transform = _camera.transform
	_random.randomize()
	_origin = _hero.global_position
	_lane_z = _origin.z
	var room: Rect2 = scene.get("ROOM_RECT")
	_wall_x = room.end.x
	_exit_x = room.end.x + 520.0
	_select_wall(room)
	_remember(_hero)
	for entity: Node2D in Bridge3D._avatars:
		var body: Node3D = Bridge3D._avatars[entity]
		if body != _hero and is_instance_valid(body) and body.visible:
			_add_target(body)
	if is_instance_valid(Bridge3D._tables):
		for table: Node3D in Bridge3D._tables.get_children():
			if table.visible:
				_add_target(table)
	_hide_badges(_world)
	var hud := scene.get("hud") as CanvasLayer
	if hud != null:
		_hidden[hud] = hud.visible
		hud.hide()
	_world.set_meta("departure_active", true)
	scene.process_mode = Node.PROCESS_MODE_DISABLED
	_hero.process_mode = Node.PROCESS_MODE_PAUSABLE
	Model3DFactory.show_state(_hero, "walk", "walk", "", true)
	_build_overlay()
	_started = true
	return true

func _input(_event: InputEvent) -> void:
	if _started:
		get_viewport().set_input_as_handled()

func _select_wall(room: Rect2) -> void:
	# Use the rendered wall segments, including the real doorway. If the player
	# faces the door gap, the sprint veers to the nearest solid piece of masonry.
	var closest := INF
	for child: Node in _world.get_children():
		if not child is MeshInstance3D or not child.mesh is BoxMesh:
			continue
		var body := child as MeshInstance3D
		var size: Vector3 = body.mesh.size
		if size.x > 40.0 or absf(body.position.x - room.end.x) > 24.0:
			continue
		if size.z < HOLE_HALF_WIDTH * 2.0:
			continue
		var candidate := clampf(_origin.z, body.position.z - size.z * 0.5 + HOLE_HALF_WIDTH,
			body.position.z + size.z * 0.5 - HOLE_HALF_WIDTH)
		var distance := absf(candidate - _origin.z)
		if distance < closest:
			closest = distance
			_wall = body
			_lane_z = candidate
			_wall_x = body.position.x

func _remember(body: Node3D) -> void:
	if not _originals.has(body):
		_originals[body] = {"transform": body.transform, "visible": body.visible,
			"process_mode": body.process_mode}

func _hide_badges(node: Node) -> void:
	for child: Node in node.get_children():
		if child is InfoBadge3D or child is AttackField3D:
			_hidden[child] = child.visible
			child.hide()
			_hide_badges(child)
		elif child is CanvasLayer:
			_hidden[child] = child.visible
			child.hide()
		else:
			_hide_badges(child)

func _add_target(body: Node3D) -> void:
	_remember(body)
	var bounds := _visual_bounds(body)
	_targets.append({"body": body, "bounds": bounds, "hit": false})

func _visual_bounds(body: Node3D) -> AABB:
	var bounds := AABB(body.global_position - Vector3(18, 0, 18), Vector3(36, 50, 36))
	for child: Node in body.get_children():
		if child is InfoBadge3D or child is AttackField3D:
			continue
		if child is MeshInstance3D:
			bounds = bounds.merge(child.global_transform * child.get_aabb())
		elif child is Node3D:
			bounds = bounds.merge(_visual_bounds(child))
	return bounds

func _build_overlay() -> void:
	_overlay = CanvasLayer.new()
	_overlay.layer = 90
	add_child(_overlay)
	_fade = ColorRect.new()
	_fade.color = Color(0.06, 0.035, 0.025, 0.0)
	_fade.mouse_filter = Control.MOUSE_FILTER_STOP
	_fade.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	_overlay.add_child(_fade)

func _process(delta: float) -> void:
	if not _started or _restored:
		return
	if not is_instance_valid(_hero):
		_complete()
		return
	elapsed += delta
	_update_particles(delta)
	_update_flying(delta)
	if elapsed < WINDUP:
		_wind_up(delta)
	else:
		_sprint(delta)
	if _impact_age >= 0.0:
		_impact_age += delta
		if is_instance_valid(_camera):
			var force := 8.0 * maxf(0.0, 1.0 - _impact_age / 0.28)
			_camera.transform = _camera_transform
			_camera.position += Vector3(sin(_impact_age * 113.0), cos(_impact_age * 97.0), 0) * force
	_fade.color.a = smoothstep(WINDUP + RUN_TIME + AFTERMATH - 0.22,
		WINDUP + RUN_TIME + AFTERMATH, elapsed)
	if elapsed >= WINDUP + RUN_TIME + AFTERMATH:
		_complete()

func _wind_up(delta: float) -> void:
	var charge := elapsed / WINDUP
	var shake := sin(elapsed * lerpf(35.0, 115.0, charge)) * charge
	_hero.global_position = _origin + Vector3(shake * 6.0 - charge * 12.0,
		absf(sin(elapsed * 39.0)) * charge * 5.0, 0)
	_hero.rotation = Vector3(0, PI * 0.5, 0.14 * charge + shake * 0.06)
	_hero.scale = Vector3(1.0 + charge * 0.16, 1.0 - charge * 0.19, 1.0 + charge * 0.12)
	_set_run_speed(lerpf(1.6, 6.0, charge))
	_puff_clock -= delta
	if _puff_clock <= 0.0:
		_puff_clock = lerpf(0.13, 0.045, charge)
		for side: float in [-1.0, 1.0]:
			_smoke(_origin + Vector3(_random.randf_range(-20, 12), 7, side * 18),
				Vector3(-25, 30, side * 30), lerpf(10, 21, charge), 0.6)

func _run_position(progress: float) -> Vector3:
	# Fast takeoff, then constant speed; the short diagonal avoids using the door.
	var align_by := minf(0.38, maxf(0.01, (_wall_x - _origin.x) / maxf(1.0, _exit_x - _origin.x) * 0.8))
	return Vector3(lerpf(_origin.x, _exit_x, progress), 0,
		lerpf(_origin.z, _lane_z, smoothstep(0.0, align_by, progress)))

func _sprint(_delta: float) -> void:
	var previous := _hero.global_position
	var progress := clampf((elapsed - WINDUP) / RUN_TIME, 0.0, 1.0)
	var at := _run_position(progress)
	_hero.global_position = at + Vector3(0, 4.0 + absf(sin(elapsed * 95.0)) * 9.0, 0)
	_hero.rotation = Vector3(0, PI * 0.5, -0.40)
	_hero.scale = Vector3(1.22, 0.83, 1.0)
	_set_run_speed(7.0)
	if previous.x < at.x:
		# Sample the swept path, so even a slow frame leaves a continuous trail
		# and catches every crossed object instead of jumping past it.
		var steps := maxi(1, ceili((at.x - previous.x) / 24.0))
		for index: int in steps:
			var sample := previous.lerp(at, float(index + 1) / float(steps))
			_smoke(sample + Vector3(-24, 10, _random.randf_range(-16, 16)),
				Vector3(-45, 24, _random.randf_range(-18, 18)), _random.randf_range(15, 27), 0.85)
			if index % 2 == 0:
				_speed_line(sample)
		_launch_crossed(previous, at)
	if not wall_broken and at.x >= _wall_x:
		_break_wall()
	if progress >= 1.0:
		_hero.hide()

func _set_run_speed(speed: float) -> void:
	var tree := _hero.get_meta("animation_tree", null) as AnimationTree
	if is_instance_valid(tree):
		tree.set("parameters/LocomotionSpeed/scale", speed)
	else:
		var animation := _hero.get_meta("animation_player", null) as AnimationPlayer
		if is_instance_valid(animation):
			animation.speed_scale = speed

func _launch_crossed(from: Vector3, to: Vector3) -> void:
	for target: Dictionary in _targets:
		if target.hit:
			continue
		var bounds: AABB = target.bounds
		# Test the whole swept segment against the object's actual visual footprint.
		var footprint := AABB(Vector3(bounds.position.x, -1000, bounds.position.z),
			Vector3(bounds.size.x, 2000, bounds.size.z)).grow(LANE_RADIUS)
		if not footprint.has_point(to) and not footprint.intersects_segment(from, to):
			continue
		target.hit = true
		var body: Node3D = target.body
		var side := -1.0 if body.global_position.z < to.z else 1.0
		_flying.append({"body": body, "velocity": Vector3(_random.randf_range(270, 490),
			_random.randf_range(290, 440), side * _random.randf_range(70, 180)),
			"spin": Vector3(_random.randf_range(-3, 3), side * 3.0, -_random.randf_range(4, 8))})
		launched_count += 1
		for puff: int in 4:
			_smoke(body.global_position + Vector3(0, 18, 0),
				Vector3(_random.randf_range(-45, 90), 65, side * 60), 15, 0.5)

func _update_flying(delta: float) -> void:
	for flight: Dictionary in _flying:
		var body: Node3D = flight.body
		if not is_instance_valid(body):
			continue
		flight.velocity.y -= 580.0 * delta
		body.global_position += flight.velocity * delta
		body.rotation += flight.spin * delta

func _break_wall() -> void:
	wall_broken = true
	_impact_age = 0.0
	if is_instance_valid(_wall):
		_remember(_wall)
		_wall.hide()
		var size: Vector3 = _wall.mesh.size
		var low := _wall.position.z - size.z * 0.5
		var high := _wall.position.z + size.z * 0.5
		# Keep both intact sides and a ragged brick lintel over the actual breach.
		_wall_piece(low, _lane_z - HOLE_HALF_WIDTH, size.y, size.y * 0.5)
		_wall_piece(_lane_z + HOLE_HALF_WIDTH, high, size.y, size.y * 0.5)
		for brick: int in 8:
			var tall := 18.0 + float(brick % 3) * 8.0
			_wall_piece(_lane_z - HOLE_HALF_WIDTH + brick * 16.0,
				_lane_z - HOLE_HALF_WIDTH + (brick + 1) * 16.0, tall, size.y - tall * 0.5)
	for index: int in 38:
		var tint := Color("c57950") if index % 3 else Color("e8bd86")
		var brick := _box(Vector3(_random.randf_range(10, 21), _random.randf_range(8, 15),
			_random.randf_range(15, 27)), tint)
		brick.position = Vector3(_wall_x, _random.randf_range(15, 115),
			_lane_z + _random.randf_range(-HOLE_HALF_WIDTH, HOLE_HALF_WIDTH))
		_particles.append({"body": brick, "velocity": Vector3(_random.randf_range(320, 760),
			_random.randf_range(100, 370), _random.randf_range(-160, 160)),
			"spin": Vector3(_random.randf_range(-8, 8), _random.randf_range(-9, 9), -8),
			"age": 0.0, "life": 1.05, "gravity": 680.0, "growth": 0.0})
	for index: int in 22:
		_smoke(Vector3(_wall_x, _random.randf_range(12, 100), _lane_z + _random.randf_range(-55, 55)),
			Vector3(_random.randf_range(110, 330), _random.randf_range(15, 110),
			_random.randf_range(-90, 90)), _random.randf_range(20, 36), 0.85)

func _wall_piece(low: float, high: float, tall: float, elevation: float) -> void:
	if high - low < 0.5:
		return
	var piece := _box(Vector3(_wall.mesh.size.x, tall, high - low), Color.WHITE)
	piece.material_override = _wall.material_override
	piece.position = Vector3(_wall.position.x, elevation, (low + high) * 0.5)

func _box(size: Vector3, tint: Color) -> MeshInstance3D:
	var mesh := BoxMesh.new()
	mesh.size = size
	return _mesh(mesh, tint)

func _mesh(mesh: Mesh, tint: Color) -> MeshInstance3D:
	var body := MeshInstance3D.new()
	body.mesh = mesh
	body.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
	var material := StandardMaterial3D.new()
	material.albedo_color = tint
	material.roughness = 1.0
	body.material_override = material
	add_child(body)
	return body

func _smoke(at: Vector3, velocity: Vector3, radius: float, life: float) -> void:
	var mesh := SphereMesh.new()
	mesh.radius = radius
	mesh.height = radius * 1.6
	mesh.radial_segments = 12
	mesh.rings = 6
	var body := _mesh(mesh, Color("e7ddc5").darkened(_random.randf_range(0.0, 0.13)))
	body.position = at
	body.material_override.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
	body.material_override.albedo_color.a = 0.82
	_particles.append({"body": body, "velocity": velocity, "spin": Vector3.ZERO,
		"age": 0.0, "life": life, "gravity": -12.0, "growth": 1.3})

func _speed_line(at: Vector3) -> void:
	var body := _box(Vector3(_random.randf_range(75, 135), 2.5, 3), Color("fff0be"))
	body.position = at + Vector3(-45, _random.randf_range(18, 58), _random.randf_range(-22, 22))
	body.material_override.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
	body.material_override.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
	_particles.append({"body": body, "velocity": Vector3(-100, 0, 0), "spin": Vector3.ZERO,
		"age": 0.0, "life": 0.19, "gravity": 0.0, "growth": 0.0})

func _update_particles(delta: float) -> void:
	for index: int in range(_particles.size() - 1, -1, -1):
		var particle: Dictionary = _particles[index]
		particle.age += delta
		var body: MeshInstance3D = particle.body
		if particle.age >= particle.life:
			body.queue_free()
			_particles.remove_at(index)
			continue
		particle.velocity.y -= float(particle.gravity) * delta
		body.position += particle.velocity * delta
		body.rotation += particle.spin * delta
		body.scale = Vector3.ONE * (1.0 + float(particle.growth) * float(particle.age))
		var material := body.material_override as StandardMaterial3D
		if material.transparency == BaseMaterial3D.TRANSPARENCY_ALPHA:
			material.albedo_color.a = 0.82 * (1.0 - smoothstep(0.25, 1.0, float(particle.age) / float(particle.life)))

func _complete() -> void:
	_restore()
	finished.emit()

func _restore() -> void:
	if _restored or not _started:
		return
	_restored = true
	for body: Variant in _originals:
		if is_instance_valid(body):
			var original: Dictionary = _originals[body]
			body.transform = original.transform
			body.visible = original.visible
			body.process_mode = original.process_mode
	for item: Variant in _hidden:
		if is_instance_valid(item):
			item.visible = _hidden[item]
	if is_instance_valid(_hero):
		var animation := _hero.get_meta("animation_player", null) as AnimationPlayer
		if is_instance_valid(animation):
			animation.speed_scale = 1.0
		Model3DFactory.show_state(_hero, "idle", "idle", "", true)
	if is_instance_valid(_camera):
		_camera.transform = _camera_transform
	if is_instance_valid(_world):
		_world.remove_meta("departure_active")
	if is_instance_valid(_scene):
		_scene.process_mode = _scene_mode
	Stage.release("dungeon_departure")

func _exit_tree() -> void:
	_restore()
