from functools import cached_property
from typing import TYPE_CHECKING, Iterator, List, Optional, Tuple, Union, cast
import logging

from axipy._internal._decorator import _experimental
from axipy._internal._shadow_instance_factory import _shadow_manager
from axipy._internal._utils import _AxiRepr, _AxiReprMeta, _Singleton

from .data_object import DataObject, QueryTable, SelectionTable, Table
from .providers.opener import _opener_instance
from .sqldialect import TypeSqlDialect

if TYPE_CHECKING:
    from axipy.cpp_core_dp import ShadowDataCatalog
    from PySide2.QtCore import Signal

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


class DataManager(_Singleton, _AxiRepr, metaclass=_AxiReprMeta):
    """
    Хранилище объектов данных. При открытии таблицы или растра эти объекты автоматически
    попадают в данный каталог. Для отслеживания изменений в каталоге используются
    события :attr:`added` и :attr:`removed`.

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

    .. literalinclude:: /../../tests/doc_examples/da/test_example_dp.py
        :caption: Пример использования.
        :pyobject: test_run_example_catalog
        :lines: 3-
        :dedent: 4
    """

    @cached_property
    def _shadow(self) -> "ShadowDataCatalog":
        return _shadow_manager.gui.catalog()

    @property
    def _all_tables(self) -> List[Table]:
        # cast - because mypy picking wrong overload of filter from the typeshed
        return cast(List[Table], list(filter(self._is_table, self.all_objects)))

    def _get_dialect(self, dialect: Union[str, TypeSqlDialect, None]) -> str:
        if isinstance(dialect, TypeSqlDialect):
            return dialect.value
        return _opener_instance._ensure_sql_dialect(dialect)

    def _query_internal(
        self,
        query_text: str,
        is_hidden: bool,
        dialect: Optional[Union[TypeSqlDialect, str]] = None,
    ) -> Optional[Table]:
        result = _opener_instance.query(query_text, *self._all_tables, dialect=self._get_dialect(dialect))
        if result is not None:
            if is_hidden:
                result._shadow.setIsHidden(True)
            self.add(result)
        return result

    @staticmethod
    def _is_table(obj: DataObject) -> bool:
        return isinstance(obj, Table)

    @property
    def updated(self) -> "Signal":
        """
        Сигнал об изменении каталога. В качестве параметра передается дополнительная
        информация в виде dict.

        .. csv-table:: Доступные параметры
            :header: Наименование, Значение, Описание

            operation, added, Таблица добавлена в каталог
            operation, removed, Таблица удалена из каталога
            operation, nameChanged, Изменено наименование таблицы
            operation, selectionChanged, Произведены изменения в выборке
            name, , "Наименование таблицы, если оно доступно"

        :rtype: Signal[typing.Dict[str, str]]
        """
        return self._shadow.updated

    @property
    def added(self) -> "Signal":
        """
        Сигнал о добавлении объекта. В качестве параметра передается наименование
        таблицы.

        :rtype: Signal[str]
        """
        return self._shadow.added

    @property
    def removed(self) -> "Signal":
        """
        Сигнал об удалении объекта.

        :rtype: Signal[str]
        """
        return self._shadow.removed

    @property
    def table_data_changed(self) -> "Signal":
        """
        Сигнал об изменении данных таблиц, присутствующих в каталоге. Испускается когда
        были изменены данные таблицы. В качестве параметров передаются имя изменяемой
        таблицы и дополнительная информация в виде dict (подробнее
        :attr:`Table.data_changed`) .

        .. literalinclude:: /../../tests/doc_examples/da/test_example_table.py
            :caption: Пример подписки на изменение таблиц.
            :pyobject: test_run_example_table_change_table
            :lines: 7-
            :dedent: 4
        """
        return self._shadow.tableDataChanged

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

    def __len__(self) -> int:
        return self._shadow.count()

    def __iter__(self) -> Iterator[DataObject]:
        return (t for t in self.objects)

    def __contains__(self, name: str) -> bool:
        return self.find(name) is not None

    def __getitem__(self, key: str) -> DataObject:
        result = self.find(key)
        if result is None:
            raise KeyError
        return result

    def add(self, data_object: DataObject) -> None:
        """
        Добавляет объект данных в хранилище.

        Args:
            data_object: Объект данных для добавления.
        """
        if data_object is not None and self._shadow is not None:
            self._shadow.add(data_object._shadow)
        else:
            raise ValueError("None is not allowed.")

    def remove(self, data_object: DataObject) -> None:
        """
        Удаляет объект данных.

        Объект данных при этом закрывается.

        Args:
            data_object: Объект данных для удаления.
        """
        # backwards compatibility
        if isinstance(data_object, str):
            found_data_object = self.find(data_object)
            if found_data_object is None:
                return None
            data_object = found_data_object

        # backwards compatibility
        if data_object is None:
            return None

        data_object.close()

    def remove_all(self) -> None:
        """Удаляет все объекты данных."""
        for obj in self.objects:
            try:
                self.remove(obj)
            except RuntimeError as e:
                if isinstance(obj, (QueryTable, SelectionTable)):
                    pass
                else:
                    raise e

    def find(self, name: str) -> Optional[DataObject]:
        """
        Производит поиск объект данных по имени.

        Args:
            name: Имя объекта данных.

        Returns:
            Искомый объект данных или None.
        """
        for obj in self.objects:
            if obj is None:
                logging.error(f'Catalog contain empty object.')
                continue
            if obj.name != name:
                continue
            return obj
        return None

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

        Args:
            id: Идентификатор.
        """
        return DataObject._wrap(self._shadow.findById(id))

    def exists(self, obj: DataObject) -> bool:
        """
        Проверяет, присутствует ли объект в каталоге. Проверяет так-же и скрытые
        объекты, которые отсутствуют в общем списке.

        Args:
            obj: проверяемый объект данных.
        """
        if obj is None or obj._shadow is None:
            return False
        return self._shadow.exists(obj._shadow)

    @property
    def objects(self) -> List[DataObject]:
        """Список объектов."""
        return cast(
            List[DataObject],  # Guaranteed not None
            [DataObject._wrap(obj) for obj in self._shadow.dataObjects()],
        )

    @property
    def all_objects(self) -> List[DataObject]:
        """Список всех объектов, включая скрытые."""
        return cast(
            List[DataObject],  # Guaranteed not None
            [DataObject._wrap(obj) for obj in self._shadow.dataObjects(True)],
        )

    @property
    def tables(self) -> List[Table]:
        """Список таблиц."""
        # cast - because mypy picking wrong overload of filter from the typeshed
        return cast(List[Table], list(filter(self._is_table, self.objects)))

    @property
    def selection(self) -> Optional[SelectionTable]:
        """
        Таблица выборки, если она существует.

        See also:
            :attr:`axipy.selection_manager`
        """
        return DataObject._wrap(self._shadow.selectionTable())

    @property
    def sql_dialect(self) -> TypeSqlDialect:
        """
        Тип используемого диалекта по умолчанию для выполнения SQL-предложений. Если
        необходимо переопределить, то для конкретного sql предложения необходимо
        указывать диалект явно :meth:`query`.

        Returns:
            Тип диалекта. Возможные значения `TypeSqlDialect.axioma` или `TypeSqlDialect.sqlite`.
        """
        return TypeSqlDialect(_opener_instance.sql_dialect)

    def check_query(self, query_text: str, dialect: TypeSqlDialect = TypeSqlDialect.sqlite) -> Tuple[bool, str]:
        """
        Производит проверку SQL-запроса на корректность.

        Args:
            query_text: Текст запроса.
            dialect: Диалект, который используется при выполнении запроса.
                Значение по умолчанию установлено как значение свойства :attr:`sql_dialect`.

        Returns:
            Пара значений [Успешность проверки, Сообщение].

        .. literalinclude:: /../../tests/doc_examples/da/test_example_dp.py
            :caption: Пример использования, если работа ведется в рамках Аксиома .
            :pyobject: test_run_example_query
            :lines: 2, 4-6, 11-
            :dedent: 4
        """
        return _opener_instance.check_query(query_text, *self._all_tables, dialect=self._get_dialect(dialect))

    def query(self, query_text: str, dialect: TypeSqlDialect = TypeSqlDialect.sqlite) -> Optional[Table]:
        """
        Выполняет SQL-запрос к перечисленным таблицам.

        Args:
            query_text: Текст запроса.
            dialect: Диалект, который используется при выполнении запроса.
                Значение по умолчанию установлено как значение свойства :attr:`sql_dialect`.

        Returns:
            Таблица, если результатом запроса является таблица.

        Raises:
            RuntimeError: При возникновении ошибки.

        .. literalinclude:: /../../tests/doc_examples/da/test_example_dp.py
            :caption: Пример использования, если работа ведется в рамках Аксиома .
            :pyobject: test_run_example_query
            :lines: 2, 4-10
            :dedent: 4
        """
        return self._query_internal(query_text, False, dialect)

    def query_hidden(self, query_text: str, dialect: TypeSqlDialect = TypeSqlDialect.sqlite) -> Optional[Table]:
        """
        Выполняет SQL-запрос к таблицам. В отличие от :meth:`query` результирующий
        объект :class:`Table` добавляется в каталог как скрытый объект. Он не
        учитывается в общем списке и от него из этого каталога не приходят события.

        Args:
            query_text: Текст запроса.
            dialect: Диалект, который используется при выполнении запроса.
                Значение по умолчанию установлено как :attr:`sql_dialect`.

        Returns:
            Таблица, если результатом запроса является таблица.
        """
        return self._query_internal(query_text, True, dialect)


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

    globals().update(
        data_manager=DataManager(),
    )


_apply_deprecated()
