extends Node2D

enum GameState { CALIBRATION, PLAYING }

@onready var platform: Polygon2D = $Platform
@onready var ball: Node2D = $Ball
@onready var status_label: Label = $UiLayer/StatusLabel
@onready var accel_label: Label = $UiLayer/AccelLabel
@onready var threshold_slider: HSlider = $UiLayer/ThresholdPanel/ThresholdSlider
@onready var threshold_caption: Label = $UiLayer/ThresholdPanel/ThresholdCaption
@onready var sensitivity_slider: HSlider = $UiLayer/ThresholdPanel/SensitivitySlider
@onready var sensitivity_caption: Label = $UiLayer/ThresholdPanel/SensitivityCaption
@onready var threshold_panel: Control = $UiLayer/ThresholdPanel
@onready var calibration_overlay: Control = $UiLayer/CalibrationOverlay
@onready var recalib_button: Button = $UiLayer/RecalibButton
@onready var menu_button: Button = $UiLayer/MenuButton

var _socket := WebSocketPeer.new()
var _velocity_x := 0.0
var _accel := Vector3i.ZERO
var _state := GameState.CALIBRATION
var _calibration := AccelCalibration.new()
var _client_id := 0
var _stream_ready := false

const WS_URL := "ws://localhost:8081/ws"
const STREAM_INTERVAL_MS := 16
const MENU_SCENE := "res://scenes/menu.tscn"
const MAX_SPEED := 900.0
const FRICTION := 0.9
const PLATFORM_HALF_WIDTH := 80.0
const MARGIN := 24.0
const PLAY_TOP := 160.0


func _ready() -> void:
	_place_platform_bottom_center()
	ball.reset_to(_play_area().get_center())
	_set_playing_ui_visible(false)

	threshold_slider.value_changed.connect(_on_threshold_changed)
	_on_threshold_changed(threshold_slider.value)
	sensitivity_slider.value_changed.connect(_on_sensitivity_changed)
	_on_sensitivity_changed(sensitivity_slider.value)

	calibration_overlay.calibration_finished.connect(_on_calibration_finished)
	recalib_button.pressed.connect(_restart_calibration)
	menu_button.pressed.connect(_return_to_menu)
	calibration_overlay.start()

	var err := _socket.connect_to_url(WS_URL)
	if err != OK:
		status_label.text = "WebSocket connect failed: %s" % error_string(err)
	else:
		status_label.text = "Connecting to %s…" % WS_URL


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


func _exit_tree() -> void:
	_disable_input_stream()


func _physics_process(delta: float) -> void:
	_socket.poll()
	_update_connection_status()

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

	if _state == GameState.CALIBRATION:
		if calibration_overlay.is_active():
			calibration_overlay.feed_accel(_accel)
		_update_accel_label()
		return

	var eff := _effective_accel()
	if eff == 0.0:
		_velocity_x *= FRICTION
	else:
		var target_v := eff * _velocity_scale()
		_velocity_x = move_toward(_velocity_x, target_v, maxf(absf(target_v) * 0.35, 120.0) * delta)
	_velocity_x = clampf(_velocity_x, -MAX_SPEED, MAX_SPEED)

	var next_x := platform.position.x + _velocity_x * delta
	platform.position.x = _clamp_x(next_x)
	ball.step(delta, platform.position, _velocity_x, _play_area())
	_update_accel_label()


func _restart_calibration() -> void:
	_state = GameState.CALIBRATION
	_velocity_x = 0.0
	_set_playing_ui_visible(false)
	calibration_overlay.start()
	if _socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
		calibration_overlay.notify_connected()
	else:
		status_label.text = "Kalibrierung – tippe „Kalibrierung starten“"


func _on_calibration_finished(calibration: AccelCalibration) -> void:
	_calibration = calibration
	threshold_slider.value = calibration.suggested_threshold
	_on_threshold_changed(calibration.suggested_threshold)
	_start_playing()


func _start_playing() -> void:
	_state = GameState.PLAYING
	_set_playing_ui_visible(true)
	_velocity_x = 0.0
	_place_platform_bottom_center()
	ball.reset_to(_play_area().get_center())
	status_label.text = "Kalibriert: Achse %s · Schwellwert %d" % [
		_calibration.axis_name(), int(threshold_slider.value)
	]


func _set_playing_ui_visible(visible: bool) -> void:
	platform.visible = visible
	ball.visible = visible
	threshold_panel.visible = visible
	recalib_button.visible = visible


func _on_threshold_changed(value: float) -> void:
	threshold_caption.text = "Schwellwert |%s|: %d" % [_calibration.axis_name(), int(value)]


func _on_sensitivity_changed(value: float) -> void:
	sensitivity_caption.text = "Bewegungsstärke: %d %%" % int(value)


func _velocity_scale() -> float:
	return sensitivity_slider.value / 100.0


func _calibrated_raw() -> float:
	return _calibration.project(_accel)


func _effective_accel() -> float:
	var raw := _calibrated_raw()
	var centered := raw - _calibration.center
	var threshold := threshold_slider.value
	var abs_v := absf(centered)
	if abs_v <= threshold:
		return 0.0
	return signf(centered) * (abs_v - threshold)


func _update_connection_status() -> void:
	if _state == GameState.CALIBRATION:
		match _socket.get_ready_state():
			WebSocketPeer.STATE_OPEN:
				status_label.text = "Kalibrierung – verbunden"
				calibration_overlay.notify_connected()
			WebSocketPeer.STATE_CONNECTING:
				status_label.text = "Kalibrierung – verbinde…"
			_:
				status_label.text = "Kalibrierung – keine Verbindung"
		return

	match _socket.get_ready_state():
		WebSocketPeer.STATE_OPEN:
			var eff := _effective_accel()
			status_label.text = "Connected · Achse %s · vx=%.0f · eff=%.0f" % [
				_calibration.axis_name(), _velocity_x, eff
			]
		WebSocketPeer.STATE_CONNECTING:
			status_label.text = "Connecting…"
		WebSocketPeer.STATE_CLOSING, WebSocketPeer.STATE_CLOSED:
			status_label.text = "Disconnected – retrying…"
			if _socket.get_ready_state() == WebSocketPeer.STATE_CLOSED:
				_socket.connect_to_url(WS_URL)


func _send(payload: Dictionary) -> void:
	if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
		return
	_socket.send_text(JSON.stringify(payload))


func _disable_input_stream() -> void:
	if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
		return
	_send({"type": "set_stream", "enable": false, "interval_ms": STREAM_INTERVAL_MS})
	if _client_id > 0:
		_send({"type": "set_input_stream", "client_id": _client_id, "enable": false})
	_stream_ready = false


func _enable_input_stream() -> void:
	if _client_id <= 0:
		return
	_send({"type": "set_input_stream", "client_id": _client_id, "enable": true})
	_send({"type": "set_stream", "enable": true, "interval_ms": STREAM_INTERVAL_MS})
	_stream_ready = true


func _handle_message(text: String) -> void:
	var data = JSON.parse_string(text)
	if typeof(data) != TYPE_DICTIONARY:
		return

	match data.get("type", ""):
		"hello":
			_send({"type": "list_clients"})
		"client_list":
			_on_client_list(data)
		"input":
			_on_input(data)


func _on_client_list(data: Dictionary) -> void:
	if not data.get("success", false):
		return

	for entry in data.get("clients", []):
		if typeof(entry) != TYPE_DICTIONARY:
			continue
		if not entry.get("available", false):
			continue
		var cid := int(entry.get("id", 0))
		if cid > 0:
			_client_id = cid
			break

	if _client_id > 0 and not _stream_ready:
		_enable_input_stream()


func _on_input(data: Dictionary) -> void:
	if not data.get("success", false):
		return

	for client in data.get("clients", []):
		if typeof(client) != TYPE_DICTIONARY:
			continue
		var cid := int(client.get("client_id", 0))
		if _client_id > 0 and cid != _client_id:
			continue
		if not client.get("valid", false):
			continue
		_accel = Vector3i(
			_to_int(client.get("x")),
			_to_int(client.get("y")),
			_to_int(client.get("z"))
		)
		return


func _to_int(value: Variant) -> int:
	match typeof(value):
		TYPE_INT:
			return value
		TYPE_FLOAT:
			return int(value)
		TYPE_STRING:
			return int(float(value))
	return 0


func _play_area() -> Rect2:
	var size := get_viewport_rect().size
	var bottom_y := size.y - MARGIN
	return Rect2(
		MARGIN,
		PLAY_TOP,
		size.x - MARGIN * 2.0,
		bottom_y - PLAY_TOP
	)


func _place_platform_bottom_center() -> void:
	var size := get_viewport_rect().size
	platform.position = Vector2(_clamp_x(size.x * 0.5), size.y - 48.0)


func _platform_x_limits() -> Vector2:
	var view_w := get_viewport_rect().size.x
	return Vector2(PLATFORM_HALF_WIDTH + MARGIN, view_w - PLATFORM_HALF_WIDTH - MARGIN)


func _clamp_x(x: float) -> float:
	var limits := _platform_x_limits()
	var min_x := limits.x
	var max_x := limits.y
	if x < min_x:
		if _velocity_x < 0.0:
			_velocity_x = 0.0
		return min_x
	if x > max_x:
		if _velocity_x > 0.0:
			_velocity_x = 0.0
		return max_x
	return x


func _update_accel_label() -> void:
	if _state == GameState.CALIBRATION:
		accel_label.text = "x: %d   y: %d   z: %d\nKalibrierung läuft…" % [_accel.x, _accel.y, _accel.z]
		return
	var eff := _effective_accel()
	accel_label.text = "x: %d   y: %d   z: %d\nAchse %s → %.0f (Schw. %d, eff %.0f)" % [
		_accel.x, _accel.y, _accel.z,
		_calibration.axis_name(), _calibration.project(_accel),
		int(threshold_slider.value), eff
	]


func _notification(what: int) -> void:
	if what == NOTIFICATION_WM_SIZE_CHANGED:
		platform.position.y = get_viewport_rect().size.y - 48.0
		platform.position.x = _clamp_x(platform.position.x)
		if _state == GameState.PLAYING:
			ball.reset_to(_play_area().get_center())
