# encoding=utf-8
__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
from enum import Enum
from typing import Optional, Union

import pyqtgraph

from cotation.acoustic.display.io_widget.combobox_input import ComboBoxInput
from cotation.acoustic.display.io_widget.info import Info, InfoBox
from cotation.acoustic.display.io_widget.int_input import IntInput
from cotation.acoustic.display.io_widget.output import Output
from cotation.acoustic.display.io_widget.ui_line_with_input import UILineWithInput
from cotation.acoustic.struct.analysis.cotation_acoustic_analysis import (
	CotationAcousticAnalysis,
)
from cotation.acoustic.struct.pm.parselmouth_intensity import ParselmouthIntensity

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

# @property transparantly have getter and setter.
# class.x instead of class.(get,set)_x()

logger = logging.getLogger(__name__)

DEFAULT_SOUND_FILE_PATTERN = "{}_{}_Module{}.wav"


class SetDurationSeconds(Enum):
	"""
	Enumeration representing preset durations for syllable analysis (in seconds).
	"""

	TWO = 2
	FOUR = 4

	@staticmethod
	def from_int(i: int) -> "SetDurationSeconds":
		if i == SetDurationSeconds.TWO.value:
			return SetDurationSeconds.TWO

		if i == SetDurationSeconds.FOUR.value:
			return SetDurationSeconds.FOUR

		distance = {
			duration: abs(i - duration.value) for duration in SetDurationSeconds
		}
		return sorted(distance, key=distance.get)[0]

	def __str__(self) -> str:
		return str(self.value)


class Syllables(CotationAcousticAnalysis):
	"""
	Handles Diadochokinetic (DDK) rate analysis for a specific repeated syllable.

	This class provides functionality to:
	- Track counted syllables over a set duration.
	- Calculate syllabic and phonemic DDK rates.
	- Manage UI input/output components for user interaction.
	- Automatically update analysis based on interval changes.
	- Generate plots representing intensity over time.

	Attributes:
	    syllable (str): The syllable being analyzed.
	    DDK_PHON_FACTOR (int): Factor to convert syllables to phonemes.
	    _counted_syllables (int): Number of syllables counted in the interval.
	    _duration (SetDurationSeconds): Duration over which counting is done.
	    suffix_id (str): Identifier suffix for result keys.
	    _user_set_duration (bool): Indicates if user explicitly set duration.
	    intensity (Optional[object]): Intensity data object for plotting.
	    duration_io (ComboBoxInput): UI element for duration selection.
	    counted_syllables_io (IntInput): UI element for inputting counted syllables.
	    rate_ddk_phon_out (Output): Output UI element for syllable rate.
	    rate_phon_out (Output): Output UI element for phoneme rate.
	    intensity_plot (pyqtgraph.PlotDataItem): Plot item showing intensity over time.
	"""

	_counted_syllables: int
	_duration: SetDurationSeconds
	syllable: str
	DDK_PHON_FACTOR: int
	duration_io: ComboBoxInput
	counted_syllables_io: IntInput
	rate_ddk_phon_out: Output
	rate_phon_out: Output

	def __init__(
		self,
		syllable: str,
		speaker_code: str,
		session_date: str,
		suffix_id: str,
		ddk_phon_factor: int = 2,
		fixed_size_allowed_margin: float=1.0
	):
		super().__init__(
			speaker_code,
			f"Diadoco_{syllable}",
			session_date,
		)
		self.is_signal_selection_movable = True
		self.is_signal_selection_manually_resizable = True
		self.signal_selection_min_length = 4
		self.signal_selection_max_length = self.signal_selection_min_length * fixed_size_allowed_margin
		self.signal_selection_default_duration = self.signal_selection_min_length

		self.syllable = syllable
		self.DDK_PHON_FACTOR = ddk_phon_factor
		self._duration = SetDurationSeconds.FOUR
		self._counted_syllables = 0
		self.suffix_id = suffix_id
		self._user_set_duration = False

		self.intensity = None
		self.duration_io = ComboBoxInput(SetDurationSeconds, False)
		self.duration_io.set_input_value(SetDurationSeconds.FOUR)
		self.counted_syllables_io = IntInput(0)
		self.rate_ddk_phon_out = Output(f"syll_{self.suffix_id}", "")
		self.rate_phon_out = Output(f"phon_{self.suffix_id}", "")

		self.intensity_plot = pyqtgraph.PlotDataItem()

		self.on_interval_change.append(self.update_duration)
		self.on_interval_change.append(self.load_data)

	def get_counted_syllables(self) -> int:
		"""
		Return the number of syllables that have been counted.
		"""
		return self._counted_syllables

	def set_counted_syllables(self, counted_syllables: int):
		"""
		Set the number of counted syllables. Raises an error if the value is less than or equal to zero.
		"""
		if counted_syllables <= 0:
			ValueError("Le nombre de syllabes compté doit être supérieur à 0")
		self._counted_syllables = counted_syllables

	def get_duration(self) -> int:
		"""
		Return the currently set duration value in seconds.
		"""
		return self._duration.value

	def set_duration(self, duration: int):
		"""
		Set the duration value using an integer, converting it to a SetDurationSeconds object.
		"""
		self._duration = SetDurationSeconds.from_int(duration)

	def update_duration(self):
		"""
		Update the duration value using the interval duration if it hasn't been explicitly set by the user.
		Also updates the UI field if it exists and hasn't been modified manually.
		"""
		# This only happens when you validate -> TODO Make it happen on all
		# interval change ?
		# Set the duration from interval only if duration hasn't been explicitly set by user
		if not hasattr(self, "_user_set_duration") or not self._user_set_duration:
			self.duration = self.get_interval_duration()

		# Only update the UI if needed and if UI exists
		if hasattr(self, "duration_io") and self.duration_io is not None:
			# Don't override user selections
			if self.duration_io.get_input_value() is None or not hasattr(
				self, "_user_set_duration"
			):
				self.duration_io.set_input_value(self._duration)

	def get_rate_DDK(self) -> float:
		"""
		Calculate and return the DDK (Diadochokinetic) rate as the number of counted syllables
		divided by the selected duration. Raises an error if the syllable count is invalid.
		"""
		if self.get_counted_syllables() <= 0:
			ValueError("Le nombre de syllabes compté doit être supérieur à 0")

		return self.get_counted_syllables() / float(self.get_duration())

	def get_rate_DDK_phon(self) -> float:
		"""
		Calculate and return the DDK rate expressed in phonemes by applying a constant factor
		to the syllabic DDK rate. Raises an error if the syllable count is invalid.
		"""
		if self.get_counted_syllables() <= 0:
			ValueError("Le nombre de syllabes compté doit être supérieur à 0")

		return self.get_rate_DDK() * self.DDK_PHON_FACTOR

	@override
	def get_results(self) -> dict[str, Union[float, str]]:
		"""
		Return a dictionary containing the DDK rates in syllables and phonemes, keyed by
		duration and a suffix identifier.
		"""
		return {
			f"syll_{self.get_duration()}s_{self.suffix_id}": self.get_rate_DDK(),
			f"phon_{self.get_duration()}s_{self.suffix_id}": self.get_rate_DDK_phon(),
		}

	def set_counted_syllables_from_input_value(self):
		"""
		Update the counted syllables value from the corresponding UI input field,
		if it contains a valid (positive) value.
		"""
		# Update the internal value whenever the input changes and is valid
		if self.counted_syllables_io.get_input_value() > 0:
			self.set_counted_syllables(self.counted_syllables_io.get_input_value())
			logger.info(f"Setting counted syllables to {self.get_counted_syllables()}")

	def set_duration_from_input(self):
		"""
		Update the internal duration value based on the user's input from the UI,
		and store it as the default duration for signal selection.
		"""
		if self.duration_io.get_input_value() is not None:
			self._duration = self.duration_io.get_input_value()
			# self.signal_selection_default_duration = self._duration.value
			# self.signal_selection_min_length = self._duration.value
			# self.signal_selection_max_length = self.signal_selection_min_length*1.25

			# Mark that user has explicitly set the duration
			self._user_set_duration = True
			logger.info(f"Setting duration to {self._duration.value}")

	@override
	def get_io(self) -> Info:
		"""
		Build and return the user interface (UI) elements for DDK segmentation.

		This includes:
		- An input for setting the number of syllables counted.
		- A dropdown for selecting the duration to analyze.
		- Result outputs for syllabic and phonemic DDK rates.

		Data callbacks are connected to update internal state when inputs are changed.
		"""
		# Only initialize if not already initialized
		if self.counted_syllables_io is None:
			self.counted_syllables_io = IntInput(0)

		uilineinput = UILineWithInput(self.syllable, "", self.counted_syllables_io)

		# def set_counted_syllables():
		# 	# Update the internal value whenever the input changes and is valid
		# 	if self.counted_syllables_io.input_value > 0:
		# 		self.counted_syllables = self.counted_syllables_io.input_value
		# 		print(f"Setting counted syllables to {self.counted_syllables}")

		# input.set_value_changed_callback(set_counted_syllables)

		# We set a SECOND callback in the INPUT itself as to what to do when the value is changed
		self.counted_syllables_io.set_value_changed_data_callback(
			self.set_counted_syllables_from_input_value
		)

		if self.duration_io is None:
			self.duration_io = ComboBoxInput(SetDurationSeconds, False)
			self.duration_io.set_input_value(SetDurationSeconds.FOUR)

		duration_input = UILineWithInput("Durée à analyser", "s", self.duration_io)

		# if self._duration is not None:
		# 	self.duration_io.input_value = self._duration

		# def set_duration():
		# 	if self.duration_io.input_value is not None:
		# 		self._duration = self.duration_io.input_value
		# 		# Mark that user has explicitly set the duration
		# 		self._user_set_duration = True

		# duration_input.set_value_changed_callback(set_duration)

		self.duration_io.set_value_changed_data_callback(self.set_duration_from_input)

		info_box = InfoBox(
			f"Segmentation {self.syllable}",
			f"Ajuster la frontière pour qu'elle corresponde précisément au début de la séquence {self.syllable}.",
			[uilineinput, duration_input],
		)

		if self.rate_ddk_phon_out is None:
			self.rate_ddk_phon_out = Output(f"syll_{self.suffix_id}", "")
		if self.rate_phon_out is None:
			self.rate_phon_out = Output(f"phon_{self.suffix_id}", "")

		resultats = InfoBox(
			"Résultats", dynamic_content=[self.rate_ddk_phon_out, self.rate_phon_out]
		)

		info = Info()
		info.add_infobox(info_box)
		info.add_infobox(resultats)

		return info

	@override
	def update_io(self):
		"""
		Updates the user interface (UI) with the internal model values.

		- Updates the counted syllables field only if it is empty or invalid.
		- Updates the duration if it has not been manually set by the user.
		- Always updates the output fields to display the calculated rates.

		This ensures that manually entered values are preserved while displaying the results.
		"""

		# Only update the input fields if they are empty or have invalid values
		# This prevents overwriting user input when validating
		if self.counted_syllables_io is not None:
			if (
				self.counted_syllables_io.get_input_value() == 0
				and self.get_counted_syllables() > 0
			):
				self.counted_syllables_io.set_input_value(self.get_counted_syllables())

		if self.duration_io is not None:
			# Only update duration if it hasn't been set by the user
			# if self.duration_io.get_input_value() is None and self._duration is not None:
			if self._duration is not None:
				self.duration_io.set_input_value(self._duration)

		# Always update output displays
		if self.rate_ddk_phon_out is not None:
			self.rate_ddk_phon_out.update_value(self.get_rate_DDK())
		if self.rate_phon_out is not None:
			self.rate_phon_out.update_value(self.get_rate_DDK_phon())

	@override
	def get_required_plots(self) -> tuple[list[pyqtgraph.PlotDataItem], float, float]:
		"""
		Returns the plots required for the analysis.

		Here, only the intensity plot is required, with a time scale ranging from 0 to 500 ms.
		"""

		return [self.intensity_plot], 0, 500

	@override
	def reload_plots(self):
		"""
		Reloads the intensity plot data from the recalculated values.

		This method is called when the acoustic analysis is performed again.
		"""

		self.intensity_plot.setData(*self.intensity.get_plot_values())
