class_name PlayersPage
extends Control

var _service: NetworkPlayerListService
var _discovery: DiscoveryClient
var _count_label: Label
var _tabs: HBoxContainer
var _list: VBoxContainer
var _status: Label
var _host_settings_panel: PanelContainer
var _room_name_edit: LineEdit
var _discoverable_toggle: CheckButton
var _host_discovery_status: Label
var _current_tab := 0


func _ready() -> void:
	set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	UtilityPageStyle.apply_page(self)
	_build()


func setup(
	service: NetworkPlayerListService,
	discovery: DiscoveryClient,
) -> void:
	_service = service
	_discovery = discovery
	_service.entries_changed.connect(_refresh)
	_service.moderation_finished.connect(func(_ok: bool, message: String) -> void:
		_status.text = message
		_refresh()
	)
	if _discovery != null:
		_discovery.host_settings_changed.connect(
			_on_host_settings_changed
		)
		_discovery.host_status_changed.connect(_on_host_status_changed)
	_refresh()


func activate() -> void:
	_refresh()
	UtilityPageStyle.animate_in(self)
	_focus_first()


func deactivate() -> void:
	_status.text = ""


func _build() -> void:
	var margin: MarginContainer = UtilityPageStyle.build_laptop_screen(self)
	var root := VBoxContainer.new()
	root.add_theme_constant_override("separation", 9)
	margin.add_child(root)
	var tab_row := HBoxContainer.new()
	tab_row.add_theme_constant_override("separation", 16)
	root.add_child(tab_row)
	_tabs = HBoxContainer.new()
	_tabs.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	_tabs.add_theme_constant_override("separation", 10)
	tab_row.add_child(_tabs)
	for index: int in 3:
		var button := Button.new()
		button.text = ["players", "relationships", "banned"][index]
		button.toggle_mode = true
		button.pressed.connect(_select_tab.bind(index))
		UtilityPageStyle.apply_ocean_button(button)
		_tabs.add_child(button)
	_count_label = Label.new()
	_count_label.add_theme_font_size_override("font_size", 17)
	_count_label.add_theme_color_override(
		"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
	)
	_count_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
	tab_row.add_child(_count_label)
	_build_host_settings(root)
	var scroll := ScrollContainer.new()
	scroll.custom_minimum_size = Vector2(0, 310)
	scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
	scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
	root.add_child(scroll)
	_list = VBoxContainer.new()
	_list.custom_minimum_size = Vector2(1032, 0)
	_list.add_theme_constant_override("separation", 7)
	scroll.add_child(_list)
	_status = Label.new()
	_status.add_theme_color_override(
		"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
	)
	root.add_child(_status)


func _build_host_settings(root: VBoxContainer) -> void:
	_host_settings_panel = PanelContainer.new()
	_host_settings_panel.add_theme_stylebox_override(
		"panel", UtilityPageStyle.row_style(false)
	)
	root.add_child(_host_settings_panel)
	var row := HBoxContainer.new()
	row.custom_minimum_size.y = 58.0
	row.add_theme_constant_override("separation", 10)
	_host_settings_panel.add_child(row)
	var label := Label.new()
	label.text = "room name"
	label.add_theme_color_override(
		"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
	)
	row.add_child(label)
	_room_name_edit = LineEdit.new()
	_room_name_edit.custom_minimum_size = Vector2(300.0, 42.0)
	_room_name_edit.max_length = DiscoveryClient.MAX_ROOM_NAME_LENGTH
	_room_name_edit.placeholder_text = DiscoveryClient.DEFAULT_ROOM_NAME
	UtilityPageStyle.apply_ocean_line_edit(_room_name_edit)
	_room_name_edit.text_submitted.connect(
		func(_value: String) -> void: _commit_room_name()
	)
	_room_name_edit.focus_exited.connect(_commit_room_name)
	row.add_child(_room_name_edit)
	_discoverable_toggle = CheckButton.new()
	_discoverable_toggle.text = "list publicly"
	_discoverable_toggle.toggled.connect(_on_discoverable_toggled)
	UtilityPageStyle.apply_ocean_button(_discoverable_toggle)
	row.add_child(_discoverable_toggle)
	_host_discovery_status = Label.new()
	_host_discovery_status.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	_host_discovery_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	_host_discovery_status.add_theme_font_size_override("font_size", 14)
	_host_discovery_status.add_theme_color_override(
		"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
	)
	row.add_child(_host_discovery_status)


func _select_tab(index: int) -> void:
	if index == 2 and (_service == null or not _service.is_local_host()):
		return
	_current_tab = index
	_refresh()
	_focus_first()


func _refresh() -> void:
	if _service == null or _list == null:
		return
	_count_label.text = "%d / %d connected" % [
		_service.get_connected_count(), _service.get_max_players(),
	]
	for index: int in _tabs.get_child_count():
		var button := _tabs.get_child(index) as Button
		button.button_pressed = index == _current_tab
		button.visible = index != 2 or _service.is_local_host()
	_refresh_host_settings()
	for child: Node in _list.get_children():
		child.queue_free()
	match _current_tab:
		0:
			_build_active_rows()
		1:
			_build_relationship_rows()
		2:
			_build_ban_rows()


func _refresh_host_settings() -> void:
	if _host_settings_panel == null:
		return
	var host_visible: bool = _service.is_local_host() and _current_tab == 0
	_host_settings_panel.visible = host_visible
	if not host_visible or _discovery == null:
		return
	if not _room_name_edit.has_focus():
		_room_name_edit.text = _discovery.get_room_name()
	_discoverable_toggle.set_pressed_no_signal(_discovery.is_discoverable())
	var can_publish: bool = (
		_service.is_local_host()
		and _discovery.is_configured()
		and _service.is_open_host()
	)
	_discoverable_toggle.disabled = not can_publish
	_discoverable_toggle.tooltip_text = (
		"Advertise this open game in the public room browser."
		if can_publish
		else "Open the game before listing it publicly."
		if _discovery.is_configured()
		else "Room discovery is not configured in this build."
	)
	_on_host_status_changed(
		_discovery.get_host_status_message(),
		_discovery.host_status_is_error(),
	)


func _commit_room_name() -> void:
	if _discovery == null:
		return
	if not _discovery.set_room_name(_room_name_edit.text):
		_room_name_edit.text = _discovery.get_room_name()
	else:
		_room_name_edit.text = _discovery.get_room_name()


func _on_discoverable_toggled(enabled: bool) -> void:
	if _discovery == null:
		return
	if not _discovery.set_discoverable(enabled):
		_discoverable_toggle.set_pressed_no_signal(
			_discovery.is_discoverable()
		)


func _on_host_settings_changed(
	room_name: String,
	discoverable: bool,
) -> void:
	if _room_name_edit != null and not _room_name_edit.has_focus():
		_room_name_edit.text = room_name
	if _discoverable_toggle != null:
		_discoverable_toggle.set_pressed_no_signal(discoverable)
	_refresh_host_settings()


func _on_host_status_changed(message: String, is_error: bool) -> void:
	if _host_discovery_status == null:
		return
	_host_discovery_status.text = message
	_host_discovery_status.add_theme_color_override(
		"font_color",
		UtilityPageStyle.OCEAN_DANGER
		if is_error
		else UtilityPageStyle.OCEAN_TEXT_SECONDARY,
	)


func _build_active_rows() -> void:
	if _service.is_local_host():
		_build_session_artwork_controls()
	var entries := _service.get_entries()
	if entries.is_empty():
		_add_empty("No authenticated players.")
		return
	for entry: PlayerListEntry in entries:
		var row := _make_row()
		var identity := Label.new()
		identity.custom_minimum_size.x = 515
		var markers: Array[String] = []
		if entry.is_host:
			markers.append("host")
		if entry.is_local_player:
			markers.append("You")
		identity.text = "%s%s · %s\n%s" % [
			entry.display_name,
			"  [%s]" % ", ".join(markers) if not markers.is_empty() else "",
			entry.compact_fingerprint,
			entry.continuity_state,
		]
		identity.tooltip_text = NetworkIdentityCrypto.format_fingerprint(
			entry.full_fingerprint
		)
		identity.add_theme_color_override(
			"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
		)
		row.add_child(identity)
		var ping := Label.new()
		ping.custom_minimum_size.x = 80
		ping.text = (
			"Local" if entry.is_host
			else "%d ms" % entry.ping_to_host_ms
			if entry.ping_to_host_ms >= 0 else "—"
		)
		ping.add_theme_color_override(
			"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
		)
		row.add_child(ping)
		var mute := Button.new()
		mute.text = "unmute" if entry.muted else "mute"
		mute.disabled = entry.is_local_player
		mute.pressed.connect(_toggle_mute.bind(entry))
		UtilityPageStyle.apply_ocean_button(mute)
		row.add_child(mute)
		var block := Button.new()
		block.text = "block"
		block.disabled = entry.is_local_player
		block.pressed.connect(_confirm_block.bind(entry))
		UtilityPageStyle.apply_ocean_button(block)
		row.add_child(block)
		var kick := Button.new()
		kick.text = "kick"
		kick.disabled = not entry.can_kick
		kick.pressed.connect(_confirm_kick.bind(entry))
		UtilityPageStyle.apply_ocean_button(kick)
		row.add_child(kick)
		var ban := Button.new()
		ban.text = "ban"
		ban.disabled = not entry.can_ban
		ban.pressed.connect(_confirm_ban.bind(entry))
		UtilityPageStyle.apply_ocean_button(ban)
		row.add_child(ban)


func _build_session_artwork_controls() -> void:
	var counts: Vector2i = _service.get_session_artwork_counts()
	var row := _make_row()
	var label := Label.new()
	label.custom_minimum_size.x = 830
	label.text = "session artwork · %d layers · %d painted pixels" % [
		counts.x, counts.y,
	]
	label.add_theme_color_override(
		"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
	)
	row.add_child(label)
	var reset := Button.new()
	reset.text = "reset paint"
	reset.disabled = counts.x == 0
	reset.tooltip_text = "Clears all shared artwork from this session."
	reset.pressed.connect(func() -> void:
		_confirm(
			"Clear all shared paint from this session?\nThis cannot be undone.",
			_service.reset_session_artwork,
		)
	)
	UtilityPageStyle.apply_ocean_button(reset)
	row.add_child(reset)


func _build_relationship_rows() -> void:
	var records := _service.get_relationships()
	if records.is_empty():
		_add_empty("No muted or blocked identities.")
		return
	for record: Dictionary in records:
		var row := _make_row()
		var label := Label.new()
		label.custom_minimum_size.x = 690
		var fingerprint := str(record["fingerprint"])
		label.text = "%s · %s    %s" % [
			str(record.get("last_known_display_name", "Player")),
			NetworkIdentityCrypto.compact_suffix(fingerprint),
			"Blocked" if bool(record.get("blocked", false)) else "Muted",
		]
		label.tooltip_text = NetworkIdentityCrypto.format_fingerprint(fingerprint)
		label.add_theme_color_override(
			"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
		)
		row.add_child(label)
		if bool(record.get("blocked", false)):
			var unblock := Button.new()
			unblock.text = "unblock"
			unblock.pressed.connect(func() -> void:
				_service.set_blocked(fingerprint, str(record["last_known_display_name"]), false)
			)
			UtilityPageStyle.apply_ocean_button(unblock)
			row.add_child(unblock)
		var unmute := Button.new()
		unmute.text = "unmute"
		unmute.disabled = bool(record.get("blocked", false))
		unmute.pressed.connect(func() -> void:
			_service.set_muted(fingerprint, str(record["last_known_display_name"]), false)
		)
		UtilityPageStyle.apply_ocean_button(unmute)
		row.add_child(unmute)


func _build_ban_rows() -> void:
	var records := _service.get_bans()
	if records.is_empty():
		_add_empty("No banned identities.")
		return
	for record: Dictionary in records:
		var row := _make_row()
		var fingerprint := str(record["target_fingerprint"])
		var label := Label.new()
		label.custom_minimum_size.x = 830
		label.text = "%s · %s    banned %s" % [
			str(record.get("last_known_display_name", "Player")),
			NetworkIdentityCrypto.compact_suffix(fingerprint),
			Time.get_date_string_from_unix_time(int(record.get("banned_unix", 0))),
		]
		label.tooltip_text = NetworkIdentityCrypto.format_fingerprint(fingerprint)
		label.add_theme_color_override(
			"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
		)
		row.add_child(label)
		var unban := Button.new()
		unban.text = "unban"
		unban.pressed.connect(_confirm_unban.bind(fingerprint))
		UtilityPageStyle.apply_ocean_button(unban)
		row.add_child(unban)


func _make_row() -> HBoxContainer:
	var panel := PanelContainer.new()
	panel.add_theme_stylebox_override(
		"panel", UtilityPageStyle.row_style(false)
	)
	_list.add_child(panel)
	var row := HBoxContainer.new()
	row.custom_minimum_size.y = 58
	row.add_theme_constant_override("separation", 8)
	panel.add_child(row)
	return row


func _add_empty(text: String) -> void:
	var label := Label.new()
	label.text = text
	label.custom_minimum_size.y = 100
	label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
	label.add_theme_color_override(
		"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
	)
	_list.add_child(label)


func _toggle_mute(entry: PlayerListEntry) -> void:
	_service.set_muted(entry.full_fingerprint, entry.display_name, not entry.muted)


func _confirm_block(entry: PlayerListEntry) -> void:
	_confirm(
		"Block %s?\nTheir local social and visual presentation will be hidden."
			% entry.display_name,
		func() -> void:
			_service.set_blocked(entry.full_fingerprint, entry.display_name, true)
	)


func _confirm_kick(entry: PlayerListEntry) -> void:
	_confirm("Remove %s from this session?" % entry.display_name, func() -> void:
		_service.kick(entry.peer_id, entry.full_fingerprint, entry.revision)
	)


func _confirm_ban(entry: PlayerListEntry) -> void:
	_confirm("Ban %s · %s from this server?" % [
		entry.display_name, entry.compact_fingerprint,
	], func() -> void:
		_service.ban(
			entry.peer_id, entry.full_fingerprint, entry.display_name, entry.revision
		)
	)


func _confirm_unban(fingerprint: String) -> void:
	_confirm("Unban %s?" % NetworkIdentityCrypto.compact_suffix(fingerprint), func() -> void:
		_service.unban(fingerprint)
	)


func _confirm(text: String, action: Callable) -> void:
	var dialog := ConfirmationDialog.new()
	dialog.dialog_text = text
	dialog.ok_button_text = "confirm"
	dialog.canceled.connect(dialog.queue_free)
	dialog.confirmed.connect(func() -> void:
		action.call()
		dialog.queue_free()
	)
	add_child(dialog)
	dialog.popup_centered(Vector2i(520, 220))


func _focus_first() -> void:
	var candidates: Array[Control] = []
	_collect_focusable_player_controls(self, candidates)
	if candidates.is_empty():
		return
	candidates.sort_custom(func(first: Control, second: Control) -> bool:
		if not is_equal_approx(first.global_position.y, second.global_position.y):
			return first.global_position.y < second.global_position.y
		return first.global_position.x < second.global_position.x
	)
	candidates[0].grab_focus()


func _collect_focusable_player_controls(
	root: Node,
	output: Array[Control],
) -> void:
	for child: Node in root.get_children():
		if child == _tabs:
			continue
		var control := child as Control
		if control != null and not control.is_visible_in_tree():
			continue
		if (
			control != null
			and control.focus_mode != Control.FOCUS_NONE
			and not control is ScrollBar
		):
			output.append(control)
		_collect_focusable_player_controls(child, output)
