from functools import cached_property
from typing import (
    TYPE_CHECKING,
    Iterable,
    Iterator,
    List,
    Optional,
    Union,
    cast,
)

from axipy._internal._shadow_instance_factory import _shadow_manager
from axipy.cpp_render import ShadowLayer, ShadowLayers, ShadowMap
from axipy.cs import AreaUnit, CoordSystem, LinearUnit
from axipy.da import DataObject, LineStyle
from axipy.utl import Rect
from PySide2.QtCore import QObject, Signal
from PySide2.QtGui import QImage, QPainter

from .context import Context
from .label import CustomLabelEndType, CustomLabelProperties
from .layer import CosmeticLayer, Layer, VectorLayer

if TYPE_CHECKING:
    from axipy.cpp_render import ShadowCustomLabels


class ListLayers:
    """
    Группа слоев.

    Может включать в себя как слои :class:`axipy.Layer` так и
    группы слоев :class:`axipy.ListLayers`.
    Пример использования см :attr:`axipy.Map.layers`
    """

    _shadow: ShadowLayers

    def __init__(self) -> None:
        raise NotImplementedError

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

    @property
    def count(self) -> int:
        """
        Возвращает количество слоев и групп слоев.

        Так же допустимо использование функции :meth:`len`.
        """
        return self.__len__()

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

    def __iter__(self) -> Iterator[Union[Layer, "ListLayers"]]:
        return (self.__getitem__(idx) for idx in range(self.__len__()))

    def at(self, index: int) -> Union[Layer, "ListLayers"]:
        """
        Возвращает слой или группы слоев по их индексу.

        Args:
          index: Индекс слоя или группы в списке.

        Например::

            layers.at(2)
            layers[2]
        """
        return self.__getitem__(index)

    def __get_layer(self, item: QObject) -> Union[Layer, "ListLayers", None]:
        if isinstance(item, ShadowLayer):
            return Layer._wrap(item)
        elif isinstance(item, ShadowLayers):
            return ListLayers._wrap(item)
        # backwards compatibility: should raise exception instead.
        return item  # type: ignore[return-value]

    def __getitem__(self, index: int) -> Union[Layer, "ListLayers"]:
        """
        Возвращает слой или группу по их индексу.

        Args:
            index: Индекс слоя или группы слоев.

        Returns:
            Искомый элемент.
        """
        # backwards compatibility
        if isinstance(index, str):
            key = index
            result = ShadowLayers.by_name(self._shadow, key)
            if result is None:
                raise KeyError(f'Layer "{key}" is not found.')
            return cast(Union[Layer, "ListLayers"], self.__get_layer(result))

        if index < 0 or index >= self.count:
            raise IndexError(f'Index "{index}" is out of range (0..{self.count - 1}).')
        item = ShadowLayers.at(self._shadow, index)
        return cast(Union[Layer, "ListLayers"], self.__get_layer(item))

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

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

    def append(self, layer: Layer) -> None:
        """
        Добавляет слой в карту. Добавление группы слоев не поддерживается и производится
        путем группировки существующих элементов посредством метода :meth:`group`.

        Args:
          layer: Добавляемый слой.

        Raises:
            ValueError: Если слой уже содержится в карте.
        """
        if layer in self:
            raise ValueError("Слой уже содержится в карте.")
        self._shadow.add(layer._shadow)

    def insert(self, layer: Layer) -> None:
        """
        Добавляет слой в карту. В отличие от :meth:`ListLayers.append` при вставке слоя
        производится попытка вставить его в список в зависимости от контента.

        Args:
          layer: Вставляемый слой.

        Raises:
            ValueError: Если слой уже содержится в карте.
        """
        if layer in self:
            raise ValueError("Слой уже содержится в карте.")
        self._shadow.insert(layer._shadow)

    def add(self, layer: Layer) -> None:
        self.append(layer)

    def remove(self, index: int) -> None:
        """
        Удаляет слой по индексу.

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

    def move(self, from_index: int, to_index: int) -> None:
        """
        Перемещает слой или вложенную группу слоев в списке слоев по его индексу.

        Args:
          from_index: Индекс слоя для перемещения.
          to_index: Целевой индекс.
        """
        self._shadow.move(from_index, to_index)

    def group(self, indexes: List[int], name: str) -> None:
        """
        Группировка слоев и групп в соответствие со списком их индексов. При этом
        создается новая группа и все элементы (слои и группы слоев) помещаются внутрь
        этой группы.

        Args:
            indexes: Список индексов элементов, которые необходимо объединить.
            name: Наименование создаваемой группы.
        """
        self._shadow.group(indexes, name)

    def ungroup(self, index: int) -> None:
        """
        Разгруппировка группы слоев по его индексу. при этом все внутренние элементы
        переносятся на верхний уровень данного списка. Если по индексу располагается не
        группа, то будет выброшено исключение.

        Args:
            index: Индекс группы слоев.
        """
        self._shadow.ungroup(index)

    def add_group(self, name: str) -> None:
        """
        Создает пустую группу.

        Args:
            name: Наименование создаваемой группы.
        """
        self._shadow.add_group(name)

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

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


class CustomLabels:
    """
    Пользовательские метки.

    Используется для задания параметров через свойство :attr:`Map.custom_labels`.
    """

    _shadow: "ShadowCustomLabels"

    def __init__(self) -> None:
        raise NotImplementedError

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

    # noinspection PyShadowingBuiltins
    def set(self, layer: VectorLayer, id: int, properties: Optional[CustomLabelProperties]) -> None:
        """
        Устанавливает параметры.

        Args:
            layer: Слой карты.
            id: Идентификатор записи.
            properties: Устанавливаемые свойства. Если задать None, существующие параметры будут сброшены.
        """
        if properties is not None:
            d = dict(properties._to_dict())
            if "endType" in d:
                d["endType"] = int(d["endType"])
        else:
            d = dict()
        self._shadow.setParams(layer._shadow, id, d)

    # noinspection PyShadowingBuiltins
    def get(self, layer: VectorLayer, id: int) -> Optional[CustomLabelProperties]:
        """
        Производит запрос параметров. Если для данного `id` не определены, возвращает
        None.

        Args:
            layer: Слой карты.
            id: Идентификатор записи.
        """
        par = self._shadow.getParams(layer._shadow, id)
        if not par:
            return None
        if "endType" in par:
            par["endType"] = CustomLabelEndType(par["endType"])
        res = CustomLabelProperties()
        res._set_dict(par)
        return res

    def ids(self, layer: VectorLayer) -> List[int]:
        """Перечень идентификаторов, для которых установлена пользовательская метка."""
        return self._shadow.ids(layer._shadow)

    def set_default_arrow_type(self, layer: VectorLayer, style: LineStyle) -> None:
        """Устанавливает стиль по умолчанию для новых меток слоя."""
        self._shadow.setDefaultArrowStyle(layer._shadow, style._shadow if style is not None else None)

    def set_default_end_type(self, layer: VectorLayer, end_type: CustomLabelEndType) -> None:
        """Устанавливает тип выноски по умолчанию для новых меток слоя."""
        self._shadow.setDefaultEndType(layer._shadow, end_type)


class Map:
    """
    Класс карты. Рассматривается как группа слоев, объединенная в единую сущность. Вне
    зависимости от СК входящих в карту слоев, карта отображает все слои в одной СК.
    Найти наиболее подходящую для этого можно с помощью :meth:`get_best_coordsystem` или
    же установить другую.

    Единицы измерения расстояний :attr:`distanceUnit` и площадей :attr:`areaUnit` берутся из настроек по умолчанию.

    Args:
        layers: Список слоев, с которым будет создана карта.
        preserve_order: Порядок слоев оставить как есть, в противном случае будет произведена попытка отсортировать
            слои в соответствии с контентом (растры вниз, точечные объекты наверх)

    Raises:
        ValueError: Если один и тот же слой был передан несколько раз.

    .. literalinclude:: /../../tests/doc_examples/render/test_example_map.py
        :caption: Пример.
        :pyobject: test_run_example_map
        :lines: 3-
        :dedent: 4
    """

    _shadow: ShadowMap

    def __init__(self, layers: Optional[Iterable[Layer]] = None, preserve_order: bool = False) -> None:
        layers_iterable: Iterable[Layer] = iter(())

        if layers is None:
            layers_iterable = []
        # check for DataObject, backwards compatibility
        elif isinstance(layers, DataObject) and layers.is_spatial:
            layers_iterable = [Layer.create(layers)]
        # check for Layer, backwards compatibility
        elif isinstance(layers, Layer):
            layers_iterable = [layers]
        else:
            layers_list = []
            for elem in layers:
                if isinstance(elem, Layer):
                    layers_list.append(elem)
                else:
                    # backwards compatibility
                    layers_list.append(Layer.create(elem))  # type: ignore[call-overload]
            if any(layers_list.count(layer) > 1 for layer in layers):
                raise ValueError("Один слой был добавлен несколько раз.")
            layers_iterable = layers_list

        shadow_layers = map(lambda obj: obj._shadow, layers_iterable)
        self._shadow = ShadowMap(list(shadow_layers), _shadow_manager.render, preserve_order)

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

    @property
    def need_redraw(self) -> Signal:
        """
        Сигнал о необходимости перерисовки карты. Возникает при изменении контента
        одного или нескольких слоев карты. Это может быть обусловлено изменением данных
        таблиц.

        :rtype: Signal[]

        .. literalinclude:: /../../tests/doc_examples/render/test_example_map.py
            :caption: Пример.
            :pyobject: test_run_example_map_redraw
            :lines: 2-
            :dedent: 4
        """
        return self._shadow.needRedraw

    def draw(self, context: Context) -> None:
        """
        Рисует карту в контексте.

        Args:
            context: Контекст рисования.

        .. literalinclude:: /../../tests/doc_examples/render/test_example_map.py
            :caption: Пример получения карты как растра.
            :pyobject: test_run_example_map_draw
            :lines: 2,4-11,13-
            :dedent: 4
        """
        self._shadow.draw(context._shadow)

    def draw_vector(self, context: Context) -> None:
        """
        Рисует карту в контексте в виде вектора. Это может потребоваться при генерации
        карты в виде, к примеру, SVG.

        Args:
            context: Контекст рисования.

        .. literalinclude:: /../../tests/doc_examples/render/test_example_map.py
            :caption: Пример получения карты в виде вектора.
            :pyobject: test_run_example_map_draw_vector
            :lines: 2,3,6-
            :dedent: 4
        """
        self._shadow.draw_vector(context._shadow)

    def get_best_coordsystem(self) -> CoordSystem:
        """Определяет координатную системы карты, наиболее подходящую исходя из
        содержимого перечня слоев."""
        return cast(CoordSystem, CoordSystem._wrap(self._shadow.bestCS()))

    def get_best_rect(self, coordsystem: Optional[CoordSystem] = None) -> Rect:
        """
        Определяет ограничивающий прямоугольник карты.

        Args:
            coordsystem:
                Координатная система, в которой необходимо получить результат.
                Если отсутствует, будет выдан результат для наиболее подходящей
                координатной системы.
        """
        if coordsystem is None:
            coordsystem = self.get_best_coordsystem()
        return Rect.from_qt(self._shadow.bestRect(coordsystem._shadow))

    # noinspection PyPep8Naming
    @property
    def distanceUnit(self) -> LinearUnit:
        """Устанавливает или возвращает единицы измерения расстояний на карте."""
        return LinearUnit._wrap(self._shadow.get_distance_unit())

    # noinspection PyPep8Naming
    @distanceUnit.setter
    def distanceUnit(self, unit: LinearUnit) -> None:
        self._shadow.set_distance_unit(unit._shadow)

    # noinspection PyPep8Naming
    @property
    def areaUnit(self) -> AreaUnit:
        """Устанавливает или возвращает единицы измерения площадей карты."""
        return AreaUnit._wrap(self._shadow.get_area_unit())

    # noinspection PyPep8Naming
    @areaUnit.setter
    def areaUnit(self, u: AreaUnit) -> None:
        self._shadow.set_area_unit(u._shadow)

    @cached_property
    def _layers(self) -> ListLayers:
        return ListLayers._wrap(ShadowMap.list_layers(self._shadow))

    @property
    def layers(self) -> ListLayers:
        """
        Возвращает список слоев и групп слоев.

        Note:
            Не содержит косметический слой :attr:`cosmetic`.

        .. literalinclude:: /../../tests/doc_examples/render/test_example_map.py
            :caption: Примеры доступа.
            :pyobject: test_run_example_map_layers
            :lines: 5-
            :dedent: 4
        """
        return self._layers

    @cached_property
    def _custom_labels(self) -> CustomLabels:
        return CustomLabels._wrap(ShadowMap.custom_labels(self._shadow))

    @property
    def custom_labels(self) -> CustomLabels:
        """
        Возвращает пользовательские метки.

        .. literalinclude:: /../../tests/doc_examples/render/test_example_map.py
            :caption: Пример.
            :pyobject: test_run_example_map_label_props
            :lines: 3-
            :dedent: 4
        """
        return self._custom_labels

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

        Raises:
            ValueError: При попытке установить слой, не принадлежащий этой карте.
        """
        editable_layer = ShadowMap.get_editableLayer(self._shadow)
        return cast(Optional[VectorLayer], Layer._wrap(editable_layer))

    @editable_layer.setter
    def editable_layer(self, layer: Optional[VectorLayer]) -> None:
        if layer is None:
            self._shadow.reset_editableLayer()
            return

        if layer not in self:
            raise ValueError("Слой не принадлежит карте.")

        self._shadow.set_editableLayer(layer._shadow)

    @property
    def cosmetic(self) -> CosmeticLayer:
        """Возвращает косметический слой карты."""
        return cast(CosmeticLayer, Layer._wrap(ShadowMap.cosmetic(self._shadow)))

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Map):
            return NotImplemented
        return self._shadow.isEqual(other._shadow)

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

    def __contains__(self, layer: Layer) -> bool:
        return layer in self.layers or layer == self.cosmetic

    def to_image(
        self,
        width: int,
        height: int,
        coordsystem: Optional[CoordSystem] = None,
        bbox: Optional[Rect] = None,
    ) -> QImage:
        """
        Рисует карту в изображение.

        Args:
            width: Ширина выходного изображения.
            height: Высота выходного изображения.
            coordsystem: Координатная система. Если не задана, берется наиболее подходящая.
            bbox: Ограничивающий прямоугольник. Если не задан, берется у карты.

        Returns:
            Изображение.
        """

        image = QImage(width, height, QImage.Format_ARGB32_Premultiplied)
        image.fill(0)
        painter = QPainter(image)
        context = Context(painter)
        context.pseudo_zoom = True
        if coordsystem is None:
            coordsystem = self.get_best_coordsystem()
        if bbox is None:
            bbox = self.get_best_rect(coordsystem)
        if isinstance(bbox, tuple):
            bbox = Rect(*bbox)
        context.coordsystem = coordsystem
        context.rect = bbox
        self.draw(context)
        return image
