# encoding=utf-8
"""
Tools to help display with QT5
"""

__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 math
import os
from datetime import datetime
from os.path import join
from typing import Optional

from PySide6.QtCore import QPoint, QRect, QSize, Qt
from PySide6.QtGui import QFont, QFontMetrics, QGuiApplication, QIcon, QPainter, QPixmap
from PySide6.QtWidgets import (
	QApplication,
	QComboBox,
	QLabel,
	QMainWindow,
	QMessageBox,
	QPushButton,
	QRadioButton,
	QSpinBox,
	QWidget,
)

from tools.csv_manager import CSVManager
from tools.migration_tools import MigrationTools
from tools.options import Options
from tools.ui_tools import UITools

logger = logging.getLogger(__name__)


class DisplayTools(object):
	"""
	class that manages the display of text and pictures that fit their respective elements
	"""

	font_sizes: dict = {"default": 12}
	window_max_width: int = 800
	window_max_height: int = 600
	window_size_ratio: float = 1.0

	calculated_positions: dict = {}
	calculated_sizes: dict = {}

	allowed_43_resolutions = [
		(640, 480),
		(800, 600),
		(960, 720),
		(1024, 768),
		(1280, 960),
		(1400, 1050),
		(1440, 1080),
		(1600, 1200),
		(1856, 1392),
		(1920, 1440),
		(2048, 1536),
	]

	screen_width = None
	screen_height = None

	@staticmethod
	def load_display_config(size: int) -> bool:
		"""
		Load the file "display_config.csv" that contains font size for all elements
		:param size: The default size value that will be applied to display
		:return: True/False whether it managed to load the config file or not
		"""
		logger.info(f"Loading display config #{size}")
		DisplayTools.font_sizes = {"default": size}
		DisplayTools.csv_manager = CSVManager()
		if Options.is_enabled(Options.Option.REMAKE_FONT_SIZE):
			logger.info("remake font size mode")
			return False
		if not os.path.isfile("display_config.csv"):
			logger.error("File display_config.csv not found")
			return False

		values = CSVManager.read_file("display_config.csv", "\t", -1)
		for val in values:
			if val[0] not in DisplayTools.font_sizes:
				DisplayTools.font_sizes[val[0]] = {}
			DisplayTools.font_sizes[val[0]][val[1]] = int(val[2])
		return True

	@staticmethod
	def save_display_config():
		"""
		Saves the font size of all the elements in "config_display.csv"
		"""
		logger.info("Saving display config")
		save = []
		for prefix in list(DisplayTools.font_sizes.keys()):
			if prefix != "default":
				for key, value in DisplayTools.font_sizes[prefix].items():
					if key != "default":
						save.append([prefix, key, value])

		CSVManager.write_file("display_config.csv", "\t", save, "w+")
		logger.info("Writing file : display_config.csv")

	@staticmethod
	def get_font_size(ui_code: str) -> int:
		"""
		Get the font size of the element corresponding to the ui_code
		:param ui_code: the code of the element we want the font size of
		:return: the font size
		"""
		try:
			return DisplayTools.font_sizes[DisplayTools.get_window_size_code()][ui_code]
		except KeyError:
			return DisplayTools.font_sizes["default"]

	@staticmethod
	def has_saved_value(ui_code: str) -> bool:
		"""
		Checks if a given element has a stored font value in the config
		:param ui_code: the code of the element to check
		:return: True/False whether there is a corresponding value for this code or not
		"""
		prefix = DisplayTools.get_window_size_code()
		if ui_code is None:
			return False
		if prefix not in DisplayTools.font_sizes:
			return False
		return ui_code in DisplayTools.font_sizes[prefix]

	@staticmethod
	def calculate_ideal_font_size(
		element: QWidget,
		ui_code: str = None,
		wordwrap: bool = False,
		default_size: int = None,
	) -> int:
		"""
		Get the font size that will fit best the element
		:param element: the QT element that needs its font calculated
		:param ui_code: the ui code of the element, to check with previously saved values
		:param wordwrap: whether the element displays its text with wordwrap or not
		:param default_size: the size we are going to use for the element if it fits, or the size we'll start
		decreasing if it does not fit
		:return: the ideal font size we calculated to be sure it will be able to be fully displayed
		"""
		prefix = DisplayTools.get_window_size_code()
		if DisplayTools.has_saved_value(ui_code):
			return DisplayTools.get_font_size(ui_code)

		if default_size is None:
			default_size = DisplayTools.get_font_size("default")

		font = QFont()
		font.setPointSize(default_size)
		fit = False

		el_width = element.width()
		el_height = element.height()

		if isinstance(element, QPushButton):
			el_width -= 8
			el_height -= 4
			if not element.icon().isNull():
				el_width -= 4 + element.iconSize().width()
		elif isinstance(element, QRadioButton):
			el_width -= 25
		elif isinstance(element, QSpinBox):
			el_width -= 25

		while not fit and font.pointSize() > 1:
			fm = QFontMetrics(font)
			flags = Qt.AlignmentFlag.AlignLeft
			if wordwrap:
				flags = Qt.TextFlag.TextWordWrap | Qt.AlignmentFlag.AlignCenter
			bound = fm.boundingRect(0, 0, el_width, el_height, flags, element.text())

			if bound.width() <= el_width and bound.height() <= el_height:
				fit = True
			else:
				font.setPointSize(font.pointSize() - 1)

		if ui_code is not None:
			if prefix not in DisplayTools.font_sizes:
				DisplayTools.font_sizes[prefix] = {}
			DisplayTools.font_sizes[prefix][ui_code] = font.pointSize()

		return font.pointSize()

	@staticmethod
	def set_font_size(
		element: QWidget,
		ui_code: str = None,
		wordwrap: bool = False,
		default_size: int = None,
	):
		"""
		Set the font size for an element
		:param element: the element we want to apply font size to
		:param ui_code: the ui code of the element
		:param wordwrap: whether the element displays its text with wordwrap or not
		:param default_size: the size we are going to use for the element if it fits, or the size we'll start
		decreasing if it
		does not fit
		:return: None
		"""
		font = QFont()
		font.setPointSize(
			DisplayTools.calculate_ideal_font_size(
				element, ui_code, wordwrap, default_size
			)
		)
		element.setFont(font)

	@staticmethod
	def display_image(
		element: QLabel, image_filename: str, auto_resize: bool = True
	) -> bool:
		"""
		Display an image in a element
		:param element: The element in which we want to display an image
		:param image_filename: the path+filename of the image to display
		:param auto_resize: whether the image can be resized or not
		:return: True/False if we managed to load the file or not
		"""

		if not os.path.isfile(image_filename):
			element.setPixmap(QPixmap(None))
			return False
		else:
			element.setText("")
			normal_pixmap = QPixmap(image_filename)
			if auto_resize and (
				element.size().width() < normal_pixmap.size().width()
				or element.size().height() < normal_pixmap.size().height()
			):
				scaled_pixmap = normal_pixmap.scaled(
					element.size(),
					Qt.AspectRatioMode.KeepAspectRatio,
					Qt.TransformationMode.SmoothTransformation,
				)
				element.setPixmap(scaled_pixmap)
			else:
				element.setPixmap(normal_pixmap)

		"""
		col = QColor(normal_pixmap.toImage().pixel(1, 1))
		pal = copy.copy(element.parent().palette())
		pal.__init__()
		pal.setColor(QPalette.Background, col)
		element.parent().setPalette(pal)
		"""

		return True

	@staticmethod
	def prepare_icons(filename: str) -> list:
		"""
		Create two QICon from a filename. One normal, the other semi-transparent
		:param filename: the path+filename of the image to load
		:return: array of index 0: normal icon, 1: semi-transparent one
		"""
		if not os.path.isfile(filename):
			return [None, None]

		normal_map = QPixmap(filename)
		disabled_map = QPixmap(normal_map.size())
		disabled_map.fill(Qt.GlobalColor.transparent)
		painter = QPainter(disabled_map)
		painter.setOpacity(0.3)
		painter.drawImage(QPoint(0, 0), normal_map.toImage())
		painter.end()

		normal = QIcon(normal_map)
		disabled = QIcon(disabled_map)

		return [normal, disabled]

	@staticmethod
	def calculate_positions_xy(
		element_bg: QLabel, element_word: QWidget, lines: int, cols: int
	) -> dict:
		"""
		Calculate the top-left (x, y) pixel positions for placing elements
		within a grid layout over a background QLabel.

		The positions are computed to center each element_word widget inside
		each grid cell defined by the number of lines and columns, taking into
		account margins and padding.

		Args:
			element_bg (QLabel): The background label containing the pixmap.
			element_word (QWidget): The widget to be positioned within each cell.
			lines (int): Number of rows in the grid.
			cols (int): Number of columns in the grid.

		Returns:
			dict: A dictionary mapping (column, row) tuples (1-based) to
				(x, y) pixel coordinates for positioning the element_word.

		If an error occurs, logs the exception and returns an empty dictionary.
		"""
		try:
			ret = {}
			pixmap = element_bg.pixmap()
			bg_width = pixmap.size().width()
			bg_height = pixmap.size().height()
			width = element_bg.width()
			height = element_bg.height()
			margin_top = round(((height - bg_height) / 2) + element_bg.pos().y())
			margin_left = round(((width - bg_width) / 2) + element_bg.pos().x())
			cell_width = round(bg_width / cols)
			cell_height = round(bg_height / lines)

			cell_padding_x = round((cell_width - element_word.size().width()) / 2)
			cell_padding_y = round((cell_height - element_word.size().height()) / 2)
			for x in range(0, cols):
				for y in range(0, lines):
					ret[(x + 1, y + 1)] = (
						margin_left + cell_padding_x + (x * cell_width),
						margin_top + cell_padding_y + (y * cell_height),
					)

			return ret
		except Exception as e:
			UITools.error_log(e, "UITools.calculate_positions_xy()")
			return {}

	@staticmethod
	def get_participant_list(parent: Optional[QMainWindow] = None) -> list:
		"""
		Retrieves a list of participants from the data directory (./data).

		This method scans the participant directory (./data/participants), processes participant data files (./data/participants/<code>/data.csv),
		and handles any necessary file structure migrations. It returns a sorted list
		of participant information with each entry containing [code, sex, age].

		:param parent: Optional QMainWindow parent for displaying error dialogs
		:return: A sorted list of participant information with [code, sex, age] for each participant
		"""

		participant_list = []
		participant_dir = join("data", "participant")

		if os.path.isdir(participant_dir):
			participants_csv = join(participant_dir, "participants.csv")
			if os.path.isfile(participants_csv):
				MigrationTools.change_participant_structure()

			for dirname in os.listdir(participant_dir):
				participant_subdir = join(participant_dir, dirname)
				if not os.path.isdir(participant_subdir):
					continue

				csv_file = join(participant_subdir, "data.csv")
				new_csv_file = join(participant_subdir, f"data_{dirname}.csv")

				if os.path.isfile(csv_file):
					os.rename(
						csv_file, new_csv_file
					)  # 1.0.21 : Participant csv file now contains its code

				if not os.path.isfile(new_csv_file):
					participant_list.append([dirname, None, None])
					continue

				try:
					tmp = CSVManager.read_file(new_csv_file, "\t")
					participant_list.append(
						[
							dirname,
							tmp[2][0],
							str(datetime.now().year - int(tmp[1][0])),
						]
					)
				except (IndexError, ValueError):
					DisplayTools.open_dialog(
						parent,
						f"Problème avec {new_csv_file}",
						f"Impossible de comprendre le format du fichier csv pour {dirname}",
					)
					logger.error(f"Impossible de traiter le fichier {new_csv_file}")
					participant_list.append([dirname, None, None])

		return sorted(participant_list, key=lambda participant: participant[0])

	@staticmethod
	def generate_participant_dropdown(parent: Optional[QWidget] = None) -> QComboBox:
		"""
		Create and return a QComboBox widget filled with participant data.

		Args:
			parent (Optional[QWidget]): Optional parent widget for the QComboBox.

		Returns:
			QComboBox: The combo box populated with participants.
		"""
		participant_list = QComboBox(parent)
		DisplayTools.fill_participant_dropdown(participant_list)
		return participant_list

	@staticmethod
	def fill_participant_dropdown(participant_list: QComboBox):
		"""
		Populate a given QComboBox with participant codes, sex, and age.

		Participants missing sex or age will be marked with a warning message.

		Args:
			participant_list (QComboBox): The combo box to fill.

		Side effects:
			- Sets the current participant in ParticipantManager when selection changes.
			- Sets the default selected index to the last participant.
		"""
		from tools.participant_manager import ParticipantManager

		participants = DisplayTools.get_participant_list()
		for code, sex, age in participants:
			ParticipantManager.set_current_participant(code)
			if sex is None or age is None:
				participant_list.addItem(f"{code} - (! data.csv manquant !)", code)
				continue

			participant_list.addItem(f"{code} - ({sex} - {age} ans)", code)

		participant_list.setCurrentIndex(len(participants) - 1)

		def current_index_changed_callback(index: int):
			"""
			Slot called when the selected index of the participant dropdown changes.

			Updates the current participant in ParticipantManager based on the selected index.

			Args:
				index (int): The newly selected index in the dropdown.

			Notes:
				Safely handles out-of-range indices by ignoring IndexError exceptions.
			"""
			try:
				ParticipantManager.set_current_participant(
					DisplayTools.get_participant_list()[index][0]
				)
			except IndexError:
				pass

		participant_list.currentIndexChanged.connect(current_index_changed_callback)

	@staticmethod
	def open_dialog(
		parent: Optional[QMainWindow], title: str, text: str, modal: bool = False
	):
		"""
		Opens a message dialog with the specified title and text.

		The text is sanitized by removing tabs, newlines, extra spaces, and replacing
		'-NL-' placeholders with actual newlines.

		Args:
			parent (Optional[QMainWindow]): The parent window of the dialog.
			title (str): The title of the dialog window.
			text (str): The message text to display.
			modal (bool): Whether the dialog should be modal (default is False).
		"""
		tmp = QMessageBox(parent)
		tmp.setWindowTitle(title)
		tmp.setText(
			text.replace("\t", "")
			.replace("\n", "")
			.replace("  ", "")
			.replace("-NL-", "\n")
			.strip()
		)
		tmp.setModal(modal)
		tmp.open()

	@staticmethod
	def open_confirm_dialog(parent: QMainWindow, title: str, text: str) -> bool:
		"""
		Opens a confirmation dialog with Yes/No buttons and returns the user's choice.

		Args:
			parent (QMainWindow): The parent window of the dialog.
			title (str): The title of the confirmation dialog.
			text (str): The confirmation message to display.

		Returns:
			bool: True if the user clicks 'Yes', False otherwise.
		"""
		return (
			QMessageBox.question(
				parent,
				title,
				text,
				QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
				QMessageBox.StandardButton.No,
			)
			== QMessageBox.StandardButton.Yes
		)

	@staticmethod
	def take_screenshot(filename: str = None, dest_dir: str = "./tmp/"):
		"""
		Takes a screenshot of the primary screen and saves it as a PNG file.

		Args:
			filename (str, optional): Name of the file to save the screenshot as.
				If None, a timestamp-based filename will be generated.
			dest_dir (str): Directory where the screenshot will be saved.
				Defaults to './tmp/'.

		Creates the destination directory if it does not exist.
		"""
		screen = QGuiApplication.primaryScreen()
		original_pixmap = screen.grabWindow()
		if filename is None:
			tstamp = (
				datetime.now()
				.isoformat(timespec="microseconds")
				.replace("-", "")
				.replace(":", "")
				.replace(".", "")
			)
			filename = tstamp + ".png"
		if not os.path.isdir(dest_dir):
			os.mkdir(dest_dir)
		original_pixmap.save(dest_dir + filename, "png")

	@staticmethod
	def init_window_size(*args):
		"""
		Initializes window size scaling parameters based on input dimensions.

		Accepts either:
		- Two integer arguments (width, height), or
		- One QWidget or window object with width() and height() methods.

		Sets scaling ratio and max width/height relative to a base size of 800x600.
		"""
		if len(args) == 2:
			w = args[0]
			h = args[1]
		elif len(args) == 1:
			w = args[0].width()
			h = args[0].height()
		else:
			return
		rw = w / 800.0
		rh = h / 600.0
		ratio = max(rw, rh)
		DisplayTools.window_size_ratio = ratio
		DisplayTools.window_max_width = math.floor(800 * ratio)
		DisplayTools.window_max_height = math.floor(600 * ratio)

	@staticmethod
	def get_window_size() -> QSize:
		"""
		Returns the maximum window size as a QSize object, calculated based on scaling.

		Returns:
			QSize: The window max width and height.
		"""
		return QSize(DisplayTools.window_max_width, DisplayTools.window_max_height)

	@staticmethod
	def get_window_size_code() -> str:
		"""
		Returns the window size formatted as a string "width_height".

		Returns:
			str: A string representation of window max width and height separated by underscore.
		"""
		return (
			str(DisplayTools.window_max_width)
			+ "_"
			+ str(DisplayTools.window_max_height)
		)

	@staticmethod
	def __get_value_for_ratio__(ratio: float, total: int) -> int:
		"""
		Calculates the scaled integer value for a given ratio and total size.

		Args:
			ratio (float): Scaling ratio.
			total (int): Total size to scale.

		Returns:
			int: Scaled value, floored to the nearest integer.
		"""
		return math.floor(ratio * total)

	@staticmethod
	def __get_value__(val: int) -> int:
		"""
		Scales an integer value by the current window size ratio.

		Args:
			val (int): Value to scale.

		Returns:
			int: Scaled value, floored to nearest integer.
		"""
		return math.floor(DisplayTools.window_size_ratio * val)

	@staticmethod
	def y2(ratio: float) -> int:
		"""
		Scales a vertical (Y) ratio to an absolute pixel value based on window max height.

		Args:
			ratio (float): Ratio of height (0 to 1).

		Returns:
			int: Scaled Y position in pixels.
		"""
		return DisplayTools.__get_value_for_ratio__(
			ratio, DisplayTools.window_max_height
		)

	@staticmethod
	def x2(ratio: float) -> int:
		"""
		Scales a horizontal (X) ratio to an absolute pixel value based on window max width.

		Args:
			ratio (float): Ratio of width (0 to 1).

		Returns:
			int: Scaled X position in pixels.
		"""
		return DisplayTools.__get_value_for_ratio__(
			ratio, DisplayTools.window_max_width
		)

	@staticmethod
	def w2(ratio: float) -> int:
		"""
		Scales a width ratio to an absolute pixel width based on window max width.

		Args:
			ratio (float): Ratio of width (0 to 1).

		Returns:
			int: Scaled width in pixels.
		"""
		return DisplayTools.__get_value_for_ratio__(
			ratio, DisplayTools.window_max_width
		)

	@staticmethod
	def h2(ratio: float) -> int:
		"""
		Scales a height ratio to an absolute pixel height based on window max height.

		Args:
			ratio (float): Ratio of height (0 to 1).

		Returns:
			int: Scaled height in pixels.
		"""
		return DisplayTools.__get_value_for_ratio__(
			ratio, DisplayTools.window_max_height
		)

	@staticmethod
	def y(y: int) -> int:
		"""
		Scales a vertical pixel value by the current window size ratio.

		Args:
			y (int): Original vertical pixel value.

		Returns:
			int: Scaled vertical pixel value.
		"""
		return DisplayTools.__get_value__(y)

	@staticmethod
	def x(x: int) -> int:
		"""
		Scales a horizontal pixel value by the current window size ratio.

		Args:
			x (int): Original horizontal pixel value.

		Returns:
			int: Scaled horizontal pixel value.
		"""
		return DisplayTools.__get_value__(x)

	@staticmethod
	def w(w: int) -> int:
		"""
		Scales a width pixel value by the current window size ratio.

		Args:
			w (int): Original width pixel value.

		Returns:
			int: Scaled width pixel value.
		"""
		return DisplayTools.__get_value__(w)

	@staticmethod
	def h(h: int) -> int:
		"""
		Scales a height pixel value by the current window size ratio.

		Args:
			h (int): Original height pixel value.

		Returns:
			int: Scaled height pixel value.
		"""
		return DisplayTools.__get_value__(h)

	@staticmethod
	def coord2(xratio: float, yratio: float) -> QPoint:
		"""
		Converts normalized coordinates (ratios) to a QPoint with scaled pixel positions.

		Uses a cache to avoid recalculating points for previously requested ratios.

		Args:
			xratio (float): Horizontal ratio (0 to 1).
			yratio (float): Vertical ratio (0 to 1).

		Returns:
			QPoint: The scaled QPoint corresponding to the given ratios.
		"""
		t = (xratio, yratio)
		if t not in DisplayTools.calculated_positions:
			DisplayTools.calculated_positions[t] = QPoint(
				DisplayTools.x2(xratio), DisplayTools.y2(yratio)
			)
		return DisplayTools.calculated_positions[t]

	@staticmethod
	def size2(wratio: float, hratio: float) -> QSize:
		"""
		Converts normalized width and height ratios to a QSize with scaled dimensions.

		Uses a cache to avoid recalculating sizes for previously requested ratio pairs.

		Args:
			wratio (float): Width ratio (0 to 1).
			hratio (float): Height ratio (0 to 1).

		Returns:
			QSize: The scaled QSize corresponding to the given width and height ratios.
		"""
		t = (wratio, hratio)
		if t not in DisplayTools.calculated_sizes:
			DisplayTools.calculated_sizes[t] = QSize(
				DisplayTools.w2(wratio), DisplayTools.h2(hratio)
			)
		return DisplayTools.calculated_sizes[t]

	@staticmethod
	def coord(x: int, y: int) -> QPoint:
		"""
		Convert (x,y) coord based on 800*600 display to current size
		:param x:
		:param y:
		:return:
		"""
		t = (x, y)
		if t not in DisplayTools.calculated_positions:
			DisplayTools.calculated_positions[t] = QPoint(
				DisplayTools.x(x), DisplayTools.y(y)
			)
		return DisplayTools.calculated_positions[t]

	@staticmethod
	def size(w: int, h: int) -> QSize:
		"""
		Returns a cached QSize object scaled by the current window size ratio.

		Args:
			w (int): Width in pixels.
			h (int): Height in pixels.

		Returns:
			QSize: The scaled QSize corresponding to the input width and height.
		"""
		t = (w, h)
		if t not in DisplayTools.calculated_sizes:
			DisplayTools.calculated_sizes[t] = QSize(
				DisplayTools.w(w), DisplayTools.h(h)
			)
		return DisplayTools.calculated_sizes[t]

	@staticmethod
	def calculate_screen_size():
		"""
		Calculates and stores the available screen width and height.

		Uses QGuiApplication to get the primary screen's available geometry.
		Defaults to 640x480 if QApplication instance is not found.
		"""
		logger.info("Calculating screen size")
		screen = QGuiApplication.primaryScreen()
		app = QApplication.instance()
		if app is None:
			logger.warning("QApplication instance not found")
			DisplayTools.screen_width = 640
			DisplayTools.screen_height = 480
		else:
			rect = screen.availableGeometry()
			DisplayTools.screen_width = rect.width()
			DisplayTools.screen_height = rect.height()

		logger.info(
			f"Using screen size {DisplayTools.screen_width}x{DisplayTools.screen_height}"
		)

	@staticmethod
	def calculate_maximum_possible_resolution():
		"""
		Determines the maximum allowed resolution fitting within the current screen size.

		Iterates over allowed 4:3 resolutions and picks the largest resolution
		that fits inside the detected screen size. Initializes window size accordingly.
		"""
		logger.info("Calculating maximum possible resolution")
		DisplayTools.calculate_screen_size()
		ret = QSize(
			DisplayTools.allowed_43_resolutions[0][0],
			DisplayTools.allowed_43_resolutions[0][1],
		)
		for s in DisplayTools.allowed_43_resolutions:
			if s[0] <= DisplayTools.screen_width and s[1] <= DisplayTools.screen_height:
				ret = QSize(s[0], s[1])
			else:
				break
		logger.info(
			f"Taille d'ecran detecte: ({DisplayTools.screen_width}, {DisplayTools.screen_height}) - resolution choisie: ({ret.width()}, {ret.height()})",
		)
		logger.info(f"Using resolution {ret.width()}x{ret.height()}")
		DisplayTools.init_window_size(ret)

	@staticmethod
	def get_window_display_rect() -> QRect:
		"""
		Calculates and returns a QRect representing the centered display rectangle
		for the application window based on the screen size and the maximum window size.

		The rectangle is centered by computing margins on the X and Y axes.

		Returns:
			QRect: The rectangle specifying the position and size of the window
				centered on the screen.
		"""
		x_margin = math.floor(
			(DisplayTools.screen_width - DisplayTools.window_max_width) / 2.0
		)
		y_margin = math.floor(
			(DisplayTools.screen_height - DisplayTools.window_max_height) / 2.0
		)

		return QRect(
			x_margin,
			y_margin,
			DisplayTools.window_max_width,
			DisplayTools.window_max_height,
		)
