#!/usr/bin/env python3
import os
import signal
import sys

from PyQt5.QtCore import QObject, QSettings, Qt, QTimer
from PyQt5.QtGui import QCursor, QIcon, QPainter, QPixmap
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
from PyQt5.QtWidgets import (
    QAction,
    QApplication,
    QButtonGroup,
    QDialog,
    QFileDialog,
    QHBoxLayout,
    QLabel,
    QMenu,
    QMessageBox,
    QPushButton,
    QRadioButton,
    QSlider,
    QSystemTrayIcon,
    QVBoxLayout,
)

# Имя сервера для проверки запущенных экземпляров
SERVER_NAME = "teacher_pointer_instance"

# Путь к изображению указки по умолчанию
DEFAULT_IMAGE = "/usr/share/teacher-pointer/img/pointer.png"

# Размер области предпросмотра в настройках
PREVIEW_SIZE = 220

# Максимальный размер изображения (по большей стороне)
MAX_IMAGE_SIZE = 800


class SettingsDialog(QDialog):
    """Диалоговое окно настроек программы"""

    def __init__(
        self, parent, current_offset, current_image_path, current_scale
    ):
        """
        Инициализация диалога настроек

        Args:
            parent: Родительское окно
            current_offset: Текущее расстояние от курсора
            current_image_path: Текущий путь к изображению указки
            current_scale: Текущий масштаб изображения
        """
        super().__init__(parent)
        self.setWindowTitle("Настройки указки")
        self.setFixedWidth(500)

        # Объект для работы с настройками
        self.settings = QSettings("TeacherPointer", "App")

        # Сохраняем переданные значения
        self.selected_image = current_image_path
        self.current_scale = current_scale

        # Основной макет диалога
        main_layout = QHBoxLayout(self)

        # Левый столбец (настройки)
        left_layout = QVBoxLayout()

        # Настройка расстояния от курсора
        offset_layout = QVBoxLayout()
        offset_layout.addWidget(QLabel("Расстояние от курсора:"))
        self.offset_value_label = QLabel(str(current_offset))
        offset_layout.addWidget(self.offset_value_label)

        # Слайдер для регулировки расстояния
        self.offset_slider = QSlider(Qt.Horizontal)
        self.offset_slider.setRange(8, 30)
        self.offset_slider.setValue(current_offset)
        self.offset_slider.valueChanged.connect(
            lambda val: self.offset_value_label.setText(str(val))
        )
        offset_layout.addWidget(self.offset_slider)
        left_layout.addLayout(offset_layout)

        # Настройка масштаба изображения
        scale_layout = QVBoxLayout()
        scale_layout.addWidget(QLabel("Масштаб изображения:"))
        self.scale_value_label = QLabel(f"{current_scale}%")
        scale_layout.addWidget(self.scale_value_label)

        # Слайдер для регулировки масштаба
        self.scale_slider = QSlider(Qt.Horizontal)
        self.scale_slider.setRange(10, 100)
        self.scale_slider.setValue(current_scale)
        self.scale_slider.valueChanged.connect(self.update_scale_label)
        scale_layout.addWidget(self.scale_slider)
        left_layout.addLayout(scale_layout)

        # Выбор изображения указки
        self.default_radio = QRadioButton("Стандартное изображение")
        self.custom_radio = QRadioButton("Своё изображение")
        self.group = QButtonGroup(self)
        self.group.addButton(self.default_radio)
        self.group.addButton(self.custom_radio)
        left_layout.addWidget(self.default_radio)
        left_layout.addWidget(self.custom_radio)

        # Кнопка выбора файла и отображение пути
        browse_layout = QHBoxLayout()
        self.browse_btn = QPushButton("Выбрать файл...")
        self.browse_btn.clicked.connect(self.browse_image)
        self.path_label = QLabel(
            os.path.basename(current_image_path) if current_image_path else ""
        )
        browse_layout.addWidget(self.browse_btn)
        browse_layout.addWidget(self.path_label)
        left_layout.addLayout(browse_layout)

        # Кнопки управления диалогом
        btn_layout = QHBoxLayout()
        self.apply_btn = QPushButton("Применить")
        self.apply_btn.clicked.connect(self.apply)
        self.cancel_btn = QPushButton("Отмена")
        self.cancel_btn.clicked.connect(self.reject)
        btn_layout.addWidget(self.apply_btn)
        btn_layout.addWidget(self.cancel_btn)
        left_layout.addLayout(btn_layout)

        # Правая часть - предпросмотр изображения
        self.preview_label = QLabel()
        self.preview_label.setFixedSize(PREVIEW_SIZE, PREVIEW_SIZE)
        self.preview_label.setFrameShape(QLabel.Box)
        self.preview_label.setAlignment(Qt.AlignCenter)
        main_layout.addLayout(left_layout)
        main_layout.addWidget(self.preview_label)

        # Инициализация состояния
        if current_image_path and current_image_path != DEFAULT_IMAGE:
            self.custom_radio.setChecked(True)
        else:
            self.default_radio.setChecked(True)

        # Обработчики событий
        self.group.buttonClicked.connect(lambda: self.update_browse_state())
        self.update_browse_state()
        self.load_preview(self.selected_image)

    def update_scale_label(self, value):
        """Обновление метки с текущим масштабом"""
        self.scale_value_label.setText(f"{value}%")
        self.load_preview(self.selected_image)

    def update_browse_state(self):
        """Обновление состояния кнопки выбора файла"""
        enabled = self.custom_radio.isChecked()
        self.browse_btn.setEnabled(enabled)
        img = self.selected_image if enabled else DEFAULT_IMAGE
        self.path_label.setText(os.path.basename(img))
        self.load_preview(img)

    def browse_image(self):
        """Открытие диалога выбора файла изображения"""
        path, _ = QFileDialog.getOpenFileName(
            self, "Выберите PNG-изображение", "", "PNG Files (*.png)"
        )
        if path:
            # Проверка формата изображения
            pix = QPixmap(path)
            if pix.isNull() or not pix.toImage().hasAlphaChannel():
                QMessageBox.critical(
                    self,
                    "Ошибка",
                    "Выбранный файл не является "
                    "корректным PNG с альфа-каналом.",
                )
                return

            # Проверка размера изображения
            if pix.width() > MAX_IMAGE_SIZE or pix.height() > MAX_IMAGE_SIZE:
                QMessageBox.information(
                    self,
                    "Большое изображение",
                    f"Выбранное изображение ({pix.width()}x{pix.height()}) "
                    f"превышает максимальный размер ({MAX_IMAGE_SIZE}px).\n"
                    "Оно будет автоматически уменьшено при отображении.",
                )

            self.selected_image = path
            self.path_label.setText(os.path.basename(path))
            self.load_preview(path)

    def load_preview(self, path):
        """Загрузка и отображение превью изображения"""
        # Проверка существования файла
        if not os.path.isfile(path):
            path = DEFAULT_IMAGE

        pix = QPixmap(path)
        if pix.isNull():
            pix = QPixmap(DEFAULT_IMAGE)

        # Применение масштаба к превью
        scale_factor = self.scale_slider.value() / 100.0
        scaled_pix = pix.scaled(
            int(PREVIEW_SIZE * scale_factor),
            int(PREVIEW_SIZE * scale_factor),
            Qt.KeepAspectRatio,
            Qt.SmoothTransformation,
        )

        # Центрирование масштабированного изображения
        result_pix = QPixmap(PREVIEW_SIZE, PREVIEW_SIZE)
        result_pix.fill(Qt.transparent)

        # Отрисовка изображения в центре области предпросмотра
        painter = QPainter(result_pix)
        painter.drawPixmap(
            (PREVIEW_SIZE - scaled_pix.width()) // 2,
            (PREVIEW_SIZE - scaled_pix.height()) // 2,
            scaled_pix,
        )
        painter.end()

        self.preview_label.setPixmap(result_pix)

    def apply(self):
        """Применение выбранных настроек"""
        offset = self.offset_slider.value()
        scale = self.scale_slider.value()
        image_path = (
            DEFAULT_IMAGE
            if self.default_radio.isChecked()
            else self.selected_image
        )

        # Проверка выбранного изображения
        if not image_path:
            QMessageBox.critical(
                self,
                "Ошибка",
                "Нужно выбрать свое изображение или оставить стандартное.",
            )
            return

        # Проверка существования файла
        if image_path != DEFAULT_IMAGE and not os.path.isfile(image_path):
            QMessageBox.warning(
                self,
                "Файл не найден",
                f"Изображение {image_path} не существует! "
                "Будет использовано стандартное.",
            )
            image_path = DEFAULT_IMAGE
            # Сбрасываем на стандартное изображение
            self.default_radio.setChecked(True)
            self.update_browse_state()

        # Сохранение настроек
        self.settings.setValue("offset", offset)
        self.settings.setValue("image_path", image_path)
        self.settings.setValue("scale", scale)
        self.accept()


class PointerOverlay(QObject):
    """Основной класс приложения, управляющий отображением указки"""

    def __init__(self, app):
        """
        Инициализация приложения

        Args:
            app: Основной объект QApplication
        """
        super().__init__()
        self.app = app
        self.app.setQuitOnLastWindowClosed(False)

        # Загрузка сохраненных настроек
        self.settings = QSettings("TeacherPointer", "App")
        self.offset = self.settings.value("offset", 8, type=int)
        self.image_path = self.settings.value(
            "image_path", DEFAULT_IMAGE, type=str
        )
        self.scale = self.settings.value(
            "scale", 100, type=int
        )  # Масштаб по умолчанию 100%
        self.pointer_enabled = True  # Флаг видимости указки

        # Флаги для отслеживания открытых диалогов
        self.settings_dialog_open = False
        self.about_dialog_open = False

        try:
            # Создание и настройка метки с указкой
            self.label = QLabel()
            self.load_and_set_pixmap(self.image_path)

            # Настройка свойств окна указки
            self.label.setAttribute(Qt.WA_TranslucentBackground)
            self.label.setWindowFlags(
                Qt.FramelessWindowHint  # Без рамки
                | Qt.WindowStaysOnTopHint  # Поверх всех окон
                | Qt.X11BypassWindowManagerHint  # Игнорировать окон. менеджер
            )
            self.label.show()

            # Создание иконки в системном трее
            self.create_tray_icon()
        except FileNotFoundError as e:
            self.show_error_message(str(e))
            sys.exit(1)

        # Таймер для отслеживания положения курсора
        self.timer = QTimer()
        self.timer.timeout.connect(self.follow_cursor)
        self.timer.start(20)  # Обновление каждые 20 мс

        # Обработчик завершения приложения
        self.app.aboutToQuit.connect(self.cleanup)

    def load_and_set_pixmap(self, path):
        """Загрузка и установка изображения указки с масштабированием"""
        # Проверка существования файла
        if not os.path.isfile(path):
            # Если файл не существует, используем изображение по умолчанию
            print(
                f"Предупреждение: файл {path} не найден, "
                "используется стандартное изображение"
            )
            path = DEFAULT_IMAGE
            # Обновляем настройки
            self.image_path = DEFAULT_IMAGE
            self.settings.setValue("image_path", DEFAULT_IMAGE)

        # Попытка загрузки изображения
        pixmap = QPixmap(path)
        if pixmap.isNull():
            # Если не удалось загрузить, используем изображение по умолчанию
            print(
                f"Ошибка загрузки: {path}, "
                "используется стандартное изображение"
            )
            pixmap = QPixmap(DEFAULT_IMAGE)
            if pixmap.isNull():
                # Если и стандартное не загружается - критическая ошибка
                raise FileNotFoundError(
                    "Не удалось загрузить даже "
                    f"стандартное изображение: {DEFAULT_IMAGE}"
                )
            # Обновляем настройки
            self.image_path = DEFAULT_IMAGE
            self.settings.setValue("image_path", DEFAULT_IMAGE)

        # Проверка размера изображения и уменьшение при необходимости
        if pixmap.width() > MAX_IMAGE_SIZE or pixmap.height() > MAX_IMAGE_SIZE:
            # Определяем коэффициент масштабирования
            scale_factor = min(
                MAX_IMAGE_SIZE / pixmap.width(),
                MAX_IMAGE_SIZE / pixmap.height(),
            )
            new_width = int(pixmap.width() * scale_factor)
            new_height = int(pixmap.height() * scale_factor)

            # Масштабируем изображение
            pixmap = pixmap.scaled(
                new_width,
                new_height,
                Qt.KeepAspectRatio,  # Сохранение пропорций
                Qt.SmoothTransformation,  # Плавное масштабирование
            )
            print(
                f"Изображение уменьшено: {new_width}x{new_height} "
                f"(было: {pixmap.width()}x{pixmap.height()})"
            )

        # Проверка альфа-канала
        if not pixmap.toImage().hasAlphaChannel():
            print(
                f"Предупреждение: изображение {path} без альфа-канала, "
                "используется стандартное"
            )
            pixmap = QPixmap(DEFAULT_IMAGE)
            # Обновляем настройки
            self.image_path = DEFAULT_IMAGE
            self.settings.setValue("image_path", DEFAULT_IMAGE)

        # Применение масштаба из настроек
        scale_factor = self.scale / 100.0
        if scale_factor != 1.0:
            pixmap = pixmap.scaled(
                int(pixmap.width() * scale_factor),
                int(pixmap.height() * scale_factor),
                Qt.KeepAspectRatio,  # Сохранение пропорций
                Qt.SmoothTransformation,  # Плавное масштабирование
            )

        # Установка изображения и подгонка размера метки
        self.label.setPixmap(pixmap)
        self.label.adjustSize()

    def follow_cursor(self):
        """Перемещение указки вслед за курсором"""
        if not self.pointer_enabled:
            return
        pos = QCursor.pos()
        self.label.move(pos.x() + self.offset, pos.y() + self.offset)

    def create_tray_icon(self):
        """Создание и настройка иконки в системном трее"""
        try:
            self.tray_icon = QSystemTrayIcon()

            # Загрузка иконки для трея
            tray_pixmap = QPixmap(
                "/usr/share/teacher-pointer/img/tray-icon.png"
            )
            self.tray_icon.setIcon(QIcon(tray_pixmap))
            self.tray_icon.setToolTip("Учительская указка")

            # Создание контекстного меню
            menu = QMenu()

            # Действие включения/выключения указки
            self.toggle_action = QAction("Показывать указку", self.app)
            self.toggle_action.setCheckable(True)
            self.toggle_action.setChecked(True)
            self.toggle_action.triggered.connect(self.toggle_pointer)
            menu.addAction(self.toggle_action)

            # Действие открытия настроек
            settings_action = QAction("Настройки", self.app)
            settings_action.triggered.connect(self.open_settings)
            menu.addAction(settings_action)

            menu.addSeparator()

            # Действие показа информации о программе
            about_action = QAction("О программе", self.app)
            about_action.triggered.connect(self.show_about)
            menu.addAction(about_action)

            menu.addSeparator()

            # Действие выхода из программы
            exit_action = QAction("Выход", self.app)
            exit_action.triggered.connect(self.quit_properly)
            menu.addAction(exit_action)

            # Установка меню и отображение иконки
            self.tray_icon.setContextMenu(menu)
            self.tray_icon.show()
        except Exception as e:
            self.show_error_message(f"Не удалось создать иконку в трее: {e}")
            sys.exit(1)

    def toggle_pointer(self, checked):
        """Переключение видимости указки"""
        self.pointer_enabled = checked
        if self.pointer_enabled:
            self.label.show()
            self.timer.start(20)
        else:
            self.label.hide()
            self.timer.stop()

    def open_settings(self):
        """Открытие диалога настроек и применение изменений"""
        if self.settings_dialog_open:
            # Если диалог уже открыт - активируем его
            self.settings_dialog.activateWindow()
            self.settings_dialog.raise_()
            return

        # Устанавливаем флаг открытого диалога
        self.settings_dialog_open = True

        # ✅ ИСПРАВЛЕНИЕ 1: передаём None вместо self (PointerOverlay — не QWidget)
        self.settings_dialog = SettingsDialog(
            None, self.offset, self.image_path, self.scale
        )

        # Обработчик закрытия диалога
        self.settings_dialog.finished.connect(self.on_settings_dialog_closed)

        # Показываем диалог
        self.settings_dialog.show()

    def on_settings_dialog_closed(self, result):
        """Обработчик закрытия диалога настроек"""
        # Сбрасываем флаг открытого диалога
        self.settings_dialog_open = False

        if result == QDialog.Accepted:
            # Обновление настроек
            self.offset = self.settings.value("offset", self.offset, type=int)
            self.image_path = self.settings.value(
                "image_path", self.image_path, type=str
            )
            self.scale = self.settings.value("scale", self.scale, type=int)
            try:
                # Перезагрузка изображения с новыми параметрами
                self.load_and_set_pixmap(self.image_path)

                # ✅ ИСПРАВЛЕНИЕ 2: учитываем состояние видимости указки
                if self.pointer_enabled:
                    self.label.show()
                    if not self.timer.isActive():
                        self.timer.start(20)
                else:
                    self.label.hide()
                    if self.timer.isActive():
                        self.timer.stop()

                # Обновление позиции (если указка видима)
                if self.pointer_enabled:
                    pos = QCursor.pos()
                    self.label.move(
                        pos.x() + self.offset, pos.y() + self.offset
                    )

            except FileNotFoundError as e:
                QMessageBox.critical(None, "Ошибка", str(e))

    def show_error_message(self, message):
        """Отображение сообщения об ошибке"""
        msg = QMessageBox()
        msg.setIcon(QMessageBox.Critical)
        msg.setText("Ошибка при запуске программы")
        msg.setInformativeText(message)
        msg.setWindowTitle("Ошибка")
        msg.exec_()

    def show_about(self):
        """Отображение информации о программе"""
        if self.about_dialog_open:
            # Если диалог уже открыт - активируем его
            self.about_dialog.activateWindow()
            self.about_dialog.raise_()
            return

        # Устанавливаем флаг открытого диалога
        self.about_dialog_open = True

        # Создаем диалог "О программе"
        self.about_dialog = QMessageBox()
        self.about_dialog.setIcon(QMessageBox.Information)
        self.about_dialog.setWindowTitle("О программе")
        self.about_dialog.setText(
            "<b>Учительская указка</b><br><br>"
            "Версия 1.4.2<br><br>"
            "Программа для отображения указки рядом с курсором.<br>"
            "<a href='https://hub.mos.ru/sapik.max/teacher_pointer'>"
            "https://hub.mos.ru/sapik.max/teacher_pointer</a><br>"
        )
        self.about_dialog.setStandardButtons(QMessageBox.Ok)

        # Обработчик закрытия диалога
        self.about_dialog.finished.connect(self.on_about_dialog_closed)

        # Показываем диалог
        self.about_dialog.show()

    def on_about_dialog_closed(self, result):
        """Обработчик закрытия диалога 'О программе'"""
        # Сбрасываем флаг открытого диалога
        self.about_dialog_open = False

    def quit_properly(self):
        """Корректное завершение работы программы"""
        self.cleanup()
        self.app.quit()

    def cleanup(self):
        """Очистка ресурсов при завершении программы"""
        if hasattr(self, "timer"):
            self.timer.stop()
        if hasattr(self, "label"):
            self.label.hide()
            self.label.deleteLater()
        if hasattr(self, "tray_icon") and self.tray_icon:
            self.tray_icon.hide()


def is_another_instance_running():
    """Проверка наличия уже запущенного экземпляра программы"""
    socket = QLocalSocket()
    socket.connectToServer(SERVER_NAME)
    return socket.waitForConnected(100)


def start_local_server():
    """Запуск локального сервера для проверки дубликатов"""
    QLocalServer.removeServer(SERVER_NAME)
    server = QLocalServer()
    server.listen(SERVER_NAME)
    return server


if __name__ == "__main__":
    """Основная точка входа в программу"""

    # Создание приложения
    app = QApplication(sys.argv)

    # Проверка на уже запущенный экземпляр
    if is_another_instance_running():
        QMessageBox.warning(None, "Уже запущено", "Программа уже запущена.")
        sys.exit(0)

    # Запуск сервера для предотвращения дублирования
    server = start_local_server()

    # Обработка сигналов прерывания
    signal.signal(signal.SIGINT, lambda *a: QApplication.quit())
    signal.signal(signal.SIGTERM, lambda *a: QApplication.quit())

    try:
        # Создание и запуск основного класса
        overlay = PointerOverlay(app)
        sys.exit(app.exec_())
    except Exception as e:
        # Обработка непредвиденных ошибок
        QMessageBox.critical(
            None,
            "Критическая ошибка",
            f"Программа завершена из-за ошибки:\n{e}",
        )
        sys.exit(1)
