import re
import xml.etree.ElementTree as ElemTree
from pathlib import Path, PurePath
from typing import List, TYPE_CHECKING, Dict
from xml.dom import minidom

import PySide2.QtCore as QtC
import PySide2.QtGui
import PySide2.QtGui as QtG
import PySide2.QtWidgets as QtW
import axipy as axp
from .model import StyleSet
from .settings_manager import StyleKey
from .ui.style_catalog_ui import Ui_StyleCatalog
from .utils import try_except_mbox

if TYPE_CHECKING:
    from .style_catalog_main import StyleCatalogMain
    from .__init__ import StyleCatalogs


class XmlManager:
    NAME = "name"

    def __init__(self, dialog: 'StyleCatalogSettings') -> None:
        self.dialog = dialog
        self.tr = self.dialog.plugin.tr

    @staticmethod
    def prettify(elem: ElemTree.Element) -> bytes:
        elem_tree_string = ElemTree.tostring(elem, encoding="UTF-8", xml_declaration=True)
        minidom_document = minidom.parseString(elem_tree_string)
        pretty_string = minidom_document.toprettyxml(encoding="UTF-8")
        return pretty_string

    @staticmethod
    def _check_if_save_path_relative(style_folder_path: Path, xml_folder: Path) -> Path:
        try:
            result_path = PurePath(style_folder_path).relative_to(xml_folder)
            result_path = Path(result_path)
            return result_path
        except ValueError:
            return style_folder_path
        except (Exception,):
            return style_folder_path

    @try_except_mbox(parent_name="dialog", text="Не удалось сохранить файл с наборами.")
    def save_to_xml(self) -> None:
        file_name, _selected_filter = QtW.QFileDialog.getSaveFileName(
            parent=self.dialog,
            caption="Выбор имени файла xml",
            dir=str(axp.CurrentSettings.LastSavePath),
            filter="Файлы xml (*.xml)",
        )
        if not file_name:
            return None

        file_name = Path(file_name)
        xml_folder = file_name.parent
        axp.CurrentSettings.LastSavePath = xml_folder

        root_elem = ElemTree.Element("catalogs")

        style_sets = self.dialog.model.get_inner_model_copy_without_default()
        for style_set in style_sets:
            attrs = {
                self.NAME: style_set.name,
                StyleKey.PEN: str(self._check_if_save_path_relative(style_set.items[StyleKey.PEN], xml_folder)),
                StyleKey.BRUSH: str(self._check_if_save_path_relative(style_set.items[StyleKey.BRUSH], xml_folder)),
                StyleKey.SYMBOL: str(self._check_if_save_path_relative(style_set.items[StyleKey.SYMBOL], xml_folder)),
            }
            ElemTree.SubElement(root_elem, "catalog", **attrs)

        with open(file_name, mode="wb") as f:
            f.write(self.prettify(root_elem))

        axp.Notifications.push(
            self.dialog.title, f"Файл с наборами '{file_name}' сохранен.", axp.Notifications.Success)

    def open_from_xml(self) -> None:
        file_name, _selected_filter = QtW.QFileDialog.getOpenFileName(
            parent=self.dialog,
            caption="Открытие файла xml",
            dir=str(axp.CurrentSettings.LastOpenPath),
            filter="Файлы xml (*.xml)",
        )
        if not file_name:
            return None

        axp.CurrentSettings.LastOpenPath = Path(file_name).parent

        success = self.load_xml_file(Path(file_name))
        if success:
            axp.Notifications.push(
                self.dialog.title,
                f"Файл с наборами '{file_name}' загружен.",
                axp.Notifications.Success
            )

    @try_except_mbox(parent_name="dialog", text="Не удалось загрузить файл с наборами.")
    def load_xml_file(self, file_name: Path) -> bool:
        if not file_name.exists():
            raise FileNotFoundError(f"Не удалось открыть файл: '{str(file_name)}'.")

        with open(file_name, mode='r', encoding="UTF-8") as stream:
            element_tree = ElemTree.parse(stream)
            root_element = element_tree.getroot()

            # Отдельный список для целостности операции при исключениях
            style_sets: List[StyleSet] = []
            for element in root_element:
                pen_path = Path(element.attrib.get(StyleKey.PEN, axp.DefaultSettings.PenCatalog))
                brush_path = Path(element.attrib.get(StyleKey.BRUSH, axp.DefaultSettings.BrushCatalog))
                symbol_path = Path(element.attrib.get(StyleKey.SYMBOL, axp.DefaultSettings.SymbolCatalog))

                xml_folder = file_name.parent

                pen_path = self._check_if_path_relative(pen_path, xml_folder, axp.DefaultSettings.PenCatalog)
                brush_path = self._check_if_path_relative(brush_path, xml_folder, axp.DefaultSettings.BrushCatalog)
                symbol_path = self._check_if_path_relative(symbol_path, xml_folder, axp.DefaultSettings.SymbolCatalog)

                items = {
                    StyleKey.PEN: pen_path,
                    StyleKey.BRUSH: brush_path,
                    StyleKey.SYMBOL: symbol_path,
                }
                style_set = StyleSet(element.attrib[self.NAME], items)
                style_sets.append(style_set)

            for style_set in style_sets:
                self.dialog.insert_style_set(style_set)

        return True

    @staticmethod
    def _check_if_path_relative(path: Path, xml_folder: Path, default_path: Path) -> Path:
        if not path.exists():
            result_path = xml_folder / path
            if result_path.exists():
                return result_path

        return default_path


class BackCompatManager:
    # backwards compatibility
    XML_FILE_NAME = "StyleCatalog.xml"

    def __init__(self, dialog: 'StyleCatalogSettings') -> None:
        self.dialog = dialog

        resource_dir = QtC.QDir(axp.gui_instance._shadow.system_resource_catalog())
        self.system_catalog_list: List[QtC.QFileInfo] = self.find_system_catalogs(resource_dir)
        # logging.debug(f"{self.system_catalog_list=}")
        app_local_data_location = QtC.QDir(axp.gui_instance._shadow.installedPluginsPath())
        self.xml_file_info = QtC.QFileInfo(app_local_data_location.absoluteFilePath(self.XML_FILE_NAME))
        # logging.debug(f"{self.xml_file_info=}")

    def find_system_catalogs(self, resource_dir: QtC.QDir) -> List[QtC.QFileInfo]:
        """
        Ищем каталоги в стандартных местах
        \\resource\\ExtendedStyles
        """
        # logging.debug(f"_find_system_catalogs({resource_dir=})")
        result = []
        if not resource_dir or not isinstance(resource_dir, QtC.QDir):
            return result

        extended_styles_dir = QtC.QDir(resource_dir.filePath("ExtendedStyles"))
        # logging.debug(f"{extended_styles_dir=}")
        if extended_styles_dir.exists():
            catalog_list = extended_styles_dir.entryInfoList(QtC.QDir.AllDirs | QtC.QDir.NoDotAndDotDot)
            # logging.debug(f"{catalog_list=}")
            for catalog in catalog_list:
                # logging.debug(f"{catalog=}")
                xml_file = QtC.QFileInfo(QtC.QDir(catalog.absoluteFilePath()), self.XML_FILE_NAME)
                # logging.debug(f"{xml_file=}")
                if xml_file.exists():
                    result.append(xml_file)
        return result

    def init_sets(self) -> None:
        for system_catalog in self.system_catalog_list:
            self.dialog.xml_manager.load_xml_file(Path(system_catalog.filePath()))

        if self.xml_file_info.exists():
            self.dialog.xml_manager.load_xml_file(Path(self.xml_file_info.filePath()))


class StyleCatalogSettings(QtW.QDialog, Ui_StyleCatalog):
    RE_EMPTY = re.compile(r"^\s*$")
    selection: QtC.QItemSelectionModel

    def __init__(self, plugin: 'StyleCatalogs', parent: 'StyleCatalogMain') -> None:
        QtW.QDialog.__init__(self, parent)
        self.init_ui_common()
        self.plugin = plugin
        self.settings_manager = plugin.settings_manager

        self.model = parent.model
        self.settings: QtC.QSettings = plugin.settings

        self.title: str = self.plugin.title

        self.xml_manager = XmlManager(self)
        self.back_compat_manager = BackCompatManager(self)

        self.ui_ok: QtW.QPushButton = self.buttonBox.button(QtW.QDialogButtonBox.Ok)
        self.ui_cancel: QtW.QPushButton = self.buttonBox.button(QtW.QDialogButtonBox.Cancel)

        self.inner_model_original = self.model.get_inner_model_copy()

        self.init_ui(parent.cbox_choose_set.currentIndex())

    def init_ui_common(self) -> None:
        self.setupUi(self)
        self.setAttribute(QtC.Qt.WA_DeleteOnClose, True)
        self.setWindowFlags(self.windowFlags() & ~QtC.Qt.WindowContextHelpButtonHint)

    def init_ui(self, starting_index: int) -> None:
        self.setWindowIcon(PySide2.QtGui.QIcon(self.plugin.icon_path))
        self.tb_pen.setIcon(QtG.QIcon.fromTheme("open"))
        self.tb_pen.clicked.connect(lambda: self.catalog_common_clicked(StyleKey.PEN))

        self.tb_brush.setIcon(QtG.QIcon.fromTheme("open"))
        self.tb_brush.clicked.connect(lambda: self.catalog_common_clicked(StyleKey.BRUSH))

        self.tb_symbol.setIcon(QtG.QIcon.fromTheme("open"))
        self.tb_symbol.clicked.connect(lambda: self.catalog_common_clicked(StyleKey.SYMBOL))

        self.list_view.setModel(self.model)

        self.selection = self.list_view.selectionModel()
        self.selection.currentRowChanged.connect(self.slot_current_row_changed)

        # Выставляется первый элемент, чтобы показать, что эта форма просто шаблон, и не зависит от активного набора.
        # self.list_view.setCurrentIndex(self.model.index(0, 0))

        if self.model.hasIndex(starting_index, 0):
            self.list_view.setCurrentIndex(self.model.index(starting_index, 0))
        else:
            self.list_view.setCurrentIndex(self.model.index(0, 0))

        self.tb_up.setIcon(QtG.QIcon.fromTheme("arrow_up"))
        self.tb_bottom.setIcon(QtG.QIcon.fromTheme("arrow_down"))
        self.tb_add.setIcon(QtG.QIcon.fromTheme("add"))
        self.tb_remove.setIcon(QtG.QIcon.fromTheme("delete"))
        self.tb_up.clicked.connect(self.on_up)
        self.tb_bottom.clicked.connect(self.on_bottom)
        self.tb_add.clicked.connect(self.on_add)
        self.tb_remove.clicked.connect(self.on_remove)

        self.pb_save_to_xml.clicked.connect(self.xml_manager.save_to_xml)
        self.pb_open_from_xml.clicked.connect(self.xml_manager.open_from_xml)

        self.ui_ok.setText(self.plugin.tr("Сохранить настройки"))

    def accept(self) -> None:
        # Здесь сохраняются только настройки (структура) наборов стилей, сами стили не применяются.
        self.settings_manager.save_style_sets(self.model.get_inner_model_copy_without_default())
        QtW.QDialog.accept(self)

    def reject(self) -> None:
        if self.model.get_inner_model_copy() != self.inner_model_original:
            mbox = QtW.QMessageBox(self)
            mbox.setWindowTitle(self.title)
            mbox.setText("Настройки наборов стилей были изменены.")
            mbox.setInformativeText("Сохранить изменения?")
            mbox.setStandardButtons(QtW.QMessageBox.Save | QtW.QMessageBox.Discard | QtW.QMessageBox.Cancel)
            mbox.setDefaultButton(QtW.QMessageBox.Save)

            @QtC.Slot(int)
            def finished(return_code: int):
                if return_code == QtW.QMessageBox.Save:
                    QtW.QDialog.reject(self)
                elif return_code == QtW.QMessageBox.Discard:
                    # reset all models changes if rejected
                    self.model.reset_inner_model(self.inner_model_original)
                    QtW.QDialog.reject(self)
                elif return_code == QtW.QMessageBox.Cancel:
                    return None

            mbox.finished.connect(finished)
            mbox.open()
        else:
            QtW.QDialog.reject(self)

    def catalog_common_clicked(self, key: StyleKey) -> None:
        if key == StyleKey.PEN:
            caption = self.plugin.tr("Выбор каталога со стилями линий")
        elif key == StyleKey.BRUSH:
            caption = self.plugin.tr("Выбор каталога со стилями заливки")
        elif key == StyleKey.SYMBOL:
            caption = self.plugin.tr("Выбор каталога с растровыми символами")
        else:
            raise RuntimeError("Internal")

        dir_name = QtW.QFileDialog.getExistingDirectory(
            self,  # parent
            caption,  # caption
            str(axp.CurrentSettings.LastOpenPath),  # dir
            QtW.QFileDialog.ShowDirsOnly | QtW.QFileDialog.DontResolveSymlinks,  # options
        )
        if not dir_name:
            return None

        dir_name_path: Path = Path(dir_name)
        axp.CurrentSettings.LastOpenPath = dir_name_path

        row = self.list_view.currentIndex().row()
        style_set: StyleSet = self.model.get_style_set(row)
        items: Dict[StyleKey, Path] = style_set.items

        if key == StyleKey.PEN:
            items[StyleKey.PEN] = dir_name_path
            self.le_pen_text = dir_name
        elif key == StyleKey.BRUSH:
            items[StyleKey.BRUSH] = dir_name_path
            self.le_brush_text = dir_name
        elif key == StyleKey.SYMBOL:
            items[StyleKey.SYMBOL] = dir_name_path
            self.le_symbol_text = dir_name

    def toggle_controls_on_default_current(self) -> None:
        controls = [
            # self._ui.tb_add,  always allowed
            self.tb_remove,
            self.tb_up,
            self.tb_bottom,

            self.tb_pen,
            self.tb_brush,
            self.tb_symbol,
        ]

        non_default_selected = self.list_view.currentIndex().row() != 0

        if non_default_selected:
            count = self.model.rowCount()
            # logging.debug(f"{count=}")
            last_row = count - 1
            current_row = self.list_view.currentIndex().row()
            # logging.debug(f"{current_row=}, {last_row=}")
            if current_row == 1:
                controls.remove(self.tb_up)
                self.tb_up.setEnabled(False)

            if current_row == last_row:
                controls.remove(self.tb_bottom)
                self.tb_bottom.setEnabled(False)

        for control in controls:
            control.setEnabled(non_default_selected)

    @QtC.Slot(QtC.QModelIndex, QtC.QModelIndex)
    def slot_current_row_changed(self, curr: QtC.QModelIndex, _prev: QtC.QModelIndex):
        # logging.debug(f"slot_current_row_changed({curr}, {_prev})")
        row = curr.row()
        self.toggle_controls_on_default_current()

        style_set = self.model.get_style_set_copy(row)
        self.le_pen_text = style_set.items[StyleKey.PEN]
        self.le_brush_text = style_set.items[StyleKey.BRUSH]
        self.le_symbol_text = style_set.items[StyleKey.SYMBOL]

    @property
    def le_pen_text(self) -> str:
        return self.le_pen.text()

    @le_pen_text.setter
    def le_pen_text(self, value: str) -> None:
        self.le_pen.setText(str(Path(value)))

    @property
    def le_brush_text(self) -> str:
        return self.le_brush.text()

    @le_brush_text.setter
    def le_brush_text(self, value: str) -> None:
        self.le_brush.setText(str(Path(value)))

    @property
    def le_symbol_text(self) -> str:
        return self.le_symbol.text()

    @le_symbol_text.setter
    def le_symbol_text(self, value: str) -> None:
        self.le_symbol.setText(str(Path(value)))

    @QtC.Slot()
    def on_up(self) -> None:
        current_row = self.list_view.currentIndex().row()
        result = self.model.moveRow(
            QtC.QModelIndex(),
            current_row,
            QtC.QModelIndex(),
            current_row - 1,
        )
        if result:
            self.toggle_controls_on_default_current()
        else:
            axp.Notifications.push(
                self.title,
                self.plugin.tr("Не удалось переместить набор вверх."),
                axp.Notifications.Critical
            )

    @QtC.Slot()
    def on_bottom(self) -> None:
        current_row = self.list_view.currentIndex().row()
        result = self.model.moveRow(
            QtC.QModelIndex(),
            current_row,
            QtC.QModelIndex(),
            current_row + 2,
        )

        if result:
            self.toggle_controls_on_default_current()
        else:
            axp.Notifications.push(
                self.title,
                self.plugin.tr("Не удалось переместить набор вниз."),
                axp.Notifications.Critical
            )

    @QtC.Slot()
    def on_add(self) -> None:
        text, success = QtW.QInputDialog.getText(
            self,  # parent
            self.plugin.tr("Название набора"),  # title
            self.plugin.tr("Набор стилей"),  # label
            QtW.QLineEdit.Normal,  # echo
            '',  # text
            self.windowFlags() & ~QtC.Qt.WindowContextHelpButtonHint  # flags
        )
        if not success:
            return None

        error_title = self.plugin.tr("Ошибка ввода названия набора")

        if self.RE_EMPTY.match(text):
            QtW.QMessageBox.critical(
                self,
                error_title,
                self.plugin.tr(f"Пустое название набора.")
            )
            return None

        for name in self.model.get_sets_names():
            if text == name:
                QtW.QMessageBox.critical(
                    self,
                    error_title,
                    self.plugin.tr(f"Название '{text}' уже существует.")
                )
                return None

        style_set = StyleSet(text, {
            StyleKey.PEN: axp.DefaultSettings.PenCatalog,
            StyleKey.BRUSH: axp.DefaultSettings.BrushCatalog,
            StyleKey.SYMBOL: axp.DefaultSettings.SymbolCatalog,
        })

        result = self.insert_style_set(style_set)
        if result:
            self.toggle_controls_on_default_current()

    def insert_style_set(self, style_set) -> bool:
        current_row = self.list_view.currentIndex().row()

        row_to_insert = current_row + 1

        result = self.model.insertRow(row_to_insert, style_set=style_set)
        if result:
            self.list_view.setCurrentIndex(self.model.index(row_to_insert, 0))
        return result

    @QtC.Slot()
    def on_remove(self) -> None:
        row = self.list_view.currentIndex().row()
        result = self.model.removeRow(row)
        if result:
            self.toggle_controls_on_default_current()
        else:
            axp.Notifications.push(
                self.title,
                self.plugin.tr("Не удалось удалить набор."),
                axp.Notifications.Critical
            )
