from pathlib import Path
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Union

from axipy._internal._decorator import _experimental
from axipy.cpp_core_dp import ShadowConverter

from ..data_object import DataObject, Table, _TabMetaData
from ..feature import Feature
from ..schema import Schema
from .opener import _opener_instance as _opener

__all__: List[str] = [
    "Source",
    "Destination",
    "ExportParameters",
]


class Source(dict):
    """
    Источник данных.

    Используется для открытия данных или для указания источника при конвертации.

    .. literalinclude:: /../../tests/doc_examples/da/test_example_export.py
        :pyobject: example_export_src_dest
        :lines: 2-3,8-
        :dedent: 4
        :caption: Пример открытия и конвертации

    Note:
        Не все провайдеры поддерживают открытие и конвертацию. См. описание
        конкретного провайдера данных.

    .. seealso::
        :ref:`ref-converter`

    .. seealso::
        :class:`Destination`
    """

    def __init__(self, *args: Any) -> None:
        if type(self) is Source:
            raise NotImplementedError
        _merged = dict()
        for d in args:
            _merged.update(d)
        super().__init__(_merged)

    @classmethod
    def _table_file(cls, filepath: Union[str, Path]) -> Dict:
        return {"src": str(filepath)}

    @classmethod
    def _table_db(cls, db_name: str, host: str, user: str, password: str, port: str) -> Dict:
        return {
            "src": host,
            "db": db_name,
            "user": user,
            "password": password,
            "port": port,
        }

    @classmethod
    def _provider(cls, provider_id: str) -> Dict:
        return {"provider": provider_id}

    @classmethod
    def _alias(cls, alias: Optional[str]) -> Dict:
        if alias:
            return {"alias": alias}
        return {}

    @classmethod
    def _prj(cls, prj: Optional[str]) -> Dict:
        if prj:
            return {"prj": prj}
        return {}

    def open(self) -> DataObject:
        """Открывает объект данных."""
        return _opener.open(self)


class Destination(dict):
    """
    Назначение объекта данных.

    Используется для создания данных или для указания назначения при конвертации.

    .. literalinclude:: /../../tests/doc_examples/da/test_example_export.py
        :pyobject: example_export_src_dest
        :lines: 2-3,8-
        :dedent: 4
        :caption: Пример открытия и конвертации


    Note:
        Не все провайдеры поддерживают создание и конвертацию. См. описание
        конкретного провайдера данных.

    .. seealso::
        :ref:`ref-converter`

    .. seealso::
        :class:`Source`
    """

    def __init__(self, schema: Schema, *args: Any) -> None:
        if type(self) is Destination:
            raise NotImplementedError
        _merged = {"schema": schema._to_dict()}
        for d in args:
            _merged.update(d)
        super().__init__(_merged)

    def create_open(self) -> DataObject:
        """Создает и открывает объект данных."""
        return _opener.create(self)

    @staticmethod
    def _feature_callback_generator(
        func_callback: Callable[[Feature, int], Optional[bool]],
        features: Iterable[Feature],
    ) -> Generator[Feature, Any, None]:
        for idx, f in enumerate(features):
            r = func_callback(f, idx)
            if r is False:
                return None
            else:
                yield f

    def export(
        self,
        features: Iterable[Feature],
        func_callback: Optional[Callable[[Feature, int], Optional[bool]]] = None,
    ) -> None:
        """
        Создает объект данных и экспортирует в него записи.

        Args:
            features: Записи.
            func_callback: Функция, которая будет вызываться после экспорта каждой записи.
                В определении должны быть параметры следующих типов:

                    * feature :class:`Feature` - текущая запись
                    * row :class:`int` - порядковый номер

                Возможно прерывание процесса экспорта, для этого нужно вернуть False в func_callback.

        .. literalinclude:: /../../tests/doc_examples/da/test_example_export.py
            :pyobject: test_run_example_export_features
            :lines: 2-7, 11-
            :dedent: 4
            :caption: Пример экспорта данных
        """
        self._export_features_common(features, self, func_callback, None)

    @_experimental()
    def _export(
        self,
        features: Iterable[Feature],
        func_callback: Optional[Callable[[Feature, int], Optional[bool]]] = None,
        metadata: Optional[_TabMetaData] = None,
    ) -> None:
        """
        Создает объект данных и экспортирует в него записи.

        Args:
            features: Записи.
            func_callback: Функция, которая будет вызываться после экспорта каждой записи.
                В определении должны быть параметры следующих типов:

                    * feature :class:`Feature` - текущая запись
                    * row :class:`int` - порядковый номер

                Возможно прерывание процесса экспорта, для этого нужно вернуть False в func_callback.
            metadata: Метаданные для записи в TAB файл

        .. literalinclude:: /../../tests/doc_examples/da/test_example_export.py
            :pyobject: test_run_example_export_features
            :lines: 2-7, 11-
            :dedent: 4
            :caption: Пример экспорта данных

        .. literalinclude:: /../../tests/doc_examples/da/test_example_export.py
            :pyobject: example_export_meta
            :start-after: # start
            :dedent: 4
            :caption: Пример экспорта с метаданными
        """
        self._export_features_common(features, self, func_callback, metadata)

    def export_from_table(
        self,
        table: Table,
        copy_schema: bool = False,
        func_callback: Optional[Callable[[Feature, int], Optional[bool]]] = None,
    ) -> None:
        """
        Создает объект данных и экспортирует в него записи из таблицы.

        Args:
            table: Таблица.
            copy_schema: Копировать схему источника без изменений.
            func_callback: Функция, которая будет вызываться после экспорта каждой записи.
                В определении должны быть параметры следующих типов:

                    * feature :class:`Feature` - текущая запись
                    * row :class:`int` - порядковый номер

                Возможно прерывание процесса экспорта, для этого нужно вернуть False в func_callback.

        .. literalinclude:: /../../tests/doc_examples/da/test_example_export.py
            :pyobject: example_export_tab
            :lines: 2-
            :dedent: 4
            :caption: Пример экспорта таблицы с прогрессом

        .. literalinclude:: /../../tests/doc_examples/da/test_example_export.py
            :pyobject: example_export_csv
            :lines: 2-
            :dedent: 4
            :caption: Пример экспорта таблицы в формат CSV
        """

        dest: dict = self.copy()
        if copy_schema:
            dest.update({"schema": table.schema._to_dict()})

        features: Iterable[Feature] = table
        if func_callback is not None:
            features = self._feature_callback_generator(func_callback, table)

        self._export_features_common(features, dest, func_callback, table._metadata)

    def _export_features_common(
        self,
        features: Iterable[Feature],
        dest: dict,
        func_callback: Optional[Callable[[Feature, int], Optional[bool]]],
        metadata: Optional[_TabMetaData] = None,
    ) -> None:
        if func_callback is not None:
            features = self._feature_callback_generator(func_callback, features)
        meta = {}
        if metadata is not None:
            for key in metadata.keys:
                meta[key] = metadata[key]
        ShadowConverter.saveAs((f._shadow for f in features), dest, meta)

    def export_from(self, source: Source, copy_schema: bool = False) -> None:
        """
        Создает объект данных и экспортирует в него записи из источника данных.

        Args:
            source: Источник данных.
            copy_schema: Копировать схему источника без изменений.
        """
        dest: dict = self.copy()
        if copy_schema:
            del dest["schema"]
        ShadowConverter.convert(source, dest)


class ExportParameters:
    """
    Дополнительные параметры экспорта в таблицу базы данных.

    .. seealso::
       Пример использования см в главе :ref:`ref-converter`
    """

    createIndex: bool = True
    """Создавать пространственный индекс."""
    dropTable: bool = False
    """Предварительно удалять существующую таблицу, если она присутствует в БД."""
    geometryColumnName: Optional[str] = None
    """Наименование геометрической колонки."""
    renditonColumnName: Optional[str] = None
    """Наименование колонки с оформлением."""
    srid: int = 0
    """Значение SRID."""
    logFile: Optional[str] = None
    """Наименование файла, куда будут прописываться успешно выполненные команды."""
    errorFile: Optional[str] = None
    """Наименование файла, куда будут прописываться команды по вставке записей, не
    принятых сервером."""
    mapCatalog: bool = True
    """Регистрация импортируемой таблицы в mapinfo.mapinfo_mapcatalog."""
    geometryAsText: bool = False
    """Геометрию экспортировать как текстовые объекты."""
    fixGeometry: bool = False
    """Пробовать исправлять инвалидную геометрию."""

    def to_dict(self) -> Dict[str, Any]:
        res: Dict[str, Any] = {
            "createIndex": self.createIndex,
            "dropTable": self.dropTable,
            "mapCatalog": self.mapCatalog,
            "geometryAsText": self.geometryAsText,
            "fixGeometry": self.fixGeometry,
            "srid": self.srid,
        }
        if self.geometryColumnName:
            res["geometryColumnName"] = self.geometryColumnName
        if self.renditonColumnName:
            res["renditonColumnName"] = self.renditonColumnName
        if self.logFile:
            res["logFile"] = self.logFile
        if self.errorFile:
            res["errorFile"] = self.errorFile
        return res
