import contextlib
import dataclasses
import gc
import os
import runpy
from pathlib import Path
from typing import (
    Any,
    Callable,
    Dict,
    List,
    Optional,
    Tuple,
    Union,
    cast,
)

import axipy.gui.gui_class as gui_class
import PySide2.QtCore as QtCore
import PySide2.QtWidgets as QtWidgets
from axipy._internal._decorator import _experimental
from axipy.axioma_plugin import tr
from axipy.cpp_gui import ShadowGuiUtils
from axipy.gui.view_manager_ import ViewManager
from axipy.settings import CurrentSettings
from PySide2.QtCore import QObject, Slot
from PySide2.QtGui import QIcon
from PySide2.QtWidgets import QApplication, QFileDialog

__all__: List[str] = [
    "SelectedFilter",
    "open_file_dialog",
    "open_files_dialog",
    "save_file_dialog",
    "select_folder_dialog",
    "execfile",
    "get_dependencies_folder",
    "run_in_gui",
    "_get_system_environment",
    "_global_parent",
]


view_manager = ViewManager()


def _ensure_folder_ends_with_separator(folder: str) -> str:
    return f"{Path(folder)}{os.sep}"


def _app_has_widgets() -> bool:
    return QApplication.instance() is not None and len(QApplication.instance().allWidgets()) > 0


def _ask_console_input(input_prompt: str) -> Optional[Path]:
    result = input(input_prompt)
    if result:
        return Path(result)
    return None


@dataclasses.dataclass
class SelectedFilter:
    """
    Класс-контейнер, используемый в файловых диалогах, для получения выбранного пользователем фильтра.
    """

    filter: Optional[str] = None
    """
    Атрибут, который примет значения выбранного фильтра после успешного открытия файлов диалогом.
    """


def _file_dialog(
    func: Callable,
    folder: Union[str, Path],
    file_name: Optional[str],
    filter_arg: Optional[str] = None,
    title: Optional[str] = None,
    selected_filter: Optional[Union[str, SelectedFilter]] = None,
    icon: Optional[QIcon] = None,
) -> Optional[Union[str, List[str]]]:
    if not _app_has_widgets():
        return None  # Guard

    params = {
        "parent": view_manager.global_parent,
        "dir": _ensure_folder_ends_with_separator(str(folder)),
        "caption": title,
    }
    if file_name:
        params["fileName"] = file_name
    if filter_arg is not None:
        params["filter"] = filter_arg
    if not CurrentSettings.UseNativeFileDialog:
        params["options"] = QFileDialog.DontUseNativeDialog
    if selected_filter is not None:
        if isinstance(selected_filter, SelectedFilter) and selected_filter.filter is not None:
            params["selectedFilter"] = selected_filter.filter
        elif isinstance(selected_filter, str):
            params["selectedFilter"] = selected_filter
    if icon is not None:
        params["icon"] = icon
    result = func(**params)

    selected_filter_updated: str = ""
    if func in (ShadowGuiUtils.getOpenFileName, ShadowGuiUtils.getSaveFileName):
        result, selected_filter_updated = result[0], result[1]
    elif func == ShadowGuiUtils.getOpenFileNames:
        result, selected_filter_updated = result[:-1], result[-1]

    if isinstance(selected_filter, SelectedFilter):
        selected_filter.filter = selected_filter_updated

    return result


def open_file_dialog(
    filter_arg: Optional[str] = None,
    title: Optional[str] = None,
    folder: Optional[Union[str, Path]] = None,
    file_name: Optional[str] = None,
    selected_filter: Optional[Union[str, SelectedFilter]] = None,
    icon: Optional[QIcon] = None,
    set_last_open_path: bool = True,
) -> Optional[Path]:
    """
    Открывает диалог выбора файла. Если нет главного окна Аксиомы, спрашивает путь к
    файлу в консоли.

    Args:
        filter_arg: Типы файлов. Например: ``'MapInfo Tab (*.tab);;Таблицы Excel (*.xls *.xlsx)'``.
        title: Заголовок диалога.
        folder: Начальная папка диалога. Если папка отсутствует, будет предложена
          первая из доступных вверх по иерархии или же папка в домашнем каталоге.
        file_name: Предлагаемое в диалоге имя файла
        selected_filter: Выбранный фильтр типов файлов. Устанавливается необходимая строка из параметра
            filter_arg. Например, ``'Таблицы Excel (*.xls *.xlsx)'``.
            Также, можно использовать класс-контейнер axipy.SelectedFilter, чтобы получить значение фильтра,
            выбранное пользователем.
        icon: Иконка для диалога, если необходимо ее переопределить
        set_last_open_path: Автоматически запоминать путь открытия в настройках Аксиомы.

    Returns:
        Возвращает путь к выбранному файлу или None.
    """
    if not _app_has_widgets():
        input_prompt: str = tr("Введите путь к файлу: ") if title is None else title
        console_result: Optional[Path] = _ask_console_input(input_prompt)
        return console_result

    folder_ensured: Union[str, Path] = CurrentSettings.LastOpenPath if folder is None else folder
    result = _file_dialog(
        func=ShadowGuiUtils.getOpenFileName,
        folder=folder_ensured,
        file_name=file_name,
        filter_arg=filter_arg,
        title=title,
        selected_filter=selected_filter,
        icon=icon,
    )
    result = cast(Optional[str], result)
    if result:
        result_path = Path(result)
        if set_last_open_path:
            CurrentSettings.LastOpenPath = result_path.parent
        return result_path
    return None


def open_files_dialog(
    filter_arg: Optional[str] = None,
    title: Optional[str] = None,
    folder: Optional[Union[str, Path]] = None,
    selected_filter: Optional[Union[str, SelectedFilter]] = None,
    icon: Optional[QIcon] = None,
    set_last_open_path: bool = True,
) -> Optional[Tuple[Path, ...]]:
    """
    Открывает диалог выбора нескольких файлов.

    Args:
        filter_arg: Типы файлов. Например: ``'MapInfo Tab (*.tab);;Таблицы Excel (*.xls *.xlsx)'``.
        title: Заголовок диалога.
        folder: Начальная папка диалога или же имя файла. Если папка отсутствует, будет предложена
          первая из доступных вверх по иерархии или же папка в домашнем каталоге.
        selected_filter: Выбранный фильтр типов файлов. Устанавливается необходимая строка из параметра
            filter_arg. Например, ``'Таблицы Excel (*.xls *.xlsx)'``.
            Также, можно использовать класс-контейнер axipy.SelectedFilter, чтобы получить значение фильтра,
            выбранное пользователем.
        icon: Иконка для диалога, если необходимо ее переопределить
        set_last_open_path: Автоматически запоминать путь открытия в настройках Аксиомы.

    Returns:
        Возвращает список с путями к выбранным файлам или None.

    Пример::

        title = 'Файлы для открытия'
        filter = 'MapInfo Tab (*.tab);;Таблицы Excel (*.xls *.xlsx)'
        folder = Path('/home/user/outfile.tab')
        selected_filter = 'MapInfo Tab (*.tab)'
        res = axipy.open_files_dialog(filter, title, folder, selected_filter)
        [print(fn) for fn in res]
    """

    folder_ensured: Union[str, Path] = CurrentSettings.LastOpenPath if folder is None else folder
    result = _file_dialog(
        func=ShadowGuiUtils.getOpenFileNames,
        folder=folder_ensured,
        file_name=None,
        filter_arg=filter_arg,
        title=title,
        selected_filter=selected_filter,
        icon=icon,
    )
    result = cast(Optional[List[str]], result)
    if result:
        paths = tuple(Path(p) for p in result)
        if set_last_open_path:
            CurrentSettings.LastOpenPath = paths[0].parent
        return paths
    return None


def save_file_dialog(
    filter_arg: Optional[str] = None,
    title: Optional[str] = None,
    folder: Optional[Path] = None,
    file_name: Optional[str] = None,
    selected_filter: Optional[Union[str, SelectedFilter]] = None,
    icon: Optional[QIcon] = None,
    set_last_save_path: bool = True,
) -> Optional[Path]:
    """
    Открывает диалог сохранения файла. Если нет главного окна Аксиомы, спрашивает путь к
    файлу в консоли.

    Args:
        filter_arg: Типы файлов. Например: ``'MapInfo Tab (*.tab);;Таблицы Excel (*.xls *.xlsx)'``.
        title: Заголовок диалога.
        folder: Начальная папка диалога. Если папка отсутствует, будет предложена
          первая из доступных вверх по иерархии или же папка в домашнем каталоге.
        file_name: Предлагаемое в диалоге имя файла.
        selected_filter: Выбранный фильтр типов файлов. Устанавливается необходимая строка из параметра
            filter_arg. Например, ``'Таблицы Excel (*.xls *.xlsx)'``.
            Также, можно использовать класс-контейнер axipy.SelectedFilter, чтобы получить значение фильтра,
            выбранное пользователем.
        icon: Иконка для диалога, если необходимо ее переопределить.
        set_last_save_path: Автоматически запоминать путь сохранения в настройках Аксиомы.
    Returns:
        Возвращает выбранный путь сохранения или None.
    """
    if not _app_has_widgets():
        input_prompt: str = tr("Введите путь сохранения файла: ") if title is None else title
        console_result: Optional[Path] = _ask_console_input(input_prompt)
        if isinstance(console_result, Path) and console_result.is_file() and console_result.exists():
            raise FileExistsError(f"File already exists: {console_result}.")
        return console_result

    folder_ensured: Path = CurrentSettings.LastSavePath if folder is None else folder
    result = _file_dialog(
        func=ShadowGuiUtils.getSaveFileName,
        folder=folder_ensured,
        file_name=file_name,
        filter_arg=filter_arg,
        title=title,
        selected_filter=selected_filter,
        icon=icon,
    )
    result = cast(Optional[str], result)
    if result:
        result_path = Path(result)
        if set_last_save_path:
            CurrentSettings.LastSavePath = result_path.parent
        return result_path
    return None


def select_folder_dialog(
    title: Optional[str] = None,
    folder: Optional[Union[str, Path]] = None,
) -> Optional[Path]:
    """
    Открывает диалог выбора папки. Если нет главного окна Аксиомы, спрашивает путь к
    файлу в консоли.

    Args:
        title: Заголовок диалога.
        folder: Начальная папка диалога. Если папка отсутствует, будет предложена
          первая из доступных вверх по иерархии или же папка в домашнем каталоге.
    Returns:
        Возвращает выбранную папку или None.
    """
    if not _app_has_widgets():
        input_prompt: str = tr("Введите путь к папке") if title is None else title
        console_result: Optional[Path] = _ask_console_input(input_prompt)
        if isinstance(console_result, Path) and not console_result.is_dir():
            raise RuntimeError("Chosen path is not dir.")
        return console_result

    kwargs: Dict[str, Any] = {}

    if title is not None:
        kwargs["caption"] = title

    if folder is not None:
        folder_ensured: str = str(folder) if isinstance(folder, Path) else folder
        kwargs["dir"] = _ensure_folder_ends_with_separator(folder_ensured)

    if not CurrentSettings.UseNativeFileDialog:
        kwargs["options"] = QFileDialog.DontUseNativeDialog

    result: str = ShadowGuiUtils.getExistingDirectory(
        view_manager.global_parent,
        **kwargs,  # type: ignore[arg-type]
    )
    if result:
        return Path(result)
    return None


def execfile(path: Union[str, Path]) -> None:
    """
    Выполняет скрипт на языке python из файла.

    Args:
        path: Путь к исполняемому файлу.
    """

    def run_path(path_arg: str) -> None:
        with contextlib.ExitStack() as stack:
            stack.callback(gc.collect)
            runpy.run_path(path_arg, run_name="__main__")

    if isinstance(path, str):
        run_path(path)
    elif isinstance(path, Path):
        run_path(str(path))
    elif isinstance(path, list) and len(path) > 0:
        run_path(path[0])
    else:
        raise TypeError("Parameter is not supported.")


def get_dependencies_folder() -> Path:
    """Возвращает папку, для установки зависимых пакетов."""
    return (
        Path(gui_class.gui_instance._shadow.installedPluginsPath())
        / "installed_modules"
        / "dependencies"
        / "site-packages"
    )


@_experimental()
def _get_system_environment() -> Dict[str, str]:
    """
    Возвращает перечень переменных окружения на момент запуска ГИС Аксиома
    """
    return gui_class.gui_instance._shadow.systemEnvironment()


def run_in_gui(function: Callable, *args: Any, **kwargs: Any) -> Any:
    """
    Выполняет переданную функцию в потоке интерфейса.

    Это может быть удобно, когда в процессе выполнения длительной фоновой задачи нужно
    спросить о чем нибудь пользователя отобразив диалог. Также
    создавать/взаимодействовать с некоторыми объектами можно только из потока
    интерфейса.
    """
    main_thread = QtWidgets.QApplication.instance().thread()
    # check if already running in main thread
    if main_thread == QtCore.QThread.currentThread():
        return function(*args, **kwargs)

    marker = object()

    class Worker(QObject):

        def __init__(self) -> None:
            super().__init__()
            self.result = marker

        @Slot()
        def do_work(self) -> None:
            self.result = function(*args, **kwargs)
            QtCore.QMetaObject.invokeMethod(loop, "quit")

    loop = QtCore.QEventLoop()
    worker = Worker()
    worker.moveToThread(main_thread)
    QtCore.QMetaObject.invokeMethod(worker, "do_work")
    if worker.result is marker:
        loop.exec_()

    return worker.result


@_experimental()
def _global_parent() -> QtWidgets.QWidget:
    """Текущее родительское окно `parent`, задаваемое при открытии диалогов"""
    return view_manager.global_parent