class_name Minigame
extends Control

enum Type {
	jellyfish,
	crabs,
	parakeets,
	monkey,
	caterpillar,
	frog,
	turtles,
	ants,
	penguin,
	fish
}

const WIN_SOUND_FX: AudioStreamMP3 = preload("res://assets/sfx/sfx_game_over_win.mp3")
const LOSE_SOUND_FX: AudioStreamMP3 = preload("res://assets/sfx/sfx_game_over_lose.mp3")
const LABEL_COLOR_NEUTRAL: Color = Color("#e6f3e0")
const LABEL_COLOR_WIN: Color = Color("#009344")
const LABEL_COLOR_LOSE: Color = Color("#be1e2d")

static var transition_data: Dictionary = {}

@export var minigame_name: Type
@export var lesson_nb: int = 1
@export_range(0, 4) var difficulty: int = 0
@export_range(0, 1) var current_lesson_stimuli_ratio: float = 0.7
@export var minigame_number: int = 0
@export_category("Difficulty")
@export var max_number_of_lives: int = 0:
	set(value):
		max_number_of_lives = value
@export var max_progression: int = 0:
	set(value):
		max_progression = value
		if minigame_ui:
			minigame_ui.set_max_progression(value)
@export_category("Hints")
@export var errors_before_help_speech: int = 2
@export var errors_before_highlight: int = 3

# Lesson
var minigame_difficulty: int
var lesson_difficulty: int
# Logs
var logs: Dictionary = {}
# Scores for the remediation engine
var remediation_gp_scores: Dictionary = {}
var remediation_syllables_scores: Dictionary = {}
var remediation_words_scores: Dictionary = {}
# Scores for the confusion matrix engine
var confusion_matrix_gp_scores: Dictionary[int, PackedInt32Array] = {}
# Final boss state
var is_final_boss: bool = false
# Stimuli
var stimuli: Array = []
var distractions: Array = []
# Lives
var current_lives: int = 0:
	set(value):
		var previous_lives: int = current_lives
		current_lives = value
		if current_lives != previous_lives:
			Log.trace("BaseMinigame: Lives changed from %d to %d (max %d) for %s" % [previous_lives, current_lives, max_number_of_lives, Type.keys()[minigame_name]])
		if current_lives < previous_lives:
			consecutive_errors += previous_lives - current_lives
		if current_lives <= max_number_of_lives - errors_before_help_speech:
			_play_kalulu_help_speech()
		elif consecutive_errors == errors_before_highlight:
			is_highlighting = true
# Progression
var current_progression: int = 0: set = set_current_progression
var current_number_of_hints: int = 0
var consecutive_errors: int = 0
var is_highlighting: bool = false:
	set(value):
		is_highlighting = value
		if is_highlighting:
			_highlight()
		else:
			_stop_highlight()
# Speeches
var intro_kalulu_speech: AudioStreamMP3
var help_kalulu_speech: AudioStreamMP3
var win_kalulu_speech: AudioStreamMP3
var lose_kalulu_speech: AudioStreamMP3
# data to go back to the right place in gardens
var gardens_data: Dictionary = {}

@onready var minigame_ui: MinigameUI = $MinigameUI
@onready var audio_player: MinigameAudioStreamPlayer = $AudioStreamPlayer
@onready var fireworks: Fireworks = $Fireworks
# Game root shall contain all the game tree.
# This node is pausable unlike the others, so the pause button can stop the game but not other essential processes.
@onready var game_root: Control = $GameRoot
# Expected number of stimuli from current lesson
@onready var current_lesson_stimuli_number: int = floori(max_progression * current_lesson_stimuli_ratio)

#region Initialisation

func _ready() -> void:
	gardens_data = transition_data
	minigame_number = transition_data.get("minigame_number", minigame_number)
	lesson_nb = transition_data.get("current_lesson_number", lesson_nb)
	is_final_boss = transition_data.get("is_final_boss", false) as bool
	
	# Difficulty
	if (UserDataManager as UserDataManagerClass)._student_difficulty:
		difficulty = UserDataManager.get_difficulty_for_minigame(Type.keys()[minigame_name] as String)
	
	intro_kalulu_speech = Database.load_external_sound(Database.get_kalulu_speech_path(Type.keys()[minigame_name] as String, "intro"))
	help_kalulu_speech = Database.load_external_sound(Database.get_kalulu_speech_path(Type.keys()[minigame_name] as String, "help"))
	win_kalulu_speech = Database.load_external_sound(Database.get_kalulu_speech_path(Type.keys()[minigame_name] as String, "end"))
	lose_kalulu_speech = Database.load_external_sound(Database.get_kalulu_speech_path("minigame", "lose"))
	
	if not Engine.is_editor_hint():
		# Stop the current music
		MusicManager.stop()
	
	_reset_logs()
	_initialize()


func _initialize() -> void:
	if not Engine.is_editor_hint():
		_find_stimuli_and_distractions()
	
	_setup_minigame()
	
	Log.info("BaseMinigame: Initialize %s (lesson %d, minigame #%d, difficulty %d)" % [Type.keys()[minigame_name], lesson_nb, minigame_number, difficulty])
	
	if not Engine.is_editor_hint():
		await _curtains_and_kalulu()
		_start()


# Find and set the parameters of the minigame, like the number of lives or the victory conditions.
func _setup_minigame() -> void:
	Log.trace("BaseMinigame: SetupMinigame")
	max_progression = max_progression
	max_number_of_lives = max_number_of_lives
	current_lives = max_number_of_lives


# Find the stimuli and distractions of the minigame.
func _find_stimuli_and_distractions() -> void:
	Log.error("BaseMinigame: Minigame type " + str(minigame_name) + " has not implemented the function _find_stimuli_and_distractions()")
	return


# Opens the curtains and Kalulu explains
func _curtains_and_kalulu() -> void:
	await OpeningCurtain.open()
	
	# Checks if intro needs to be played
	if not UserDataManager.is_speech_played(Type.keys()[minigame_name] as String):
		minigame_ui.play_kalulu_speech(intro_kalulu_speech)
		await minigame_ui.kalulu_speech_ended
		UserDataManager.mark_speech_as_played(Type.keys()[minigame_name] as String)
#endregion

#region Timer
var _start_time: float = 0.0
var _elapsed_paused: float = 0.0
var _pause_start: float = 0.0
var _is_paused: bool = false


# Launch the minigame
func _start() -> void:
	Log.info("BaseMinigame: Start minigame=%s lesson=%d difficulty=%d" % [Type.keys()[minigame_name], lesson_nb, difficulty])
	_start_time = Time.get_ticks_msec() / 1000.0
	_elapsed_paused = 0.0
	_is_paused = false
	return


func _notification(what: int) -> void:
	if _start_time == 0.0:
		return
	match what:
		NOTIFICATION_APPLICATION_FOCUS_OUT:
			if not _is_paused:
				_pause_start = Time.get_ticks_msec() / 1000.0
				_is_paused = true

		NOTIFICATION_APPLICATION_FOCUS_IN:
			if _is_paused:
				var resumed: float = Time.get_ticks_msec() / 1000.0
				var pause_duration: float = resumed - _pause_start
				_elapsed_paused += pause_duration
				_is_paused = false
#endregion

#region Ending

func _reset() -> void:
	get_tree().paused = false
	await OpeningCurtain.close()
	get_tree().reload_current_scene()


func _win() -> void:
	# Lock the UI
	minigame_ui.lock()
	
	_submit_student_level_time()
	
	if gardens_data:
		gardens_data.minigame_completed = true
	
	if UserDataManager.student_progression:
		if gardens_data.has("boss_gate_lesson"):
			gardens_data.boss_completed = UserDataManager.student_progression.boss_completed(gardens_data.boss_gate_lesson as int)
			gardens_data.first_clear = gardens_data.boss_completed
			if not is_final_boss:
				UserDataManager.student_progression.reset_boss_failure_streak()
		else:
			gardens_data.first_clear = UserDataManager.student_progression.game_completed(lesson_nb, minigame_number)
	
	update_scores()
	
	Log.info("BaseMinigame: %s won in %d seconds with progression %d/%d and %d/%d lives" % [Type.keys()[minigame_name], _get_elapsed_time_seconds(), current_progression, max_progression, current_lives, max_number_of_lives])
	
	# Difficulty
	if current_lives <= 0:
		UserDataManager.update_difficulty_for_minigame(Type.keys()[minigame_name] as String, false)
	else:
		UserDataManager.update_difficulty_for_minigame(Type.keys()[minigame_name] as String, true)
	
	audio_player.stream = WIN_SOUND_FX
	audio_player.play()
	
	fireworks.play()
	await fireworks.finished
	
	minigame_ui.play_kalulu_speech(win_kalulu_speech)
	await minigame_ui.kalulu_speech_ended
	
	_go_back_to_the_garden()


func update_scores() -> void:
	# Remediation
	if remediation_gp_scores:
		UserDataManager.update_remediation_gp_scores(remediation_gp_scores)
	if remediation_syllables_scores:
		UserDataManager.update_remediation_syllables_scores(remediation_syllables_scores)
	if remediation_words_scores:
		UserDataManager.update_remediation_words_scores(remediation_words_scores)
	
	# Confusion Matrix
	if confusion_matrix_gp_scores:
		UserDataManager.update_confusion_matrix_gp_scores(confusion_matrix_gp_scores)


func _lose() -> void:
	# Lock the UI
	minigame_ui.lock()
	
	_submit_student_level_time()
	
	if gardens_data:
		gardens_data.minigame_completed = false
	
	update_scores()
	
	Log.info("BaseMinigame: %s Lose in %d seconds with progression %d/%d and %d/%d lives" % [Type.keys()[minigame_name], _get_elapsed_time_seconds(), current_progression, max_progression, current_lives, max_number_of_lives])
	
	# Difficulty
	UserDataManager.update_difficulty_for_minigame(Type.keys()[minigame_name] as String, false)
	
	audio_player.stream = LOSE_SOUND_FX
	audio_player.play()
	await audio_player.finished
	
	minigame_ui.play_kalulu_speech(lose_kalulu_speech)
	await minigame_ui.kalulu_speech_ended
	if gardens_data.has("boss_gate_lesson") and not is_final_boss and UserDataManager.student_progression:
		var is_blocked: bool = UserDataManager.student_progression.register_boss_failure()
		if is_blocked:
			if has_method("show_adult_block"):
				call("show_adult_block")
			else:
				Log.error("BaseMinigame: Adult block requested but no handler exists for %s" % Type.keys()[minigame_name])
			return
	
	_reset()


func _submit_student_level_time() -> void:
	if gardens_data.has("boss_gate_lesson"):
		return
	UserDataManager.add_level_time(lesson_nb, minigame_number, _get_elapsed_time_seconds())


func _get_elapsed_time_seconds() -> int:
	return int(Time.get_ticks_msec() / 1000.0 - _start_time - _elapsed_paused)

#endregion

#region Logs

func _save_logs() -> void:
	var logs_size: int = -1
	if logs.has("answers") and logs.get("answers", []) is Array:
		logs_size = (logs.get("answers", []) as Array).size()
	Log.info("BaseMinigame: Saving logs for %s with %d answer(s)" % [Type.keys()[minigame_name], logs_size])
	LessonLogger.save_logs(logs, UserDataManager.get_student_folder(), Type.keys()[minigame_name] as String, lesson_nb, Time.get_time_string_from_system())
	_reset_logs()


func _reset_logs() -> void:
	logs = {"answers": []}


func _log_new_response(response: Dictionary, current_stimulus: Dictionary) -> void:
	var response_log: Dictionary = {
		"reponse": response,
		"awaited_response": current_stimulus,
		"is_right": response == current_stimulus,
		"minigame": Type.keys()[minigame_name],
		"number_of_hints": current_number_of_hints,
		"current_progression": current_progression,
		"max_progression": max_progression,
		"current_lives": current_lives,
		"max_number_of_lives": max_number_of_lives,
	}
	Log.trace("BaseMinigame: Log new response minigame=%s response=%s expected=%s right=%s progression=%d/%d lives=%d/%d" % [
				Type.keys()[minigame_name],
				str(response),
				str(current_stimulus),
				str(response_log.is_right),
				current_progression,
				max_progression,
				current_lives,
				max_number_of_lives])
	
	var answers: Array = logs["answers"]
	answers.append(response_log)

#endregion

#region Remediation

# Calculates the stimulus remediation score
func _get_stimulus_score(stimulus: Dictionary) -> int:
	var score: int = 0
	if stimulus.has("GPs"):
		for gp: Dictionary in stimulus.GPs:
			score += UserDataManager.get_gp_remediation_score(gp.ID as int)
	return score


# Sorting function to sort arrays of stimuli based on their remediation score
# If the score is lower (had more errors in the past), then the element is moved in first place
func _sort_scoring(stimulus1: Dictionary, stimulus2: Dictionary) -> bool:
	return _get_stimulus_score(stimulus1) < _get_stimulus_score(stimulus2)


# Updates the remediation score of a GP defined by his ID
func _update_remediation_gp_score(id: int, score: int) -> void:
	var new_remediation_gp_score: int = 0
	if remediation_gp_scores.has(id):
		new_remediation_gp_score += remediation_gp_scores[id]
	new_remediation_gp_score += score
	remediation_gp_scores[id] = new_remediation_gp_score


# Updates the remediation score of a syllable defined by his ID
func _update_remediation_syllable_score(id: int, score: int) -> void:
	var new_remediation_syllable_score: int = 0
	if remediation_syllables_scores.has(id):
		new_remediation_syllable_score += remediation_syllables_scores[id]
	new_remediation_syllable_score += score
	remediation_syllables_scores[id] = new_remediation_syllable_score


# Updates the remediation score of a word defined by his ID
func _update_remediation_word_score(id: int, score: int) -> void:
	var new_remediation_word_score: int = 0
	if remediation_words_scores.has(id):
		new_remediation_word_score += remediation_words_scores[id]
	new_remediation_word_score += score
	remediation_words_scores[id] = new_remediation_word_score

#endregion

#region Confusion Matrix

func _update_confusion_matrix_gp_score(expected_id: int, selected_id: int) -> void:
	var stored: PackedInt32Array = PackedInt32Array()
	if confusion_matrix_gp_scores.has(expected_id):
		stored = confusion_matrix_gp_scores[expected_id]
	stored.append(selected_id)
	confusion_matrix_gp_scores[expected_id] = stored

#endregion

#region UI Callbacks

func _go_back_to_the_garden() -> void:
	get_tree().paused = false
	await OpeningCurtain.close()
	
	_save_logs()
	
	Gardens.transition_data = gardens_data
	get_tree().change_scene_to_file("res://sources/gardens/gardens.tscn")


func _play_stimulus() -> void:
	return


func _pause_game() -> bool:
	var pause: bool = not get_tree().paused
	get_tree().paused = pause
	Log.trace("BaseMinigame: Pause toggled to %s for %s" % [str(pause), Type.keys()[minigame_name]])
	return pause


func _highlight() -> void:
	pass


func _stop_highlight() -> void:
	pass


func _play_kalulu_help_speech() -> void:
	minigame_ui.play_kalulu_speech(help_kalulu_speech)
	await minigame_ui.kalulu_speech_ended


#endregion

#region Setters

func set_current_progression(p_current_progression: int) -> void:
	var previous_progression: int = current_progression
	current_progression = p_current_progression
	Log.trace("BaseMinigame: Progression changed from %d to %d/%d for %s" % [previous_progression, current_progression, max_progression, Type.keys()[minigame_name]])
	
	consecutive_errors = 0
	is_highlighting = false
	
	if minigame_ui:
		minigame_ui.set_progression(p_current_progression)
	if p_current_progression == max_progression and previous_progression != max_progression:
		await _win()
	else:
		await _on_current_progression_changed()

#endregion

#region Connections

func _on_minigame_ui_garden_button_pressed() -> void:
	_go_back_to_the_garden()
	update_scores()


func _on_minigame_ui_stimulus_button_pressed() -> void:
	_pause_game()
	minigame_ui.lock()
	
	#await minigame_ui.repeat_stimulus_animation(true)
	
	@warning_ignore("redundant_await")
	await _play_stimulus()
	
	#await minigame_ui.repeat_stimulus_animation(false)
	
	minigame_ui.unlock()
	_pause_game()


func _on_minigame_ui_kalulu_button_pressed() -> void:
	minigame_ui.play_kalulu_speech(help_kalulu_speech)


func _on_minigame_ui_restart_button_pressed() -> void:
	_reset()


func _on_current_progression_changed() -> void:
	# Make Godot understand that this function is a coroutine even if it does nothing, to avoid warning
	await get_tree().create_timer(0).timeout

#endregion
