from functools import wraps
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    Iterator,
    List,
    Optional,
    Tuple,
    Type,
    TypeVar,
    Union,
    cast,
)

import axipy
from axipy._internal._decorator import _experimental
from axipy._internal._shadow_instance_factory import _shadow_manager
from axipy._internal._utils import _AxiRepr, _AxiReprMeta, _Singleton
from axipy.da import DataObject, Table
from axipy.render import Layer, Map, Report
from PySide2.QtCore import QObject, Signal, Slot
from PySide2.QtWidgets import QWidget

from .gui_class import gui_instance
from .view import LegendView, MapView, ReportView, TableView, View
from .widgets import DataManagerWidget, LayerControlWidget

if TYPE_CHECKING:
    from axipy.cpp_gui import ShadowWidgetManager


__all__: List[str] = [
    "ViewManager",
]


def _add_view(func: Callable) -> Callable:
    @wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        result = func(*args, **kwargs)

        mainwindow = axipy.mainwindow
        if isinstance(result, View) and mainwindow.is_valid:
            window = mainwindow.add(result)
            window.show()

        return result

    return wrapper


class ViewManager(_Singleton, _AxiRepr, metaclass=_AxiReprMeta):
    """
    Менеджер содержимого окон.

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

    .. literalinclude:: /../../tests/doc_examples/gui/test_example_view_manager.py
        :caption: Пример использования
        :pyobject: example_view_manager
        :lines: 2-
        :dedent: 4
    """

    # _shadow: "ShadowWidgetManager"

    class _ViewManagerSignals(QObject):
        mainwindow_activated = Signal()
        active_view_id_changed = Signal()

    def __init__(self) -> None:
        self._signals = ViewManager._ViewManagerSignals()
        self.__mainwindow_activated = self._signals.mainwindow_activated
        self.__mainwindow_activated.connect(self.__slot_on_mainwindow_activated_single_run)
        self._active_view_id: int = -1
        self._active_view_id_prev: int = -1

    @Slot()
    def __slot_on_mainwindow_activated_single_run(self) -> None:
        self._active_id_changed.connect(self.__slot_on_active_id_changed)
        self.mainwindow_activated.disconnect(self.__slot_on_mainwindow_activated_single_run)

    @Slot(int)
    def __slot_on_active_id_changed(self, active_view_id: int) -> None:
        self._active_view_id_prev = self._active_view_id
        self._active_view_id = active_view_id
        self._signals.active_view_id_changed.emit()

    @property
    def mainwindow_activated(self) -> Signal:
        """
        Возникает когда главное окно приложения инициализировано и активировано. Данное
        событие может использоваться в плагинах, когда есть необходимость обратиться к
        главному окну приложения. Но ввиду того, что сами плагины загружаются до
        инициализации главного окна, данную процедуру можно выполнить используя данное
        событие (:attr:`axipy.AxiomaInterface.window`).

        :rtype: Signal[]
        """
        return self.__mainwindow_activated

    @property
    def _shadow(self) -> "ShadowWidgetManager":
        return _shadow_manager.widget_manager

    @property
    def active_changed(self) -> Signal:
        """
        Активное окно изменилось.

        :rtype: Signal[QWidget]
        """
        return self._shadow.activeWidgetChanged

    @property
    @_experimental()
    def _active_id_changed(self) -> Signal:
        """
        Активное окно изменилось.

        :rtype: Signal[int]
        """
        return self._shadow.activeWidgetIdChanged

    @property
    def count_changed(self) -> Signal:
        """
        Количество окон изменилось.

        :rtype: Signal[]
        """
        return self._shadow.countChanged

    def activate(self, view: View) -> None:
        """
        Делает заданное окно активным.

        Args:
            view: Содержимое окна.
        """
        self._shadow.activate(view._shadow)

    def close(self, view: View) -> None:
        """
        Закрывает окно.

        Args:
            view: Содержимое окна.
        """
        self._shadow.close(view._shadow)

    def close_all(self) -> None:
        """Закрывает все окна."""
        for w in self.views:
            self.close(w)

    @property
    def active(self) -> Optional[View]:
        """
        Текущее активное окно.

        Return:
          None, если нет активных окон.
        """
        return View._wrap(self._shadow.active())

    @active.setter
    def active(self, view: View) -> None:
        self._shadow.activate(view._shadow)

    @property
    def views(self) -> List[View]:
        """Список всех известных окон."""
        return list(iter(self))

    _ViewT = TypeVar("_ViewT", bound=View)

    def __filter_view(self, view_type: Type["_ViewT"]) -> List["_ViewT"]:
        return cast(List[ViewManager._ViewT], list(filter(lambda v: isinstance(v, view_type), iter(self))))

    @property
    def mapviews(self) -> List[MapView]:
        """
        Список всех окон с картами.

        Пример::

            for v in view_manager.mapviews:
                print('Widget:', v.title)

            ```>>> Widget: Карта: world```
            ```>>> Widget: Карта: rus_obl```
        """
        return self.__filter_view(MapView)

    @property
    def reportviews(self) -> List[ReportView]:
        """Список всех окон с отчетами."""
        return self.__filter_view(ReportView)

    @property
    def tableviews(self) -> List[TableView]:
        """Список всех окон с таблицами просмотра."""
        return self.__filter_view(TableView)

    @property
    def legendviews(self) -> List[LegendView]:
        """Список всех окон с легендами."""
        return self.__filter_view(LegendView)

    _WidgetT = TypeVar("_WidgetT", LayerControlWidget, DataManagerWidget)

    def __widget(self, widget_type: Type[_WidgetT]) -> Tuple[_WidgetT, ...]:
        return tuple(filter(None, (widget_type._wrap(view) for view in self._shadow.widgets())))

    @property
    def layer_controls(self) -> Tuple[LayerControlWidget, ...]:
        """Список виджетов управления слоями карты."""
        return self.__widget(LayerControlWidget)

    @property
    def data_managers(self) -> Tuple[DataManagerWidget, ...]:
        """Список виджетов управления списком открытых данных."""
        return self.__widget(DataManagerWidget)

    @property
    def count(self) -> int:
        """Количество окон."""
        return self.__len__()

    def __len__(self) -> int:
        return len(self.views)

    def __getitem__(self, key: str) -> View:
        """
        Возвращает существующее окно по имени.

        Raises:
            KeyError: Окно с заданным именем не найдено.
        """
        found = self._find_by_name(key)
        if found is None:
            raise KeyError(f"Key '{key}' not found")
        return found

    def __contains__(self, key: str) -> bool:
        """
        Проверяет существование окна с заданным именем.

        Args:
            key: Имя окна.
        """
        found = self._find_by_name(key)
        return found is not None

    def _find_by_name(self, name: str) -> Optional[View]:
        for view in self.views:
            if view.widget.windowTitle() == name:
                return View._wrap(view._shadow)
        return None

    def __iter__(self) -> Iterator[View]:
        return (cast(View, View._wrap(view)) for view in self._shadow.views())

    def create_view(
        self, value: Union[Map, Layer, DataObject, Report, MapView]
    ) -> Union[MapView, TableView, ReportView, LegendView]:
        """
        Создает окно из для переданного объекта соответствующего типа.

        Args:
            value: таблица, слой, карта или отчет.

        Return:
            Окно.
        """
        if isinstance(value, (Layer, Map)):
            return self.create_mapview(value)
        elif isinstance(value, Table):
            return self.create_tableview(value)
        elif isinstance(value, Report):
            return self.create_reportview(value)
        elif isinstance(value, MapView):
            return self.create_legendview(value)
        raise ValueError()

    def add_to_current_mapview(self, value: Union[Layer, DataObject]) -> None:
        """
        Добавляет слой в текущее окно. Если текущее окно карты отсутствует, создается
        новое окно.

        Args:
            value: Слой или объект данных
        """
        if isinstance(value, DataObject) and value.is_spatial:
            value = Layer.create(value)
        if not isinstance(value, Layer):
            raise TypeError("Parameter is not Layer instance.")
        active = self.active
        if active and isinstance(active, MapView):
            active.map.layers.append(value)
        else:
            self.create_view(value)

    # noinspection PyShadowingBuiltins
    @_add_view
    def create_mapview(self, map: Union[Map, Layer, DataObject, List[Layer]]) -> MapView:
        """
        Создает окно из для переданного объекта карты.

        Args:
            map: Карта, слой или объект данных.

        Note:

            Переданная карта копируется.

        Return:
            Окно карты.
        """
        map_ensured: Map
        if isinstance(map, DataObject) and map.is_spatial:
            layer = Layer.create(map)
            map_ensured = Map([layer])
        elif isinstance(map, Layer):
            map_ensured = Map([map])
        elif isinstance(map, list) and map and isinstance(map[0], Layer):
            map_ensured = Map(map)
        elif isinstance(map, Map):
            map_ensured = map
        else:
            raise TypeError(f"Parameter is not supported {type(map)}.")

        return cast(MapView, View._wrap(gui_instance._shadow.createMapView(map_ensured._shadow)))

    @_add_view
    def create_tableview(self, table: Table) -> TableView:
        """
        Создает окно в виде табличного представления из объекта данных.

        Args:
            table: Таблица.

        Return:
            Окно таблицы.
        """
        if not isinstance(table, Table):
            raise TypeError("Parameter is not Table instance.")
        return cast(TableView, View._wrap(gui_instance._shadow.createTableView(table._shadow)))

    @_add_view
    def create_reportview(self, report: Optional[Report] = None) -> ReportView:
        """
        Создает окно с планом отчета.

        Args:
            report: План отчета. Если не передан, то создается по умолчанию.

        Return:
            Окно отчета.
        """
        if report is None:
            shadow_report = gui_instance._shadow.createReportViewDefault()
        elif isinstance(report, Report):
            shadow_report = gui_instance._shadow.createReportView(report._shadow)
        else:
            raise TypeError(f"Unsupported parameter type. {type(report)=}.")

        return cast(ReportView, View._wrap(shadow_report))

    @_add_view
    def create_legendview(self, mapview: MapView) -> LegendView:
        """
        Создает окно легенды для карты.

        Args:
            mapview: Окно с картой.

        Return:
            Окно с легендой.
        """
        return cast(LegendView, View._wrap(gui_instance._shadow.createLegendView(mapview._shadow)))

    @property
    def global_parent(self) -> QWidget:
        """
        Этот параметр можно использовать как родительское окно 'parent' при открытии диалогов. 
        Если не указывать родителя, диалог может отображаться как отдельное окно, независимое от основного окна программы.

        Пример::

            if QMessageBox.question(view_manager.global_parent, 'Вопрос', 'Отменить действие?') == QMessageBox.Yes:
                pass
        """
        return gui_instance._shadow.global_parent()

    # noinspection PyShadowingBuiltins
    @_experimental()
    def _find_by_id(self, id: int) -> Optional[View]:
        """
        Производит поиск окна по его идентификатору :attr:`axipy.View._id`

        Args:
            id: Идентификатор.
        """
        if id < 1:
            return None
        for view in self.views:
            if view._id == id:
                return View._wrap(view._shadow)
        return None


def _apply_deprecated() -> None:
    # Using getattr to hide deprecated objects from axipy namespace on IDE inspections
    getattr(__all__, "extend")(("view_manager",))

    globals().update(
        view_manager=ViewManager(),
    )


_apply_deprecated()
