from pathlib import Path
from typing import List, NamedTuple, Optional, Tuple, cast

from axipy._internal._shadow_instance_factory import _shadow_manager
from axipy._internal._utils import _AxiRepr, _AxiReprMeta, _Singleton
from axipy.cpp_core_core import ShadowPluginManager as _ShadowPluginManager
from PySide2.QtCore import Signal

__all__: List[str] = [
    "PluginManager",
    "plugin_manager",
    "PluginInfo",
]


class PluginInfo(NamedTuple):
    """
    Информация о плагине.

    .. seealso::
        Менеджер плагинов :class:`PluginManager`
    """

    name: str
    """Наименование."""
    description: str
    """Краткое описание."""
    path: Path
    """Путь в файловой системе."""
    author: Optional[str]
    """Автор плагина."""
    documentation: Optional[Path]
    """Ссылка на документацию."""
    version: Optional[str]
    """Версия."""
    homepage: Optional[str]
    """Домашняя страница."""


class PluginManager(_Singleton, _AxiRepr, metaclass=_AxiReprMeta):
    """
    Менеджер плагинов.

    Note:
        Создание :class:`axipy.PluginManager` не требуется,
        используйте объект :attr:`axipy.plugin_manager`.

    Пример::

        import axipy

        # Отслеживание загрузки и выгрузки плагинов
        axipy.plugin_manager.loaded.connect(lambda id_: print(f'Загружен плагин {id_}'))
        axipy.plugin_manager.unloaded.connect(lambda id_: print(f'Выгружен плагин {id_}'))
        plugin_name = 'my_module_id'
        # Загрузка плагина
        axipy.plugin_manager.load(plugin_name)
        # Получение кортежа идентификаторов доступных плагинов
        ids = axipy.plugin_manager.ids()
        # Получение информации о плагине
        pi = axipy.plugin_manager.info(plugin_name)
        print(pi.name, pi.author, pi.path)
    """

    def __init__(self) -> None:
        self._inner_shadow: Optional[_ShadowPluginManager] = None

    def _check_existence(self, plugin_id: str) -> None:
        if plugin_id not in self.ids():
            raise ValueError(f"Plugin '{plugin_id}' not found.")

    @property
    def _shadow(self) -> _ShadowPluginManager:
        if self._inner_shadow is None:
            self._inner_shadow = _ShadowPluginManager(_shadow_manager.core)
        return self._inner_shadow

    @property
    def loaded(self) -> Signal:
        """
        Плагин успешно загружен.

        :rtype: Signal[str], где str - идентификатор плагина.
        """
        return cast(Signal, self._shadow.loaded)

    @property
    def unloaded(self) -> Signal:
        """
        Плагин успешно выгружен.

        :rtype: Signal[str], где str - идентификатор плагина.
        """
        return cast(Signal, self._shadow.unloaded)

    def is_loaded(self, plugin_id: str) -> bool:
        """
        Загружен ли плагин.

        Args:
            plugin_id: Идентификатор плагина.

        Returns:
            True если плагин загружен. В противном случае False, также, если плагин отсутствует.
        """
        return self._shadow.isLoaded(plugin_id)

    def load(self, plugin_id: str) -> None:
        """
        Загрузка плагина. При ошибке выбрасывается исключение.

        Args:
            plugin_id: Идентификатор плагина.

        Raises:
            ValueError: Если плагин не найден.
        """
        self._check_existence(plugin_id)
        self._shadow.load(plugin_id)

    def unload(self, plugin_id: str) -> None:
        """
        Выгрузка плагина.

        Args:
            plugin_id: Идентификатор плагина.

        Raises:
            ValueError: Если плагин не найден.
        """
        self._check_existence(plugin_id)
        self._shadow.unload(plugin_id)

    def reload(self, plugin_id: str) -> None:
        self._check_existence(plugin_id)
        if self.is_loaded(plugin_id):
            self.unload(plugin_id)
        self.load(plugin_id)

    def ids(self) -> Tuple[str, ...]:
        """Кортеж идентификаторов плагинов."""
        return tuple(p["id"] for p in self._shadow.metadata())

    def info(self, plugin_id: str) -> PluginInfo:
        """
        Информация о плагине.

        Args:
            plugin_id: Идентификатор плагина.

        Raises:
            ValueError: Если плагин не найден.
        """
        self._check_existence(plugin_id)

        index = self.ids().index(plugin_id)
        p = self._shadow.metadata()[index]

        # name, description and path should never be None in runtime, so cast for static type checkers
        pi = PluginInfo(
            name=cast(str, p["name"]["default"] if "name" in p else None),
            author=p["author"] if "author" in p else None,
            documentation=Path(p["documentation"][0]) if "documentation" in p else None,
            version=p["version"] if "version" in p else None,
            homepage=p["homepage"] if "homepage" in p else None,
            description=cast(str, p["description"]["default"] if "description" in p else None),
            path=cast(Path, Path(p["path"]) if "path" in p else None),
        )
        return pi


plugin_manager = PluginManager()
