class_name OptionsMenu
extends Control

signal closed
signal settings_changed
## Effacer une partie se demande d'ici, mais ne se fait pas ici: l'ecran des
## emplacements appartient a l'accueil, et c'est lui qui sait quoi montrer.
signal delete_requested

## Le meme menu sert a l'accueil et a la pause. Effacer une partie n'a de sens
## qu'au premier: en pleine partie, le bouton proposerait de supprimer ce qu'on
## est en train de jouer.
var allow_delete := false:
	set(value):
		allow_delete = value
		if is_node_ready():
			refresh()

@onready var title_label: Label = %TitleLabel
@onready var display_label: Label = %DisplayLabel
@onready var display_button: Button = %DisplayButton
@onready var language_label: Label = %LanguageLabel
@onready var language_button: Button = %LanguageButton
@onready var volume_label: Label = %VolumeLabel
@onready var volume_value: Label = %VolumeValue
@onready var volume_slider: HSlider = %VolumeSlider
@onready var preview_label: Label = %PreviewLabel
@onready var preview_toggle: CheckButton = %PreviewToggle
@onready var vsync_label: Label = %VSyncLabel
@onready var vsync_toggle: CheckButton = %VSyncToggle
@onready var antialiasing_label: Label = %AntialiasingLabel
@onready var antialiasing_button: Button = %AntialiasingButton
@onready var shake_label: Label = %ShakeLabel
@onready var shake_button: Button = %ShakeButton
@onready var hit_stop_label: Label = %HitStopLabel
@onready var hit_stop_toggle: CheckButton = %HitStopToggle
@onready var impact_light_label: Label = %ImpactLightLabel
@onready var impact_light_toggle: CheckButton = %ImpactLightToggle
@onready var impact_stains_label: Label = %ImpactStainsLabel
@onready var impact_stains_toggle: CheckButton = %ImpactStainsToggle
@onready var delete_button: Button = %DeleteButton
@onready var close_button: Button = %CloseButton
@onready var display_menu: DisplayMenu = %DisplayMenu

var _refreshing := false

const ControlsMenu := preload("res://scripts/core/controls_menu.gd")
## Les commandes (mapping des touches, étape 5b) : une ligne de plus dans la
## grille, et leur écran par-dessus celui-ci. Bâtis ici plutôt que dans la
## scène, que l'éditeur ouvert pourrait réécrire.
var controls_label: Label
var controls_button: Button
var controls_menu: Control

func _ready() -> void:
	process_mode = Node.PROCESS_MODE_ALWAYS
	_build_controls_row()
	display_button.pressed.connect(display_menu.open)
	language_button.pressed.connect(_toggle_language)
	volume_slider.value_changed.connect(_preview_volume)
	volume_slider.drag_ended.connect(_commit_volume)
	vsync_toggle.toggled.connect(_toggle_vsync)
	antialiasing_button.pressed.connect(_cycle_antialiasing)
	preview_toggle.toggled.connect(_toggle_attack_preview)
	shake_button.pressed.connect(_cycle_shake)
	hit_stop_toggle.toggled.connect(_toggle_hit_stop)
	impact_light_toggle.toggled.connect(_toggle_impact_light)
	impact_stains_toggle.toggled.connect(_toggle_impact_stains)
	close_button.pressed.connect(close)
	# Un bouton qui REVIENT n'est pas une confirmation. Le marquer ici evite
	# aussi le doublon avec le son de fermeture du panneau: le directeur
	# garde le geste et tait ce qui suit dans la meme image.
	close_button.set_meta("audio_event", "ui_back")
	delete_button.pressed.connect(_request_delete)
	display_menu.menu_closed.connect(_on_display_closed)
	for button: BaseButton in [display_button, language_button, preview_toggle, vsync_toggle, antialiasing_button,
			shake_button, hit_stop_toggle, impact_light_toggle, impact_stains_toggle, delete_button, close_button]:
		button.mouse_entered.connect(button.grab_focus)
	refresh()

func _build_controls_row() -> void:
	var grid := display_label.get_parent()
	controls_label = Label.new()
	controls_label.name = "ControlsLabel"
	controls_label.add_theme_font_size_override("font_size", display_label.get_theme_font_size("font_size"))
	controls_label.size_flags_horizontal = display_label.size_flags_horizontal
	grid.add_child(controls_label)
	controls_button = Button.new()
	controls_button.name = "ControlsButton"
	controls_button.custom_minimum_size = display_button.custom_minimum_size
	controls_button.add_theme_font_size_override("font_size", display_button.get_theme_font_size("font_size"))
	controls_button.size_flags_horizontal = display_button.size_flags_horizontal
	grid.add_child(controls_button)
	controls_button.pressed.connect(_open_controls)
	controls_button.mouse_entered.connect(controls_button.grab_focus)
	controls_menu = ControlsMenu.new()
	add_child(controls_menu)
	controls_menu.connect("closed", func() -> void: controls_button.grab_focus())

func _open_controls() -> void:
	controls_menu.call("open")

func open() -> void:
	AudioDirector.play_ui("ui_open")
	visible = true
	Stage.push_interface(self)
	refresh()
	display_button.grab_focus()

func close() -> void:
	AudioDirector.play_ui("ui_close")
	if not visible:
		return
	if display_menu.visible:
		display_menu.close()
		return
	GameState.save_settings()
	visible = false
	Stage.pop_interface(self)
	closed.emit()

## Tout refermer d'un coup : la pause qu'on quitte emporte aussi le sous-menu.
func close_all() -> void:
	if controls_menu != null and controls_menu.visible:
		controls_menu.call("close")
	if display_menu.visible:
		display_menu.close()
	close()

func refresh() -> void:
	_refreshing = true
	title_label.text = Loc.ui("ui.options.title")
	display_label.text = Loc.ui("ui.options.display")
	display_button.text = Loc.ui("ui.options.display_value") % DisplayMenu.option_label(GameState.display_mode_id)
	language_label.text = Loc.ui("ui.options.language")
	language_button.text = Loc.ui("ui.options.language_fr") if GameState.language == "fr" else Loc.ui("ui.options.language_en")
	volume_label.text = Loc.ui("ui.options.master_volume")
	volume_slider.value = GameState.master_volume * 100.0
	volume_value.text = "%d%%" % roundi(volume_slider.value)
	preview_label.text = Loc.ui("ui.options.attack_preview")
	preview_toggle.button_pressed = GameState.attack_preview_enabled
	preview_toggle.text = Loc.ui("ui.options.on") if GameState.attack_preview_enabled else Loc.ui("ui.options.off")
	vsync_label.text = Loc.ui("ui.options.vsync")
	vsync_toggle.button_pressed = GameState.vsync_enabled
	vsync_toggle.text = Loc.ui("ui.options.on") if GameState.vsync_enabled else Loc.ui("ui.options.off")
	antialiasing_label.text = Loc.ui("ui.options.antialiasing")
	antialiasing_button.text = antialiasing_text(GameState.antialiasing)
	shake_label.text = Loc.ui("ui.options.impact_shake")
	shake_button.text = shake_text(GameState.impact_shake)
	hit_stop_label.text = Loc.ui("ui.options.impact_hitstop")
	hit_stop_toggle.button_pressed = GameState.impact_hitstop_enabled
	hit_stop_toggle.text = Loc.ui("ui.options.on") if GameState.impact_hitstop_enabled else Loc.ui("ui.options.off")
	impact_light_label.text = Loc.ui("ui.options.impact_light")
	impact_light_toggle.button_pressed = GameState.impact_light_enabled
	impact_light_toggle.text = Loc.ui("ui.options.on") if GameState.impact_light_enabled else Loc.ui("ui.options.off")
	impact_stains_label.text = Loc.ui("ui.options.impact_stains")
	impact_stains_toggle.button_pressed = GameState.impact_stains_enabled
	impact_stains_toggle.text = Loc.ui("ui.options.on") if GameState.impact_stains_enabled else Loc.ui("ui.options.off")
	delete_button.text = Loc.ui("ui.main.delete_game")
	delete_button.visible = allow_delete
	# Rien a effacer: le bouton reste visible mais inerte, plutot que de
	# disparaitre et faire sauter le reste du menu d'une ligne.
	delete_button.disabled = not GameState.has_any_save()
	close_button.text = Loc.ui("ui.common.back")
	if controls_label != null:
		controls_label.text = Loc.ui("ui.options.controls")
		controls_button.text = Loc.ui("ui.options.controls_edit")
	_refreshing = false

## Une aide de lecture, pas une regle: rien du combat ne change, seulement ce
## que le joueur voit avant de frapper.
func _toggle_attack_preview(enabled: bool) -> void:
	if _refreshing:
		return
	GameState.set_attack_preview_enabled(enabled)
	refresh()
	settings_changed.emit()

func _request_delete() -> void:
	GameState.save_settings()
	visible = false
	delete_requested.emit()

func _toggle_language() -> void:
	GameState.set_language("en" if GameState.language == "fr" else "fr")
	refresh()
	settings_changed.emit()
	language_button.grab_focus()

func _preview_volume(value: float) -> void:
	if _refreshing:
		return
	GameState.master_volume = clampf(value / 100.0, 0.0, 1.0)
	GameState.apply_audio_settings()
	volume_value.text = "%d%%" % roundi(value)

func _commit_volume(_value_changed: bool) -> void:
	GameState.save_settings()
	settings_changed.emit()

func _toggle_vsync(enabled: bool) -> void:
	if _refreshing:
		return
	GameState.set_vsync_enabled(enabled)
	vsync_toggle.text = Loc.ui("ui.options.on") if enabled else Loc.ui("ui.options.off")
	settings_changed.emit()

static func antialiasing_text(mode: String) -> String:
	var samples := {"msaa_2x": 2, "msaa_4x": 4, "msaa_8x": 8}
	if not samples.has(mode):
		return Loc.ui("ui.options.antialiasing_off")
	return Loc.ui("ui.options.antialiasing_msaa") % samples[mode]

## Le niveau suivant, en boucle: aucun, 2×, 4×, 8×.
func _cycle_antialiasing() -> void:
	var modes: Array = GameState.ANTIALIASING_MSAA.keys()
	GameState.set_antialiasing(str(modes[(modes.find(GameState.antialiasing) + 1) % modes.size()]))
	antialiasing_button.text = antialiasing_text(GameState.antialiasing)
	settings_changed.emit()

static func shake_text(level: String) -> String:
	return Loc.ui("ui.options.impact_shake_" + (level if GameState.IMPACT_SHAKE_LEVELS.has(level) else "normal"))

## La force suivante, en boucle: normale, douce, aucune.
func _cycle_shake() -> void:
	var levels: Array = GameState.IMPACT_SHAKE_LEVELS
	GameState.set_impact_shake(str(levels[(levels.find(GameState.impact_shake) + 1) % levels.size()]))
	shake_button.text = shake_text(GameState.impact_shake)
	settings_changed.emit()

## Le ressenti des coups ne change aucune regle: seulement ce qu'on voit.
func _toggle_hit_stop(enabled: bool) -> void:
	if _refreshing:
		return
	GameState.set_impact_hitstop_enabled(enabled)
	hit_stop_toggle.text = Loc.ui("ui.options.on") if enabled else Loc.ui("ui.options.off")
	settings_changed.emit()

func _toggle_impact_light(enabled: bool) -> void:
	if _refreshing:
		return
	GameState.set_impact_light_enabled(enabled)
	impact_light_toggle.text = Loc.ui("ui.options.on") if enabled else Loc.ui("ui.options.off")
	settings_changed.emit()

func _toggle_impact_stains(enabled: bool) -> void:
	if _refreshing:
		return
	GameState.set_impact_stains_enabled(enabled)
	impact_stains_toggle.text = Loc.ui("ui.options.on") if enabled else Loc.ui("ui.options.off")
	settings_changed.emit()

func _on_display_closed() -> void:
	refresh()
	settings_changed.emit()
	display_button.grab_focus()

func _unhandled_input(event: InputEvent) -> void:
	if not visible or not Stage.accepts_input(self):
		return
	if Stage.is_back(event) or event.is_action_pressed("console_toggle"):
		close()
		get_viewport().set_input_as_handled()
