import sys
import os
import json
import ctypes
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QFileDialog, QLabel, QListWidget,
    QListWidgetItem, QHBoxLayout, QVBoxLayout, QPushButton, QMenu, QAction,
    QMessageBox, QScrollArea, QSizePolicy, QSplitter
)
from PyQt5.QtGui import QPixmap, QIcon, QPalette
from PyQt5.QtCore import Qt, QSize, QSettings, QDir

from PIL import Image

# Путь к папке кеша
CACHE_DIR = os.path.join(os.path.expanduser("~"), ".image_viewer_cache")
FAVORITES_FILE = os.path.join(CACHE_DIR, "favorites.json")

SUPPORTED_FORMATS = ('.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tiff')


def ensure_cache():
    if not os.path.exists(CACHE_DIR):
        os.makedirs(CACHE_DIR)


class ImageLabel(QLabel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setBackgroundRole(QPalette.Window)
        self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
        self.setScaledContents(False)  # Отключаем автоматическое масштабирование
        self.scale_factor = 1.0
        self.original_pixmap = None

    def set_pixmap(self, pixmap):
        self.original_pixmap = pixmap
        self.scale_factor = 1.0
        self.update_pixmap()

    def wheelEvent(self, event):
        """
        Обработка события прокрутки колесика мыши для масштабирования изображения.
        """
        if event.angleDelta().y() > 0:
            self.zoom_in()
        else:
            self.zoom_out()

    def zoom_in(self):
        """
        Увеличение изображения.
        """
        self.scale_image(1.25)

    def zoom_out(self):
        """
        Уменьшение изображения.
        """
        self.scale_image(0.8)

    def scale_image(self, factor):
        """
        Масштабирование изображения на заданный коэффициент.
        """
        self.scale_factor *= factor
        self.update_pixmap()

    def update_pixmap(self):
        """
        Обновление отображаемого изображения с учетом текущего коэффициента масштабирования.
        """
        if self.original_pixmap:
            new_size = self.scale_factor * self.original_pixmap.size()
            scaled_pixmap = self.original_pixmap.scaled(
                new_size, Qt.KeepAspectRatio, Qt.SmoothTransformation
            )
            # Убедимся, что изображение помещается в область просмотра
            self.setPixmap(scaled_pixmap)
            self.adjustSize()


class ImageViewer(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Просмотровщик Изображений")
        self.resize(1200, 800)

        ensure_cache()
        self.favorites = self.load_favorites()
        self.clipboard_path = None
        self.copied = False

        # Основной виджет и компоновка
        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)
        self.main_layout = QVBoxLayout()
        self.central_widget.setLayout(self.main_layout)

        # Панель инструментов
        self.toolbar = QHBoxLayout()
        self.main_layout.addLayout(self.toolbar)

        self.open_folder_btn = QPushButton("Открыть папку")
        self.open_folder_btn.clicked.connect(self.open_folder)
        self.toolbar.addWidget(self.open_folder_btn)

        self.open_file_btn = QPushButton("Открыть файл")
        self.open_file_btn.clicked.connect(self.open_file)
        self.toolbar.addWidget(self.open_file_btn)

        self.edit_btn = QPushButton("Изменить (Paint)")
        self.edit_btn.clicked.connect(self.edit_image)
        self.toolbar.addWidget(self.edit_btn)

        self.delete_btn = QPushButton("Удалить")
        self.delete_btn.clicked.connect(self.delete_image)
        self.toolbar.addWidget(self.delete_btn)

        self.fullscreen_btn = QPushButton("На весь экран")
        self.fullscreen_btn.clicked.connect(self.toggle_fullscreen)
        self.toolbar.addWidget(self.fullscreen_btn)

        self.png_info_btn = QPushButton("PNG info")
        self.png_info_btn.clicked.connect(self.png_info)
        self.toolbar.addWidget(self.png_info_btn)

        self.favorites_btn = QPushButton("Избранное")
        self.favorites_btn.clicked.connect(self.show_favorites)
        self.toolbar.addWidget(self.favorites_btn)

        self.save_size_btn = QPushButton("Сохранить размер")
        self.save_size_btn.clicked.connect(self.save_window_size)
        self.toolbar.addWidget(self.save_size_btn)

        # Основная область с разделителем и списками и изображением
        self.splitter_main = QSplitter(Qt.Horizontal)
        self.main_layout.addWidget(self.splitter_main)

        # Левая панель: Жесткие диски и Рабочий стол
        self.navigation_list = QListWidget()
        self.navigation_list.setIconSize(QSize(32, 32))
        self.navigation_list.itemClicked.connect(self.navigate_to_directory)
        self.populate_navigation()
        self.splitter_main.addWidget(self.navigation_list)

        # Средняя панель: Превью изображений
        self.splitter_images = QSplitter(Qt.Vertical)
        self.splitter_main.addWidget(self.splitter_images)

        self.image_list = QListWidget()
        self.image_list.setViewMode(QListWidget.IconMode)
        self.image_list.setResizeMode(QListWidget.Adjust)
        self.image_list.setMovement(QListWidget.Static)
        self.image_list.setWordWrap(True)
        self.image_list.setWrapping(True)
        self.image_list.setSpacing(10)
        self.image_list.setIconSize(QSize(150, 150))  # Увеличенные иконки для лучшего отображения
        self.image_list.setSelectionMode(QListWidget.SingleSelection)
        self.image_list.itemClicked.connect(self.display_image)
        self.splitter_images.addWidget(self.image_list)

        # Правая панель: Область просмотра изображения
        self.image_area = QScrollArea()
        self.image_label = ImageLabel()
        self.image_area.setWidget(self.image_label)
        self.image_area.setWidgetResizable(True)
        self.splitter_main.addWidget(self.image_area)

        self.splitter_main.setStretchFactor(1, 1)  # Средняя панель растягивается
        self.splitter_images.setStretchFactor(0, 1)  # Превью изображения растягиваются

        # Переменные состояния
        self.current_folder = None
        self.current_image = None
        self.fullscreen = False

        # Загрузка окна
        self.load_window_state()

        # Поддержка drag and drop
        self.setAcceptDrops(True)

    def populate_navigation(self):
        """
        Заполнение левой панели списком жестких дисков и рабочего стола.
        """
        # Получение списка локальных дисков
        drives = QDir.drives()
        for drive in drives:
            item = QListWidgetItem(QIcon.fromTheme("drive-harddisk"), drive.path())
            item.setData(Qt.UserRole, drive.path())
            self.navigation_list.addItem(item)

        # Добавление рабочего стола
        desktop_path = os.path.join(os.path.expanduser("~"), "Desktop")
        if os.path.exists(desktop_path):
            item = QListWidgetItem(QIcon.fromTheme("folder-desktop"), "Рабочий стол")
            item.setData(Qt.UserRole, desktop_path)
            self.navigation_list.addItem(item)

    def load_favorites(self):
        """
        Загрузка списка избранных изображений из файла.
        """
        if os.path.exists(FAVORITES_FILE):
            try:
                with open(FAVORITES_FILE, 'r', encoding='utf-8') as f:
                    return set(json.load(f))
            except Exception as e:
                print(f"Ошибка загрузки избранного: {e}")
                return set()
        return set()

    def save_favorites(self):
        """
        Сохранение списка избранных изображений в файл.
        """
        try:
            with open(FAVORITES_FILE, 'w', encoding='utf-8') as f:
                json.dump(list(self.favorites), f, ensure_ascii=False, indent=4)
        except Exception as e:
            print(f"Ошибка сохранения избранного: {e}")

    def open_folder(self):
        """
        Открытие диалога выбора папки и загрузка изображений из выбранной папки.
        """
        folder = QFileDialog.getExistingDirectory(self, "Выбрать папку", "")
        if folder:
            self.current_folder = folder
            self.load_images(folder)
            self.select_first_image()

    def open_file(self):
        """
        Открытие диалога выбора файла и загрузка соответствующей папки.
        """
        file, _ = QFileDialog.getOpenFileName(
            self, "Выбрать файл", "", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.tiff)"
        )
        if file:
            self.current_folder = os.path.dirname(file)
            self.load_images(self.current_folder)
            self.select_image(file)

    def load_images(self, folder):
        """
        Загрузка изображений из указанной папки и отображение их в виде превью.
        """
        self.image_list.clear()
        for filename in os.listdir(folder):
            if filename.lower().endswith(SUPPORTED_FORMATS):
                filepath = os.path.join(folder, filename)
                thumbnail = self.get_thumbnail(filepath)
                if thumbnail:
                    item = QListWidgetItem()
                    item.setIcon(QIcon(thumbnail))
                    # Убираем текст, оставляем только иконку
                    item.setData(Qt.UserRole, filepath)
                    self.image_list.addItem(item)

    def get_thumbnail(self, filepath):
        """
        Создание или загрузка превью изображения из кеша.
        """
        ensure_cache()
        thumb_path = os.path.join(CACHE_DIR, self.sanitize_filename(filepath) + ".png")
        if not os.path.exists(thumb_path):
            try:
                img = Image.open(filepath)
                img.thumbnail((150, 150))  # Размер превью соответствует установленному IconSize
                img.save(thumb_path, "PNG")
            except Exception as e:
                print(f"Ошибка создания превью для {filepath}: {e}")
                return None
        return thumb_path

    def sanitize_filename(self, filepath):
        """
        Преобразование пути к файлу в допустимое имя файла для превью.
        """
        return filepath.replace(os.sep, "_").replace(":", "_").replace("\\", "_").replace("/", "_").replace(" ", "_")

    def display_image(self, item):
        """
        Отображение выбранного изображения в основном окне с возможностью масштабирования и панорамирования.
        """
        filepath = item.data(Qt.UserRole)
        pixmap = QPixmap(filepath)
        if pixmap.isNull():
            QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить изображение: {filepath}")
            return
        self.image_label.set_pixmap(pixmap)
        self.current_image = filepath

    def select_image(self, filepath):
        """
        Выбор изображения в списке по его пути и отображение.
        """
        for index in range(self.image_list.count()):
            item = self.image_list.item(index)
            if item.data(Qt.UserRole) == filepath:
                self.image_list.setCurrentItem(item)
                self.display_image(item)
                break

    def select_first_image(self):
        """
        Выбор и отображение первого изображения в списке.
        """
        if self.image_list.count() > 0:
            self.image_list.setCurrentRow(0)
            item = self.image_list.item(0)
            self.display_image(item)

    def edit_image(self):
        """
        Открытие выбранного изображения в Paint для редактирования.
        """
        if self.current_image:
            try:
                os.startfile(self.current_image, 'edit')
            except Exception as e:
                QMessageBox.warning(self, "Ошибка", f"Не удалось открыть изображение для редактирования: {e}")

    def delete_image(self):
        """
        Удаление выбранного изображения с подтверждением.
        """
        if self.current_image:
            reply = QMessageBox.question(
                self, 'Удаление',
                f"Вы действительно хотите удалить {os.path.basename(self.current_image)}?",
                QMessageBox.Yes | QMessageBox.No, QMessageBox.No
            )
            if reply == QMessageBox.Yes:
                try:
                    os.remove(self.current_image)
                    self.load_images(self.current_folder)
                    self.image_label.clear()
                    self.current_image = None
                except Exception as e:
                    QMessageBox.warning(self, "Ошибка", f"Не удалось удалить файл: {e}")

    def toggle_fullscreen(self):
        """
        Переключение полноэкранного режима.
        """
        if self.fullscreen:
            self.showNormal()
            self.fullscreen = False
        else:
            self.showFullScreen()
            self.fullscreen = True

    def png_info(self):
        """
        Отображение информации о PNG-изображении.
        """
        if self.current_image and self.current_image.lower().endswith('.png'):
            try:
                img = Image.open(self.current_image)
                info = img.info
                msg = "\n".join([f"{k}: {v}" for k, v in info.items()])
                QMessageBox.information(self, "PNG Info", msg)
            except Exception as e:
                QMessageBox.warning(self, "Ошибка", f"Не удалось получить информацию о PNG: {e}")
        else:
            QMessageBox.information(self, "Информация", "Текущее изображение не является PNG.")

    def show_favorites(self):
        """
        Отображение списка избранных изображений.
        """
        favorite_images = list(self.favorites)
        if not favorite_images:
            QMessageBox.information(self, "Избранное", "Список избранных изображений пуст.")
            return
        self.image_list.clear()
        for filepath in favorite_images:
            if os.path.exists(filepath):
                thumbnail = self.get_thumbnail(filepath)
                if thumbnail:
                    item = QListWidgetItem()
                    item.setIcon(QIcon(thumbnail))
                    item.setData(Qt.UserRole, filepath)
                    self.image_list.addItem(item)

    def save_window_size(self):
        """
        Сохранение текущего размера и положения окна.
        """
        settings = QSettings("MyCompany", "ImageViewer")
        settings.setValue("size", self.size())
        settings.setValue("pos", self.pos())
        QMessageBox.information(self, "Сохранено", "Размер и положение окна сохранены.")

    def load_window_state(self):
        """
        Загрузка сохраненного размера и положения окна.
        """
        settings = QSettings("MyCompany", "ImageViewer")
        size = settings.value("size")
        pos = settings.value("pos")
        if size:
            self.resize(size)
        if pos:
            self.move(pos)

    def contextMenuEvent(self, event):
        """
        Создание и отображение контекстного меню при правом клике на изображение.
        """
        if not self.current_image:
            return  # Не показывать контекстное меню, если нет выбранного изображения

        context_menu = QMenu(self)

        if self.current_image in self.favorites:
            remove_fav_action = QAction("Удалить из избранного", self)
            remove_fav_action.triggered.connect(self.remove_from_favorites)
            context_menu.addAction(remove_fav_action)
        else:
            add_fav_action = QAction("Добавить в избранное", self)
            add_fav_action.triggered.connect(self.add_to_favorites)
            context_menu.addAction(add_fav_action)

        copy_action = QAction("Копировать", self)
        copy_action.triggered.connect(self.copy_file)
        context_menu.addAction(copy_action)

        paste_action = QAction("Вставить", self)
        paste_action.triggered.connect(self.paste_file)
        context_menu.addAction(paste_action)

        cut_action = QAction("Вырезать", self)
        cut_action.triggered.connect(self.cut_file)
        context_menu.addAction(cut_action)

        delete_action = QAction("Удалить", self)
        delete_action.triggered.connect(self.delete_image)
        context_menu.addAction(delete_action)

        set_wallpaper_action = QAction("Сделать изображением рабочего стола", self)
        set_wallpaper_action.triggered.connect(self.set_as_wallpaper)
        context_menu.addAction(set_wallpaper_action)

        properties_action = QAction("Свойства", self)
        properties_action.triggered.connect(self.show_properties)
        context_menu.addAction(properties_action)

        context_menu.exec_(event.globalPos())

    def add_to_favorites(self):
        """
        Добавление текущего изображения в избранное.
        """
        if self.current_image:
            self.favorites.add(self.current_image)
            self.save_favorites()
            QMessageBox.information(
                self, "Избранное",
                f"Добавлено в избранное: {os.path.basename(self.current_image)}"
            )

    def remove_from_favorites(self):
        """
        Удаление текущего изображения из избранного.
        """
        if self.current_image and self.current_image in self.favorites:
            self.favorites.remove(self.current_image)
            self.save_favorites()
            QMessageBox.information(
                self, "Избранное",
                f"Удалено из избранного: {os.path.basename(self.current_image)}"
            )

    def copy_file(self):
        """
        Копирование текущего изображения в буфер обмена.
        """
        if self.current_image:
            self.copied = True
            self.clipboard_path = self.current_image
            QMessageBox.information(
                self, "Копирование",
                f"Скопировано: {os.path.basename(self.current_image)}"
            )

    def paste_file(self):
        """
        Вставка изображения из буфера обмена в текущую папку.
        """
        if self.clipboard_path:
            try:
                base, name = os.path.split(self.clipboard_path)
                dest = os.path.join(self.current_folder, name)
                if os.path.exists(dest):
                    QMessageBox.warning(
                        self, "Копирование",
                        "Файл уже существует в папке назначения."
                    )
                    return
                if self.copied:
                    # Копирование
                    with open(self.clipboard_path, 'rb') as fr:
                        with open(dest, 'wb') as fw:
                            fw.write(fr.read())
                    self.load_images(self.current_folder)
                    QMessageBox.information(
                        self, "Копирование",
                        f"Вставлено: {dest}"
                    )
                else:
                    # Вырезание
                    if os.path.exists(self.clipboard_path):
                        with open(self.clipboard_path, 'rb') as fr:
                            with open(dest, 'wb') as fw:
                                fw.write(fr.read())
                        os.remove(self.clipboard_path)
                        self.load_images(self.current_folder)
                        QMessageBox.information(
                            self, "Вырезание",
                            f"Вставлено: {dest}"
                        )
            except Exception as e:
                QMessageBox.warning(
                    self, "Копирование/Вырезание",
                    f"Не удалось вставить файл: {e}"
                )

    def cut_file(self):
        """
        Вырезание текущего изображения.
        """
        if self.current_image:
            self.copied = False
            self.clipboard_path = self.current_image
            QMessageBox.information(
                self, "Вырезание",
                f"Вырезано: {os.path.basename(self.current_image)}"
            )

    def set_as_wallpaper(self):
        """
        Установка текущего изображения как фон рабочего стола (только для Windows).
        """
        if self.current_image:
            try:
                SPI_SETDESKWALLPAPER = 20
                abs_path = os.path.abspath(self.current_image)
                ctypes.windll.user32.SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, abs_path, 3)
                QMessageBox.information(
                    self, "Рабочий стол",
                    "Фон рабочего стола установлен."
                )
            except Exception as e:
                QMessageBox.warning(
                    self, "Ошибка",
                    f"Не удалось установить фон: {e}"
                )

    def show_properties(self):
        """
        Отображение свойств текущего изображения.
        """
        if self.current_image:
            try:
                stat = os.stat(self.current_image)
                info = (
                    f"Путь: {self.current_image}\n"
                    f"Размер: {stat.st_size} байт\n"
                    f"Последнее изменение: {self.format_time(stat.st_mtime)}"
                )
                QMessageBox.information(self, "Свойства", info)
            except Exception as e:
                QMessageBox.warning(
                    self, "Ошибка",
                    f"Не удалось получить свойства: {e}"
                )

    def format_time(self, epoch_time):
        """
        Форматирование времени из epoch в читаемый формат.
        """
        import datetime
        return datetime.datetime.fromtimestamp(epoch_time).strftime('%Y-%m-%d %H:%M:%S')

    def navigate_to_directory(self, item):
        """
        Обработка выбора директории из левой панели (жесткие диски и рабочий стол).
        """
        folder = item.data(Qt.UserRole)
        if os.path.isdir(folder):
            self.current_folder = folder
            self.load_images(folder)
            self.select_first_image()

    def dragEnterEvent(self, event):
        """
        Обработка события перетаскивания файлов в окно приложения.
        """
        if event.mimeData().hasUrls():
            event.acceptProposedAction()

    def dropEvent(self, event):
        """
        Обработка события отпуска файлов после перетаскивания в окно приложения.
        """
        urls = event.mimeData().urls()
        if urls:
            filepath = urls[0].toLocalFile()
            if os.path.isdir(filepath):
                self.current_folder = filepath
                self.load_images(filepath)
                self.select_first_image()
            elif os.path.isfile(filepath) and filepath.lower().endswith(SUPPORTED_FORMATS):
                self.current_folder = os.path.dirname(filepath)
                self.load_images(self.current_folder)
                self.select_image(filepath)

    def closeEvent(self, event):
        """
        Обработка события закрытия окна приложения.
        """
        self.save_favorites()
        event.accept()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    viewer = ImageViewer()
    viewer.show()
    sys.exit(app.exec_())

Перетягивайте границы между панелями, чтобы изменить их размеры. - запоминать границы при закрытии приложения автоматически сохранять.
Навигация по Жестким Дискам и Рабочему Столу: - добавить разворачивающуюся структуру папок
Используйте колесико мыши для увеличения и уменьшения изображения. - увеличивать в том месте где находится курсор
Перемещайтесь по увеличенному изображению с помощью полос прокрутки - добавить перемещения по изображению удержанием левой кнопки мыши
Перенесите изображение в окно просмотра и убедитесь, что оно отображается сразу без необходимости дополнительного выбора. - исправить не работает
внеси изменения проверь и напиши полный код