from typing import (
    TYPE_CHECKING,
    Any,
    Dict,
    Iterator,
    List,
    Optional,
    Tuple,
    Union,
    cast,
)

from axipy._internal._decorator import _experimental
from axipy.cpp_core_dp import ShadowFeature, ShadowFeatureIterator

from .geometry import Geometry
from .style import Style

__all__: List[str] = [
    "GEOMETRY_ATTR",
    "STYLE_ATTR",
    "Feature",
]

GEOMETRY_ATTR = "+geometry"
"""Имя геометрического атрибута."""
STYLE_ATTR = "+style"
"""Имя атрибута со стилем."""


class Feature:
    """
    Запись в таблице.

    Работа с записью похожа на работу со словарем :any:`dict`. Но также
    допускает обращение по индексу.

    .. literalinclude:: /../../tests/doc_examples/da/test_example_feature.py
        :caption: Примеры.
        :pyobject: test_run_example_feature
        :lines: 2-
        :dedent: 4

    Note:
        Для доступа к геометрическому атрибуту и стилю по наименованию можно использовать предопределенные идентификаторы
        `+geometry` и `+style` соответственно:

        * GEOMETRY_ATTR=+geometry
        * STYLE_ATTR=+style
    """

    # noinspection PyDefaultArgument
    # noinspection PyShadowingBuiltins
    def __init__(
        self,
        properties: dict = dict(),
        geometry: Optional[Geometry] = None,
        style: Optional[Style] = None,
        id: Optional[int] = None,
        **kwargs: Any,
    ) -> None:
        """
        Конструктор класса.

        Args:
            properties: Значения атрибутов.
            geometry: Геометрия.
            style: Стиль.
            id: Идентификатор.
            **kwargs: Значения атрибутов.
        """
        # default properties dict should not be changed!
        self._shadow = ShadowFeature({**properties, **kwargs})

        if geometry is not None:
            self.geometry = geometry

        if style is not None:
            self.style = style
        elif geometry:
            self.style = Style.for_geometry(geometry)

        if id is not None:
            self.id = id

    @classmethod
    def _wrap(cls, shadow: ShadowFeature) -> "Feature":
        result = cls.__new__(cls)
        result._shadow = shadow
        return result

    @property
    def id(self) -> int:
        """
        Устанавливает или возвращает идентификатор записи в таблице.

        Несохраненные записи в таблице будут иметь отрицательное значение.

        See Also:
            :attr:`Table.is_modified`, :meth:`Table.commit`

        Returns:
            0 если идентификатор не задан.
        """
        return self._shadow.id()

    @id.setter
    def id(self, value: int) -> None:
        self._shadow.setId(value)

    def __contains__(self, key: Union[int, str]) -> bool:
        """
        Проверяет наличие атрибута у записи.

        Args:
            key: Имя или номер атрибута.
        """
        return key in self._shadow

    def __len__(self) -> int:
        """Возвращает количество атрибутов в записи."""
        return len(self._shadow)

    def __getitem__(self, index: Union[int, str]) -> Any:
        """
        Возвращает значение атрибута по индексу или ключу.

        Args:
            index: Индекс атрибута или его имя.
        """
        if isinstance(index, int):
            if index not in self:
                raise IndexError("Index out of bounds")
            index = self._shadow.indexToKey(index)
        elif isinstance(index, str):
            if index not in self:
                raise KeyError(f"Key '{index}' not found")
        if index == GEOMETRY_ATTR:
            return Geometry._wrap(ShadowFeature.geometry(self._shadow))
        if index == STYLE_ATTR:
            return Style._wrap(ShadowFeature.style(self._shadow))
        result = self._shadow[index]
        return result

    if TYPE_CHECKING:

        def __iter__(self) -> Iterator[Any]: ...

    def __setitem__(self, index: Union[int, str], value: Any) -> None:
        """
        Присваивает атрибуту значение по индексу или ключу.

        Args:
            index: индекс атрибута или его имя.
            value: Присваиваемое значение.
        """
        if isinstance(index, int):
            if index not in self:
                raise IndexError("Index out of bounds")
            index = self._shadow.indexToKey(index)
        if index == GEOMETRY_ATTR:
            self._set_geometry(value)
            return None
        if index == STYLE_ATTR:
            self._set_style(value)
            return None
        self._shadow[index] = value

    def get(self, key: str, default: Optional[Any] = None) -> Any:
        """
        Возвращает значение заданного атрибута.

        Args:
            key: Имя атрибута.
            default: Значение по умолчанию.

        Returns:
            Искомое значение, или значение по умолчанию, если заданный
            атрибут отсутствует.
        """
        return self[key] if key in self else default

    @property
    def geometry(self) -> Optional[Geometry]:
        """
        Устанавливает или возвращает геометрию записи.

        See Also:

            :meth:`Feature.has_geometry`, :attr:`GEOMETRY_ATTR`

        Returns:
            Значение геометрического атрибута; или None, если значение
            пустое или отсутствует.
        """
        if GEOMETRY_ATTR not in self:
            return None
        return self[GEOMETRY_ATTR]

    @geometry.setter
    def geometry(self, value: Optional[Geometry]) -> None:
        self._set_geometry(value)

    def has_geometry(self) -> bool:
        """Проверяет, имеет ли запись атрибут с геометрией."""
        return self._shadow.hasGeometry()

    @property
    def style(self) -> Optional[Style]:
        """
        Устанавливает или возвращает стиль записи.

        See Also:

            :meth:`Feature.has_style`, :attr:`STYLE_ATTR`

        Returns:
            Значение атрибута со стилем; или None, если значение
            пустое или отсутствует.
        """
        if STYLE_ATTR not in self:
            return None
        return self[STYLE_ATTR]

    @style.setter
    def style(self, value: Optional[Style]) -> None:
        self._set_style(value)

    def has_style(self) -> bool:
        """Проверяет, имеет ли запись атрибут со стилем."""
        return self._shadow.hasStyle()

    def _set_geometry(self, value: Optional[Geometry]) -> None:
        if value is None:
            self._shadow[GEOMETRY_ATTR] = value
            return None
        self._shadow.setGeometry(value._shadow)

    def _set_style(self, value: Optional[Style]) -> None:
        if value is None:
            self._shadow[STYLE_ATTR] = value
            return None
        self._shadow.setStyle(value._shadow)

    def to_geojson(self) -> dict:
        """Представляет запись в виде, похожем на 'GeoJSON'."""
        result: Dict[str, Any] = {"type": "Feature"}
        if self.has_geometry():
            geometry: Geometry = cast(Geometry, self.geometry)
            bbox = geometry.bounds
            result["bbox"] = [bbox.xmin, bbox.ymin, bbox.xmax, bbox.ymax]
            result["geometry"] = geometry.to_geojson()
        if self.has_style():
            result["style"] = cast(Style, self.style).to_mapinfo()
        result["properties"] = {item[0]: item[1] for item in self.items() if item[0] not in [GEOMETRY_ATTR, STYLE_ATTR]}
        return result

    def keys(self) -> List[str]:
        """Возвращает список имен атрибутов."""
        return self._shadow.keys()

    def values(self) -> List[Any]:
        """Возвращает список значений атрибутов."""
        return [self[key] for key in self._shadow.keys()]

    def items(self) -> List[Tuple[str, Any]]:
        """Возвращает список пар имя — значение."""
        keys = self.keys()
        return [(key, self[key]) for key in keys]

    @_experimental()
    def _to_clipboard(self) -> None:
        """Копирует данную запись в буфер обмена"""
        self._shadow.to_clipboard()

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Feature):
            return NotImplemented

        if len(self) != len(other):
            return False
        other_keys = other.keys()
        for key in self.keys():
            if key in other_keys:
                continue
            return False
        for key in self.keys():
            if self[key] == other[key]:
                continue
            return False
        return True


class _FeatureIterator:
    def __init__(self, shadow: ShadowFeatureIterator) -> None:
        self._shadow: ShadowFeatureIterator = shadow

    def __iter__(self) -> Iterator[Feature]:
        return self

    def __next__(self) -> Feature:
        return Feature._wrap(next(self._shadow))

    def __repr__(self) -> str:
        return "Iterator[axipy.Feature]"
