extends CanvasLayer
## Handles displaying debug info.

signal mode_changed(mode: Mode)

enum Mode { DISABLED, PERFORMANCE, FULL }

const LINE_WIDTH: float = 2
const MARKER_RADIUS: float = 0.2
const CIRCLE_RADIUS: float = 3
const DEFAULT_COLOR: Color = Color.RED

var mode: Mode = Mode.PERFORMANCE

var _vectors_to_draw: Dictionary = {}
var _lines_to_draw: Dictionary = {}
var _markers_to_draw: Dictionary = {}
var _circles_to_draw: Dictionary = {}
var _text_to_draw: Dictionary = {}
var _events_to_draw: Dictionary = {}

var _label1_text: String = ""
var _label2_text: String = ""
var _label3_text: String = ""

@onready var _control: Control = $Control
@onready var _label1: RichTextLabel = $Control/LabelContainer1/Label1
@onready var _label2: RichTextLabel = $Control/LabelContainer2/Label2
@onready var _label3: RichTextLabel = $Control/LabelContainer3/Label3
@onready var _label4: RichTextLabel = $Control/LabelContainer4/Label4


func _ready() -> void:
	assert(_control, str(self) + ": _control missing!")
	assert(_label1, str(self) + ": _label1 missing!")
	assert(_label2, str(self) + ": _label2 missing!")
	assert(_label3, str(self) + ": _label2 missing!")
	assert(_label4, str(self) + ": _label2 missing!")
	_control.draw.connect(_on_control_draw)
	Inputer.mode_changed.connect(_on_inputer_mode_changed)
	# enabled = OS.has_feature("editor")
	_update_visibility()
	_update_controls_label()
	mode_changed.emit(mode)


func _process(_delta: float) -> void:
	if mode == Mode.DISABLED:
		return

	_control.queue_redraw()


func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("toggle_debug"):
		mode = wrapi(mode + 1, 0, Mode.size()) as Mode
		_update_visibility()
		mode_changed.emit(mode)


func show_debug() -> bool:
	return mode == Mode.FULL


func text(key: String, value: Variant, label_index: int = 1) -> void:
	if not show_debug():
		return

	_text_to_draw[key] = {"value": value, "label_index": label_index}


func add_event(key: String) -> void:
	_events_to_draw[key] = {"frame": -9999, "args": []}


func event_emitted(key: String, args: Array[Variant] = []) -> void:
	if not show_debug():
		return

	_events_to_draw[key] = {"frame": Engine.get_physics_frames(), "args": args}


func vector(
	key: String, from: Vector3, to: Vector3, color: Color = DEFAULT_COLOR
) -> void:
	if not show_debug():
		return

	_vectors_to_draw[key] = {"from": from, "to": to, "color": color, "on": true}


func line(
	key: String, from: Vector3, to: Vector3, color: Color = DEFAULT_COLOR
) -> void:
	if not show_debug():
		return

	_lines_to_draw[key] = {"from": from, "to": to, "color": color, "on": true}


func marker(
	key: String,
	pos: Vector3,
	radius: float = MARKER_RADIUS,
	color: Color = DEFAULT_COLOR
) -> void:
	if not show_debug():
		return

	_markers_to_draw[key] = ({"pos": pos, "radius": radius, "color": color, "on": true})


func circle(key: String, pos: Vector3, color: Color = DEFAULT_COLOR) -> void:
	if not show_debug():
		return

	_circles_to_draw[key] = {"pos": pos, "color": color, "on": true}


func _update_visibility() -> void:
	visible = mode != Mode.DISABLED


func _unproject(pos: Vector3) -> Vector2:
	return Referencer.main_camera.unproject_position(pos)


func _update_controls_label() -> void:
	var label_text: String = (
		"DEBUG CONTROLS\ntoggle debug: %s"
		% [
			Inputer.get_action_prompt("toggle_debug"),
		]
	)

	_label4.text = label_text


func _append_text(key: String, value: Variant, label_index: int) -> void:
	var line_text := str(value)

	if value is int:
		line_text = (" " if value >= 0 else "") + line_text
		if (value as int) > 0:
			line_text = "[color=sky_blue]%s[/color]" % line_text
		elif (value as int) < 0:
			line_text = "[color=salmon]%s[/color]" % line_text

	elif value is float:
		line_text = (" " if value >= 0 else "") + "%.6f" % value
		if value > 0:
			line_text = "[color=sky_blue]%s[/color]" % line_text
		elif value < 0:
			line_text = "[color=salmon]%s[/color]" % line_text

	elif value is bool:
		if value:
			line_text = "[color=sky_blue]%s[/color]" % line_text
		else:
			line_text = "[color=salmon]%s[/color]" % line_text

	elif value is Vector3:
		line_text = (
			"(%s, %s, %s)"
			% [
				(" " if value.x >= 0 else "") + ("%.6f" % value.x),
				(" " if value.y >= 0 else "") + ("%.6f" % value.y),
				(" " if value.z >= 0 else "") + ("%.6f" % value.z),
			]
		)

	elif value is Vector2:
		line_text = (
			"(%s, %s)"
			% [
				(" " if value.x >= 0 else "") + ("%.6f" % value.x),
				(" " if value.y >= 0 else "") + ("%.6f" % value.y),
			]
		)

	line_text = "%s: %s\n" % [key, line_text]

	if label_index == 2:
		_label2_text += line_text
	else:
		_label1_text += line_text


func _append_event(key: String, frame: int, args: Array[Variant]) -> void:
	var line_text := key

	if args.size() > 0:
		line_text += "(%s)" % ", ".join(args.map(str))

	var physics_frame := Engine.get_physics_frames()
	var color := Color.SALMON.lerp(
		Color.WHITE, clampf(float(physics_frame - frame) / 30.0, 0, 1)
	)
	if physics_frame - frame < 5:
		color = Color.SKY_BLUE

	line_text = "[color=#%s]%s[/color]\n" % [color.to_html(), line_text]

	_label3_text += line_text


func _draw_vector(from: Vector3, to: Vector3, color: Color) -> void:
	if (
		not Referencer.main_camera.is_position_in_frustum(from)
		and not Referencer.main_camera.is_position_in_frustum(to)
	):
		return

	var start := _unproject(from)
	var end := _unproject(to)
	if (start - end).length() > 0:
		_control.draw_line(start, end, color, LINE_WIDTH)
		if get_viewport().get_visible_rect().has_point(end):
			_draw_triangle(end, start.direction_to(end), 5, color)


func _draw_line(from: Vector3, to: Vector3, color: Color) -> void:
	if (
		not Referencer.main_camera.is_position_in_frustum(from)
		and not Referencer.main_camera.is_position_in_frustum(to)
	):
		return

	var start := _unproject(from)
	var end := _unproject(to)
	if (start - end).length() > 0:
		_control.draw_line(start, end, color, LINE_WIDTH)


func _draw_triangle(
	pos: Vector2,
	dir: Vector2,
	size: float,
	color: Color,
) -> void:
	var a := pos + dir * size
	var b := pos + dir.rotated(2 * PI / 3) * size
	var c := pos + dir.rotated(4 * PI / 3) * size
	var points := PackedVector2Array([a, b, c])
	_control.draw_polygon(points, PackedColorArray([color]))


func _draw_marker(pos: Vector3, radius: float, color: Color) -> void:
	if not Referencer.main_camera.is_position_in_frustum(pos):
		return

	var x_start := _unproject(pos + (Vector3.LEFT * radius))
	var x_end := _unproject(pos + (Vector3.RIGHT * radius))
	_control.draw_line(x_start, x_end, color, LINE_WIDTH)

	var y_start := _unproject(pos + (Vector3.UP * radius))
	var y_end := _unproject(pos + (Vector3.DOWN * radius))
	_control.draw_line(y_start, y_end, color, LINE_WIDTH)

	var z_start := _unproject(pos + (Vector3.FORWARD * radius))
	var z_end := _unproject(pos + (Vector3.BACK * radius))
	_control.draw_line(z_start, z_end, color, LINE_WIDTH)


func _draw_circle(pos: Vector3, color: Color) -> void:
	if not Referencer.main_camera.is_position_in_frustum(pos):
		return

	var point := _unproject(pos)
	_control.draw_circle(point, CIRCLE_RADIUS, color)


func _set_label_texts() -> void:
	_label1.text = _label1_text
	_label2.text = _label2_text
	_label3.text = _label3_text


func _on_control_draw() -> void:
	if mode != Mode.DISABLED:
		_label1_text = ""
		_label2_text = ""
		_label3_text = ""

		_append_text("fps", Engine.get_frames_per_second() as int, 0)

	if mode != Mode.FULL:
		if mode != Mode.DISABLED:
			_set_label_texts()
		return

	for k: String in _text_to_draw.keys():
		var v: Dictionary = _text_to_draw[k]
		_append_text(k, v["value"] as Variant, v["label_index"] as int)

	for k: String in _events_to_draw.keys():
		var v: Dictionary = _events_to_draw[k]
		_append_event(k, v["frame"] as int, v["args"] as Array[Variant])

	_set_label_texts()

	if !Referencer.main_camera:
		return

	for v: Dictionary in _vectors_to_draw.values():
		if v["on"]:
			_draw_vector(
				v["from"] as Vector3,
				v["to"] as Vector3,
				v["color"] as Color,
			)
			# v["on"] = false

	for v: Dictionary in _lines_to_draw.values():
		if v["on"]:
			_draw_line(
				v["from"] as Vector3,
				v["to"] as Vector3,
				v["color"] as Color,
			)
			# v["on"] = false

	for v: Dictionary in _markers_to_draw.values():
		if v["on"]:
			_draw_marker(
				v["pos"] as Vector3,
				v["radius"] as float,
				v["color"] as Color,
			)
			# v["on"] = false

	for v: Dictionary in _circles_to_draw.values():
		if v["on"]:
			_draw_circle(v["pos"] as Vector3, v["color"] as Color)
			# v["on"] = false


func _on_inputer_mode_changed(_input_mode: Inputer.Mode) -> void:
	_update_controls_label()
