import pprint
from functools import cached_property
from typing import (
    TYPE_CHECKING,
    Iterator,
    List,
    Optional,
    Type,
    Union,
    cast,
    overload,
)

import axipy
from axipy._internal._check_shadow import _is_valid
from axipy._internal._decorator import _deprecated_by, _experimental
from axipy._internal._shadow_instance_factory import _shadow_manager
from axipy.cpp_render import (
    ShadowCosmeticLayer,
    ShadowLayer,
    ShadowRasterLayer,
    ShadowThematic,
    ShadowThematicBar,
    ShadowThematicDensity,
    ShadowThematicIndividual,
    ShadowThematicList,
    ShadowThematicPie,
    ShadowThematicRange,
    ShadowThematicSymbol,
    ShadowVectorLayer,
)
from axipy.cs import CoordSystem
from axipy.da import DataObject, Raster, Style, Table
from axipy.utl import Rect
from PySide2.QtCore import Signal
from PySide2.QtGui import QColor

from .label import Label

if TYPE_CHECKING:
    from axipy import (
        BarThematicLayer,
        DensityThematicLayer,
        IndividualThematicLayer,
        PieThematicLayer,
        RangeThematicLayer,
        SymbolThematicLayer,
    )

__all__: List[str] = [
    "Layer",
    "ThematicLayer",
    "ListThematic",
    "VectorLayer",
    "CosmeticLayer",
    "RasterLayer",
]


class Layer:
    """
    Абстрактный базовый класс для слоя карты.

    Для создания нового экземпляра для векторного или растрового источника данных
    необходимо использовать метод :meth:`Layer.create`.
    Для тематических слоев — использовать соответствующие им конструкторы.
    """

    _shadow: ShadowLayer

    def __init__(self) -> None:
        raise NotImplementedError

    @classmethod
    def __wrap_typed(cls, shadow: "ShadowLayer") -> "Layer":
        result = cls.__new__(cls)
        result._shadow = shadow
        return result

    @overload
    @classmethod
    def _wrap(cls, shadow: "ShadowCosmeticLayer") -> Optional["CosmeticLayer"]: ...

    @overload
    @classmethod
    def _wrap(cls, shadow: "ShadowVectorLayer") -> Optional["VectorLayer"]: ...

    @overload
    @classmethod
    def _wrap(cls, shadow: "ShadowRasterLayer") -> Optional["RasterLayer"]: ...

    @overload
    @classmethod
    def _wrap(cls, shadow: "ShadowThematicRange") -> Optional["RangeThematicLayer"]: ...

    @overload
    @classmethod
    def _wrap(cls, shadow: "ShadowThematicPie") -> Optional["PieThematicLayer"]: ...

    @overload
    @classmethod
    def _wrap(cls, shadow: "ShadowThematicBar") -> Optional["BarThematicLayer"]: ...

    @overload
    @classmethod
    def _wrap(cls, shadow: "ShadowThematicSymbol") -> Optional["SymbolThematicLayer"]: ...

    @overload
    @classmethod
    def _wrap(cls, shadow: "ShadowThematicIndividual") -> Optional["IndividualThematicLayer"]: ...

    @overload
    @classmethod
    def _wrap(cls, shadow: "ShadowThematicDensity") -> Optional["DensityThematicLayer"]: ...

    @overload
    @classmethod
    def _wrap(cls, shadow: "ShadowThematic") -> Optional["ThematicLayer"]: ...

    @overload
    @classmethod
    def _wrap(cls, shadow: "ShadowLayer") -> Optional["Layer"]: ...

    @classmethod
    def _wrap(cls, shadow: "ShadowLayer") -> Optional["Layer"]:
        if shadow is None:
            return None

        obj_type: Type[Layer]
        if isinstance(shadow, ShadowCosmeticLayer):
            obj_type = CosmeticLayer
        elif isinstance(shadow, ShadowVectorLayer):
            obj_type = VectorLayer
            return obj_type._VectorLayer__wrap_typed(shadow)  # type: ignore[attr-defined]
        elif isinstance(shadow, ShadowRasterLayer):
            obj_type = RasterLayer
        elif isinstance(shadow, ShadowThematicRange):
            obj_type = axipy.RangeThematicLayer
        elif isinstance(shadow, ShadowThematicPie):
            obj_type = axipy.PieThematicLayer
        elif isinstance(shadow, ShadowThematicBar):
            obj_type = axipy.BarThematicLayer
        elif isinstance(shadow, ShadowThematicSymbol):
            obj_type = axipy.SymbolThematicLayer
        elif isinstance(shadow, ShadowThematicIndividual):
            obj_type = axipy.IndividualThematicLayer
        elif isinstance(shadow, ShadowThematicDensity):
            obj_type = axipy.DensityThematicLayer
        else:
            obj_type = Layer

        return obj_type.__wrap_typed(shadow)

    # noinspection PyPep8Naming
    @overload
    @classmethod
    def create(cls, dataObject: "Raster") -> "RasterLayer": ...

    # noinspection PyPep8Naming
    @overload
    @classmethod
    def create(cls, dataObject: "Table") -> "VectorLayer": ...

    # noinspection PyPep8Naming
    @overload
    @classmethod
    def create(cls, dataObject: "DataObject") -> "Layer": ...

    # noinspection PyPep8Naming
    @classmethod
    def create(cls, dataObject: "DataObject") -> "Layer":
        """
        Создает слой на базе открытой таблицы или растра.

        Args:
            dataObject: Таблица или растр. В зависимости от переданного объекта будет создан
                :class:`VectorLayer`  или :class:`RasterLayer`.

        .. literalinclude:: /../../tests/doc_examples/render/test_example_layer.py
            :caption: Пример создания слоя на базе файла.
            :pyobject: test_run_example_layer
            :lines: 3-
            :dedent: 4
        """
        if dataObject is None or dataObject._shadow is None:
            raise ValueError("Object is not valid. Unable to create layer.")

        render = _shadow_manager.render
        if not render:
            raise RuntimeError("axipy is not initialized")

        shadow: Union[ShadowRasterLayer, ShadowVectorLayer]
        if isinstance(dataObject, Raster):
            shadow = ShadowRasterLayer(render, dataObject._shadow)
        elif dataObject.is_spatial:
            if isinstance(dataObject, Table):
                shadow = ShadowVectorLayer(render, dataObject._shadow)
            else:
                shadow = ShadowRasterLayer(render, dataObject._shadow)
        else:
            raise ValueError("Non spatial table")

        if shadow is None:
            msg = "Cannot create layer."
            if dataObject._shadow is not None and not dataObject._shadow.isSpatial():
                msg = msg + " Table is not spatial."
            raise ValueError(msg)

        res = cls._wrap(shadow)
        if res is None:
            raise TypeError("Invalid layer type")
        return res

    @property
    def data_changed(self) -> Signal:
        """
        Сигнал об изменении контента слоя.

        :rtype: Signal[]
        """
        return self._shadow.dataChanged

    @property
    def need_redraw(self) -> Signal:
        """
        Сигнал о необходимости перерисовать слой.

        :rtype: Signal[]
        """
        return self._shadow.needRedraw

    @property
    def title(self) -> str:
        """Устанавливает или возвращает наименование слоя."""
        return self._shadow.get_name()

    @title.setter
    def title(self, n: str) -> None:
        self._shadow.set_name(n)

    @property
    def coordsystem(self) -> CoordSystem:
        """Возвращает координатную систему, в которой находятся данные, отображаемые слоем."""
        return cast(CoordSystem, CoordSystem._wrap(self._shadow.cs()))

    @property
    @_deprecated_by("get_bounds")
    def bounds(self) -> Rect:
        return self.get_bounds()

    def get_bounds(self) -> Rect:
        """Возвращает область, которая содержит все данные слоя."""
        return Rect.from_qt(self._shadow.bounds())

    @property
    def data_object(self) -> DataObject:
        """Возвращает источник данных для слоя."""
        return cast(DataObject, DataObject._wrap(self._shadow.data_object()))

    @property
    def opacity(self) -> int:
        """
        Устанавливает или возвращает прозрачность слоя в составе карты.

        Доступные значения от 0 до 100.
        """
        return self._shadow.get_opacity()

    @opacity.setter
    def opacity(self, n: int) -> None:
        self._shadow.set_opacity(n)

    @property
    def zoom_restrict(self) -> bool:
        """
        Устанавливает или возвращает признак, будет ли использоваться ограничение по отображению.

        Если установлено True, то для ограничения отображения слоя в
        зависимости от масштаба используются значения свойств `zoom_min` и `zoom_max`.
        """
        return self._shadow.zoomRestrict()

    @zoom_restrict.setter
    def zoom_restrict(self, v: bool) -> None:
        self._shadow.setZoomRestrict(v)

    @property
    def min_zoom(self) -> float:
        """
        Устанавливает или возвращает минимальную ширину окна, при которой слой
        отображается на карте.

        Учитывается только при установленном `zoom_restrict=True`.
        """
        return self._shadow.zoomMin()

    @min_zoom.setter
    def min_zoom(self, v: float) -> None:
        self._shadow.setZoomMin(v)

    @property
    def max_zoom(self) -> float:
        """
        Устанавливает или возвращает максимальную ширину окна, при которой слой
        отображается на карте.

        Учитывается только при установленном `zoom_restrict=True`.
        """
        return self._shadow.zoomMax()

    @max_zoom.setter
    def max_zoom(self, v: float) -> None:
        self._shadow.setZoomMax(v)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Layer):
            return NotImplemented
        return ShadowLayer.isEquals(self._shadow, other._shadow if other._shadow is not None else None)

    def __str__(self) -> str:
        return str(type(self))

    def __repr__(self) -> str:
        return f"<axipy.{self.__class__.__name__} title={self.title}>"

    @property
    def visible(self) -> bool:
        """
        Устанавливает или возвращает признак видимости слоя.

        Выключение видимости верхнего слоя для активной карты::

            if axipy.view_manager.active is not None:
                axipy.view_manager.active.map.layers[0].visible = False
        """
        return self._shadow.is_visible()

    @visible.setter
    def visible(self, v: bool) -> None:
        self._shadow.set_visible(v)

    @property
    @_experimental()
    def _hidden(self) -> bool:
        return self._shadow.is_hidden()

    @_hidden.setter
    @_experimental()
    def _hidden(self, v: bool) -> None:
        self._shadow.set_hidden(v)

    @property
    def selectable(self) -> bool:
        """Устанавливает или возвращает признак доступности выбора объектов слоя, если
        это поддерживается."""
        return self._shadow.is_selectable()

    @selectable.setter
    def selectable(self, v: bool) -> None:
        self._shadow.set_selectable(v)

    @property
    def is_valid(self) -> bool:
        """
        Возвращает признак валидности слоя.

        Слой мог быть удален, как пример, в связи с закрытием таблицы.
        """
        return _is_valid(self._shadow)

    @property
    def extended_properties(self) -> dict:
        return self._shadow.extendedProperties()

    @extended_properties.setter
    def extended_properties(self, v: dict) -> None:
        self._shadow.setExtendedProperties(v)


class ThematicLayer(Layer):
    """Абстрактный класс слоя с тематическим оформлением векторного слоя карты на базе
    атрибутивной информации."""

    _shadow: ShadowThematic

    pass


# noinspection PyPep8Naming
class ListThematic:
    """Список тематических слоев (тематик) карты."""

    _shadow: ShadowThematicList

    def __init__(self) -> None:
        raise NotImplementedError

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

    def append(self, lay: ThematicLayer) -> None:
        """
        Добавить тематику.

        Args:
            lay: Добавляемый тематический слой.
        """
        self._shadow.add(lay._shadow)

    def add(self, lay: ThematicLayer) -> None:
        self._shadow.add(lay._shadow)

    def remove(self, idx: int) -> None:
        """
        Удалить тематику.

        Args:
            idx: Индекс удаляемого слоя.
        """
        self._shadow.remove(idx)

    def move(self, fromIdx: int, toIdx: int) -> None:
        """
        Поменять тематики местами.

        Args:
            fromIdx: Текущий индекс.
            toIdx: Новое положение.
        """
        self._shadow.move(fromIdx, toIdx)

    @property
    def count(self) -> int:
        """Возвращает количество тематик слоя."""
        return len(self)

    def at(self, idx: int) -> Optional[ThematicLayer]:
        """
        Получение тематики по ее индексу.

        Args:
            idx: Индекс запрашиваемой тематики.
        """
        return ThematicLayer._wrap(self._shadow.at(idx))

    def __getitem__(self, item: Union[int, str]) -> Optional[ThematicLayer]:
        if isinstance(item, str):
            return ThematicLayer._wrap(self._shadow.by_name(item))
        return self.at(item)

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

    def __iter__(self) -> Iterator[ThematicLayer]:
        return cast(Iterator[ThematicLayer], (self.at(idx) for idx in range(len(self))))

    def __repr__(self) -> str:
        return f"<axipy.{self.__class__.__name__}>\n{pprint.pformat(list(self))}"


# noinspection PyPep8Naming
class VectorLayer(Layer):
    """
    Слой, основанный на базе векторных данных.

    Note:
        Создание слоя производится посредством метода вызова :meth:`Layer.create`

    .. literalinclude:: /../../tests/doc_examples/render/test_example_layer.py
        :caption: Примеры работы со свойствами слоя.
        :pyobject: test_run_example_layer_props
        :lines: 3-
        :dedent: 4
    """

    _shadow: ShadowVectorLayer

    @classmethod
    def __wrap_typed(cls, shadow: "ShadowVectorLayer") -> "VectorLayer":
        result = cls.__new__(cls)
        result._shadow = shadow
        # bugfix #5998
        result._shadow.internalDeleted.connect(result.__internalDeletedSlot)
        return result

    def __internalDeletedSlot(self) -> None:
        """If shadow obj has been deleted by signal from core."""
        self._thematics = None  # type: ignore[assignment]
        self._label = None  # type: ignore[assignment]
        self._shadow = None  # type: ignore[assignment]

    @cached_property
    def _thematics(self) -> "ListThematic":
        return ListThematic._wrap(self._shadow.get_thematics())

    @cached_property
    def _label(self) -> "Label":
        return Label._wrap(self._shadow)

    @property
    def showCentroid(self) -> bool:
        """Устанавливает или возвращает признак отображения центроидов на слое."""
        return self._shadow.get_show_centroid()

    @showCentroid.setter
    def showCentroid(self, n: bool) -> None:
        self._shadow.set_show_centroid(n)

    @property
    def nodesVisible(self) -> bool:
        """Устанавливает или возвращает признак отображения узлов линий и полигонов."""
        return self._shadow.get_nodesVisible()

    @nodesVisible.setter
    def nodesVisible(self, n: bool) -> None:
        self._shadow.set_nodesVisible(n)

    @property
    def linesDirectionVisibile(self) -> bool:
        """Устанавливает или возвращает признак отображения направлений линий."""
        return self._shadow.get_linesDirectionVisibile()

    @linesDirectionVisibile.setter
    def linesDirectionVisibile(self, n: bool) -> None:
        self._shadow.set_linesDirectionVisibile(n)

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

        Если задан как None (по умолчанию), объекты будут отображены на основании
        оформления источника данных.
        """
        return Style._wrap(self._shadow.get_override_style())

    @overrideStyle.setter
    def overrideStyle(self, style: Optional[Style]) -> None:
        self._shadow.set_override_style(style._shadow if style is not None else None)

    @property
    def thematic(self) -> ListThematic:
        """
        Возвращает перечень тематик для данного слоя. Работа с тематическими слоями
        похожа на работу со списком `list`.

        .. literalinclude:: /../../tests/doc_examples/render/test_example_layer.py
            :caption: Пример.
            :pyobject: test_run_example_layer_thematic
            :lines: 3-11
            :dedent: 4
        """
        return self._thematics

    @property
    def label(self) -> Label:
        """
        Возвращает метки слоя.

        В качестве формулы может использоваться или наименование поля таблицы или
        выражение.
        """
        return self._label

    @property
    def hotlink(self) -> str:
        """
        Устанавливает или возвращает наименование атрибута таблицы для хранения
        гиперссылки.

        .. csv-table:: Возможны следующие варианты
            :header: Значение, Описание

            axioma://world.tab, Открывает файл или рабочее пространство в аксиоме
            addlayer://world, Добавляет слой world в текущую карту
            exec://gimp,  Запускает на выполнение программу gimp
            https://axioma-gis.ru/, Открывает ссылку в браузере

        Если префикс отсутствует, то производится попытка запустить по ассоциации.
        """
        return self._shadow.get_hotlink()

    @hotlink.setter
    def hotlink(self, expr: str) -> None:
        self._shadow.set_hotlink(expr)

    if TYPE_CHECKING:

        @property
        def data_object(self) -> Table:
            """Возвращает источник данных для слоя."""
            return self.data_object


class CosmeticLayer(VectorLayer):
    """Косметический слой."""

    _shadow: ShadowCosmeticLayer

    pass


# noinspection PyPep8Naming
class RasterLayer(Layer):
    """
    Класс, который должен использоваться в качестве базового класса для тех слоев, в
    которых используются свойства отрисовки растрового изображения.

    Note:
        Создание слоя производится посредством метода вызова :meth:`Layer.create`.

    .. literalinclude:: /../../tests/doc_examples/render/test_example_layer.py
        :caption: Примеры создания растрового слоя.
        :pyobject: test_run_example_layer_raster
        :lines: 3-
        :dedent: 4
    """

    _shadow: ShadowRasterLayer

    @property
    def transparentColor(self) -> QColor:
        """Устанавливает или возвращает цвет растра, который обрабатывается как прозрачный."""
        return self._shadow.get_transparentColor()

    @transparentColor.setter
    def transparentColor(self, cl: QColor) -> None:
        self._shadow.set_transparentColor(cl)

    @property
    def brightness(self) -> int:
        """
        Устанавливает или возвращает значение яркости.

        Значение может быть в пределах от -100 до 100.
        """
        return self._shadow.get_brightness()

    @brightness.setter
    def brightness(self, v: int) -> None:
        self._shadow.set_brightness(v)

    @property
    def contrast(self) -> int:
        """
        Устанавливает или возвращает значение контраста.

        Значение может быть в пределах от -100 до 100.
        """
        return self._shadow.get_contrast()

    @contrast.setter
    def contrast(self, v: int) -> None:
        self._shadow.set_contrast(v)

    @property
    def grayscale(self) -> bool:
        """Устанавливает или возвращает признак, является ли данное изображение черно-белым."""
        return self._shadow.get_grayscale()

    @grayscale.setter
    def grayscale(self, v: bool) -> None:
        self._shadow.set_grayscale(v)
