# encoding=utf-8
"""
File for the PassationMainWindow class
"""

__author__ = "Roland Trouville"
__copyright__ = "Copyright 2015+, Consortium MonPaGe"
__license__ = "Creative Commons 4.0 By-Nc-Sa"
__maintainer__ = "Roland Trouville"
__email__ = "contact.monpage@gmail.com"
__status__ = "Production"

import logging
import os
from datetime import datetime
from typing import TypeVar

try:
	from typing import override
except ImportError:
	from typing_extensions import override  # noqa: F401

from PySide6.QtCore import QObject, Qt
from PySide6.QtGui import QColor, QFont, QPalette
from PySide6.QtWidgets import (
	QApplication,
	QCheckBox,
	QColorDialog,
	QComboBox,
	QLabel,
	QLineEdit,
	QMainWindow,
	QProgressBar,
	QPushButton,
	QRadioButton,
	QSpinBox,
)

from ui.LoadedWindow import LoadedWindow
from passation.module_intelligibilite_window import ModuleIntelligibiliteWindow
from passation.module_window import ModuleWindow
from passation.passation_advanced_window import PassationAdvancedWindow
from tools.audio_manager import AudioManager
from tools.csv_manager import CSVManager
from tools.db_manager import DBManager
from tools.display_tools import DisplayTools
from tools.general_tools import GeneralTools
from tools.options import Options
from tools.participant_manager import ParticipantManager

logger = logging.getLogger(__name__)

PlaceHolderType = TypeVar("PlaceHolderType", bound=QObject)


class PassationMainWindow(LoadedWindow):
	"""
	The first window to open when application is launched
	"""
	create_participant_code: QLineEdit
	create_participant_male_radio: QRadioButton
	create_participant_female_radio: QRadioButton
	create_participant_birthyear: QSpinBox
	create_participant_button: QPushButton
	module_list: QComboBox
	test_module_button: QPushButton
	advanced_start: QCheckBox
	language_list: QComboBox
	load_module_button: QPushButton
	microphone_list: QComboBox
	test_microphone_btn: QPushButton
	listen_microphone_button: QPushButton
	audio_output_label: QLabel
	microphone_test_label: QLabel
	microphone_test_progressbar: QProgressBar
	background_color_button: QPushButton
	text_color_button: QPushButton
	value_spin_box: QSpinBox
	reset_button: QPushButton
	example_label: QLabel
	stimuli_accent: str
	advanced_window: PassationAdvancedWindow
	module_window: ModuleWindow

	def __init__(self, parent: QMainWindow):
		super().__init__(os.path.join("ui", "passation.ui"))

		self.setWindowTitle(
			f"{GeneralTools.get_name()} Passation - v{GeneralTools.get_version()}"
		)

		self.participant_list: QComboBox = self.find_element(
			QComboBox, "participant_list"
		)

		self.parentWindow = parent

		AudioManager.init()
		self.display_palette = QPalette()
		self.display_font = QFont()
		self.load_config()

	# participant section
	def display_participant(self):
		"""
		Initializes and displays the participant creation UI components.

		- Populates the participant dropdown with existing entries.
		- Initializes and configures form fields for:
		- Participant code input
		- Gender selection (male/female radio buttons)
		- Birth year (spin box with valid range)
		- Sets default values for gender and birth year fields.
		- Connects the create button to trigger the participant creation method.
		"""
		DisplayTools.fill_participant_dropdown(self.participant_list)

		self.create_participant_code: QLineEdit = self.find_element(
			QLineEdit, "code_participant"
		)

		self.create_participant_male_radio: QRadioButton = self.find_element(
			QRadioButton, "rb_homme"
		)
		self.create_participant_female_radio: QRadioButton = self.find_element(
			QRadioButton, "rb_femme"
		)
		self.create_participant_male_radio.toggle()

		self.create_participant_birthyear: QSpinBox = self.find_element(
			QSpinBox, "birth_year"
		)
		self.create_participant_birthyear.setRange(1900, datetime.now().year)
		self.create_participant_birthyear.setSingleStep(1)
		self.create_participant_birthyear.setValue(
			1900 + round((datetime.now().year - 1900) / 2)
		)

		self.create_participant_button: QPushButton = self.find_element(
			QPushButton, "b_create_participant"
		)
		self.create_participant_button.clicked.connect(self.create_participant)

	# module selection section
	def select_module(self):
		"""
		Initializes the module selection UI components.

		- Populates the module dropdown based on an ordered list and available CSV files.
		- Sets up buttons for testing and loading modules, connecting them to their handlers.
		- Manages the advanced start checkbox visibility and state based on application options.
		- Populates the language dropdown with predefined options and selects the current accent.
		- Connects language selection changes to the appropriate handler.
		"""
		self.module_list: QComboBox = self.find_element(QComboBox, "list_module")

		ordered_module = GeneralTools.open_module_order_file()
		filenames = []
		for filename in os.listdir("./data/module/"):
			if os.path.isfile("./data/module/" + filename) and filename.endswith(
				".csv"
			):
				filenames.append(filename)

		for filename in ordered_module:
			if filename in filenames:
				self.module_list.addItem(filename)
		for filename in filenames:
			if filename.endswith(".csv") and filename not in ordered_module:
				self.module_list.addItem(filename)

		self.test_module_button: QPushButton = self.find_element(
			QPushButton, "b_test_module"
		)
		self.test_module_button.clicked.connect(self.test_module)

		self.advanced_start: QCheckBox = self.find_element(
			QCheckBox, "b_advance_startup"
		)

		if not Options.is_enabled(Options.Option.RESEARCH):
			self.advanced_start.setChecked(False)
			self.advanced_start.setVisible(False)

		languages = ["BE", "CH", "FR", "QC"]

		self.language_list: QComboBox = self.find_element(QComboBox, "list_language")
		for i, language in enumerate(languages):
			self.language_list.addItem(language)
			if self.stimuli_accent == language:
				self.language_list.setCurrentIndex(i)

		self.language_list.currentIndexChanged.connect(self.select_new_accent)

		self.load_module_button: QPushButton = self.find_element(
			QPushButton, "b_load_module"
		)
		self.load_module_button.clicked.connect(self.play_module)

	# microphone selection section
	def select_microphone(self):
		"""
		Initializes the microphone selection UI components.

		- Populates the microphone dropdown with available audio input devices including their ID, name, and default sample rate.
		- Sets the dropdown to the current input device by default.
		- Connects the microphone selection change event to update the selected microphone.
		- Sets up buttons for testing recording and playback, connecting them to their respective handlers.
		- Displays the currently selected audio output device.
		- Prepares labels and progress bar for microphone test feedback.
		"""
		self.microphone_list: QComboBox = self.find_element(
			QComboBox, "list_select_micro"
		)
		mic_list = AudioManager.get_input_list()

		index = 0
		s_index = -1
		for mic in mic_list:
			self.microphone_list.addItem(
				str(mic[0])
				+ " - "
				+ mic[1]["name"]
				+ " | "
				+ str(int(mic[1]["default_samplerate"]))
				+ " Hz"
			)
			if mic[0] == AudioManager.input_device_id:
				s_index = index
			index += 1

		self.microphone_list.currentIndexChanged.connect(self.select_new_microphone)
		self.microphone_list.setCurrentIndex(s_index)

		self.test_microphone_btn: QPushButton = self.find_element(
			QPushButton, "b_test_enr_micro"
		)
		self.test_microphone_btn.clicked.connect(self.test_recording)

		self.listen_microphone_button: QPushButton = self.find_element(
			QPushButton, "b_test_play_micro"
		)
		self.listen_microphone_button.clicked.connect(self.replay_test_recording)

		self.audio_output_label: QLabel = self.find_element(QLabel, "selected_micro")
		self.audio_output_label.setText(AudioManager.output_device_to_str())

		self.microphone_test_label: QLabel = self.find_element(
			QLabel, "l_micro_duration"
		)

		self.microphone_test_progressbar: QProgressBar = self.find_element(
			QProgressBar, "progressBar_micro"
		)
		self.microphone_test_progressbar.setTextVisible(False)

	# stimuli section
	def display_stimuli(self):
		"""
		Initializes and sets up the UI components related to stimuli display customization.

		- Finds and connects buttons for changing background and text colors to their handlers.
		- Configures a spin box for adjusting font size, including range, step, prefix, suffix, and connects value changes to font size adjustment.
		- Sets up a reset button to revert font and color settings.
		- Prepares an example label that reflects the current font and color settings, with centered alignment and background fill.
		"""
		self.background_color_button: QPushButton = self.find_element(
			QPushButton, "b_background_color"
		)
		self.background_color_button.clicked.connect(self.set_bg_color)

		self.text_color_button: QPushButton = self.find_element(
			QPushButton, "b_text_color"
		)
		self.text_color_button.clicked.connect(self.set_fg_color)

		self.value_spin_box: QSpinBox = self.find_element(QSpinBox, "sb_pointSize")
		self.value_spin_box.setRange(25, 85)
		self.value_spin_box.setSingleStep(1)
		self.value_spin_box.setValue(self.display_font.pointSize())
		self.value_spin_box.setSuffix(" pt")
		self.value_spin_box.setPrefix("Police: ")
		self.value_spin_box.valueChanged.connect(self.set_font_size)

		self.reset_button: QPushButton = self.find_element(QPushButton, "b_reset_font")
		self.reset_button.clicked.connect(self.reset_color)

		self.example_label: QLabel = self.find_element(QLabel, "example_text_label")
		self.example_label.setAutoFillBackground(True)
		self.example_label.setPalette(self.display_palette)
		self.example_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
		self.example_label.setFont(self.display_font)

	@override
	def setup(self):
		"""
		Initialize the UI
		We base our design on a 800*600 window, and will automatically adapt it to actual window size
		:return: None
		"""

		self.display_participant()

		self.select_module()

		self.select_microphone()

		self.display_stimuli()



	def load_config(self):
		"""
		Will load the "config.csv" file to init palette, font, and default accent config
		:return: True/False
		"""
		if os.path.isfile("config.csv"):
			try:
				values = CSVManager.read_file("config.csv", "\t", -1)
				color = QColor()
				color.setRed(int(values[0][0]))
				color.setGreen(int(values[0][1]))
				color.setBlue(int(values[0][2]))
				self.display_palette.setColor(QPalette.ColorRole.Window, color)
				color.setRed(int(values[1][0]))
				color.setGreen(int(values[1][1]))
				color.setBlue(int(values[1][2]))
				self.display_palette.setColor(QPalette.ColorRole.WindowText, color)
				self.display_font.setPointSize(int(values[2][0]))
				self.stimuli_accent: str = str(values[3][0])
				return True
			except IndexError:
				return False
		else:
			color = QColor()
			color.setRgb(255, 255, 255)
			self.display_palette.setColor(QPalette.ColorRole.Window, color)
			color.setRgb(0, 0, 0)
			self.display_palette.setColor(QPalette.ColorRole.WindowText, color)
			self.display_font.setPointSize(45)
			self.stimuli_accent = "FR"

	def test_recording(self):
		"""
		Test audio recording using microphone
		:return:
		"""
		if AudioManager is not None:
			AudioManager.stop()
			try:
				AudioManager.record_wave(
					["./test.wav"],
					2,
					self.microphone_test_progressbar,
					self.microphone_test_label,
				)
			except Exception as e:
				GeneralTools.alert_box(self, repr(e))

	def replay_test_recording(self):
		"""
		Listen to wav recorded in microphone test
		:return: None
		"""
		if AudioManager is not None:
			AudioManager.stop()
			if os.path.isfile("./test.wav"):
				AudioManager.play_wave(
					"./test.wav",
					self.microphone_test_progressbar,
					self.microphone_test_label,
				)

	def select_new_microphone(self, index):
		"""
		Change which microphone to use
		:param index: index of the newly selected microphone
		:return: None
		"""
		AudioManager.change_input_device(self.microphone_list.itemText(index))

	def select_new_accent(self, index):
		"""
		Change which accent to use in stimuli
		:param index: index of the newly selected accent
		:return: None
		"""
		self.stimuli_accent = self.language_list.itemText(index)
		self.save_config()

	# verification if participant exist:True or not:False
	@staticmethod
	def participant_exist(code):
		"""
		Check if a participant exists based on their code.

		:param code: The participant code to verify.
		:return: True if the participant exists, False otherwise.
		"""
		for x in DisplayTools.get_participant_list():
			if code == x[0]:
				return True
		return False

	def create_participant(self):
		"""
		Create a new participant based on what was input in the main window
		:return: None
		"""
		code = (
			str(self.create_participant_code.text())
			.replace("-", "_")
			.replace("/", "_")
			.upper()
			.strip()
		)
		if code != "":
			if self.participant_exist(code):
				# self.create_participant_code.setStyleSheet("color: red;  background-color: black")
				GeneralTools.alert_box(
					self, "Un participant avec ce code existe déjà !"
				)
				return
			self.create_participant_code.setStyleSheet(
				"color: black;  background-color: white"
			)
			# ParticipantManager.set_current_participant(code)

			gender = "Homme"
			if not self.create_participant_male_radio.isChecked():
				gender = "Femme"
			try:
				birthyear = int(self.create_participant_birthyear.value())
			except ValueError as e:
				logger.error(
					f"Error converting participant birthyear {self.create_participant_birthyear.value()} to integer: {e}"
				)
				return

			ParticipantManager.set_current_participant(code)
			ParticipantManager.save_meta_data(birthyear, gender)
			data_participant = (
				code
				+ " - ("
				+ gender
				+ " - "
				+ str(ParticipantManager.get_participant_age(datetime.now().year))
				+ " ans)"
			)
			# self.participant_list.addItem(
			# code + " - (" + gender + " - " + str(
			# ParticipantManager.get_participant_age(datetime.now().year)) + " ans)")

			self.participant_list.addItem(data_participant)
			self.participant_list.setCurrentIndex(self.participant_list.count() - 1)

	def remove_participant(self):
		"""
		Delete currently selected participant. Not permis with this button because after, all data are deleted. Do manually for more protection of data.
		:return:None
		"""
		GeneralTools.alert_box(
			self,
			"Pour effacer un participant, effacez le repertoire correspondant dans le repertoire data/participant",
		)

	def play_module(self):
		"""
		Play the selected module by opening a new ModuleWindow
		:return: None
		"""
		filename = self.module_list.currentText()
		module_name = str(filename[:-4])
		tmp = self.participant_list.currentText().split("-")
		participant_name = str(tmp[0]).strip()
		if participant_name == "":
			GeneralTools.alert_box(
				self,
				"Vous devez selectionner ou creer un participant avant de pouvoir lancer la passation",
			)
			return
		if module_name == "":
			GeneralTools.alert_box(
				self, "Vous devez selectionner un module pour le lancer"
			)
			return

		logger.info(
			f"Launching {module_name} module - {participant_name} {self.stimuli_accent}"
		)

		if module_name == "ModuleIntelligibilite":
			self.module_window = ModuleIntelligibiliteWindow(
				self, participant_name, module_name, self.stimuli_accent
			)
			self.hide()
		else:
			if self.advanced_start.isChecked():
				logger.info("Advanced mode enabled")
				self.advanced_window = PassationAdvancedWindow(
					self, filename, module_name, participant_name
				)
			else:
				values = CSVManager.read_file("./data/module/" + filename, "\t", -1)
				self.module_window = ModuleWindow(
					self, participant_name, module_name, self.stimuli_accent, values
				)

			self.hide()

	def play_module_from_advanced_start(self, participant_name, module_name, values):
		"""
		Launch a module with advanced startup settings.

		Closes the advanced start window, initializes the module window
		with the given participant, module name, accent, and values, then hides the current window.

		:param participant_name: Name of the participant.
		:param module_name: Name of the module to launch.
		:param values: Configuration values for the module.
		"""
		logger.info(
			f"Launching {module_name} module - Advanced - {participant_name} {self.stimuli_accent}"
		)
		self.advanced_window.close()
		self.module_window = ModuleWindow(
			self, participant_name, module_name, self.stimuli_accent, values
		)
		self.hide()

	def test_module(self):
		"""
		Checks if all the files referenced in the module are present
		:return: None
		"""
		filename = str(self.module_list.currentText())
		module_name = filename[:-4]
		ret = "Test du module " + module_name

		if filename == "ModuleIntelligibilite.csv":
			good = True
			if not os.path.isfile("./data/module/" + module_name + "/fond.png"):
				ret += "\nImage de fond manquante"
				good = False
			if not os.path.isfile("./data/module/" + module_name + "/liste_mots.db"):
				ret += "\nBase de donnée des mots manquante"
				good = False
			if not os.path.isdir("./data/module/" + module_name + "/images/"):
				ret += "\nRépertoire des images manquant"
				good = False

			with DBManager("./data/module/" + module_name + "/liste_mots.db") as db:
				for r in db.execute("select filename from mots"):
					if not os.path.isfile(
						"./data/module/" + module_name + "/images/" + r[0]
					):
						good = False
						ret += "\nImage " + r[0] + " manquante"
				if good:
					ret += "\nModule correct"
		else:
			values = CSVManager.read_file("./data/module/" + filename, "\t", -1)

			good = True
			for i in range(1, len(values)):
				if values[i][1] != "" and not os.path.isfile(
					"./data/module/" + module_name + "/" + values[i][1]
				):
					ret += (
						"\nFichier manquant : data/module/"
						+ module_name
						+ "/"
						+ values[i][1]
					)
					good = False
				if values[i][2] != "" and not os.path.isfile(
					"./data/module/" + module_name + "/" + values[i][2]
				):
					ret += (
						"\nFichier manquant : data/module/"
						+ module_name
						+ "/"
						+ values[i][2]
					)
					good = False
			if good:
				ret += "\nModule correct"

		GeneralTools.alert_box(self, ret)

	def set_bg_color(self):
		"""
		Calls __set_color for the Background
		:return: None
		"""
		self.__set_color(QPalette.ColorRole.Window)

	def set_fg_color(self):
		"""
		Calls __set_color for the Foreground
		:return: None
		"""
		self.__set_color(QPalette.ColorRole.WindowText)

	def reset_color(self):
		"""
		Reset colors to hard coded black on white values and 45 pt police size
		:return: None
		"""
		black = QColor(0, 0, 0)
		white = QColor(255, 255, 255)
		self.display_palette.setColor(QPalette.ColorRole.WindowText, black)
		self.display_palette.setColor(QPalette.ColorRole.Window, white)
		self.display_font.setPointSize(45)
		self.value_spin_box.setValue(45)
		self.save_config()
		self.example_label.setPalette(self.display_palette)
		self.example_label.setFont(self.display_font)
		self.update()
		# noinspection PyArgumentList
		QApplication.processEvents()

	def set_font_size(self):
		"""
		Set the font size for displaying text in module
		:return:
		"""
		self.display_font.setPointSize(int(self.value_spin_box.value()))
		self.save_config()
		self.example_label.setFont(self.display_font)
		# noinspection PyArgumentList
		QApplication.processEvents()

	def __set_color(self, ground_type):
		"""
		Open a color picker dialog to set the specified color in the display palette.

		The method opens a QColorDialog initialized with the current color of the given
		`ground_type` (e.g., background or foreground). If the user cancels, the original
		color is kept. After selecting, it updates the palette, saves the configuration,
		and refreshes the example label to reflect the new color.

		:param ground_type: QPalette.ColorRole specifying which color to change (e.g., QPalette.Background).
		"""
		original_color = self.display_palette.color(ground_type)
		color_dialog = QColorDialog(self)
		color_dialog.setOption(QColorDialog.ColorDialogOption.DontUseNativeDialog)
		color = color_dialog.getColor(original_color)
		if not color.isValid():
			color = original_color
		self.display_palette.setColor(ground_type, color)
		self.save_config()
		self.example_label.setPalette(self.display_palette)
		# noinspection PyArgumentList
		QApplication.processEvents()

	def save_config(self):
		"""
		Save configuration to file
		:return: None
		"""
		to_save = [
			[
				self.display_palette.color(QPalette.ColorRole.Window).red(),
				self.display_palette.color(QPalette.ColorRole.Window).green(),
				self.display_palette.color(QPalette.ColorRole.Window).blue(),
			],
			[
				self.display_palette.color(QPalette.ColorRole.WindowText).red(),
				self.display_palette.color(QPalette.ColorRole.WindowText).green(),
				self.display_palette.color(QPalette.ColorRole.WindowText).blue(),
			],
			[self.display_font.pointSize()],
			[self.stimuli_accent],
		]
		CSVManager.write_file("config.csv", "\t", to_save, "w")

	@override
	def closeEvent(self, event):
		"""
		Handle the window close event.

		This method ensures that the AudioManager is properly closed, then shows
		the parent window before proceeding with the default close event behavior.

		:param event: QCloseEvent object representing the close event.
		:return: The result of the superclass closeEvent method.
		"""
		AudioManager.close()

		self.parentWindow.show()

		return super().closeEvent(event)
