"""NormCap window.

Supposed to show a screenshot of the current desktop in full screen, on which the
user can draw a selection rectangle.

One instance (single-display) or multiple instances (multi-display) might be created.

On Linux's Wayland DE, where the compositor and not the application is supposed to
control the window's position, some platform specific workarounds are implemented to
nevertheless display potentially multiple windows in fullscreen on multiple displays.
"""

import logging
from collections.abc import Callable
from dataclasses import dataclass
from typing import cast

from PySide6 import QtCore, QtGui, QtWidgets

from normcap import positioning
from normcap.gui.menu_button import MenuButton
from normcap.gui.settings import Settings
from normcap.system import info
from normcap.system.models import DesktopEnvironment, Rect, Screen

logger = logging.getLogger(__name__)


@dataclass
class DebugInfo:
    screen: Screen | None = None
    window: QtWidgets.QMainWindow | None = None
    scale_factor: float = 1


class UiContainerLabel(QtWidgets.QLabel):
    """Widget to draw border, selection rectangle and potentially debug infos."""

    def __init__(
        self,
        parent: QtWidgets.QWidget,
        color: QtGui.QColor,
        parse_text_func: Callable,
    ) -> None:
        super().__init__(parent)

        self.color: QtGui.QColor = color

        self.debug_info: DebugInfo | None = None

        self.rect: QtCore.QRect = QtCore.QRect()
        self.rect_pen = QtGui.QPen(self.color, 2, QtCore.Qt.PenStyle.DashLine)
        self.get_parse_text = parse_text_func

        self.setObjectName("ui_container")
        self.setStyleSheet(f"#ui_container {{border: 3px solid {self.color.name()};}}")
        self.setCursor(QtCore.Qt.CursorShape.CrossCursor)
        self.setScaledContents(True)

    def _draw_debug_infos(self, painter: QtGui.QPainter, rect: QtCore.QRect) -> None:
        """Draw debug information to top left."""
        if (
            not self.debug_info
            or not self.debug_info.screen
            or not self.debug_info.screen.screenshot
            or not self.debug_info.window
        ):
            return

        selection = Rect(*cast(tuple, rect.normalized().getCoords()))
        selection_scaled = selection.scale(self.debug_info.scale_factor)

        lines = (
            "[ Screen ]",
            f"Size: {self.debug_info.screen.size}",
            f"Position: {self.debug_info.screen.coords}",
            f"Device pixel ratio: {self.debug_info.screen.device_pixel_ratio}",
            "",
            "[ Window ]",
            f"Size: {self.debug_info.window.size().toTuple()}",
            f"Position: {cast(tuple, self.debug_info.window.geometry().getCoords())}",
            f"Device pixel ratio: {self.debug_info.window.devicePixelRatio()}",
            f"Selected region: {selection.coords}",
            "",
            "[ Screenshot ]",
            f"Size: {self.debug_info.screen.screenshot.size().toTuple()}",
            f"Selected region (scaled): {selection_scaled.coords}",
            "",
            "[ Scaling detected ]",
            f"Factor: {self.debug_info.scale_factor:.2f}",
        )

        painter.setPen(QtGui.QColor(0, 0, 0, 0))
        painter.setBrush(QtGui.QColor(0, 0, 0, 175))
        painter.drawRect(3, 3, 300, 20 * len(lines) + 5)
        painter.setBrush(QtGui.QColor(0, 0, 0, 0))

        painter.setPen(self.color)
        painter.setFont(QtGui.QFont(QtGui.QFont().family(), 10, 600))
        for idx, line in enumerate(lines):
            painter.drawText(10, 20 * (idx + 1), line)

    def paintEvent(self, event: QtGui.QPaintEvent) -> None:  # noqa: N802
        """Draw selection rectangle and mode indicator icon."""
        super().paintEvent(event)

        if not (self.rect or self.debug_info):
            return

        painter = QtGui.QPainter(self)
        self.rect = self.rect.normalized()

        if self.debug_info:
            self._draw_debug_infos(painter, self.rect)

        if not self.rect:
            return

        painter.setPen(self.rect_pen)
        painter.drawRect(self.rect)

        if self.get_parse_text():
            selection_icon = QtGui.QIcon(":parse")
        else:
            selection_icon = QtGui.QIcon(":raw")
        selection_icon.paint(
            painter, self.rect.right() - 24, self.rect.top() - 30, 24, 24
        )

        painter.end()


class Communicate(QtCore.QObject):
    """Window's communication bus."""

    on_esc_key_pressed = QtCore.Signal()
    on_region_selected = QtCore.Signal(Rect, int)


class Window(QtWidgets.QMainWindow):
    """Provide fullscreen UI for interacting with NormCap."""

    def __init__(
        self,
        screen: Screen,
        index: int,
        settings: Settings,
        installed_languages: list[str],
        debug_language_manager: bool = False,
    ) -> None:
        """Initialize window."""
        super().__init__()
        logger.debug("Create window for screen %s", screen.index)

        self.screen_ = screen
        self.index = index
        self.settings = settings
        self.installed_languages = installed_languages
        self.debug_language_manager = debug_language_manager

        self.com = Communicate(parent=self)
        self.color: QtGui.QColor = QtGui.QColor(str(settings.value("color")))

        self.setWindowTitle(f"NormCap [{screen.index}]")
        self.setWindowIcon(QtGui.QIcon(":normcap"))
        self.setWindowFlags(
            QtGui.Qt.WindowType.FramelessWindowHint
            | QtGui.Qt.WindowType.CustomizeWindowHint
            | QtGui.Qt.WindowType.WindowStaysOnTopHint
        )
        self.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus)
        self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose)
        self.setAnimated(False)
        self.setEnabled(True)

        self.selection_rect: QtCore.QRect = QtCore.QRect()

        self.image_container = QtWidgets.QLabel(scaledContents=True)
        self.setCentralWidget(self.image_container)
        self.ui_container = self._create_ui_container(
            geometry=self.image_container.geometry()
        )

        self.menu_button = None

        if self.index == 0:
            self.menu_button = self._create_menu_button()
            layout = self._create_layout()
            layout.addWidget(self.menu_button, 0, 1)
            self.ui_container.setLayout(layout)

    def _get_scale_factor(self) -> float:
        """Calculate scale factor from image and screen dimensions."""
        if not self.screen_.screenshot:
            raise ValueError("Screenshot image is missing!")
        return self.screen_.screenshot.width() / self.width()

    def _create_ui_container(self, geometry: QtCore.QRect) -> UiContainerLabel:
        """Add widget for showing selection rectangle and settings button."""
        ui_container = UiContainerLabel(
            parent=self,
            color=self.color,
            parse_text_func=lambda: bool(self.settings.value("parse-text", type=bool)),
        )

        if logger.getEffectiveLevel() is logging.DEBUG:
            ui_container.debug_info = DebugInfo(
                scale_factor=self._get_scale_factor(), screen=self.screen_, window=self
            )

        ui_container.color = self.color
        ui_container.setGeometry(geometry)
        ui_container.raise_()
        return ui_container

    def _draw_background_image(self) -> None:
        """Draw screenshot as background image."""
        pixmap = QtGui.QPixmap()
        pixmap.convertFromImage(self.screen_.screenshot)
        self.image_container.setPixmap(pixmap)

    def _create_menu_button(self) -> QtWidgets.QWidget:
        menu_button = MenuButton(
            settings=self.settings,
            show_language_manager=self.debug_language_manager or info.is_packaged(),
            installed_languages=self.installed_languages,
        )
        return menu_button

    @staticmethod
    def _create_layout() -> QtWidgets.QGridLayout:
        layout = QtWidgets.QGridLayout()
        layout.setContentsMargins(26, 26, 26, 26)
        layout.setRowStretch(1, 1)
        layout.setColumnStretch(0, 1)
        return layout

    def set_fullscreen(self) -> None:
        """Set window to full screen using platform specific methods."""
        # TODO: Test in Multi Display setups with different scaling
        # TODO: Test in Multi Display setups with different position
        # TODO: Position in Multi Display probably problematic!
        logger.debug("Set window of screen %s to fullscreen", self.screen_.index)

        if not self.screen_.screenshot:
            raise ValueError(f"Screenshot is missing on screen {self.screen_}")

        # Using scaled window dims to fit sizing with dpr in case scaling is enabled
        # See: https://github.com/dynobo/normcap/issues/397
        if (
            info.display_manager_is_wayland()
            and self.screen_.size == self.screen_.screenshot.size().toTuple()
            and self.screen_.device_pixel_ratio != 1
        ):
            # TODO: Check if still necessary on latest supported Ubuntu.
            # If not, remove Screen.scale() and this condition.
            self.setGeometry(*self.screen_.scale().geometry)
        else:
            self.setGeometry(*self.screen_.geometry)

        if info.desktop_environment() != DesktopEnvironment.UNITY:
            # On some DEs, setting a fixed window size can enforce the correct size.
            # However, on Unity, it breaks the full screen view.
            self.setMinimumSize(self.geometry().size())
            self.setMaximumSize(self.geometry().size())

        if info.display_manager_is_wayland():
            # For unknown reason .showFullScreen() on Ubuntu 24.04 does not show the
            # window. Showing the Window in normal state upfront seems to help.
            # (It seems like .setWindowState(WindowFullScreen) should not be set before
            # .setVisible(True) on that system. Might be a QT bug.)
            self.show()

        self.showFullScreen()
        self.setFocus()

        # On Wayland, setting geometry doesn't move the window to the right screen, as
        # only the compositor is allowed to do this. That's why in case of multi-display
        # setups, we need to use hacks to position the window:
        if info.display_manager_is_wayland():
            # The delay should ensure window is active & registered in window manager.
            QtCore.QTimer.singleShot(
                20, lambda: positioning.move(window=self, screen=self.screen_)
            )

    def clear_selection(self) -> None:
        self.selection_rect = QtCore.QRect()
        self.ui_container.rect = self.selection_rect
        self.update()

    def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:  # noqa: N802
        """Handle ESC key pressed.

        Cancels the selection progress (if ongoing), otherwise emit signal.
        """
        super().keyPressEvent(event)
        if event.key() == QtCore.Qt.Key.Key_Escape:
            if self.selection_rect:
                self.clear_selection()
            else:
                self.com.on_esc_key_pressed.emit()

    def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:  # noqa: N802
        """Handle left mouse button clicked.

        Creates a new selection rectangle with both points set to pointer position.
        Note that the points "topLeft" and "bottomRight" are more like start and end
        points and not necessarily relate to the geometrical position.

        The selection rectangle will be normalized just before emitting the signal in
        the mouseReleaseEvent.
        """
        super().mousePressEvent(event)
        if event.button() == QtCore.Qt.MouseButton.LeftButton:
            self.selection_rect = QtCore.QRect()
            self.selection_rect.setTopLeft(event.position().toPoint())
            self.selection_rect.setBottomRight(event.position().toPoint())
            self.ui_container.rect = self.selection_rect
            self.update()

    def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:  # noqa: N802
        """Update position of bottom right point of selection rectangle."""
        super().mouseMoveEvent(event)
        if self.selection_rect:
            self.selection_rect.setBottomRight(event.position().toPoint())
            self.ui_container.rect = self.selection_rect
            self.update()

    def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None:  # noqa: N802
        """Start OCR workflow on left mouse button release."""
        super().mouseReleaseEvent(event)
        if (
            event.button() != QtCore.Qt.MouseButton.LeftButton
            or not self.selection_rect
        ):
            return

        self.selection_rect.setBottomRight(event.position().toPoint())

        selection_coords = cast(tuple, self.selection_rect.normalized().getCoords())
        scaled_selection_rect = Rect(*selection_coords).scale(self._get_scale_factor())

        self.clear_selection()

        # Emit as last action, cause self might get destroyed by the slots
        self.com.on_region_selected.emit(scaled_selection_rect, self.screen_.index)

    def resizeEvent(self, event: QtGui.QResizeEvent) -> None:  # noqa: N802
        """Adjust child widget on resize."""
        super().resizeEvent(event)
        self.ui_container.resize(self.size())
        if self.ui_container.debug_info:
            self.ui_container.debug_info.scale_factor = self._get_scale_factor()

    def showEvent(self, event: QtGui.QShowEvent) -> None:  # noqa: N802
        """Update background image on show/reshow."""
        super().showEvent(event)
        self._draw_background_image()
