import os.path
from pathlib import Path
from typing import (
    TYPE_CHECKING,
    Any,
    Dict,
    Iterator,
    List,
    Optional,
    Tuple,
    Type,
    TypeVar,
    Union,
)

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

from .csv_data_provider import CsvDataProvider
from .data_provider import DataProvider
from .dwg_data_provider import DwgDataProvider
from .excel_data_provider import ExcelDataProvider
from .gdal_data_provider import GdalDataProvider
from .generic_provider import GenericDestination, GenericSource
from .mif_data_provider import MifMidDataProvider
from .mssql_data_provider import MsSqlDataProvider
from .ogr_data_provider import OgrDataProvider
from .opener import _opener_instance
from .oracle_data_provider import OracleDataProvider
from .panorama_data_provider import PanoramaDataProvider
from .postgre_data_provider import PostgreDataProvider
from .rest_data_provider import RestDataProvider
from .shp_data_provider import ShapeDataProvider
from .source import Source
from .sqlite_data_provider import SqliteDataProvider
from .svg_data_provider import SvgDataProvider
from .tab_data_provider import TabDataProvider
from .tms_data_provider import TmsDataProvider
from .wms_data_provider import WmsDataProvider
from .wmts_data_provider import WmtsDataProvider

if TYPE_CHECKING:
    from axipy.da import DataObject, QueryTable, Schema, Table, _MapCatalog

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

_DataProviderT = TypeVar("_DataProviderT", bound=DataProvider)


class ProviderManager(_Singleton, _AxiRepr, metaclass=_AxiReprMeta):
    """
    Класс открытия/создания объектов данных :class:`DataProvider`.

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

    Note:
        Для удобного задания параметров используйте экземпляры провайдеров:
        :attr:`tab`, :attr:`shp`, :attr:`csv`, :attr:`mif`, :attr:`excel`,
        :attr:`sqlite`, :attr:`postgre`, :attr:`oracle`, :attr:`mssql`,
        :attr:`ogr`, :attr:`svg`, :attr:`gdal`, :attr:`rest`, :attr:`tms`, :attr:`wms`,
        :attr:`wmts`, :attr:`dwg`, :attr:`panorama`.

    Note:
        Открытые данные автоматически попадают в хранилище данных
        :class:`axipy.DataManager`.

    Пример открытия локальной таблицы::

        table = provider_manager.openfile('../path/to/datadir/table.tab')
    """

    if TYPE_CHECKING:
        _PROVIDER_TYPES = Union[
            Type[TabDataProvider],
            Type[ShapeDataProvider],
            Type[CsvDataProvider],
            Type[MifMidDataProvider],
            Type[ExcelDataProvider],
            Type[SqliteDataProvider],
            Type[PostgreDataProvider],
            Type[OracleDataProvider],
            Type[MsSqlDataProvider],
            Type[RestDataProvider],
            Type[GdalDataProvider],
            Type[TmsDataProvider],
            Type[WmtsDataProvider],
            Type[OgrDataProvider],
            Type[SvgDataProvider],
            Type[WmsDataProvider],
            Type[DwgDataProvider],
            Type[PanoramaDataProvider],
        ]

        _PROVIDER_CLASSES = Union[
            TabDataProvider,
            ShapeDataProvider,
            CsvDataProvider,
            MifMidDataProvider,
            ExcelDataProvider,
            SqliteDataProvider,
            PostgreDataProvider,
            OracleDataProvider,
            MsSqlDataProvider,
            RestDataProvider,
            GdalDataProvider,
            TmsDataProvider,
            WmtsDataProvider,
            OgrDataProvider,
            SvgDataProvider,
            WmsDataProvider,
            DwgDataProvider,
            PanoramaDataProvider,
        ]

    def __init__(self) -> None:
        self.__providers_inner: Optional[List[DataProvider]] = None

    def __init_providers_inner(self) -> List[DataProvider]:
        known_types: Tuple[Type[DataProvider], ...] = (
            TabDataProvider,
            ShapeDataProvider,
            CsvDataProvider,
            MifMidDataProvider,
            ExcelDataProvider,
            SqliteDataProvider,
            PostgreDataProvider,
            OracleDataProvider,
            MsSqlDataProvider,
            RestDataProvider,
            GdalDataProvider,
            TmsDataProvider,
            WmtsDataProvider,
            OgrDataProvider,
            SvgDataProvider,
            WmsDataProvider,
            DwgDataProvider,
            PanoramaDataProvider,
        )

        providers = []
        infos = ShadowConverter.providersInfo()

        # Preprocess known_types into a dictionary for faster lookups
        known_types_dict: Dict[str, List[Type[DataProvider]]] = {}
        for p in known_types:
            provider_id: str = p._identifier()
            if provider_id not in known_types_dict:
                known_types_dict[provider_id] = []
            known_types_dict[provider_id].append(p)

        # Iterate over infos and find matching providers
        for info in infos:
            provider_id = info["id"]
            if provider_id in known_types_dict:
                for p in known_types_dict[provider_id]:
                    providers.append(p(info))

        return providers

    @property
    def __providers(self) -> List[DataProvider]:
        if self.__providers_inner is None:
            self.__providers_inner = self.__init_providers_inner()
        return self.__providers_inner

    @property
    def svg(self) -> SvgDataProvider:
        """Провайдер для SVG."""
        return self._find_by_type(SvgDataProvider)

    @property
    def tab(self) -> TabDataProvider:
        """Провайдер MapInfo."""
        return self._find_by_type(TabDataProvider)

    @property
    def shp(self) -> ShapeDataProvider:
        """Векторный провайдер SHP."""
        return self._find_by_type(ShapeDataProvider)

    @property
    def csv(self) -> CsvDataProvider:
        """Файловый провайдер — Текст с разделителями."""
        return self._find_by_type(CsvDataProvider)

    @property
    def mif(self) -> MifMidDataProvider:
        """Провайдер данных MIF-MID."""
        return self._find_by_type(MifMidDataProvider)

    @property
    def excel(self) -> ExcelDataProvider:
        """Провайдер чтения файлов Excel."""
        return self._find_by_type(ExcelDataProvider)

    @property
    def sqlite(self) -> SqliteDataProvider:
        """Векторный провайдер sqlite."""
        return self._find_by_type(SqliteDataProvider)

    @property
    def postgre(self) -> PostgreDataProvider:
        """Провайдер для базы данных PostgreSQL."""
        return self._find_by_type(PostgreDataProvider)

    @property
    def mssql(self) -> MsSqlDataProvider:
        """Провайдер для базы данных MSSQLServer."""
        return self._find_by_type(MsSqlDataProvider)

    @property
    def oracle(self) -> OracleDataProvider:
        """Провайдер для базы данных Oracle."""
        return self._find_by_type(OracleDataProvider)

    @property
    def rest(self) -> RestDataProvider:
        """Провайдер REST."""
        return self._find_by_type(RestDataProvider)

    @property
    def gdal(self) -> GdalDataProvider:
        """Растровый провайдер GDAL."""
        return self._find_by_type(GdalDataProvider)

    @property
    def tms(self) -> TmsDataProvider:
        """Тайловый провайдер."""
        return self._find_by_type(TmsDataProvider)

    @property
    def wms(self) -> WmsDataProvider:
        """Web Map Service."""
        return self._find_by_type(WmsDataProvider)

    @property
    def wmts(self) -> WmtsDataProvider:
        """Web Map Tile Service."""
        return self._find_by_type(WmtsDataProvider)

    @property
    def ogr(self) -> OgrDataProvider:
        """Векторный провайдер OGR."""
        return self._find_by_type(OgrDataProvider)

    @property
    def dwg(self) -> DwgDataProvider:
        """Провайдер данных AutoCAD."""
        return self._find_by_type(DwgDataProvider)

    @property
    def panorama(self) -> PanoramaDataProvider:
        """Провайдер данных ГИС Панорама."""
        return self._find_by_type(PanoramaDataProvider)

    # noinspection PyShadowingBuiltins
    def _find_by_id(self, id: str) -> Optional[DataProvider]:
        for provider in self.__providers:
            if provider.id == id:
                return provider
        return None

    def find_for_extension(self, file_extension: str) -> List[DataProvider]:
        """
        Возвращает список провайдеров, поддерживающих расширение.

        Args:
            file_extension: Расширение файла.
        """
        if file_extension.startswith("."):
            file_extension = file_extension[1:]

        providers = []
        for provider in self.__providers:
            if file_extension.lower() in [ext.lower() for ext in provider.file_extensions()]:
                providers.append(provider)
        return providers

    def _find_by_type(self, typedef: Type[_DataProviderT]) -> _DataProviderT:
        for provider in self.__providers:
            if type(provider) is typedef:
                return provider
        raise RuntimeError(f"Could not find provider of type {typedef}.")

    def __openfile_find_provider(self, filepath: str, kwargs: dict) -> Optional[DataProvider]:
        _, ext = os.path.splitext(filepath)
        if len(ext) == 0:
            raise ValueError("Extension of the file is empty.")

        # Try to get the provider from kwargs
        if "provider" in kwargs:
            return self._find_by_id(kwargs.pop("provider"))

        # Try to find the provider by extension
        providers = self.find_for_extension(ext)
        if providers:
            return providers[0]

        # Try to find provider by filepath
        provider_id = _opener_instance._shadow.findProviderToOpen({"src": filepath})
        if provider_id:
            return self._find_by_id(provider_id)

        return None

    def openfile(self, filepath: Union[str, Path], *args: Any, **kwargs: Any) -> "DataObject":
        """
        Открывает данные из файла.

        Args:
            filepath: Путь к открываемому файлу.
            **kwargs: Именованные аргументы. Возможные варианты от провайдера. Подробнее см. :meth:`open`

        Пример::

            table = provider_manager.openfile('../path/to/datadir/example.gpkg')
        """
        if isinstance(filepath, Path):
            filepath = str(filepath)

        provider = self.__openfile_find_provider(filepath, kwargs)

        # Raise an exception if no provider is found
        if not provider:
            raise ValueError(f"No provider found for file: {filepath}")

        # Open the file with the found provider
        return provider.open(filepath, *args, **kwargs)

    def open(self, definition: dict) -> "DataObject":
        """
        Открывает данные по описанию.

        Формат описания объектов данных (набор и тип параметров) индивидуален для каждого провайдера данных,
        однако многие элементы используются для всех провайдеров данных.
        В нижеприведенной таблице можно определить какие параметр можно указывать при открытии того или иного источника.
        Т.е. допустимые параметры для конкретного провайдера указаны в соответствующем методе open для этого провайдера.

        .. csv-table:: Доступные провайдеры данных и ссылки на дополнительные параметры:
            :header: "Провайдер", "Краткое описание", "Ссылка"
            :align: left
            :widths: 10, 5, 40

            :attr:`tab`, Провайдер MapInfo, :meth:`axipy.TabDataProvider.open`
            :attr:`csv`, Текст с разделителями, :meth:`axipy.CsvDataProvider.open`
            :attr:`ogr`, Векторный провайдер OGR, :meth:`axipy.OgrDataProvider.open`
            :attr:`excel`, Провайдер чтения файлов Excel, :meth:`axipy.ExcelDataProvider.open`
            :attr:`shp`, Векторный провайдер SHP, :meth:`axipy.ShapeDataProvider.open`
            :attr:`sqlite`, Векторный провайдер sqlite, :meth:`axipy.SqliteDataProvider.open`
            :attr:`svg`, Провайдер для SVG, :meth:`axipy.SvgDataProvider.open`
            :attr:`gdal`, Растровый провайдер GDAL, :meth:`axipy.GdalDataProvider.open`
            :attr:`postgre`, Провайдер для базы данных PostgreSQL, :meth:`axipy.PostgreDataProvider.open`
            :attr:`oracle`, Провайдер для базы данных Oracle, :meth:`axipy.OracleDataProvider.open`
            :attr:`mssql`, Провайдер для базы данных MSSQLServer, :meth:`axipy.MsSqlDataProvider.open`
            :attr:`rest`, Провайдер REST, :meth:`axipy.RestDataProvider.open`
            :attr:`tms`, Тайловый провайдер, :meth:`axipy.TmsDataProvider.open`
            :attr:`wms`, Web Map Service, :meth:`axipy.WmsDataProvider.open`
            :attr:`wmts`, Web Map Tile Service, :meth:`axipy.WmtsDataProvider.open`
            :attr:`dwg`, Провайдер файлов AutoCAD, :meth:`axipy.DwgDataProvider.open`
            :attr:`panorama`, Провайдер файлов ГИС Панорама, :meth:`axipy.PanoramaDataProvider.open`

        Также существуют параметры, которые допустимы независимо от типа провайдера

        .. csv-table:: Доступные провайдеры данных и ссылки на дополнительные параметры:
            :header: "Параметр", "Краткое описание"
            :align: left
            :widths: 10, 40

            provider, "| Используемый провайдер. Допустимые значения можно получить :meth:`loaded_providers`.
            | Если не задан, то система пытается его определить самостоятельно."
            src, "| Ссылка на источник. Как правило, это имя файла.
            | Для конкретного провайдера может дублироваться под другим именем."
            dataobject, "Если источник содержит несколько таблиц, то имя конкретного указывается через данный параметр "
            alias, "| Псевдоним для открываемого источника данных.
            | В системе открытый объект будет доступен по этому имени"


        Args:
            definition: Описание объекта данных.

        Пример открытия файла (аналогичен :meth:`openfile`)::

            json = {'src':'../path/to/datadir/world.tab'}
            table_world = provider_manager.open(json)

        или, что тоже самое::

            json = {'filepath':'../path/to/datadir/world.tab'}
            table_world = provider_manager.open(json)


        Пример открытия файла с несколькими таблицами::

            # Пример открытия GPKG файла::
            definition = { 'src': '../path/to/datadir/example.gpkg',
                           'dataobject': 'tablename' }
            table = provider_manager.open(definition)

        Пример открытия таблицы базы данных::

            definition = {"host": "localhost",
                          "db": "sample",
                          "user": "postgres",
                          "password": "postgres",
                          "dataobject": "public.world",
                          "provider": "PgDataProvider"}
            table = provider_manager.open(definition)
        """
        source = GenericSource(definition)
        return source.open()

    def open_hidden(self, definition: dict) -> "DataObject":
        """
        Открывает данные по описанию. Аналогична функции :meth:`open`, за исключением
        того, что когда данный объект добавляется в каталог, он не учитывается в общем
        списке и от него из этого каталога не приходят события.

        Note:
            См. также :meth:`open`

        Args:
            definition: Описание объекта данных.

        Пример::

            table = axipy.provider_manager.open_hidden({'src':'world.tab'})
            print(len(axipy.data_manager), axipy.data_manager.exists(table))
            axipy.data_manager.remove(table)
            >>> 0 True
        """
        if "hidden" not in definition:
            definition_copy = definition.copy()
            definition_copy["hidden"] = True
            return self.open(definition_copy)
        return self.open(definition)

    @_experimental()
    def _open_hidden_check_if_close_needed(self, definition: Source) -> Tuple["DataObject", bool]:
        table_file = Path(definition["src"])

        all_tables: Iterator[axipy.Table] = filter(
            lambda x: isinstance(x, axipy.Table),  # type: ignore[arg-type]
            axipy.data_manager.all_objects,
        )
        all_file_names: Iterator[Optional[Path]] = map(lambda x: x.file_name, all_tables)
        all_file_names_filtered: Iterator[Path] = filter(None, all_file_names)

        for file_name in all_file_names_filtered:
            if file_name == table_file:
                return definition.open(), False

        definition["hidden"] = True
        return definition.open(), True

    def createfile(self, filepath: str, schema: "Schema", *args: Any, **kwargs: Any) -> "DataObject":
        """
        Создает таблицу.

        :meth:`create` выполняет ту же функцию, но в более обобщенном виде.

        Args:
            filepath: Путь к создаваемой таблице.
            schema: Схема таблицы.
        """
        _, ext = os.path.splitext(filepath)
        if len(ext) == 0:
            raise ValueError("Extension of the file is empty.")

        result = self.find_for_extension(ext[1:])
        if result:
            provider: DataProvider = result[0]
        else:
            raise ValueError(f"Could not find provider for file extension: {ext}")
        return provider.create(filepath, schema, *args, **kwargs)

    def create(self, definition: dict) -> "DataObject":
        """
        Создает и открывает данные из описания.

        Args:
            definition: Описание объекта данных.

        Псевдоним :meth:`create_open`.
        """
        return self.create_open(definition)

    def create_open(self, definition: dict) -> "DataObject":
        """
        Создает и открывает данные из описания.

        Возможные параметры:
            - ``src`` - Строка, определяющая местоположение источника данных. Это может быть либо путь к файлу с расширением TAB, либо пустая строка (для таблицы, размещаемой в памяти).
            - ``schema`` - Схема таблицы. Задается массивом объектов, содержащих атрибуты.
            - ``hidden`` - Если указано True, то созданный объект не будет зарегистрирован в каталоге. См. также :meth:`open_hidden`
            - ``override`` - Если указано True, то при наличии файла в файловой системе, он перезаписывается. В противном случае вызывается исключение.

        Args:
            definition: Описание объекта данных.

        Пример::

            definition = {
                'src': '../path/to/datadir/edit/table.tab',
                'schema': attr.schema(
                    attr.string('field1'),
                    attr.integer('field2'),
                ),
            }
            table = provider_manager.create(definition)
        """
        copy_definition = definition.copy()
        schema = copy_definition.pop("schema")
        dest = GenericDestination(schema, copy_definition)
        return dest.create_open()

    def loaded_providers(self) -> Dict[str, str]:
        """
        Возвращает список всех загруженных провайдеров данных.

        Returns:
            Провайдеры в виде пар ``(Идентификатор : Описание)``.
        """
        return _opener_instance.loaded_providers()

    def providers(self) -> List[DataProvider]:
        """
        Возвращает список всех загруженных провайдеров данных.

        Returns:
            Список провайдеров.
        """
        return self.__providers.copy()

    def read_contents(self, definition: dict) -> List[str]:
        """
        Читает содержимое источника данных.

        Обычно используется для источников, способных содержать несколько
        объектов данных.

        Args:
            definition: Описание источника данных.

        Returns:
            Имена объектов данных.

        Пример::

            contents = axipy.provider_manager.read_contents('../path/to/datadir/example.gpkg')
            print(contents)
            >>> ['world', 'worldcap']

            world = provider_manager.openfile('../path/to/datadir/example.gpkg', dataobject='world')


        Пример с СУБД::

            src = provider_manager.postgre.get_source(host='ip_server',
                                                            db_name='test',
                                                            user='user',
                                                            password='pass')
            contents = provider_manager.read_contents(src)
            print(contents)

        Пример с файлом AutoCAD::

            src = axipy.provider_manager.dwg.get_source("sample.dwg")
            contents = provider_manager.read_contents(src)
            print(contents)
        """
        return _opener_instance.read_contents(definition)

    @_experimental()
    def _mapinfo_mapcatalog(self, definition: dict) -> "_MapCatalog":
        """
        Возвращает объект класса :class:`_MapCatalog` работы с каталогом

        Пример::

            src = provider_manager.postgre.get_source(host='ip_address', db_name='test', user='postgres', password='postgres')
            catalog = axipy.provider_manager._mapinfo_mapcatalog(src)

        Raises:
            NotImplementedError: Если источник не поддерживает работу с каталогом
        """
        catalog = _shadow_manager.shadow_io.mapinfo_mapcatalog(definition)
        if catalog is None:
            raise NotImplementedError("Провайдер не поддерживает работу с каталогом")
        return axipy.da._MapCatalog._wrap(catalog)


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

    def query(_self: ProviderManager, query_text: str, *tables: "Table") -> Optional["QueryTable"]:
        """
        Warning:
            .. deprecated:: 6.3.0
        """
        return _opener_instance.query(query_text, *tables)

    def check_query(_self: ProviderManager, query_text: str, *tables: "Table") -> Tuple[bool, str]:
        """
        Warning:
            .. deprecated:: 6.3.0
        """
        return _opener_instance.check_query(query_text, *tables)

    def find_by_extension(self: ProviderManager, file_extension: str) -> DataProvider:
        """
        Warning:
            .. deprecated:: 6.3.0
        """
        for provider in self.__providers:
            if file_extension.lower() in [ext.lower() for ext in provider.file_extensions()]:
                return provider
        raise ValueError(f"Could not find provider for file extension: {file_extension}")

    setattr(ProviderManager, "query", _deprecated_by("axipy.DataManager.query")(query))
    setattr(
        ProviderManager,
        "check_query",
        _deprecated_by("axipy.DataManager.check_query")(check_query),
    )
    setattr(
        ProviderManager,
        "find_by_extension",
        _deprecated_by("axipy.ProviderManager.find_for_extension")(find_by_extension),
    )

    provider_manager = ProviderManager()

    globals().update(provider_manager=provider_manager)


_apply_deprecated()
