__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 PIL import Image, ImageColor, ImageDraw

from tools.csv_manager import CSVManager
from tools.db_manager import DBManager
from tools.general_tools import GeneralTools
from tools.participant_manager import ParticipantManager

logger = logging.getLogger(__name__)


class IndicatorReportingTools(object):
	"""
	Utility class for calculating grid cell coordinates and dimensions
	used in indicator reporting layouts.
	"""

	grid_x_offset = 5
	grid_y_offset = 25

	widths = [200, 50, 50, 50, 100, 100, 50, 50, 50]
	row_height = 20

	@staticmethod
	def get_cell_coord(x: int, y: int) -> tuple:
		"""
		Calculate the pixel coordinates of a grid cell's bounding box.

		Determines the top-left and bottom-right coordinates of a cell
		in a grid layout based on column widths and row heights.

		Args:
			x (int): The column index of the cell.
			y (int): The row index of the cell.

		Returns:
			tuple: A tuple of two (x, y) tuples:
				- Top-left coordinate of the cell.
				- Bottom-right coordinate of the cell.

		Raises:
			KeyError: If the x index exceeds the number of available columns.
		"""
		if x > len(IndicatorReportingTools.widths) - 1:
			raise KeyError("x coord outside of cell range")
		left_x = IndicatorReportingTools.grid_x_offset
		for i in range(x):
			left_x += IndicatorReportingTools.widths[i]
		right_x = left_x + IndicatorReportingTools.widths[x]

		upper_y = (
			y * IndicatorReportingTools.row_height
		) + IndicatorReportingTools.grid_y_offset
		lower_y = upper_y + IndicatorReportingTools.row_height
		return (left_x, upper_y), (right_x, lower_y)

	@staticmethod
	def get_cell_center_coord(x: int, y: int) -> tuple:
		"""
		Get the pixel coordinates of the center of a grid cell.

		Uses the bounding box from `get_cell_coord()` to compute the
		central point of the cell for alignment or placement purposes.

		Args:
			x (int): The column index of the cell.
			y (int): The row index of the cell.

		Returns:
			tuple: A (x, y) tuple representing the center of the cell.
		"""
		tmp = IndicatorReportingTools.get_cell_coord(x, y)
		mid_x = round(tmp[0][0] + (tmp[1][0] - tmp[0][0]) / 2)
		mid_y = round(tmp[0][1] + (tmp[1][1] - tmp[0][1]) / 2)
		return mid_x, mid_y

	@staticmethod
	def get_total_height(nb_indicator: int) -> int:
		"""
		Calculate the total height in pixels required for a number of indicators.

		Adds vertical grid offset to the total height based on row height
		and the number of indicator rows.

		Args:
			nb_indicator (int): The number of indicators (rows).

		Returns:
			int: The total height in pixels.
		"""
		return (
			IndicatorReportingTools.row_height * nb_indicator
		) + IndicatorReportingTools.grid_y_offset

	@staticmethod
	def get_total_width() -> int:
		"""
		Calculate the total width in pixels of the grid layout.

		Sums the width of all columns and adds the horizontal grid offset.

		Returns:
			int: The total width in pixels.
		"""
		return (
			sum(IndicatorReportingTools.widths) + IndicatorReportingTools.grid_x_offset
		)


class IndicatorReporting(object):
	"""
	Manages reporting of acoustic and deviant score indicators for participants.

	Handles loading acoustic analysis results, saving data to the database,
	calculating percentiles and composite deviant scores, and generating reports
	and filenames for participant sessions.
	"""

	default_acoustic_result_filename = "resultats_acou.csv"
	default_acoustic_result_filename_v2 = "AnalysesScreening.txt"

	participant_code: str = None
	judge_code: str = None
	session_date: str = None
	participant_result_db: DBManager = None
	indicator_results: list = None
	devscore_results: list = None

	@staticmethod
	def init():
		"""
		Initialize or reset all internal reporting variables.

		Resets participant code, judge code, session date, database connection,
		and cached results to None.

		Returns:
			bool: Always returns True upon completion.
		"""
		IndicatorReporting.participant_code = None
		IndicatorReporting.judge_code = None
		IndicatorReporting.session_date = None
		IndicatorReporting.participant_result_db = None
		IndicatorReporting.indicator_results = None
		IndicatorReporting.devscore_results = None
		return True

	@staticmethod
	def get_acoustic_results_filename():
		"""
		Construct the file path for the default acoustic results file.

		Returns:
			str | None: Full path to the acoustic result file if a participant is selected, otherwise None.
		"""
		if ParticipantManager.current_participant is not None:
			return (
				ParticipantManager.folder_path
				+ IndicatorReporting.session_date
				+ "/Analyse/"
				+ IndicatorReporting.default_acoustic_result_filename
			)
		return None

	@staticmethod
	def get_acoustic_results_filename_v2():
		"""
		Search for the newer version of the acoustic result file in the participant's session folder.

		Returns:
			str | None: The first matching result file path if found, otherwise None.
		"""
		if ParticipantManager.current_participant is not None:
			resulting_acoustic_files = GeneralTools.find_files(
				os.path.join(
					ParticipantManager.folder_path,
					IndicatorReporting.session_date,
					"Analyse",
				),
				IndicatorReporting.default_acoustic_result_filename_v2,
			)

			try:
				return next(resulting_acoustic_files)
			except StopIteration:
				pass

		# return ParticipantManager.folder_path + IndicatorReporting.session_date + "/Analyse/" + \
		#        IndicatorReporting.default_acoustic_result_filename_v2
		return None

	@staticmethod
	def get_reporting_filename():
		"""
		Construct the file path for the PDF report file for the current participant and session.

		Returns:
			str | None: Full path to the PDF report file if a participant is selected, otherwise None.
		"""
		if ParticipantManager.current_participant is not None:
			fname = (
				ParticipantManager.current_participant
				+ "_"
				+ IndicatorReporting.session_date
				+ "_RapportMonPaGe.pdf"
			)
			return os.path.join(
				ParticipantManager.folder_path, IndicatorReporting.session_date, fname
			)
		return None

	@staticmethod
	def get_exporting_filename():
		"""
		Construct the file path for the data export text file for the current participant and session.

		Returns:
			str | None: Full path to the export file if a participant is selected, otherwise None.
		"""
		if ParticipantManager.current_participant is not None:
			fname = (
				ParticipantManager.current_participant
				+ "_"
				+ IndicatorReporting.session_date
				+ "_DonneesReporting.txt"
			)
			return os.path.join(
				ParticipantManager.folder_path, IndicatorReporting.session_date, fname
			)
		return None

	@staticmethod
	def get_participant_uniquestamp():
		"""
		Generate a unique identifier string for the participant-session-judge combination.

		Returns:
			str: A unique stamp string in the format: participant_session_judge
		"""
		return (
			IndicatorReporting.participant_code
			+ "_"
			+ IndicatorReporting.session_date
			+ "_"
			+ IndicatorReporting.judge_code
		)

	@staticmethod
	def get_indicator_graph_output_filename():
		"""
		Construct the file path for saving the indicator graph image.

		Returns:
			str | None: Full path to the image file if a participant is selected, otherwise None.
		"""
		if ParticipantManager.current_participant is not None:
			return (
				ParticipantManager.folder_path
				+ IndicatorReporting.session_date
				+ "/Analyse/indicator_output_"
				+ IndicatorReporting.get_participant_uniquestamp()
				+ ".png"
			)
		return None

	@staticmethod
	def set_current_values(participant_code, judge_code, session_date):
		"""
		Set and initialize the current participant, judge, and session date for reporting.

		Also creates a personalized cotation database for the current participant.

		Args:
			participant_code (str): Identifier of the participant.
			judge_code (str): Identifier of the judge.
			session_date (str): Date of the session.
		"""
		IndicatorReporting.init()
		IndicatorReporting.participant_code = participant_code
		ParticipantManager.set_current_participant(participant_code)
		IndicatorReporting.judge_code = judge_code
		IndicatorReporting.session_date = session_date
		with DBManager.get_cotation_db() as reference_cotation_db:
			IndicatorReporting.participant_result_db = (
				reference_cotation_db.create_user_cotation_db(
					ParticipantManager.cotation_db_filename
				)
			)

	@staticmethod
	def reset_acoustic_values():
		"""
		Remove all stored acoustic values in the database for the current judge, session, and participant.
		"""
		sql = "DELETE FROM results_acou where judge=? and session=? and participant=?"
		t = (
			IndicatorReporting.judge_code,
			IndicatorReporting.session_date,
			ParticipantManager.current_participant,
		)
		IndicatorReporting.participant_result_db.execute(sql, t)

	@staticmethod
	def save_acoustic_value(indicator_id, value, error_msg: str = None):
		"""
		Save or update an acoustic value for a specific indicator in the results database.

		Args:
			indicator_id (str): The ID of the indicator being saved.
			value (Any): The value to store for the indicator.
			error_msg (str, optional): Any error message to associate with the result.
		"""
		sql = (
			"insert or replace into results_acou(`indicator_id`,`value`,`session`, `participant`, `judge`, "
			"`version`, `erreur`) VALUES (?,?,?,?,?,?,?)"
		)
		t = (
			indicator_id,
			value,
			IndicatorReporting.session_date,
			ParticipantManager.current_participant,
			IndicatorReporting.judge_code,
			GeneralTools.get_version(),
			error_msg,
		)
		IndicatorReporting.participant_result_db.execute(sql, t)

	@staticmethod
	def import_acoustic_results_v2(filename: str = None) -> bool:
		"""
		Imports and saves acoustic analysis results from a TSV file (v2 format), maps them to indicators,
		performs computations for composite indicators, and stores the results in the database.

		Steps:
		- Loads raw acoustic results from file.
		- Converts text-based float values, handles errors, and stores relevant values.
		- Computes derived indicators (means and differences) using grouped components.
		- Retrieves and saves additional indicators like "words_not_recognized" and "segmental_errors".
		- Saves results into the results_acou database table.
		- Commits the transaction.

		Args:
			filename (str, optional): Path to the TSV file to import. If None, the method uses the default filename.

		Returns:
			bool: True if data was successfully imported and saved; False if an error occurred (e.g., file not found).
		"""
		sql = "select id, code from indicator"
		tmp = IndicatorReporting.participant_result_db.execute(sql).fetchall()
		corres_code_indicatorid = {}
		for t in tmp:
			corres_code_indicatorid[t["code"]] = t["id"]

		error_msg_colmumn = 3

		corres_indicatorid_column = {
			"mpt": 6,
			"rate_series": 7,
			"syll_2s_amrcv_ba": 8,
			"phon_2s_amrcv_ba": 9,
			"syll_4s_amrcv_ba": 10,
			"phon_4s_amrcv_ba": 11,
			"syll_2s_amrcv_de": 12,
			"phon_2s_amrcv_de": 13,
			"syll_4s_amrcv_de": 14,
			"phon_4s_amrcv_de": 15,
			"syll_2s_amrcv_go": 16,
			"phon_2s_amrcv_go": 17,
			"syll_4s_amrcv_go": 18,
			"phon_4s_amrcv_go": 19,
			"syll_2s_smrcv_badego": 20,
			"phon_2s_smrcv_badego": 21,
			"syll_4s_smrcv_badego": 22,
			"phon_4s_smrcv_badego": 23,
			"syll_2s_amrccv_kla": 24,
			"phon_2s_amrccv_kla": 25,
			"syll_4s_amrccv_kla": 26,
			"phon_4s_amrccv_kla": 27,
			"syll_2s_amrccv_tra": 28,
			"phon_2s_amrccv_tra": 29,
			"syll_4s_amrccv_tra": 30,
			"phon_4s_amrccv_tra": 31,
			"voice_a_jitterppq5": 32,
			"voice_a_shimmerapq11": 33,
			"voice_a_hnr": 34,
			"voice_a_sdf0": 35,
			"voice_a_cpps": 36,
			"rate_sentence_phon": 37,
			"rate_sentence_syll": 38,
			"voice_speaking_meanf0": 39,
			"voice_speaking_sdf0": 40,
			"voice_speaking_varcof0": 41,
			"voice_speaking_cpps": 42,
			"laurieN_temp": 43,
			"laurieQ_temp": 44,
		}

		if filename is None:
			filename = IndicatorReporting.get_acoustic_results_filename_v2()
		try:
			data = CSVManager.read_file(filename, "\t")
		except FileNotFoundError as e:
			logger.error(f"Error importing acoustic results: {e}")
			return False
		# On charge les données du fichier resultat des analyses acoustiques
		loaded_data = {}
		for l in data[1:]:  # On skippe la 1ere ligne qui est le titre de colonne
			for k in list(corres_indicatorid_column.keys()):
				val = None
				try:
					val = float(l[corres_indicatorid_column[k]].replace(",", "."))
					if val != 0:
						if k not in loaded_data:
							loaded_data[k] = {"vals": [], "msg": ""}
						msg = l[error_msg_colmumn]
						loaded_data[k]["vals"].append(val)
						loaded_data[k]["msg"] = msg
				except (IndexError, ValueError):
					pass

		calculated_values = [
			{
				"code": "syll_4s_amrcv",
				"components": [
					"syll_4s_amrcv_ba",
					"syll_4s_amrcv_de",
					"syll_4s_amrcv_go",
				],
				"op": "mean",
			},
			{
				"code": "syll_4s_amrccv",
				"components": ["syll_4s_amrccv_kla", "syll_4s_amrccv_tra"],
				"op": "mean",
			},
			{
				"code": "syll_4s_smrcv_amrcv",
				"components": ["syll_4s_smrcv_badego", "syll_4s_amrcv"],
				"op": "diff",
			},
			{
				"code": "syll_2s_amrcv",
				"components": [
					"syll_2s_amrcv_ba",
					"syll_2s_amrcv_de",
					"syll_2s_amrcv_go",
				],
				"op": "mean",
			},
			{
				"code": "syll_2s_amrccv",
				"components": ["syll_2s_amrccv_kla", "syll_2s_amrccv_tra"],
				"op": "mean",
			},
			{
				"code": "syll_2s_smrcv_amrcv",
				"components": ["syll_2s_smrcv_badego", "syll_2s_amrcv"],
				"op": "diff",
			},
			{
				"code": "phon_4s_amrcv",
				"components": [
					"phon_4s_amrcv_ba",
					"phon_4s_amrcv_de",
					"phon_4s_amrcv_go",
				],
				"op": "mean",
			},
			{
				"code": "phon_4s_amrccv",
				"components": ["phon_4s_amrccv_kla", "phon_4s_amrccv_tra"],
				"op": "mean",
			},
			{
				"code": "phon_4s_smrcv_amrcv",
				"components": ["phon_4s_smrcv_badego", "phon_4s_amrcv"],
				"op": "diff",
			},
			{
				"code": "phon_2s_amrcv",
				"components": [
					"phon_2s_amrcv_ba",
					"phon_2s_amrcv_de",
					"phon_2s_amrcv_go",
				],
				"op": "mean",
			},
			{
				"code": "phon_2s_amrccv",
				"components": ["phon_2s_amrccv_kla", "phon_2s_amrccv_tra"],
				"op": "mean",
			},
			{
				"code": "phon_2s_smrcv_amrcv",
				"components": ["phon_2s_smrcv_badego", "phon_2s_amrcv"],
				"op": "diff",
			},
			{
				"code": "proso_melodic_contrast",
				"components": ["laurieQ_temp", "laurieN_temp"],
				"op": "diff",
			},
		]

		for cv in calculated_values:
			possible = True
			vals = []
			res = None
			for el in cv["components"]:
				if el not in loaded_data:
					possible = False
				else:
					vals.append(loaded_data[el]["vals"][-1])
			if not possible:
				continue
			if cv["op"] == "sum":
				res = sum(vals)
			if cv["op"] == "mean":
				res = sum(vals) / (len(vals) * 1.0)
			if cv["op"] == "diff":
				res = vals[0] - vals[1]

			if possible and res is not None:
				loaded_data[cv["code"]] = {"vals": [res], "msg": ""}

		wnr = IndicatorReporting.get_words_not_recognized()
		if wnr is not None:
			loaded_data["words_not_recognized"] = {"vals": [wnr], "msg": ""}

		segerr = IndicatorReporting.get_segmental_errors()
		if segerr is not None:
			loaded_data["segmental_errors"] = {"vals": [segerr], "msg": ""}

		# devscore MARINA
		# Devs_intell
		ParticipantManager.get_participant_age(
			int(IndicatorReporting.session_date.split("_")[0])
		)
		# print(IndicatorReporting.getDevScoreIntell(age, loaded_data["words_not_recognized"]["vals"][0]))

		reset_made = False
		for k in list(corres_code_indicatorid.keys()):
			if k in loaded_data and len(loaded_data[k]["vals"]) > 0:
				if not reset_made:
					reset_made = True
					IndicatorReporting.reset_acoustic_values()
				val = loaded_data[k]["vals"][-1]
				error = None
				if loaded_data[k]["msg"] != "":
					error = loaded_data[k]["msg"]
				if (
					k == "mpt"
				):  # pour le maximum phonation time, on prends le meilleur resultat des deux
					val = max(loaded_data[k]["vals"])
				IndicatorReporting.save_acoustic_value(
					corres_code_indicatorid[k], val, error
				)
		IndicatorReporting.participant_result_db.commit()
		return True

	@staticmethod
	def calculate_indicator_percentiles():
		"""
		Calculates percentile rankings and z-scores for acoustic indicators based on participant data,
		using gender- and age-matched normative ranges from the `indicator_norm` table.

		Steps:
		- Queries results for each active indicator.
		- Matches participant’s results with applicable normative data by gender and age.
		- Computes:
			- Which percentile band the participant's value falls into.
			- Z-score (standard deviation from the mean), if normative stats are available.
		- Populates `IndicatorReporting.indicator_results` with dictionaries holding:
			- Indicator code and label
			- Raw value
			- Percentile ranking
			- Z-score
			- Unit
			- Any associated error

		This is essential for creating statistical reports or feedback dashboards.
		"""
		IndicatorReporting.indicator_results = None
		sql = (
			"select r.value, i.id, i.code, i.label, i.unit, n.*, r.erreur from indicator as i "
			"inner join indicator_norm as n "
			"on n.indicator_id = i.id and n.gender in ('a',?) and n.min_age <= ? and n.max_age >= ? "
			"left join results_acou as r on r.indicator_id = i.id and r.judge = ? and r.participant = ? "
			"and r.session = ? where i.active = 1 order by i.`group` ASC, i.`order` ASC"
		)

		gender = "f"
		if ParticipantManager.gender == "Homme":
			gender = "m"
		year = int(IndicatorReporting.session_date.split("_")[0])
		t = (
			gender,
			ParticipantManager.get_participant_age(year),
			ParticipantManager.get_participant_age(year),
			IndicatorReporting.judge_code,
			ParticipantManager.current_participant,
			IndicatorReporting.session_date,
		)
		tmp = IndicatorReporting.participant_result_db.execute(sql, t).fetchall()
		# print(repr(tmp))
		IndicatorReporting.indicator_results = []

		percentiles = [
			(1, "val_1st"),
			(5, "val_5th"),
			(10, "val_10th"),
			(50, "val_median"),
			(90, "val_90th"),
			(95, "val_95th"),
			(99, "val_99th"),
		]

		for line in tmp:
			# print(repr(tmp))
			zvalue = None
			val = line["value"]
			val_mean = line["val_mean"]
			val_sd = line["val_sd"]
			label = line["label"]
			erreur = line["erreur"]
			if label is None:
				label = line["code"]
			if val is not None:
				p_down = 0
				p_up = 100
				for p in percentiles:
					val_percentile = line[p[1]]
					if val >= val_percentile:
						p_down = p[0]
					else:
						p_up = p[0]
						break
				if val_mean is not None and val_sd is not None:
					zvalue = (val - val_mean) / val_sd * 1.0
			else:
				p_up = None
				p_down = None

			t = {
				"indicator_code": line["code"],
				"label": label,
				"value": val,
				"percentile_above": p_down,
				"percentile_below": p_up,
				"z-value": zvalue,
				"erreur": erreur,
				"unit": line["unit"],
			}
			IndicatorReporting.indicator_results.append(t)

	@staticmethod
	def generate_percentile_datafile(filename: str = None) -> bool:
		"""
		Generates and exports a tab-separated values (TSV) file containing acoustic indicator results,
		including percentiles and z-scores for a participant.

		If percentile data has not already been calculated, this method will trigger its calculation.

		Args:
			filename (str): The path of the output file to write. If None, no file will be written.

		Returns:
			bool: True if the file was successfully written; False if no filename was provided.
		"""
		if filename is None:
			return False
		data = [
			[
				"Participant",
				"Code",
				"Label",
				"Valeur",
				"Unite",
				"> Percentile",
				"< Percentile",
				"Z-value",
				"Erreur",
			]
		]

		if IndicatorReporting.indicator_results is None:
			IndicatorReporting.calculate_indicator_percentiles()

		for result in IndicatorReporting.indicator_results:
			dataline = [
				IndicatorReporting.participant_code,
				result["indicator_code"],
				result["label"],
				result["value"],
				result["unit"],
				result["percentile_above"],
				result["percentile_below"],
				result["z-value"],
				result["erreur"],
			]
			data.append(dataline)
		CSVManager.add_lines(filename, "\t", data)
		return True

	@staticmethod
	def generate_percentile_output(filename: str = None):
		"""
		Generate a visual percentile chart of acoustic indicator results as an image file.

		This function creates a graphical representation of the participant's acoustic
		indicator percentiles by drawing a grid with percentile columns and placing
		marks corresponding to each indicator's percentile range. It saves the image
		to the specified filename.

		The percentile columns include: 1st, 5th, 10th, 50th (median), 90th, 95th, and 99th percentiles.
		Each indicator is represented by a labeled row with a mark placed in the appropriate
		percentile column based on the indicator's value percentile.

		Args:
			filename (str, optional): Path to the output image file where the chart will be saved.
									Supported formats depend on the Pillow library (e.g., PNG, JPEG).
									If None, the function will raise an error or not save.

		Returns:
			bool: True if the image was successfully saved; False or raises Exception otherwise.

		Raises:
			Exception: Always raises Exception("deprecated function") currently.
		"""
		raise Exception("deprecated function")

		width = IndicatorReportingTools.get_total_width()
		height = IndicatorReportingTools.get_total_height(
			len(IndicatorReporting.indicator_results)
		)

		im = Image.new("RGB", (width, height), ImageColor.getrgb("#fff"))
		draw = ImageDraw.Draw(im)

		draw.text((0, 0), IndicatorReporting.get_participant_uniquestamp(), fill="#000")

		# drawing separations between percentile and percentile key
		headers = ["", "", "1", "5", "10", "50", "90", "95", "99"]
		wanted_column_color = [
			None,
			"#f55442",
			"#edd815",
			"#edd815",
			"#08bd6e",
			"#08bd6e",
			"#edd815",
			"#edd815",
			"#f55442",
		]
		for x in range(0, 9):
			start = IndicatorReportingTools.get_cell_coord(x, 0)
			end = IndicatorReportingTools.get_cell_coord(
				x, len(IndicatorReporting.indicator_results)
			)
			# drawing background color
			col = wanted_column_color[x]
			if col is not None:
				coord = (start[0][0], start[0][1], end[0][1], end[1][1])
				draw.rectangle(coord, fill=col)

			draw.text(
				(
					start[0][0] - 5,
					start[0][1] - IndicatorReportingTools.grid_y_offset + 5,
				),
				headers[x],
				fill="#000",
			)
		# 2 special headers
		tmp = IndicatorReportingTools.get_cell_center_coord(1, 0)
		draw.text((tmp[0] - 5, 5), "<1", fill="#000")
		tmp = IndicatorReportingTools.get_cell_center_coord(8, 0)
		draw.text((tmp[0] - 5, 5), ">99", fill="#000")

		# the results
		line = 0
		percentile_grid_x = {
			"0-1": 1,
			"1-5": 2,
			"5-10": 3,
			"10-50": 4,
			"50-90": 5,
			"90-95": 6,
			"95-99": 7,
			"99-100": 8,
		}
		for result in IndicatorReporting.indicator_results:
			have_result = True
			if result["percentile_above"] is None or result["percentile_below"] is None:
				have_result = False
			label = result["label"]
			label_cell = IndicatorReportingTools.get_cell_coord(0, line)
			if have_result:
				percentile_bracket = (
					str(result["percentile_above"])
					+ "-"
					+ str(result["percentile_below"])
				)
				cross_center = IndicatorReportingTools.get_cell_center_coord(
					percentile_grid_x[percentile_bracket], line
				)
				cross_size = 5
				# draw.line( (cross_center[0]-cross_size, cross_center[1]-cross_size, cross_center[0]+cross_size, cross_center[1]+cross_size), fill="#000" )
				# draw.line((cross_center[0] - cross_size, cross_center[1] + cross_size, cross_center[0] + cross_size,
				# cross_center[1] - cross_size), fill="#000")
				draw.ellipse(
					(
						cross_center[0] - cross_size,
						cross_center[1] - cross_size,
						cross_center[0] + cross_size,
						cross_center[1] + cross_size,
					),
					fill="#000",
				)
			else:
				grey_start = IndicatorReportingTools.get_cell_coord(1, line)
				draw.rectangle(
					(
						grey_start[0][0],
						grey_start[0][1] + 1,
						IndicatorReportingTools.get_total_width(),
						grey_start[1][1] - 1,
					),
					fill="#ccc",
				)
			draw.text((label_cell[0][0], label_cell[0][1] + 5), label, fill="#000")
			draw.line(
				(
					0,
					label_cell[1][1],
					IndicatorReportingTools.get_total_width(),
					label_cell[1][1],
				),
				fill="#000",
			)

			line += 1

		wanted_separators = [1, 2, 4, 6, 8]
		max_height = IndicatorReportingTools.get_total_height(
			len(IndicatorReporting.indicator_results)
		)
		for x in wanted_separators:
			start = IndicatorReportingTools.get_cell_coord(x, 0)
			draw.line((start[0][0], start[0][1], start[0][0], max_height), fill="#000")

		del draw
		# im.show()
		im.save(filename)
		return True

	@staticmethod
	def tostr():
		"""
		Print the label and percentile range for each indicator result.

		Outputs lines in the format:
			"<label> : <percentile_above>-<percentile_below>"

		Useful for quick console inspection of indicator percentile data.
		"""
		for el in IndicatorReporting.indicator_results:
			# print(el)
			print(
				el["label"]
				+ " : "
				+ str(el["percentile_above"])
				+ "-"
				+ str(el["percentile_below"])
			)

	@staticmethod
	def get_words_not_recognized():
		"""
		Retrieve the count of words not recognized by the participant during the session.

		Executes an SQL query to calculate:
			15 - SUM(score) from the results_int table
		filtered by participant, judge, and session.

		Returns:
			float: Number of words not recognized, if available.
			None: If no data found or on error.
		"""
		try:
			sql = """
			SELECT (15 - SUM(score)) as wnr
			FROM results_int
			WHERE participant = ? AND judge = ? AND session_path LIKE ? || '%'
			"""
			t = (
				ParticipantManager.current_participant,
				IndicatorReporting.judge_code,
				IndicatorReporting.session_date,
			)
			tmp = IndicatorReporting.participant_result_db.execute(sql, t).fetchall()
			val = tmp[0]["wnr"]
			if val is not None:
				return float(val)
			return None
		except Exception as e:
			logger.error(f"Error getting not recognized words: {e}")
			return None

	@staticmethod
	def get_segmental_errors():
		"""
		Retrieve the total count of segmental errors made by the participant during the session.

		Executes an SQL query counting entries in results_pw where `state` indicates an error.
		Filters by participant, judge, and session.

		Returns:
			int: Number of segmental errors, if available.
			None: If no data found or on error.
		"""
		try:
			sql = """
			SELECT IFNULL(`state`,0) as `state`,IFNULL(`ajout`,0) as `ajout`,IFNULL(`inversion`,0) as `inversion`
			FROM results_pw
			WHERE participant = ? AND judge = ? AND session_path LIKE ? || '%'
			"""

			t = (
				ParticipantManager.current_participant,
				IndicatorReporting.judge_code,
				IndicatorReporting.session_date,
			)
			tmp = IndicatorReporting.participant_result_db.execute(sql, t).fetchall()
			nb_errors = 0
			for segment in tmp:
				if (
					int(segment["state"]) == 1
					or int(segment["ajout"]) == 1
					or int(segment["inversion"]) == 1
				):
					nb_errors += 1
			return nb_errors
			# return None
		except Exception as e:
			logger.error(f"Error getting segmental errors: {e}")
			return None

	@staticmethod
	def calculate_composite_voice_devscore(values) -> dict:
		"""
		Calculate a composite voice deviance score from individual voice-related deviance scores.

		The composite score formula is:
			1/2 * [max(voice_a1, voice_a2) + max(voice_b, voice_c) + max(voice_d1, voice_d2)]

		Args:
			values (dict): Dictionary containing individual deviance scores with keys:
						"voice_a1", "voice_a2", "voice_b", "voice_c", "voice_d1", "voice_d2".

		Returns:
			dict: {
				"deviant_score_code": "voice",
				"label": "Voix",
				"score": float,  # composite deviance score
				"complete": bool # True if all required keys were present, False otherwise
			}
		"""
		vals = {
			"voice_a1": 0,
			"voice_a2": 0,
			"voice_b": 0,
			"voice_c": 0,
			"voice_d1": 0,
			"voice_d2": 0,
		}
		complete = True
		for key in vals.keys():
			if key in values:
				vals[key] = values[key]
			else:
				complete = False

		# composite
		# "voice" = 1/2*[max DevS (a1, a2) + max DevS (b, c) + max DevS (d1,d2)]
		tmp = max(vals["voice_a1"], vals["voice_a2"])
		tmp += max(vals["voice_b"], vals["voice_c"])
		tmp += max(vals["voice_d1"], vals["voice_d2"])
		tmp = tmp / 2.0
		t = {
			"deviant_score_code": "voice",
			"label": "Voix",
			"score": tmp,
			"complete": complete,
		}
		return t

	@staticmethod
	def calculate_composite_ddk_devscore(values) -> dict:
		"""
		Calculate a composite diadochokinesis (DDK) deviance score from individual DDK-related scores.

		The composite score formula is:
			1/2 * [max(ddk_cv_amr, ddk_cv_smr) + ddk_ccv_amr + ddk_smrcv_amrcv]

		Args:
			values (dict): Dictionary containing individual deviance scores with keys:
						"ddk_cv_amr", "ddk_cv_smr", "ddk_ccv_amr", "ddk_smrcv_amrcv".

		Returns:
			dict: {
				"deviant_score_code": "ddk_rate",
				"label": "Répétition Maximale",
				"score": float,  # composite deviance score
				"complete": bool # True if all required keys were present, False otherwise
			}
		"""
		vals = {
			"ddk_cv_amr": 0,
			"ddk_cv_smr": 0,
			"ddk_ccv_amr": 0,
			"ddk_smrcv_amrcv": 0,
		}
		complete = True
		for key in vals.keys():
			if key in values:
				vals[key] = values[key]
			else:
				complete = False

		# composite
		# "ddk_rate" = = 1/2*[DevS (A) + maximum DevS (B,C) + DevS D)]
		tmp = max(vals["ddk_cv_amr"], vals["ddk_cv_smr"])
		tmp += vals["ddk_ccv_amr"]
		tmp += vals["ddk_smrcv_amrcv"]
		tmp = tmp / 2.0
		t = {
			"deviant_score_code": "ddk_rate",
			"label": "Répétition Maximale",
			"score": tmp,
			"complete": complete,
		}
		return t

	@staticmethod
	def calculate_devscore_values():
		"""
		Calculate and populate the deviant score results for the current participant and session.

		This method performs the following steps:
		- Resets the devscore_results to None.
		- Executes an SQL query joining multiple tables to fetch deviant score values,
		filtered by participant, judge, session, age, and gender.
		- Converts the fetched results into a list of dictionaries with keys:
		'deviant_score_code', 'label', 'group_label', 'score', and 'complete'.
		- Calculates two composite deviant scores ('voice' and 'ddk_rate') using helper methods.
		- Calculates a global MonPaGe total deviant score by weighting and summing individual scores.
		- Appends all calculated scores to IndicatorReporting.devscore_results.

		Requires:
		- ParticipantManager.current_participant, ParticipantManager.gender, ParticipantManager.get_participant_age()
		- IndicatorReporting.judge_code, IndicatorReporting.session_date
		- IndicatorReporting.participant_result_db (database connection)

		Side Effects:
		- Updates IndicatorReporting.devscore_results with detailed and composite deviant scores.

		Returns:
		- None
		"""
		IndicatorReporting.devscore_results = None
		sql = (
			"select ds.code, ds.label, ds.sub_ds, ds.group_name, dsn.devscore_value "
			"from deviant_score as ds inner join indicator as i on i.code = ds.ref_indicator_code "
			"left outer join results_acou as ra on ra.indicator_id = i.id and ra.judge = ? and ra.participant = ? "
			"and ra.session = ?"
			"left outer join deviant_score_norm as dsn on dsn.deviant_score_id = ds.id and "
			"dsn.min_age <= ? and dsn.max_age >= ? and dsn.gender in ('a',?) "
			"and dsn.min_score < ra.`value` and dsn.max_score >= ra.`value` "
			"WHERE ds.active = 1 ORDER BY ds.`group`, ds.`order`"
		)

		gender = "f"
		if ParticipantManager.gender == "Homme":
			gender = "m"
		year = int(IndicatorReporting.session_date.split("_")[0])
		age = ParticipantManager.get_participant_age(year)
		t = (
			IndicatorReporting.judge_code,
			ParticipantManager.current_participant,
			IndicatorReporting.session_date,
			age,
			age,
			gender,
		)
		tmp = IndicatorReporting.participant_result_db.execute(sql, t).fetchall()
		# print(repr(tmp))
		IndicatorReporting.devscore_results = []
		for_composite = {}

		for line in tmp:
			val = line["devscore_value"]
			label = line["label"]
			if label is None:
				label = line["code"]
			if val is not None:
				val = int(val)
				for_composite[line["code"]] = val
			t = {
				"deviant_score_code": line["code"],
				"label": label,
				"group_label": line["group_name"],
				"score": val,
				"complete": True,
			}
			IndicatorReporting.devscore_results.append(t)
		# composite
		# "voice" = 1/2*[max DevS (a1, a2) + max DevS (b, c) + max DevS (d1,d2)]
		t_voice = IndicatorReporting.calculate_composite_voice_devscore(for_composite)
		IndicatorReporting.devscore_results.append(t_voice)
		for_composite[t_voice["deviant_score_code"]] = t_voice["score"]
		# "ddk_rate" = = 1/2*[DevS (A) + maximum DevS (B,C) + DevS D)]
		t_ddk = IndicatorReporting.calculate_composite_ddk_devscore(for_composite)
		IndicatorReporting.devscore_results.append(t_ddk)
		for_composite[t_ddk["deviant_score_code"]] = t_ddk["score"]

		# global score
		score = 0
		total = 0
		final = {
			"voice": 6,
			"intell": 4,
			"articulatory_accu": 4,
			"max_phon_time": 4,
			"speech_rate": 4,
			"proso_contrast": 4,
			"ddk_rate": 6,
		}
		for code in final:
			if code in for_composite:
				total += final[code]
				score += for_composite[code]
		t = {
			"deviant_score_code": "monpage_total",
			"label": "MonPaGe Total DevS",
			"score": score,
		}
		IndicatorReporting.devscore_results.append(t)
		t = {
			"deviant_score_code": "monpage_total_max",
			"label": "",
			"score": total,
		}
		IndicatorReporting.devscore_results.append(t)

	# print(repr(IndicatorReporting.devscore_results))

	@staticmethod
	def get_indicator_percentile_value(indicator_code: str = None) -> dict:
		"""
		Retrieve the percentile result dictionary for a specific indicator code.

		Args:
			indicator_code (str): The code of the indicator to look up.

		Returns:
			dict: The indicator result dictionary containing percentile information if found.
			None: If the indicator_code is None or no matching result is found.
		"""
		if IndicatorReporting.indicator_results is None:
			IndicatorReporting.calculate_indicator_percentiles()
		for res in IndicatorReporting.indicator_results:
			if res["indicator_code"] == indicator_code:
				return res
		return None

	@staticmethod
	def get_devscore_value(devscore_code: str = None) -> dict:
		"""
		Retrieve the deviant score result dictionary for a specific deviant score code.

		Args:
			devscore_code (str): The code of the deviant score to look up.

		Returns:
			dict: The deviant score result dictionary if found.
			None: If the devscore_code is None or no matching result is found.
		"""
		if IndicatorReporting.devscore_results is None:
			IndicatorReporting.calculate_devscore_values()
		for res in IndicatorReporting.devscore_results:
			if res["deviant_score_code"] == devscore_code:
				return res
		return None


#
# devs_intell = {
# 	(0, 74): {
# 		(-1, 1): 0,
# 		(1, 2): 1,
# 		(2, 5): 2,
# 		(5, 7): 3,
# 		(7, 100): 4
# 	},
# 	(75, 200): {
# 		(-1, 1.5): 0,
# 		(1.5, 3.5): 1,
# 		(3.5, 5): 2,
# 		(5, 7): 3,
# 		(7, 100): 4
# 	},
# }
# for age_tuple in devs_intell:
# 	if participant_age > age_tuple[0] and participant_age <= age_tuple[1]:
# 		for score_tuple in devs_intell[age_tuple]:
# 			if intell_score > score_tuple[0] and intell_score <= score_tuple[1]:
# 				return devs_intell[age_tuple][score_tuple]
# return None
