# encoding=utf-8
"""
UI Window for the acoustic cotation in Monpage.

This module provides the main window for acoustic measurements and analysis in the
Monpage application. It handles loading, displaying, and analyzing audio files
for various acoustic measurements, and manages saving measurement data to the database.
"""

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

import functools
import os
from typing import Any, Callable, Tuple, TypeVar

import psutil
from pyqtgraph.Qt.QtWidgets import QMainWindow

from cotation.acoustic.display.qt_graph_display import QtGraphDisplay
from cotation.acoustic.display.summary_tab import SummaryTab
from cotation.acoustic.display.summary_tree import SummaryTree
from cotation.acoustic.struct.analysis.a_tenu import Atenu
from cotation.acoustic.struct.analysis.aa2s import Aa2s
from cotation.acoustic.struct.analysis.cotation_acoustic_analysis import (
	CotationAcousticAnalysis,
)
from cotation.acoustic.struct.analysis.laurie import Laurie, LaurieType
from cotation.acoustic.struct.analysis.melanie import Melanie, MelanieType
from cotation.acoustic.struct.analysis.syllabes import Syllables
from cotation.acoustic.struct.analysis.week import Week
from cotation.acoustic.struct.indicator_persistence import IndicatorPersistence
from cotation.acoustic.struct.pm.parselmouth_audio_file import ParselmouthAudioFile
from cotation.acoustic.struct.step_input_persistence import StepInputPersistence

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

import pyqtgraph as pg
from PySide6.QtCore import Qt, QTimer
from PySide6.QtWidgets import (
	QApplication,
	QLabel,
	QMessageBox,
	QTabWidget,
	QVBoxLayout,
)

from cotation.acoustic.display.one_file_tab import OneFileTab
from cotation.cotation_window import CotationWindow
from tools.audio_manager import AudioManager
from tools.general_tools import GeneralTools

DATA_PATH = "../../data/participant/"

pg.setConfigOptions(foreground="black", background="w")

T = TypeVar("T")


def wait_wait_cursor(func: Callable[..., T]) -> Callable[..., T]:
	"""
	Decorator that shows a wait cursor while the decorated function executes.

	This decorator temporarily changes the cursor to a wait cursor during function
	execution to provide visual feedback that a potentially time-consuming
	operation is in progress.

	Args:
		func: The function to decorate

	Returns:
		The decorated function
	"""

	@functools.wraps(func)
	def wrapper(*args: Any, **kwargs: Any) -> T:
		try:
			QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
			QApplication.processEvents()  # the cursor will not change otherwise

			result = func(*args, **kwargs)

			return result
		except Exception as e:
			raise e
		finally:
			QApplication.restoreOverrideCursor()

	return wrapper


class CotationAcousticWindow(CotationWindow):
	"""
	Main window for acoustic measurements in the Monpage application.

	This window provides a tabbed interface for different acoustic measurement tasks.
	Each tab contains an audio file visualization and analysis tools specific to the
	measurement type. The window handles loading audio files, displaying them with graphs,
	managing user interactions, and saving measurement data to a database.

	Attributes:
		tab_widget: The main tab container for the window
		speaker_code: Identifier for the speaker being analyzed
		judge_code: Identifier for the person performing the analysis
		session_date: Date of the recording session
		cur_idx: Index of the currently displayed tab
		old_idx: Index of the previously displayed tab
		pages: List of [OneFileTab, validation_status] pairs for each measurement
		steps: List of acoustic measurement objects
		indicator_persistence: Object handling saving indicator data to database
		step_input_persistence: Object handling saving user input data to database
	"""

	TITLE: str = "MonPaGe Cotation Acoustique - v{} (RAM: {})"

	FIXED_SIZE_ALLOWED_MARGIN: float = 1.25
	"""The tolerance allowed for the resizing of fixed interval. Set to 1.0 for hard enforced fixed size"""

	tab_widget: QTabWidget
	"""The main tab container for the window"""

	speaker_code: str
	"""Identifier for the speaker being analyzed"""

	judge_code: str
	"""Identifier for the person performing the analysis"""

	session_date: str
	"""Date of the recording session"""

	cur_idx: int
	"""Index of the currently displayed tab"""

	old_idx: int
	"""Index of the previously displayed tab"""

	pages: list[Tuple[OneFileTab, bool]]
	"""List of [OneFileTab, validation_status] pairs for each measurement"""

	steps: list[CotationAcousticAnalysis]
	"""List of acoustic measurement objects"""

	indicator_persistence: IndicatorPersistence
	"""Object handling saving indicator data to database"""

	step_input_persistence: StepInputPersistence
	"""Object handling saving user input data to database"""

	recap: SummaryTab
	"""Summary tab showing overall progress"""

	timer: QTimer
	""" Timer used to update the RAM status """

	def __init__(
		self,
		parent: QMainWindow,
		speaker_code: str,
		judge_code: str,
		session_date: str,
		advanced_mode: bool,
	):
		"""
		Initialize the acoustic cotation window.

		Args:
			parent: Parent window for this window
			speaker_code: Identifier for the speaker being analyzed
			judge_code: Identifier for the person performing the analysis
			session_date: Date of the recording session
			advanced_mode: Whether to enable advanced features
		"""

		super().__init__(speaker_code, parent)
		self.tab_widget = QTabWidget()
		layout = QVBoxLayout()
		layout.addWidget(self.tab_widget)
		self.setLayout(layout)

		self.init_cotation()
		self.init_participant_result_db()

		AudioManager.init()

		self.speaker_code = speaker_code
		self.judge_code = judge_code
		self.session_date = session_date

		self.cur_idx = 0
		self.old_idx = 0

		self.pages = []
		self.steps = self.init_steps()

		self.indicator_persistence = IndicatorPersistence(
			speaker_code, judge_code, session_date
		)

		self.step_input_persistence = StepInputPersistence()
		self.tab_widget.setStyleSheet("QTabBar::scroller {width: 80px;}")
		self.tab_widget.setUsesScrollButtons(True)

		self.tab_widget.currentChanged.connect(self.change_index)

		step_names = []
		step_status = []
		step_when = []
		for s in self.steps:
			name = self.load_page(s)
			step_names.append(name)
			step_when.append(s.entry_date)
			step_status.append(self.pages[-1][1])

		self.recap = SummaryTab(SummaryTree(step_names, step_status, step_when))
		self.recap.set_quit_cotation_callback(self.close)

		self.tab_widget.addTab(self.recap, "Récapitulatif")

		self.update_window_title()
		self.showMaximized()

		self.timer = QTimer(self)
		self.timer.timeout.connect(self.on_timer_timeout)
		self.timer.start(1000)  # Each second

	def on_timer_timeout(self):
		"""
		Slot called when the timer times out.
		This method is triggered by a timer event. It updates the window title
		by calling the `update_window_title` method. Typically used to refresh
		the display or indicate a change in state at regular intervals.
		"""

		self.update_window_title()

	def update_window_title(self):
		"""
		Updates the window title with the current application version and memory usage.

		The window title is set using a predefined format string, including:
			- The application version retrieved from GeneralTools.get_version().
			- The current process's memory usage in megabytes (Mb), obtained via psutil.

		Returns:
			None
		"""
		process = psutil.Process()
		self.setWindowTitle(
			CotationAcousticWindow.TITLE.format(
				GeneralTools.get_version(),
				str(round(process.memory_info().rss / 1024**2)) + " Mb",
			)
		)

	def init_steps(self) -> list[CotationAcousticAnalysis]:
		"""
		Initialize all the acoustic measurement steps.

		Creates measurement objects for each type of acoustic analysis to be performed.

		Returns:
			A list of CotationAcousticMeasures objects representing each measurement step
		"""

		return [
			Atenu(1, self.speaker_code, self.session_date),
			Atenu(2, self.speaker_code, self.session_date),
			Week(self.speaker_code, self.session_date),
			Syllables(
				"tratratra",
				self.speaker_code,
				self.session_date,
				suffix_id="amrccv_tra",
				ddk_phon_factor=3,
				fixed_size_allowed_margin=CotationAcousticWindow.FIXED_SIZE_ALLOWED_MARGIN
			),
			Syllables(
				"claclacla",
				self.speaker_code,
				self.session_date,
				suffix_id="amrccv_kla",  # TODO verify
				ddk_phon_factor=3,
				fixed_size_allowed_margin=CotationAcousticWindow.FIXED_SIZE_ALLOWED_MARGIN
			),
			Syllables(
				"bababa", self.speaker_code, self.session_date, suffix_id="amrcv_ba",
				fixed_size_allowed_margin=CotationAcousticWindow.FIXED_SIZE_ALLOWED_MARGIN
			),
			Syllables(
				"dedede", self.speaker_code, self.session_date, suffix_id="amrcv_de",
				fixed_size_allowed_margin=CotationAcousticWindow.FIXED_SIZE_ALLOWED_MARGIN
			),
			Syllables(
				"gogogo", self.speaker_code, self.session_date, suffix_id="amrcv_go",
				fixed_size_allowed_margin=CotationAcousticWindow.FIXED_SIZE_ALLOWED_MARGIN
			),
			Syllables(
				"badego", self.speaker_code, self.session_date, suffix_id="smrcv_badego",
				fixed_size_allowed_margin=CotationAcousticWindow.FIXED_SIZE_ALLOWED_MARGIN
			),
			# SyllablesTypeB()
			Aa2s(self.speaker_code, self.session_date),
			Melanie(MelanieType.NEUTRE, self.speaker_code, self.session_date),
			Laurie(LaurieType.NEUTRE, self.speaker_code, self.session_date),
			Laurie(LaurieType.QUESTION, self.speaker_code, self.session_date),
		]

	def load_page(self, step: CotationAcousticAnalysis) -> str:
		"""
		Load a measurement page for the given step.

		Creates a tab for the specified acoustic measurement step, loads the audio file,
		sets up the graph display, and restores any previously saved state.

		Args:
			step: The acoustic measurement step to load

		Returns:
			The module code (name) of the loaded step
		"""

		has_data = False
		file_path = os.path.join(
			"data",
			"participant",
			step.speaker_code,
			step.session_date,
			f"Module{step.module_code.split('_')[0]}",
			step.get_audio_file_name(),
		)
		if not os.path.exists(file_path):
			no_file = QLabel(
				f"Le fichier {file_path} du module {step.module_code} n'a pas été trouvé!"
			)
			self.pages.append((no_file, False))
			self.tab_widget.addTab(no_file, step.module_code)
			return step.module_code
		audio_data = ParselmouthAudioFile(file_path)
		duration = audio_data.get_duration()
		step.duration = duration

		graph_display = QtGraphDisplay(
			audio_data.get_sound(),
			audio_data.get_spectrogram(),
			step.get_default_interval_duration(),
			fixed_size_allowed_margin=CotationAcousticWindow.FIXED_SIZE_ALLOWED_MARGIN
		)
		graph_display.add_secondary_plotlines(step.get_required_plots())

		# We initialize the selection constraints of the graph with the step values
		graph_display.set_selection_parameters(
			step.is_signal_selection_movable,
			step.is_signal_selection_manually_resizable,
			step.signal_selection_min_length,
			step.signal_selection_max_length,
		)

		# First load saved inputs from database to restore state
		self.step_input_persistence.fill_from_saved_inputs(step, self.judge_code)

		# Then create the page with the restored state
		page = OneFileTab(audio_data, step.get_io(), graph_display)
		# We have acces to the CotationAcousticMeasure (step.io) and the graph_display
		# We need, when graph display selection is changed to go to CotationAcousticMeasure to validate if new
		# selection is valid

		graph_display.set_selection_changed_callback(self.selected_interval_changed)
		# graph_display.selectionchangedfinished.connect(self.on_achange_intervale)

		if step.get_interval() is not None:
			step.update_io()
			# TODO : get the entry_date value and update the FinishScreen
			if step.entry_date is not None and isinstance(step.entry_date, datetime):
				has_data = True
			page.set_selected_interval(step.get_interval())

		page.set_go_previous_callback(self.__previous_window)
		page.set_go_next_callback(self.__next_window)
		page.set_validated_callback(self.validation)

		# page.do_resize.connect(self.resize)
		self.pages.append((page, has_data & page.graph_display.was_moved))
		title = f"{step.module_code}*"

		if has_data:
			title = f"{step.module_code}"
		self.tab_widget.addTab(page, title)
		return step.module_code

	@wait_wait_cursor
	def validation(self):
		"""
		Validate the current tab's data and save it to the database.

		Updates the model with data from the view, saves to the database,
		and updates the UI to reflect the validated state.
		"""

		is_recap_tab = self.cur_idx == self.tab_widget.count() - 1
		if is_recap_tab:
			return

		s = self.steps[self.cur_idx]
		p = self.pages[self.cur_idx][0]

		if not isinstance(p, OneFileTab):
			return

		# Update the model from the view
		s.set_interval(p.get_selected_interval())
		s.update_io()

		# Save to database immediately
		self.step_input_persistence.save_all_step_input_data(s, self.judge_code)
		self.step_input_persistence.commit()  # Make sure changes are committed
		self.indicator_persistence.save_all_indicators(self.steps)

		# Update UI state
		self.pages[self.cur_idx] = (self.pages[self.cur_idx][0], True)
		self.tab_widget.setTabText(self.cur_idx, s.module_code)
		self.recap.update(self.cur_idx, True, s.entry_date)

	def change_index(self, new_idx: int):
		"""
		Handle tab changes and manage data persistence between tabs.

		When switching tabs, this method:
		1. Stops any playing audio in the previous tab
		2. Saves the state of the previous tab if needed
		3. Validates data completeness and prompts user if data is incomplete

		Args:
			new_idx: The index of the tab being switched to
		"""

		is_recap = lambda idx: (idx == self.tab_widget.count() - 1)

		if is_recap(self.old_idx):
			self.old_idx = new_idx
			self.cur_idx = new_idx
			return

		p, was_validated = self.pages[self.old_idx]
		if isinstance(p, OneFileTab):
			p.stop_sound()

			# Save the current tab's state when switching away from it
			if self.old_idx < len(self.steps):
				s = self.steps[self.old_idx]
				s.set_interval(p.get_selected_interval())
				s.update_io()
				# Only save to database if the tab was validated
				if was_validated:
					self.step_input_persistence.save_all_step_input_data(
						s, self.judge_code
					)

		if self.old_idx == new_idx:
			return

		if was_validated:
			self.old_idx = new_idx
			self.cur_idx = new_idx
			return

		file_found = isinstance(p, OneFileTab)
		if not file_found:
			self.old_idx = new_idx
			self.cur_idx = new_idx
			return

		if p.get_all_filled():
			self.validation()
			self.old_idx = new_idx
			self.cur_idx = new_idx
			return

		res = QMessageBox.question(
			self,
			"Confirmation",
			"Vous n'avez pas remplis tout les champs. Voulez vous continuer?",
			QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
			QMessageBox.StandardButton.Cancel,
		)

		if res == QMessageBox.StandardButton.Cancel:
			self.tab_widget.setCurrentIndex(self.old_idx)
			return

		self.old_idx = new_idx
		self.cur_idx = new_idx

	def __cycle_window(self):
		"""
		Cycle to the next tab in a circular fashion.

		Moves to the next tab, or back to the first tab if currently on the last tab.
		"""

		self.cur_idx = (self.cur_idx + 1) % self.tab_widget.count()
		self.tab_widget.setCurrentIndex(self.cur_idx)

	def __next_window(self):
		"""
		Move to the next tab if available.

		Does nothing if already on the last tab.
		"""

		if self.cur_idx + 1 >= self.tab_widget.count():
			return

		self.tab_widget.setCurrentIndex(self.cur_idx + 1)

	def __previous_window(self):
		"""
		Move to the previous tab if available.

		Does nothing if already on the first tab.
		"""

		if self.cur_idx - 1 < 0:
			return

		self.tab_widget.setCurrentIndex(self.cur_idx - 1)

	@override
	def closeEvent(self, event):
		"""
		Handle window close event by saving current state and cleaning up resources.

		Args:
			event: The close event
		"""

		# Save current state before closing
		self.save_current_state()
		AudioManager.close()

		return super().closeEvent(event)

	def save_current_state(self):
		"""
		Save the current state of all steps.

		Saves the current tab data if it's a valid OneFileTab, and commits all
		changes to the database. This ensures no data is lost when closing the window.
		"""

		# Only process if we have steps and pages
		if not hasattr(self, "steps") or not hasattr(self, "pages"):
			return

		# Save current tab data if it's a valid OneFileTab
		if self.cur_idx < len(self.pages):
			p = self.pages[self.cur_idx][0]
			if isinstance(p, OneFileTab):
				s = self.steps[self.cur_idx]
				s.set_interval(p.get_selected_interval())
				s.update_io()
				self.step_input_persistence.save_all_step_input_data(s, self.judge_code)

		# Commit all changes to database
		self.step_input_persistence.commit()
		self.indicator_persistence.save_all_indicators(self.steps)

	def selected_interval_changed(self):
		"""
		Handle changes to the selected interval in the graph display.

		This method is called when the user changes the selection in the graph.
		Currently only logs a message, but can be expanded to validate or process
		the new selection.
		"""

		pass
