from typing import (
    TYPE_CHECKING,
    Callable,
    Iterable,
    List,
    Optional,
    Tuple,
    Union,
    cast,
    overload,
)

from axipy._internal._decorator import _deprecated_by, _experimental
from axipy._internal._shadow_instance_factory import _shadow_manager
from axipy.cpp_cs import ShadowCoordSystem
from axipy.utl import CalcMode, Pnt, Rect
from PySide2.QtCore import QPointF, QRectF

from .unit import LinearUnit

if TYPE_CHECKING:
    from axipy.cpp_cs import ShadowCoordSysFactory

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


@overload
def _internal_transform(value: Union[QRectF, Rect], func: Callable) -> Rect: ...


@overload
def _internal_transform(value: Union[Tuple[float, float], Pnt, QPointF], func: Callable) -> Pnt: ...


@overload
def _internal_transform(value: Iterable[Union[Tuple[float, float], Pnt, QPointF]], func: Callable) -> List[Pnt]: ...


def _internal_transform(
    value: Union[QRectF, Rect, Tuple[float, float], Pnt, QPointF, Iterable[Union[Tuple[float, float], Pnt, QPointF]]],
    func: Callable,
) -> Union[Rect, Pnt, List[Pnt]]:
    if isinstance(value, (QRectF, Rect)):
        return Rect._rect_value_from_qt(func(Rect._rect_value_to_qt(value)))
    return Pnt._point_value_from_qt(func(Pnt._point_value_to_qt(value)))


class CoordSystem:
    """
    Система координат (СК). СК описывает каким образом реальные объекты на земной
    поверхности могут быть представлены в виде двумерной проекции. Выбор СК для
    представления данных зависит от конкретных исходных условий по представлению
    исходных данных.

    Note:
        Проверка на идентичность параметров двух СК производится простым сравнением.

    Note:
        Для получения текстового представления можно воспользоваться функцией :class:`str`.

    Поддерживается создание СК посредством следующих вариантов:

    * Из строки MapInfo PRJ :meth:`from_prj`
    * Из строки PROJ :meth:`from_proj`
    * Из строки WKT :meth:`from_wkt`
    * Из значения EPSG :meth:`from_epsg`
    * План/Схему с указанием единиц измерения и охвата :meth:`from_units`

    .. literalinclude:: /../../tests/doc_examples/cs/test_example_cs.py
        :caption: Пример создания СК разного типа.
        :pyobject: test_run_example_cs_create
        :lines: 2-
        :dedent: 4

    .. literalinclude:: /../../tests/doc_examples/cs/test_example_cs.py
        :caption: Проверка на идентичность координатных систем производится простым сравнением.
        :pyobject: test_run_example_cs_compare
        :lines: 2-
        :dedent: 4
    """

    _shadow: ShadowCoordSystem

    def __init__(self) -> None:
        raise NotImplementedError

    @classmethod
    def _wrap(cls, shadow: ShadowCoordSystem) -> Optional["CoordSystem"]:
        if shadow.isEmpty():
            return None
        return cls.__wrap_common(shadow)

    @classmethod
    def __wrap_common(cls, shadow: ShadowCoordSystem) -> "CoordSystem":
        result = cls.__new__(cls)
        result._shadow = shadow
        return result

    @classmethod
    def __wrap_ensured(cls, shadow: ShadowCoordSystem) -> "CoordSystem":
        if shadow.isEmpty():
            raise RuntimeError("Internal object is not valid.")
        return cls.__wrap_common(shadow)

    @staticmethod
    def _factory() -> "ShadowCoordSysFactory":
        return _shadow_manager.cs_factory

    @property
    def title(self) -> str:
        """Возвращает наименование системы координат."""
        return self._shadow.description()

    @property
    def rect(self) -> Rect:
        """Устанавливает или возвращает максимально допустимый охват."""
        return Rect.from_qt(self._shadow.rect())

    @rect.setter
    def rect(self, r: Rect) -> None:
        self._shadow.setRect(Rect._rect_value_to_qt(r))

    @property
    @_experimental()
    def _default_rect(self) -> Rect:
        return Rect.from_qt(self._shadow.default_rect())

    @property
    def unit(self) -> LinearUnit:
        """Возвращает единицы измерения."""
        return LinearUnit._wrap(self._shadow.unit())

    @overload
    def convert_to_degree(self, value: Union[Rect, QRectF]) -> Rect: ...

    @overload
    def convert_to_degree(self, value: Union[Tuple[float, float], Pnt, QPointF]) -> Pnt: ...

    @overload
    def convert_to_degree(self, value: List[Union[Tuple[float, float], Pnt, QPointF]]) -> List[Pnt]: ...

    def convert_to_degree(
        self,
        value: Union[Rect, QRectF, Tuple[float, float], Pnt, QPointF, List[Union[Tuple[float, float], Pnt, QPointF]]],
    ) -> Union[Rect, Pnt, List[Pnt]]:
        """
        Переводит из единиц измерения системы координат в градусы.

        .. literalinclude:: /../../tests/doc_examples/cs/test_example_cs.py
            :caption: Пример.
            :pyobject: test_run_example_cs_to_degree
            :lines: 2-
            :dedent: 4
        """
        return _internal_transform(value, self._shadow.to_degree)

    @overload
    def convert_from_degree(self, value: Union[Rect, QRectF]) -> Rect: ...

    @overload
    def convert_from_degree(self, value: Union[Tuple[float, float], Pnt, QPointF]) -> Pnt: ...

    @overload
    def convert_from_degree(self, value: List[Union[Tuple[float, float], Pnt, QPointF]]) -> List[Pnt]: ...

    def convert_from_degree(
        self,
        value: Union[Rect, QRectF, Tuple[float, float], Pnt, QPointF, List[Union[Tuple[float, float], Pnt, QPointF]]],
    ) -> Union[Rect, Pnt, List[Pnt]]:
        """Переводит из градусов в единицы измерения системы координат."""
        return _internal_transform(value, self._shadow.from_degree)

    @property
    def epsg(self) -> Optional[int]:
        """Возвращает код EPSG если существует, иначе None."""
        return self._factory().toEpsg(self._shadow)

    @property
    def proj(self) -> str:
        """Возвращает строку PROJ или пустую строку, если аналога не найдено."""
        return self._factory().toProj(self._shadow)

    @property
    def prj(self) -> str:
        """Возвращает строку prj формата MapBasic или пустую строку, если аналога не
        найдено."""
        return self._factory().toPrj(self._shadow)

    @property
    def wkt(self) -> str:
        """Возвращает строку WKT или пустую строку, если аналога не найдено."""
        return self._factory().toWkt(self._shadow)

    @property
    @_experimental()
    def _wkt2(self) -> str:
        """Возвращает строку WKT2. Если не найдено, возвращается исключение"""
        return self._factory().toWktEx(self._shadow, {"FORMAT": "WKT2"})

    @property
    def non_earth(self) -> bool:
        """Возвращает признак, является ли СК декартовой."""
        return self._shadow.is_non_earth()

    @property
    def lat_lon(self) -> bool:
        """Возвращает признак, является ли СК широтой/долготой."""
        return self._shadow.is_lat_lon()

    @property
    def name(self) -> str:
        """Возвращает наименование системы координат."""
        return self._shadow.name()

    @property
    def semi_major(self) -> float:
        """Возвращает большую полуось."""
        return self._shadow.semi_major()

    @property
    def semi_minor(self) -> float:
        """Возвращает малую полуось."""
        return self._shadow.semi_minor()

    @property
    def inv_flattening(self) -> float:
        """Возвращает полярное сжатие."""
        return self._shadow.inv_flattening()

    @classmethod
    def from_epsg(cls, code: int) -> "CoordSystem":
        """
        Создает координатную систему по коду EPSG.

        See also:
            Подробнее см. `EPSG <https://epsg.org>`_

        Args:
            code: Стандартное значение EPSG.
        """
        return cls.__wrap_ensured(cls._factory().fromEpsg(code))

    @classmethod
    def from_wkt(cls, wkt: str) -> "CoordSystem":
        """
        Создает координатную систему из строки WKT.

        See also:
            Подробнее см.
                `WKT <https://en.wikipedia.org/wiki/Well-known_text_representation_of_coordinate_reference_systems>`_

        Args:
            wkt: Строка WKT.
        """
        return cls.__wrap_ensured(cls._factory().fromWkt(wkt))

    @classmethod
    def from_proj(cls, proj: str) -> "CoordSystem":
        """
        Создает координатную систему из строки proj.

        See also:
            Подробнее см. `PROJ <https://proj.org/usage/quickstart.html>`_

        Args:
            proj: Строка proj.
        """
        return cls.__wrap_ensured(cls._factory().fromProj(proj))

    @classmethod
    def from_prj(cls, prj: str) -> "CoordSystem":
        """
        Создает координатную систему из строки MapBasic.

        Args:
            prj: Строка MapBasic. Допустима короткая нотация.

        .. literalinclude:: /../../tests/doc_examples/cs/test_example_cs.py
            :caption: Пример.
            :pyobject: test_run_example_cs_create_prj
            :lines: 2-
            :dedent: 4
        """
        return cls.__wrap_ensured(cls._factory().fromPrj(prj))

    @classmethod
    def from_units(
        cls,
        unit: LinearUnit,
        rect: Optional[Union[Rect, QRectF]] = None,
    ) -> "CoordSystem":
        """
        Создает декартову систему координат.

        Args:
            unit: Единицы измерения системы координат.
            rect: Охват системы координат.
                  По умолчанию, ``axipy.Rect(xmin=-10000, ymin=-10000, xmax=10000, ymax=10000)``.

        .. literalinclude:: /../../tests/doc_examples/cs/test_example_cs.py
            :caption: Пример.
            :pyobject: test_run_example_cs_create_prj_ne
            :lines: 2-
            :dedent: 4
        """
        if rect is None:
            rect = Rect(-10000, -10000, 10000, 10000)
        return cls.__wrap_ensured(cls._factory().fromUnits(unit._shadow, Rect._rect_value_to_qt(rect)))

    @classmethod
    def from_string(cls, string: str) -> "CoordSystem":
        """
        Создает систему координат из строки. Строка состоит из двух частей: префикса и
        строки представления СК. Возможные значения префиксов: «proj», «wkt», «epsg»,
        «prj».

        Args:
            string: Строка.
        """
        return cls.__wrap_ensured(cls._factory().fromString(string))

    def to_string(self) -> str:
        """Текстовое представление в виде `<тип>:<строка>`."""
        return self._shadow.get_str()

    @property
    def default_calc_mode(self) -> CalcMode:
        """Возвращает режим расчета для данной СК по умолчанию."""
        return CalcMode(self._shadow.calc_mode())

    @property
    def supported_calc_modes(self) -> Tuple[CalcMode, ...]:
        """Возвращает возможный перечень режимов расчета для данной СК."""
        return tuple(CalcMode(m) for m in self._shadow.calc_modes())

    def __str__(self) -> str:
        return self._shadow.get_str()

    def __repr__(self) -> str:
        return f"axipy.CoordSystem.from_string({self.to_string()!r})"

    @classmethod
    def current(cls) -> "CoordSystem":
        """
        Текущая установленная система координат (СК).

        Данная СК используется как значение по умолчанию, когда она не определена.
        Например, в диалоге создания новой таблицы.
        """
        return cls.__wrap_ensured(cls._factory().current_cs())

    @classmethod
    def set_current(cls, coordsystem: "CoordSystem") -> None:
        """
        Устанавливает новую текущую систему координат.

        Args:
            coordsystem: Новое значение системы координат.

        Пример установки нового значения::

            CoordSystem.set_current(CoordSystem.from_prj("10, 104, 7"))
        """
        cls._factory().set_current_cs(coordsystem._shadow)

    def clone(self) -> "CoordSystem":
        """Возвращает копию координатной системы."""
        return self.__wrap_ensured(self._shadow.clone())

    def __copy__(self) -> "CoordSystem":
        return self.clone()

    def __deepcopy__(self, _mem_dict: dict) -> "CoordSystem":
        return self.clone()

    def is_equal(self, other: "CoordSystem", ignore_rect: bool = False) -> bool:
        """Сравнивает координатные системы, с возможностью не учитывать охват."""
        return self._shadow.isEqual(other._shadow, ignore_rect)

    def __eq__(self, other: object) -> bool:
        if isinstance(other, CoordSystem):
            return self.is_equal(cast(CoordSystem, other))
        return NotImplemented

    @_experimental()
    def _to_mgrs(self, point: Pnt, precision: int = 4) -> str:
        """
        Преобразование координат в формат `MGRS <http://en.wikipedia.org/wiki/Military_grid_reference_system>`_

        .. literalinclude:: /../../tests/doc_examples/cs/test_example_cs.py
            :pyobject: test_run_example_cs_mgrs
            :lines: 2-
            :dedent: 4

        Args:
            point: Координата.
            precision: Точность.

        .. csv-table:: Возможные значения точности
            :header: Значение, Наименование
            :align: left

            0, precision level 100 km
            1, precision level 10 km
            2, precision level 1 km
            3, precision level 100 m
            4, precision level 10 m
            5, precision level 1 m

        Return:
            Строка MGRS если успешно.

        Raises:
            ValueError: Если не удается получить результат.
        """
        p = Pnt._point_value_to_qt(point)
        return self._shadow.to_mgrs(p, precision)


def _apply_deprecated() -> None:

    @_deprecated_by("axipy.CoordSystem.title")
    def description(self: CoordSystem) -> str:
        return self._shadow.description()

    setattr(CoordSystem, "description", property(description))


_apply_deprecated()
