import traceback
from functools import cached_property
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    Dict,
    ItemsView,
    Iterator,
    KeysView,
    List,
    Mapping,
    Optional,
    Protocol,
    ValuesView,
    cast,
)

import axipy
from axipy._internal._utils import _AxiRepr, _Singleton
from axipy.concurrent.task_utils import _to_cpp_progress_spec
from axipy.cpp_core_core import ShadowTaskManager as _ShadowTaskManager
from axipy.cpp_gui import ShadowProgressElementFactory, ShadowProgressSpecification
from PySide2.QtCore import QObject, QRunnable, QThreadPool, Signal
from PySide2.QtWidgets import QDialog

from .task import Task
from .task_utils import ProgressSpecification

if TYPE_CHECKING:
    from axipy.cpp_core_core import ShadowTask

__all__: List[str] = [
    "TaskManager",
    "task_manager",
]


class _ProgressElementFactoryInterface(Protocol):
    def make_dialog_for_task(self, task: "ShadowTask", spec: ShadowProgressSpecification) -> QDialog: ...


class _ProgressElementFactory:

    def __init__(self) -> None:
        self.__default_factory: _ProgressElementFactoryInterface = ShadowProgressElementFactory()
        self.__current_factory: _ProgressElementFactoryInterface = self.__default_factory

    @property
    def factory(self) -> _ProgressElementFactoryInterface:
        return self.__current_factory

    @factory.setter
    def factory(self, factory: _ProgressElementFactoryInterface) -> None:
        self.__current_factory = factory

    def make_dialog_for_task(self, task: "ShadowTask", spec: ShadowProgressSpecification) -> QDialog:
        return self.factory.make_dialog_for_task(task, spec)

    def restore(self) -> None:
        self.__current_factory = self.__default_factory


_progress_element_factory_instance = _ProgressElementFactory()


class TaskManager(Mapping, _Singleton, _AxiRepr):
    """
    Менеджер пользовательских задач. Класс является словарем, доступным только для
    чтения (:class:`collections.abc.Mapping`), где ключи это идентификаторы действий
    :attr:`axipy.Task.id`, a значения это объекты класса :class:`axipy.Task`.
    Поддерживает обращение по ключу.

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

    Задачи (объекты класса :class:`axipy.Task`) добавляются в менеджер при создании конструктором, и удаляются при
    завершении задачи (успешно или с ошибкой). Еще не запущенную задачу, можно удалить из менеджера, вызвав метод
    :attr:`axipy.Task.cancel`.
    """

    class _Runnable(QRunnable):
        """Method run will be added later, this empty class needed, because it's not
        allowed to instantiate pure QRunnable class."""

        def __init__(self, task: "Task") -> None:
            QRunnable.__init__(self)
            self._task = task

        def run(self) -> None:
            try:
                self._task._Task__run_internally()  # type: ignore[attr-defined]
            except (Exception,):
                traceback.print_exc()
            finally:
                del self._task

    class _TaskManagerObject(QObject):
        added = Signal(Task)
        removed = Signal(Task)

    def __init__(self) -> None:
        self._thread_pool: QThreadPool = QThreadPool()
        self._task_manager_object: TaskManager._TaskManagerObject = TaskManager._TaskManagerObject()
        self._current_id: int = 1

        # deprecated
        self.__progress_element_factory = _progress_element_factory_instance

    def _tasks(self) -> "Dict[int, Task]":
        task_list = cast(List["Task"], self._task_manager_object.children())
        return {elem.id: elem for elem in task_list}

    @cached_property
    def _shadow(self) -> _ShadowTaskManager:
        return _ShadowTaskManager()

    def _start_runnable_task(self, task: "Task") -> None:
        # if no threads are available, runnable is added to a run queue.
        task.finished.connect(self._task_manager_object.removed)
        self._thread_pool.start(self._Runnable(task))

    def _start_shadow_task(self, task: "Task", shadow_task: "ShadowTask") -> None:
        task.finished.connect(self._task_manager_object.removed)
        self._shadow.start_task(shadow_task)

    def __getitem__(self, key: int) -> "Task":
        return self._tasks()[key]

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

    def __iter__(self) -> Iterator[int]:
        return iter(self._tasks())

    def keys(self) -> KeysView[int]:
        """Возвращает набор ключей, где ключи это идентификаторы задач
        :attr:`axipy.Task.id`."""
        return Mapping.keys(self)

    def values(self) -> ValuesView["Task"]:
        """Возвращает коллекцию значений, где значения это объекты класса
        :class:`axipy.Task`."""
        return Mapping.values(self)

    def items(self) -> ItemsView[int, "Task"]:
        """Возвращает набор кортежей ключ-значение, где ключи это идентификаторы задач
        :attr:`axipy.Task.id`, a значения это объекты класса :class:`axipy.Task`."""
        return Mapping.items(self)

    def get(self, key: str, default_value: Optional[Any] = None) -> Optional["Task"]:
        """Возвращает значение по ключу."""
        return Mapping.get(self, key, default_value)

    @property
    def added(self) -> Signal:
        """
        Возвращает сигнал, испускаемый при добавлении (создании) новой задачи.

        :rtype: Signal[:class:`axipy.Task`]
        """
        return self._task_manager_object.added

    @property
    def removed(self) -> Signal:
        """
        Возвращает сигнал, испускаемый при удалении (завершении) задачи.

        :rtype: Signal[:class:`axipy.Task`]
        """
        return self._task_manager_object.removed

    def _generate_dialog_for_task(self, task: "ShadowTask", spec: ProgressSpecification) -> QDialog:
        return self.__progress_element_factory.make_dialog_for_task(task, _to_cpp_progress_spec(spec))


def _apply_deprecated() -> None:

    def start_task(self: TaskManager, task: "axipy.AxipyTask") -> None:  # type: ignore[name-defined]
        """
        Warning:
            .. deprecated:: 6.0.0
        """
        self._shadow.start_task(task)

    def run_and_get(self: TaskManager, spec: ProgressSpecification, func: Callable, *args: Any, **kwargs: Any) -> Any:
        """
        Warning:
            .. deprecated:: 6.0.0
        """
        task = axipy.AxipyAnyCallableTask(func, *args, **kwargs)  # type: ignore[attr-defined]
        task.with_handler(spec.with_handler)
        ph = task.progress_handler()
        dialog = self.generate_dialog_for_task(task, spec)  # type: ignore[attr-defined]
        self.start_task(task)  # type: ignore[attr-defined]
        if not ph.is_finished():
            dialog.exec_()
        return ph.result

    def run_in_gui(self: TaskManager, func: Callable, *args: Any, **kwargs: Any) -> Any:
        """
        Warning:
            .. deprecated:: 6.0.0
        """
        task = axipy.AxipyAnyCallableTask(func, *args, **kwargs)  # type: ignore[attr-defined]
        task.with_handler(False)
        ph = task.progress_handler()
        self._shadow.run_in_gui(task)
        return ph.result

    def generate_dialog_for_task(
        self: TaskManager,
        task: "axipy.AxipyTask",  # type: ignore[name-defined]
        spec: ProgressSpecification,
    ) -> QDialog:
        """
        Warning:
            .. deprecated:: 6.0.0
        """
        return self._generate_dialog_for_task(task, spec)

    setattr(TaskManager, "start_task", start_task)
    setattr(TaskManager, "run_and_get", run_and_get)
    setattr(TaskManager, "run_in_gui", run_in_gui)
    setattr(TaskManager, "generate_dialog_for_task", generate_dialog_for_task)


_apply_deprecated()


task_manager = TaskManager()
