extends Control

const WS_URL := "ws://localhost:8081/ws"
const MENU_SCENE := "res://scenes/menu.tscn"
const MAX_LOG_LINES := 200
const TAP_FLASH_SEC := 2.0

const PANEL_STYLE_NORMAL := Color(0.11, 0.12, 0.17, 1.0)
const PANEL_BORDER_NORMAL := Color(0.22, 0.25, 0.32, 1.0)
const PANEL_STYLE_FLASH := Color(0.32, 0.28, 0.06, 1.0)
const PANEL_BORDER_FLASH := Color(1.0, 0.82, 0.15, 1.0)

const LED_MODES: PackedStringArray = ["color", "clear", "progress", "digit", "blink", "find-me"]
const LED_MODES_COLOR_INTENSITY: PackedStringArray = ["color", "progress", "digit", "blink"]

@onready var status_label: Label = $UiLayer/TopBar/StatusLabel
@onready var log_output: TextEdit = $UiLayer/MainSplit/LogPanel/LogOutput
@onready var stream_output: Label = $UiLayer/MainSplit/LogPanel/StreamOutput
@onready var back_button: Button = $UiLayer/TopBar/BackButton
@onready var interval_spin: SpinBox = $UiLayer/MainSplit/LeftPanel/Toolbar/IntervalSpin
@onready var list_clients_btn: Button = $UiLayer/MainSplit/LeftPanel/Toolbar/ListClientsBtn
@onready var clients_list: VBoxContainer = $UiLayer/MainSplit/LeftPanel/ClientsScroll/ClientsList
@onready var empty_hint: Label = $UiLayer/MainSplit/LeftPanel/ClientsScroll/ClientsList/EmptyHint
@onready var start_stream_btn: Button = $UiLayer/MainSplit/LeftPanel/StreamBar/StartStreamBtn
@onready var stop_stream_btn: Button = $UiLayer/MainSplit/LeftPanel/StreamBar/StopStreamBtn

var _socket := WebSocketPeer.new()
var _connected := false
var _streaming := false
var _log_lines: PackedStringArray = []
var _client_ui: Dictionary = {}


func _ready() -> void:
	back_button.pressed.connect(_return_to_menu)
	list_clients_btn.pressed.connect(func(): _send({"type": "list_clients"}))
	start_stream_btn.pressed.connect(_start_stream)
	stop_stream_btn.pressed.connect(_stop_stream)
	_connect_ws()


func _return_to_menu() -> void:
	get_tree().change_scene_to_file(MENU_SCENE)


func _connect_ws() -> void:
	_connected = false
	var err := _socket.connect_to_url(WS_URL)
	if err != OK:
		_set_status("Verbindung fehlgeschlagen: %s" % error_string(err))
	else:
		_set_status("Verbinde mit %s…" % WS_URL)


func _interval_ms() -> int:
	return int(interval_spin.value)


func _send(payload: Dictionary) -> void:
	if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
		_log("Nicht verbunden – Befehl verworfen: %s" % payload.get("type", "?"))
		return
	var text := JSON.stringify(payload)
	_socket.send_text(text)
	_log("→ %s" % text)


func _start_stream() -> void:
	if _client_ui.is_empty():
		_log("Keine Clients geladen")
		return

	var want_accel := false
	var want_tap := false

	for cid in _client_ui:
		var ui: Dictionary = _client_ui[cid]
		if not ui.get("available", false):
			continue

		var accel_on: bool = ui["accel_cb"].button_pressed
		_send({"type": "set_input_stream", "client_id": cid, "enable": accel_on})
		if accel_on:
			want_accel = true

		var tap_single: bool = ui["tap_single"].button_pressed
		var tap_double: bool = ui["tap_double"].button_pressed
		var tap_triple: bool = ui["tap_triple"].button_pressed
		var tap_on := tap_single or tap_double or tap_triple
		_send({
			"type": "set_tap_notify",
			"client_id": cid,
			"single": tap_single,
			"double_tap": tap_double,
			"triple": tap_triple,
		})
		if tap_on:
			want_tap = true

	var want_input := want_accel or want_tap
	_send({"type": "set_stream", "enable": want_input, "interval_ms": _interval_ms()})

	_streaming = true
	start_stream_btn.disabled = true
	stop_stream_btn.disabled = false
	_log("Stream gestartet (input=%s, accel=%s, tap=%s)" % [want_input, want_accel, want_tap])


func _stop_stream() -> void:
	_send({"type": "set_stream", "enable": false, "interval_ms": _interval_ms()})

	for cid in _client_ui:
		var ui: Dictionary = _client_ui[cid]
		if not ui.get("available", false):
			continue
		_send({"type": "set_input_stream", "client_id": cid, "enable": false})
		_send({
			"type": "set_tap_notify",
			"client_id": cid,
			"single": false,
			"double_tap": false,
			"triple": false,
		})
		ui["values_label"].text = "—"

	_streaming = false
	start_stream_btn.disabled = _client_ui.is_empty()
	stop_stream_btn.disabled = true
	stream_output.text = "—"
	_log("Stream gestoppt")


func _clear_client_panels() -> void:
	_client_ui.clear()
	for child in clients_list.get_children():
		if child != empty_hint:
			child.queue_free()


func _populate_clients(clients: Array) -> void:
	_clear_client_panels()
	var has_available := false

	for entry in clients:
		if typeof(entry) != TYPE_DICTIONARY:
			continue
		var cid := int(entry.get("id", 0))
		if cid <= 0:
			continue

		var available: bool = entry.get("available", false)
		if available:
			has_available = true

		var panel := PanelContainer.new()
		panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
		var normal_style := _make_panel_style(PANEL_STYLE_NORMAL, PANEL_BORDER_NORMAL, 1)
		var flash_style := _make_panel_style(PANEL_STYLE_FLASH, PANEL_BORDER_FLASH, 2)
		panel.add_theme_stylebox_override("panel", normal_style)

		var margin := MarginContainer.new()
		margin.add_theme_constant_override("margin_left", 10)
		margin.add_theme_constant_override("margin_right", 10)
		margin.add_theme_constant_override("margin_top", 8)
		margin.add_theme_constant_override("margin_bottom", 8)
		panel.add_child(margin)

		var vbox := VBoxContainer.new()
		vbox.add_theme_constant_override("separation", 6)
		margin.add_child(vbox)

		var mac: String = str(entry.get("mac", "?"))
		var status := "online" if available else "offline"
		var header := Label.new()
		header.text = "Client #%d  ·  %s  ·  %s" % [cid, mac, status]
		header.add_theme_font_size_override("font_size", 14)
		header.add_theme_color_override("font_color", Color(0.9, 0.92, 0.98) if available else Color(0.55, 0.58, 0.65))
		vbox.add_child(header)

		var accel_cb := CheckBox.new()
		accel_cb.text = "Accel Stream"
		accel_cb.button_pressed = entry.get("input_stream", false)
		accel_cb.disabled = not available
		vbox.add_child(accel_cb)

		var tap_label := Label.new()
		tap_label.text = "Tap Notify:"
		tap_label.add_theme_font_size_override("font_size", 12)
		tap_label.add_theme_color_override("font_color", Color(0.7, 0.75, 0.85))
		vbox.add_child(tap_label)

		var tap_row := HBoxContainer.new()
		tap_row.add_theme_constant_override("separation", 12)
		vbox.add_child(tap_row)

		var tap_single := CheckBox.new()
		tap_single.text = "Single"
		tap_single.button_pressed = entry.get("tap_notify_single", false)
		tap_single.disabled = not available

		var tap_double := CheckBox.new()
		tap_double.text = "Double"
		tap_double.button_pressed = entry.get("tap_notify_double", false)
		tap_double.disabled = not available

		var tap_triple := CheckBox.new()
		tap_triple.text = "Triple"
		tap_triple.button_pressed = entry.get("tap_notify_triple", false)
		tap_triple.disabled = not available

		tap_row.add_child(tap_single)
		tap_row.add_child(tap_double)
		tap_row.add_child(tap_triple)

		var led_label := Label.new()
		led_label.text = "LED Ring:"
		led_label.add_theme_font_size_override("font_size", 12)
		led_label.add_theme_color_override("font_color", Color(0.7, 0.75, 0.85))
		vbox.add_child(led_label)

		var led_row := HBoxContainer.new()
		led_row.add_theme_constant_override("separation", 8)
		vbox.add_child(led_row)

		var led_mode := OptionButton.new()
		for mode in LED_MODES:
			led_mode.add_item(mode)
		led_mode.disabled = not available
		led_row.add_child(led_mode)

		var led_apply := Button.new()
		led_apply.text = "Anwenden"
		led_apply.disabled = not available
		led_apply.pressed.connect(_apply_led_for_client.bind(cid))
		led_row.add_child(led_apply)

		var led_params := VBoxContainer.new()
		led_params.add_theme_constant_override("separation", 4)
		vbox.add_child(led_params)

		var led_color_row := HBoxContainer.new()
		led_color_row.add_theme_constant_override("separation", 8)
		led_params.add_child(led_color_row)
		var led_color_lbl := Label.new()
		led_color_lbl.text = "Farbe:"
		led_color_row.add_child(led_color_lbl)
		var led_color := ColorPickerButton.new()
		led_color.color = Color(1.0, 0.0, 0.0)
		led_color.custom_minimum_size = Vector2(36, 28)
		led_color.disabled = not available
		led_color_row.add_child(led_color)

		var led_intensity_row := HBoxContainer.new()
		led_intensity_row.add_theme_constant_override("separation", 8)
		led_params.add_child(led_intensity_row)
		var led_intensity_lbl := Label.new()
		led_intensity_lbl.text = "Intensität:"
		led_intensity_row.add_child(led_intensity_lbl)
		var led_intensity := SpinBox.new()
		led_intensity.min_value = 0
		led_intensity.max_value = 255
		led_intensity.value = 128
		led_intensity.custom_minimum_size = Vector2(80, 0)
		_set_spinbox_enabled(led_intensity, available)
		led_intensity_row.add_child(led_intensity)

		var led_progress_row := HBoxContainer.new()
		led_progress_row.add_theme_constant_override("separation", 8)
		led_params.add_child(led_progress_row)
		var led_progress_lbl := Label.new()
		led_progress_lbl.text = "Fortschritt %:"
		led_progress_row.add_child(led_progress_lbl)
		var led_progress := SpinBox.new()
		led_progress.min_value = 0
		led_progress.max_value = 100
		led_progress.value = 50
		led_progress.custom_minimum_size = Vector2(80, 0)
		_set_spinbox_enabled(led_progress, available)
		led_progress_row.add_child(led_progress)

		var led_digit_row := HBoxContainer.new()
		led_digit_row.add_theme_constant_override("separation", 8)
		led_params.add_child(led_digit_row)
		var led_digit_lbl := Label.new()
		led_digit_lbl.text = "Ziffer:"
		led_digit_row.add_child(led_digit_lbl)
		var led_digit := SpinBox.new()
		led_digit.min_value = 0
		led_digit.max_value = 10
		led_digit.value = 3
		led_digit.custom_minimum_size = Vector2(80, 0)
		_set_spinbox_enabled(led_digit, available)
		led_digit_row.add_child(led_digit)

		var led_blink_row := HBoxContainer.new()
		led_blink_row.add_theme_constant_override("separation", 8)
		led_params.add_child(led_blink_row)
		var led_blink_ms_lbl := Label.new()
		led_blink_ms_lbl.text = "Blink ms:"
		led_blink_row.add_child(led_blink_ms_lbl)
		var led_blink_ms := SpinBox.new()
		led_blink_ms.min_value = 1
		led_blink_ms.max_value = 10000
		led_blink_ms.value = 500
		led_blink_ms.custom_minimum_size = Vector2(80, 0)
		_set_spinbox_enabled(led_blink_ms, available)
		led_blink_row.add_child(led_blink_ms)
		var led_blink_count_lbl := Label.new()
		led_blink_count_lbl.text = "Anzahl:"
		led_blink_row.add_child(led_blink_count_lbl)
		var led_blink_count := SpinBox.new()
		led_blink_count.min_value = 1
		led_blink_count.max_value = 100
		led_blink_count.value = 3
		led_blink_count.custom_minimum_size = Vector2(60, 0)
		_set_spinbox_enabled(led_blink_count, available)
		led_blink_row.add_child(led_blink_count)

		led_mode.item_selected.connect(func(_idx: int) -> void: _update_led_fields(cid))

		var led_presets := HBoxContainer.new()
		led_presets.add_theme_constant_override("separation", 6)
		vbox.add_child(led_presets)

		_add_led_preset_btn(led_presets, "Aus", cid, available, func() -> void: _led_apply_preset(cid, "clear"))
		_add_led_preset_btn(led_presets, "Rot", cid, available, func() -> void: _led_apply_preset(cid, "color", Color(1, 0.2, 0.2), 128))
		_add_led_preset_btn(led_presets, "Grün", cid, available, func() -> void: _led_apply_preset(cid, "color", Color(0.2, 1, 0.3), 128))
		_add_led_preset_btn(led_presets, "Blau", cid, available, func() -> void: _led_apply_preset(cid, "color", Color(0.2, 0.4, 1), 128))
		_add_led_preset_btn(led_presets, "Find", cid, available, func() -> void: _led_apply_preset(cid, "find-me"))

		var values_label := Label.new()
		values_label.text = "—"
		values_label.add_theme_font_size_override("font_size", 12)
		values_label.add_theme_color_override("font_color", Color(0.65, 0.85, 0.75))
		values_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
		vbox.add_child(values_label)

		clients_list.add_child(panel)

		_client_ui[cid] = {
			"available": available,
			"panel": panel,
			"normal_style": normal_style,
			"flash_style": flash_style,
			"flash_token": 0,
			"accel_cb": accel_cb,
			"tap_single": tap_single,
			"tap_double": tap_double,
			"tap_triple": tap_triple,
			"led_color": led_color,
			"led_mode": led_mode,
			"led_intensity": led_intensity,
			"led_progress": led_progress,
			"led_digit": led_digit,
			"led_blink_ms": led_blink_ms,
			"led_blink_count": led_blink_count,
			"led_color_row": led_color_row,
			"led_intensity_row": led_intensity_row,
			"led_progress_row": led_progress_row,
			"led_digit_row": led_digit_row,
			"led_blink_row": led_blink_row,
			"values_label": values_label,
		}
		_update_led_fields(cid)

	empty_hint.visible = _client_ui.is_empty()
	start_stream_btn.disabled = _client_ui.is_empty() or not has_available or _streaming
	_log("%d Client(s) geladen" % _client_ui.size())


func _physics_process(_delta: float) -> void:
	_socket.poll()
	var state := _socket.get_ready_state()

	if state == WebSocketPeer.STATE_OPEN:
		while _socket.get_available_packet_count() > 0:
			_handle_message(_socket.get_packet().get_string_from_utf8())

	match state:
		WebSocketPeer.STATE_OPEN:
			if _connected:
				_set_status("Verbunden · %s" % WS_URL)
		WebSocketPeer.STATE_CONNECTING:
			_set_status("Verbinde…")
		WebSocketPeer.STATE_CLOSING, WebSocketPeer.STATE_CLOSED:
			_connected = false
			_streaming = false
			start_stream_btn.disabled = _client_ui.is_empty()
			stop_stream_btn.disabled = true
			_set_status("Getrennt – erneuter Verbindungsversuch…")
			if state == WebSocketPeer.STATE_CLOSED:
				_connect_ws()


func _handle_message(text: String) -> void:
	var data = JSON.parse_string(text)
	if typeof(data) != TYPE_DICTIONARY:
		_log("← (ungültiges JSON) %s" % text.substr(0, mini(text.length(), 120)))
		return

	var msg_type: String = data.get("type", "")
	match msg_type:
		"hello":
			_on_hello(data)
		"client_list":
			_on_client_list(data)
		"input":
			_on_input(data)
		"led_ring_status":
			_log("← LED #%s: %s" % [data.get("client_id", "?"), "OK" if data.get("success", false) else data.get("error", "?")])
		_:
			if not _streaming or not msg_type.ends_with("_status"):
				_log("← %s" % _format_response(data))


func _on_hello(data: Dictionary) -> void:
	_connected = true
	if data.has("interval_ms"):
		interval_spin.value = int(data["interval_ms"])
	_log("← hello (Port: %s)" % data.get("serial_port", "?"))


func _on_client_list(data: Dictionary) -> void:
	if not data.get("success", false):
		_log("← client_list FAILED: %s" % data.get("error", "?"))
		return
	_populate_clients(data.get("clients", []))


func _on_input(data: Dictionary) -> void:
	if not data.get("success", false):
		stream_output.text = "input FEHLER: %s" % data.get("error", "?")
		return

	var summary: PackedStringArray = []
	for client in data.get("clients", []):
		if typeof(client) != TYPE_DICTIONARY:
			continue
		var cid := int(client.get("client_id", 0))
		if not _client_ui.has(cid):
			continue

		var ui: Dictionary = _client_ui[cid]
		var lines: PackedStringArray = []
		if client.get("valid", false):
			lines.append(
				"x=%d y=%d z=%d (age %sms)" % [
					int(client.get("x", 0)),
					int(client.get("y", 0)),
					int(client.get("z", 0)),
					str(client.get("accel_age_ms", "?")),
				]
			)

		var tap_kind := str(client.get("tap_kind", "none"))
		if tap_kind != "none":
			var tap_text := "tap: %s (age %sms)" % [tap_kind, str(client.get("tap_age_ms", "?"))]
			lines.append(tap_text)
			_flash_client_panel(cid)

		if lines.is_empty():
			ui["values_label"].text = "—"
		else:
			ui["values_label"].text = "\n".join(lines)
			summary.append("#%d: %s" % [cid, " · ".join(lines)])

	if summary.is_empty():
		stream_output.text = "input (keine Daten)"
	else:
		stream_output.text = "input\n" + "\n".join(summary)


func _set_spinbox_enabled(spin: SpinBox, enabled: bool) -> void:
	spin.editable = enabled
	spin.get_line_edit().editable = enabled


func _add_led_preset_btn(
	row: HBoxContainer,
	text: String,
	cid: int,
	available: bool,
	on_pressed: Callable
) -> void:
	var btn := Button.new()
	btn.text = text
	btn.disabled = not available
	btn.pressed.connect(on_pressed)
	row.add_child(btn)


func _update_led_fields(cid: int) -> void:
	if not _client_ui.has(cid):
		return
	var ui: Dictionary = _client_ui[cid]
	var mode: String = ui["led_mode"].get_item_text(ui["led_mode"].selected)

	ui["led_color_row"].visible = mode in LED_MODES_COLOR_INTENSITY
	ui["led_intensity_row"].visible = mode in LED_MODES_COLOR_INTENSITY
	ui["led_progress_row"].visible = mode == "progress"
	ui["led_digit_row"].visible = mode == "digit"
	ui["led_blink_row"].visible = mode == "blink"


func _select_led_mode(ui: Dictionary, mode: String) -> void:
	for i in ui["led_mode"].item_count:
		if ui["led_mode"].get_item_text(i) == mode:
			ui["led_mode"].select(i)
			return


func _led_apply_preset(
	cid: int,
	mode: String,
	color: Color = Color.WHITE,
	intensity: int = 128,
	param: int = 50
) -> void:
	if not _client_ui.has(cid):
		return
	var ui: Dictionary = _client_ui[cid]
	_select_led_mode(ui, mode)
	if mode in LED_MODES_COLOR_INTENSITY:
		ui["led_color"].color = color
		ui["led_intensity"].value = intensity
	if mode == "progress":
		ui["led_progress"].value = param
	if mode == "digit":
		ui["led_digit"].value = param
	if mode == "blink":
		ui["led_blink_ms"].value = param
		ui["led_blink_count"].value = 3
	_update_led_fields(cid)
	_apply_led_for_client(cid)


func _apply_led_for_client(cid: int) -> void:
	if not _client_ui.has(cid):
		return
	_send(_build_led_payload(cid))


func _build_led_payload(cid: int) -> Dictionary:
	var ui: Dictionary = _client_ui[cid]
	var mode: String = ui["led_mode"].get_item_text(ui["led_mode"].selected)
	var payload := {"type": "set_led_ring", "client_id": cid, "mode": mode}

	match mode:
		"clear", "find-me":
			pass
		"color":
			_payload_add_color_intensity(payload, ui)
		"progress":
			payload["progress"] = int(ui["led_progress"].value)
			_payload_add_color_intensity(payload, ui)
		"digit":
			payload["digit"] = int(ui["led_digit"].value)
			_payload_add_color_intensity(payload, ui)
		"blink":
			payload["blink_ms"] = int(ui["led_blink_ms"].value)
			payload["blink_count"] = int(ui["led_blink_count"].value)
			_payload_add_color_intensity(payload, ui)

	return payload


func _payload_add_color_intensity(payload: Dictionary, ui: Dictionary) -> void:
	_payload_add_rgb(payload, ui["led_color"].color)
	payload["intensity"] = int(ui["led_intensity"].value)


func _payload_add_rgb(payload: Dictionary, color: Color) -> void:
	payload["r"] = int(color.r * 255)
	payload["g"] = int(color.g * 255)
	payload["b"] = int(color.b * 255)


func _make_panel_style(bg: Color, border: Color, border_width: int) -> StyleBoxFlat:
	var style := StyleBoxFlat.new()
	style.bg_color = bg
	style.border_color = border
	style.set_border_width_all(border_width)
	style.set_corner_radius_all(6)
	style.set_content_margin_all(0)
	return style


func _flash_client_panel(cid: int) -> void:
	if not _client_ui.has(cid):
		return

	var ui: Dictionary = _client_ui[cid]
	var panel: PanelContainer = ui["panel"]
	panel.add_theme_stylebox_override("panel", ui["flash_style"])

	ui["flash_token"] = int(ui.get("flash_token", 0)) + 1
	var token: int = ui["flash_token"]
	get_tree().create_timer(TAP_FLASH_SEC).timeout.connect(
		func() -> void:
			if not _client_ui.has(cid):
				return
			var current: Dictionary = _client_ui[cid]
			if int(current.get("flash_token", 0)) != token:
				return
			current["panel"].add_theme_stylebox_override("panel", current["normal_style"]),
		CONNECT_ONE_SHOT
	)


func _format_response(data: Dictionary) -> String:
	var msg_type: String = data.get("type", "?")
	if not data.get("success", true) and data.has("error"):
		return "%s FAILED: %s" % [msg_type, data["error"]]
	return JSON.stringify(data)


func _set_status(text: String) -> void:
	status_label.text = text


func _log(text: String) -> void:
	var line := "[%s] %s" % [_time_str(), text]
	_log_lines.append(line)
	while _log_lines.size() > MAX_LOG_LINES:
		_log_lines.remove_at(0)
	log_output.text = "\n".join(_log_lines)
	log_output.set_caret_line(log_output.get_line_count() - 1)


func _time_str() -> String:
	var t := Time.get_time_dict_from_system()
	return "%02d:%02d:%02d" % [t.hour, t.minute, t.second]
