class_name ControllerMappingPanel
extends Control

signal closed

const ControllerMappingManagerType = preload(
	"res://settings/controller_mapping_manager.gd"
)
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
const CAPTURE_NEUTRAL_SECONDS: float = 0.22
const CANCEL_COMBO_HOLD_SECONDS: float = 1.25

var _mapping_manager: ControllerMappingManagerType
var _binding_buttons: Dictionary = {}
var _controller_label: Label
var _instruction_label: Label
var _progress_label: Label
var _auto_map_button: Button
var _reset_button: Button
var _close_button: Button
var _capturing_role: StringName = &""
var _auto_map_active: bool = false
var _auto_map_index: int = -1
var _auto_map_draft: Dictionary = {}
var _capture_device_id: int = 0
var _waiting_for_neutral: bool = false
var _neutral_elapsed: float = 0.0
var _cancel_left_bumper_pressed: bool = false
var _cancel_right_bumper_pressed: bool = false
var _cancel_combo_elapsed: float = 0.0


func _ready() -> void:
	set_process_input(true)
	set_process(true)
	_build_interface()
	hide()


func setup(mapping_manager: ControllerMappingManagerType) -> void:
	_mapping_manager = mapping_manager
	if not _mapping_manager.active_controller_changed.is_connected(
		_on_active_controller_changed
	):
		_mapping_manager.active_controller_changed.connect(
			_on_active_controller_changed
		)
	if not _mapping_manager.active_profile_changed.is_connected(
		_refresh_bindings
	):
		_mapping_manager.active_profile_changed.connect(_refresh_bindings)
	if not _mapping_manager.controller_input_observed.is_connected(
		_on_controller_input_observed
	):
		_mapping_manager.controller_input_observed.connect(
			_on_controller_input_observed
		)
	_refresh_bindings()


func open_panel() -> void:
	if _mapping_manager == null:
		return
	_cancel_capture()
	_mapping_manager.recalibrate_active_device()
	show()
	_refresh_bindings()
	_auto_map_button.grab_focus()


func is_open() -> bool:
	return visible


func is_capturing() -> bool:
	return visible and not _capturing_role.is_empty()


func request_back() -> void:
	if not visible:
		return
	if not _capturing_role.is_empty():
		_cancel_capture()
		return
	close_panel()


func close_panel() -> void:
	_cancel_capture()
	hide()
	closed.emit()


func _input(event: InputEvent) -> void:
	if not visible or _mapping_manager == null:
		return
	if _capturing_role.is_empty():
		return
	var key_event := event as InputEventKey
	if (
		key_event != null
		and key_event.pressed
		and not key_event.echo
		and key_event.keycode == KEY_ESCAPE
	):
		get_viewport().set_input_as_handled()
		_cancel_capture()
		_progress_label.text = "controller mapping cancelled"
		return


func _process(delta: float) -> void:
	if (
		not visible
		or _mapping_manager == null
		or _capturing_role.is_empty()
	):
		return
	if _cancel_left_bumper_pressed and _cancel_right_bumper_pressed:
		_cancel_combo_elapsed += delta
		_progress_label.text = "keep holding both bumpers to cancel"
		if _cancel_combo_elapsed >= CANCEL_COMBO_HOLD_SECONDS:
			_cancel_capture()
			_progress_label.text = "controller mapping cancelled"
		return
	_cancel_combo_elapsed = 0.0
	if _waiting_for_neutral:
		if not _mapping_manager.are_capture_inputs_neutral(_capture_device_id):
			_neutral_elapsed = 0.0
			return
		_neutral_elapsed += delta
		if _neutral_elapsed < CAPTURE_NEUTRAL_SECONDS:
			return
		_waiting_for_neutral = false
		_neutral_elapsed = 0.0
		_refresh_capture_prompt()
		return
	var pressed_button: int = _mapping_manager.get_pressed_capture_button(
		_capture_device_id
	)
	if pressed_button < 0:
		return
	var button_event := InputEventJoypadButton.new()
	button_event.device = _capture_device_id
	button_event.button_index = pressed_button as JoyButton
	button_event.pressed = true
	_try_capture_event(button_event)


func _on_controller_input_observed(event: InputEvent) -> void:
	if not visible or _mapping_manager == null:
		return
	if _capturing_role.is_empty():
		return
	get_viewport().set_input_as_handled()
	var button_event := event as InputEventJoypadButton
	var motion_event := event as InputEventJoypadMotion
	var event_device: int = -1
	if button_event != null:
		event_device = button_event.device
	elif motion_event != null:
		event_device = motion_event.device
	else:
		return
	if event_device != _capture_device_id:
		return
	_track_cancel_combo_button(button_event)
	if _waiting_for_neutral:
		return
	_try_capture_event(event)


func _try_capture_event(event: InputEvent) -> void:
	var binding: Dictionary = _mapping_manager.binding_from_event(
		_capturing_role,
		event,
	)
	if binding.is_empty():
		return
	if not _mapping_manager.validate_binding(_capturing_role, binding):
		return
	_accept_captured_binding(binding)


func _build_interface() -> void:
	set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	mouse_filter = Control.MOUSE_FILTER_STOP
	z_index = 100
	UtilityPageStyleType.apply_page(self)

	var frame := PanelContainer.new()
	frame.set_anchors_preset(Control.PRESET_FULL_RECT)
	frame.offset_left = 18.0
	frame.offset_top = 18.0
	frame.offset_right = -18.0
	frame.offset_bottom = -18.0
	frame.add_theme_stylebox_override(
		"panel",
		UtilityPageStyleType.rounded_style(
			UtilityPageStyleType.OCEAN_PANEL_DEEP,
			20,
		),
	)
	add_child(frame)

	var outer_margin := MarginContainer.new()
	outer_margin.add_theme_constant_override("margin_left", 24)
	outer_margin.add_theme_constant_override("margin_top", 20)
	outer_margin.add_theme_constant_override("margin_right", 24)
	outer_margin.add_theme_constant_override("margin_bottom", 20)
	frame.add_child(outer_margin)

	var page := VBoxContainer.new()
	page.add_theme_constant_override("separation", 12)
	outer_margin.add_child(page)

	var heading_row := HBoxContainer.new()
	heading_row.add_theme_constant_override("separation", 12)
	page.add_child(heading_row)

	var title := Label.new()
	title.text = "controller mapping"
	title.add_theme_font_size_override("font_size", 28)
	title.add_theme_color_override(
		"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
	)
	title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	heading_row.add_child(title)

	_controller_label = Label.new()
	_controller_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
	_controller_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
	_controller_label.add_theme_font_size_override("font_size", 15)
	_controller_label.add_theme_color_override(
		"font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY
	)
	heading_row.add_child(_controller_label)

	_instruction_label = Label.new()
	_instruction_label.text = (
		"auto-map walks through NETfishing's controller actions. "
		+ "select any row afterward to override it; the mapped back "
		+ "control cancels capture."
	)
	_instruction_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	_instruction_label.add_theme_font_size_override("font_size", 15)
	_instruction_label.add_theme_color_override(
		"font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY
	)
	page.add_child(_instruction_label)

	_progress_label = Label.new()
	_progress_label.custom_minimum_size.y = 34.0
	_progress_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
	_progress_label.add_theme_font_size_override("font_size", 18)
	_progress_label.add_theme_color_override(
		"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
	)
	page.add_child(_progress_label)

	var scroll := ScrollContainer.new()
	scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
	scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
	page.add_child(scroll)

	var role_grid := GridContainer.new()
	role_grid.columns = 2
	role_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	role_grid.add_theme_constant_override("h_separation", 12)
	role_grid.add_theme_constant_override("v_separation", 7)
	scroll.add_child(role_grid)

	for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
		var role_label := Label.new()
		role_label.text = ControllerMappingManagerType.ROLE_LABELS.get(
			role,
			str(role),
		)
		role_label.custom_minimum_size = Vector2(330.0, 42.0)
		role_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
		role_label.add_theme_font_size_override("font_size", 16)
		role_label.add_theme_color_override(
			"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
		)
		role_grid.add_child(role_label)

		var binding_button := Button.new()
		binding_button.custom_minimum_size = Vector2(210.0, 42.0)
		binding_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
		binding_button.focus_mode = Control.FOCUS_ALL
		binding_button.tooltip_text = (
			"change " + str(ControllerMappingManagerType.ROLE_LABELS.get(
				role,
				str(role),
			))
		)
		UtilityPageStyleType.apply_compact_ocean_button(binding_button)
		binding_button.pressed.connect(_begin_manual_capture.bind(role))
		role_grid.add_child(binding_button)
		_binding_buttons[role] = binding_button

	var footer := HBoxContainer.new()
	footer.alignment = BoxContainer.ALIGNMENT_END
	footer.add_theme_constant_override("separation", 10)
	page.add_child(footer)

	_auto_map_button = Button.new()
	_auto_map_button.text = "auto-map controller"
	_auto_map_button.tooltip_text = (
		"walk through every input on a standard xbox-style controller"
	)
	UtilityPageStyleType.apply_ocean_button(_auto_map_button)
	_auto_map_button.pressed.connect(_begin_auto_map)
	footer.add_child(_auto_map_button)

	_reset_button = Button.new()
	_reset_button.text = "restore defaults"
	_reset_button.tooltip_text = "remove this controller's custom mapping"
	UtilityPageStyleType.apply_ocean_button(_reset_button)
	_reset_button.pressed.connect(_reset_mapping)
	footer.add_child(_reset_button)

	_close_button = Button.new()
	_close_button.text = "done"
	UtilityPageStyleType.apply_ocean_button(_close_button)
	_close_button.pressed.connect(close_panel)
	footer.add_child(_close_button)


func _begin_auto_map() -> void:
	if _mapping_manager == null:
		return
	_auto_map_active = true
	_auto_map_index = 0
	_auto_map_draft = {}
	_reset_cancel_combo()
	_capture_device_id = _mapping_manager.get_active_device_id()
	_set_capture_role(ControllerMappingManagerType.ROLE_ORDER[_auto_map_index])


func _begin_manual_capture(role: StringName) -> void:
	_auto_map_active = false
	_auto_map_index = -1
	_auto_map_draft.clear()
	_reset_cancel_combo()
	_capture_device_id = _mapping_manager.get_active_device_id()
	_set_capture_role(role)


func _set_capture_role(role: StringName) -> void:
	_capturing_role = role
	_waiting_for_neutral = true
	_neutral_elapsed = 0.0
	_set_action_buttons_disabled(true)
	_progress_label.text = "release all controller inputs"


func _refresh_capture_prompt() -> void:
	if _capturing_role.is_empty():
		return
	if _auto_map_active:
		_progress_label.text = (
			"step %d of %d: %s\n"
			+ "hold both bumpers for %.2f seconds to cancel"
		) % [
			_auto_map_index + 1,
			ControllerMappingManagerType.ROLE_ORDER.size(),
			_mapping_manager.get_role_prompt(_capturing_role),
			CANCEL_COMBO_HOLD_SECONDS,
		]
		return
	var input_instruction: String = "press any controller button"
	if _mapping_manager.role_expects_axis(_capturing_role):
		input_instruction = "move any controller axis"
	elif _mapping_manager.role_accepts_axis(_capturing_role):
		input_instruction = "press any button or move any controller axis"
	_progress_label.text = "%s for %s" % [
		input_instruction,
		_mapping_manager.get_role_label(_capturing_role),
	]


func _accept_captured_binding(binding: Dictionary) -> void:
	if _auto_map_active:
		_auto_map_draft[str(_capturing_role)] = binding.duplicate(true)
		_auto_map_index += 1
		if _auto_map_index < ControllerMappingManagerType.ROLE_ORDER.size():
			_set_capture_role(
				ControllerMappingManagerType.ROLE_ORDER[_auto_map_index]
			)
			return
		var profile_saved: bool = _mapping_manager.replace_active_bindings(
			_auto_map_draft
		)
		_cancel_capture()
		_progress_label.text = (
			"controller mapped successfully"
			if profile_saved else "could not save the controller mapping"
		)
		_refresh_bindings()
		return
	var role: StringName = _capturing_role
	var binding_saved: bool = _mapping_manager.set_binding(role, binding)
	_cancel_capture()
	_progress_label.text = (
		"updated " + _mapping_manager.get_role_label(role)
		if binding_saved else "could not save that controller input"
	)
	_refresh_bindings()

func _cancel_capture() -> void:
	_capturing_role = &""
	_auto_map_active = false
	_auto_map_index = -1
	_auto_map_draft.clear()
	_waiting_for_neutral = false
	_neutral_elapsed = 0.0
	_reset_cancel_combo()
	_set_action_buttons_disabled(false)
	if _progress_label != null:
		_progress_label.text = ""


func _track_cancel_combo_button(button_event: InputEventJoypadButton) -> void:
	if button_event == null:
		return
	match button_event.button_index:
		JOY_BUTTON_LEFT_SHOULDER:
			_cancel_left_bumper_pressed = button_event.pressed
		JOY_BUTTON_RIGHT_SHOULDER:
			_cancel_right_bumper_pressed = button_event.pressed
		_:
			return
	if not (
		_cancel_left_bumper_pressed and _cancel_right_bumper_pressed
	):
		_cancel_combo_elapsed = 0.0


func _reset_cancel_combo() -> void:
	_cancel_left_bumper_pressed = false
	_cancel_right_bumper_pressed = false
	_cancel_combo_elapsed = 0.0


func _set_action_buttons_disabled(disabled: bool) -> void:
	if _auto_map_button != null:
		_auto_map_button.disabled = disabled
	if _reset_button != null:
		_reset_button.disabled = disabled
	for button_value: Variant in _binding_buttons.values():
		var button := button_value as Button
		if button != null:
			button.disabled = disabled


func _reset_mapping() -> void:
	if _mapping_manager == null:
		return
	var reset: bool = _mapping_manager.reset_active_profile()
	_progress_label.text = (
		"restored the project controller defaults"
		if reset else "could not restore controller defaults"
	)
	_refresh_bindings()


func _refresh_bindings() -> void:
	if _mapping_manager == null or _controller_label == null:
		return
	_controller_label.text = _mapping_manager.get_active_controller_name()
	var bindings: Dictionary = _mapping_manager.get_active_bindings()
	for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
		var button := _binding_buttons.get(role) as Button
		if button == null:
			continue
		var raw_binding: Variant = bindings.get(str(role), {})
		button.text = (
			_mapping_manager.binding_label(raw_binding as Dictionary)
			if typeof(raw_binding) == TYPE_DICTIONARY else "unmapped"
		)
		var conflict_role: StringName = _find_binding_conflict(
			role,
			raw_binding as Dictionary,
			bindings,
		)
		_apply_binding_button_style(button, not conflict_role.is_empty())
		button.tooltip_text = (
			"warning: also assigned to "
			+ _mapping_manager.get_role_label(conflict_role)
			if not conflict_role.is_empty()
			else "change " + _mapping_manager.get_role_label(role)
		)
	_reset_button.disabled = not _mapping_manager.has_custom_mapping()


func _find_binding_conflict(
	role: StringName,
	binding: Dictionary,
	bindings: Dictionary,
) -> StringName:
	if binding.is_empty():
		return &""
	for other_role: StringName in ControllerMappingManagerType.ROLE_ORDER:
		if other_role == role:
			continue
		var raw_other: Variant = bindings.get(str(other_role), {})
		if typeof(raw_other) != TYPE_DICTIONARY:
			continue
		var other := raw_other as Dictionary
		if _bindings_conflict(role, binding, other_role, other):
			return other_role
	return &""


func _bindings_conflict(
	first_role: StringName,
	first: Dictionary,
	second_role: StringName,
	second: Dictionary,
) -> bool:
	var binding_kind: String = str(first.get("kind", ""))
	if binding_kind != str(second.get("kind", "")):
		return false
	if binding_kind == "button":
		return int(first.get("button", -1)) == int(
			second.get("button", -1)
		)
	if binding_kind != "axis":
		return false
	if int(first.get("axis", -1)) != int(second.get("axis", -1)):
		return false
	var opposite_trigger_halves: bool = (
		first_role in ControllerMappingManagerType.TRIGGER_ROLES
		and second_role in ControllerMappingManagerType.TRIGGER_ROLES
		and signf(float(first.get("direction", 0.0)))
			!= signf(float(second.get("direction", 0.0)))
	)
	return not opposite_trigger_halves


func _apply_binding_button_style(button: Button, has_conflict: bool) -> void:
	UtilityPageStyleType.apply_compact_ocean_button(button)
	if not has_conflict:
		return
	var colors: Dictionary = {
		&"normal": UtilityPageStyleType.OCEAN_DANGER,
		&"hover": UtilityPageStyleType.OCEAN_DANGER.lightened(0.12),
		&"pressed": UtilityPageStyleType.OCEAN_DANGER.darkened(0.12),
		&"focus": UtilityPageStyleType.OCEAN_DANGER.lightened(0.12),
	}
	for state: StringName in colors:
		var style := UtilityPageStyleType.ocean_button_style(
			colors[state] as Color
		)
		style.content_margin_left = 10.0
		style.content_margin_right = 10.0
		style.content_margin_top = 4.0
		style.content_margin_bottom = 4.0
		button.add_theme_stylebox_override(state, style)


func _on_active_controller_changed(_controller_name: String) -> void:
	if not _capturing_role.is_empty():
		_cancel_capture()
	_refresh_bindings()
