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

from axipy._internal._decorator import _experimental
from axipy._internal._shadow_instance_factory import _shadow_manager
from axipy.cpp_render import (
    ShadowClicheReportItem,
    ShadowGeometryReportItem,
    ShadowLegendReportItem,
    ShadowMapReportItem,
    ShadowRasterReportItem,
    ShadowReport,
    ShadowReportItem,
    ShadowReportItemsBase,
    ShadowScaleBarReportItem,
    ShadowTableReportItem,
)
from axipy.cs import CoordSystem, LinearUnit
from axipy.da import DataObject, Geometry, Style, Table
from axipy.utl import Pnt, Rect
from PySide2.QtCore import QObject, QRectF, QSizeF, Signal, Slot
from PySide2.QtPrintSupport import QPrinter

from .clip import ClipGeometry
from .context import Context
from .legend import Legend
from .map_ import Map

__all__: List[str] = [
    "ReportItem",
    "ReportItems",
    "Report",
    "GeometryReportItem",
    "MapReportItem",
    "RasterReportItem",
    "TableReportItem",
    "LegendReportItem",
    "ScaleBarReportItem",
    "PrototypeReportItem",
]


class ReportItem:
    """Базовый класс элемента отчета."""

    if TYPE_CHECKING:
        _shadow: ShadowReportItem

    @classmethod
    def __wrap_typed(cls, shadow: "ShadowReportItem") -> "ReportItem":
        obj = cls.__new__(cls)
        obj._shadow = shadow
        return obj

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

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

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

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

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

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

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

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

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

        obj_type: Type[ReportItem]
        if isinstance(shadow, ShadowGeometryReportItem):
            obj_type = GeometryReportItem
        elif isinstance(shadow, ShadowMapReportItem):
            obj_type = MapReportItem
        elif isinstance(shadow, ShadowRasterReportItem):
            obj_type = RasterReportItem
        elif isinstance(shadow, ShadowTableReportItem):
            obj_type = TableReportItem
        elif isinstance(shadow, ShadowLegendReportItem):
            obj_type = LegendReportItem
        elif isinstance(shadow, ShadowScaleBarReportItem):
            obj_type = ScaleBarReportItem
        elif isinstance(shadow, ShadowClicheReportItem):
            obj_type = PrototypeReportItem
        else:
            return None
        return obj_type.__wrap_typed(shadow)

    @property
    def rect(self) -> Rect:
        """Размер (ограничивающий прямоугольник) элемента отчета в единицах измерения отчета."""
        return Rect.from_qt(self._shadow.get_rect())

    @rect.setter
    def rect(self, r: Rect) -> None:
        # backwards compatibility: should support QRect and QRectF.
        self._shadow.set_rect(Rect._rect_value_to_qt(r))

    # noinspection PyPep8Naming
    def intersects(self, checkRect: Union[Rect, QRectF]) -> bool:
        """
        Пересекается ли с переданным прямоугольником.

        Args:
            checkRect: Прямоугольник для анализа.
        """
        return self._shadow.intersects(Rect._rect_value_to_qt(checkRect))

    @property
    def border_style(self) -> Style:
        """Стиль обводки элемента отчета."""
        return cast(Style, Style._wrap(self._shadow.get_borderStyle()))

    @border_style.setter
    def border_style(self, s: Style) -> None:
        self._shadow.set_borderStyle(s._shadow)

    @property
    def fill_style(self) -> Style:
        """Стиль заливки элемента отчета."""
        return cast(Style, Style._wrap(self._shadow.get_fillStyle()))

    @fill_style.setter
    def fill_style(self, s: Style) -> None:
        self._shadow.set_fillStyle(s._shadow)

    def update(self) -> None:
        """Перерисовка элемента отчета."""
        self._shadow.redraw()

    def __eq__(self, other: object) -> bool:
        if isinstance(other, ReportItem):
            return self._shadow.equals(other._shadow)
        return NotImplemented

    @property
    @_experimental()
    def _id(self) -> int:
        """
        Уникальный идентификатор в рамках сессии. Инициализируется после добавления элемента в отчет
        """
        return self._shadow.id()

    @property
    @_experimental()
    def _parent_id(self) -> int:
        """
        Уникальный идентификатор элемента отчета, от которого зависит данный элемент.
        """
        return self._shadow.parent_id()

    @property
    @_experimental()
    def _title(self) -> str:
        """
        Возвращает описание типа элемента
        """
        return self._shadow.title()


class ReportItems:
    """Список элементов отчета."""

    if TYPE_CHECKING:
        _shadow: ShadowReportItemsBase

    def __init__(self) -> None:
        raise NotImplementedError

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

    def add(self, item: ReportItem) -> None:
        """
        Добавляет новый элемент в отчет.

        Args:
            item: Вставляемый элемент
        """
        self._shadow.add(item._shadow)

    @_experimental()
    def _move(self, item: ReportItem, index: int) -> None:
        """
        Изменяет порядок элемента.

        Args:
            item: Элемент
            index: Новый индекс
        """
        self._shadow.move(item._shadow, index)

    @property
    def count(self) -> int:
        """Количество элементов отчета в текущем отчете на данный момент."""
        return self.__len__()

    def at(self, idx: int) -> Optional[ReportItem]:
        """
        Возвращает элемент отчета по его индексу.

        Args:
            idx: Индекс.

        Returns:
            Элемент отчета. Возвращает None в случае, если не найдено.
        """
        return ReportItem._wrap(ShadowReportItemsBase.at(self._shadow, idx))

    def __getitem__(self, idx: int) -> Optional[ReportItem]:
        return self.at(idx)

    def remove(self, idx: int) -> None:
        """
        Удаляет элемент по его индексу. Если индекс корректен, элемент будет удален.

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

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

    def __iter__(self) -> Iterator[ReportItem]:
        return cast(Iterator[ReportItem], (self.__getitem__(idx) for idx in range(self.__len__())))

    @_experimental()
    def _find_by_id(self, id: int) -> Optional[ReportItem]:
        """
        Производит поиск элемента по его идентификатору :attr:`axipy.ReportItem._id`

        Args:
            id: Идентификатор.
        """
        if id < 1:
            return None
        for item in self:
            if item._id == id:
                return item
        return None


class Report:
    """
    План отчета для последующей печати.

    .. literalinclude:: /../../tests/doc_examples/render/test_example_report.py
        :caption: Пример создания пустого отчета и вывод его в pdf.
        :pyobject: test_run_example_report
        :lines: 3-
        :dedent: 4

    See also:
        :ref:`ref-report-class`
    """

    # if TYPE_CHECKING:
    #     _shadow: ShadowReport

    class _ReportSignals(QObject):
        _need_redraw = Signal(Rect)

    def __init__(self, printer: QPrinter) -> None:
        self._shadow = ShadowReport(printer)

    def _init_shadow(self, shadow: ShadowReport) -> None:
        shadow.needRedraw.connect(self._slot_shadow_need_redraw)

    @property
    def _shadow(self) -> ShadowReport:
        return self._inner_shadow

    @_shadow.setter
    def _shadow(self, value: ShadowReport) -> None:
        self._inner_shadow = value
        self._inner_shadow.needRedraw.connect(self._slot_shadow_need_redraw)

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

    @cached_property
    def _report_signals(self) -> _ReportSignals:
        return self._ReportSignals()

    @cached_property
    def _signal_need_redraw(self) -> Signal:
        return self._report_signals._need_redraw

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

        Args:
            rect: Часть отчета, которую необходимо обновить.

        :rtype: Signal[QRectF]
        """
        return self._signal_need_redraw

    @Slot(QRectF)
    def _slot_shadow_need_redraw(self, rect: QRectF) -> None:
        self._signal_need_redraw.emit(Rect._rect_value_to_qt(rect))

    def draw(self, context: Context) -> None:
        """
        Выводит отчета в заданном контексте.

        Args:
            context: Контекст, в котором будет отрисован отчет.
        """
        self._shadow.draw(context._shadow)

    @cached_property
    def _items(self) -> ReportItems:
        return ReportItems._wrap(ShadowReport.get_items(self._shadow))

    @property
    def items(self) -> ReportItems:
        """Элементы отчета."""
        return self._items

    @property
    def horisontal_pages(self) -> int:
        """Количество страниц отчета по горизонтали."""
        return self._shadow.get_horisontal_pages()

    @horisontal_pages.setter
    def horisontal_pages(self, v: int) -> None:
        self._shadow.set_horisontal_pages(v)

    @property
    def vertical_pages(self) -> int:
        """Количество страниц отчета по вертикали."""
        return self._shadow.get_vertical_pages()

    @vertical_pages.setter
    def vertical_pages(self, v: int) -> None:
        self._shadow.set_vertical_pages(v)

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

    @unit.setter
    def unit(self, unit: LinearUnit) -> None:
        self._shadow.set_unit(unit._shadow)

    @property
    def page_size(self) -> QSizeF:
        """Размеры одного листа отчета."""
        return self._shadow.page_size()

    @property
    def name(self) -> str:
        """Наименование отчета."""
        return self._shadow.get_name()

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

    def fill_on_pages(self) -> None:
        """
        Максимально заполняет страницу(ы) отчета.

        При этом элементы отчета пропорционально масштабируются.
        """
        self._shadow.fill_on_pages()

    def fit_pages(self) -> None:
        """
        Подгоняет число страниц отчета под размер существующих элементов отчета.

        При этом параметры элементов отчета не меняются.
        """
        self._shadow.fit_pages()


class GeometryReportItem(ReportItem):
    """
    Элемент отчета типа геометрия.

    .. literalinclude:: /../../tests/doc_examples/render/test_example_report.py
        :caption: Пример создания полигона и добавления его в отчет.
        :pyobject: test_run_example_report_geometry
        :lines: 3-
        :dedent: 4

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

    if TYPE_CHECKING:
        _shadow: ShadowGeometryReportItem

    def __init__(self) -> None:
        self._shadow = ShadowGeometryReportItem()

    @property
    def geometry(self) -> Geometry:
        """Геометрическое представление объекта."""
        return cast(Geometry, Geometry._wrap(self._shadow.get_geometry()))

    @geometry.setter
    def geometry(self, g: Geometry) -> None:
        self._shadow.set_geometry(g._shadow)

    @property
    def style(self) -> Style:
        """Стиль геометрического представления объекта."""
        return cast(Style, Style._wrap(self._shadow.get_style()))

    @style.setter
    def style(self, s: Style) -> None:
        self._shadow.set_style(s._shadow)


class MapReportItem(ReportItem):
    """
    Элемент отчета, основанный на созданной ранее карте.

    Note:
        Перед созданием элемента отчета необходимо предварительно создать карту,
        на основе которой будет создан элемент отчета.


    .. literalinclude:: /../../tests/doc_examples/render/test_example_report.py
        :caption: Пример создания карты и добавления ее в отчет.
        :pyobject: test_run_example_report_map
        :start-after: # start map
        :end-before: # finish map
        :dedent: 4
    """

    if TYPE_CHECKING:
        _shadow: ShadowMapReportItem

    # noinspection PyShadowingBuiltins
    def __init__(
        self,
        rect: Union[Rect, QRectF],
        map: Map,
        coordsystem: Optional[CoordSystem] = None,
    ):
        """
        Args:
            rect: Размер элемента отчета в единицах измерения отчета.
            map: Карта, на базе которой будет создан элемент отчета.
            coordsystem: Система координат
        """
        self._shadow = ShadowMapReportItem(
            Rect._rect_value_to_qt(rect),
            map._shadow,
            coordsystem._shadow if coordsystem is not None else None,
        )

    def map(self) -> Map:
        """Возвращает элемент типа карта, на основании которой создается элемент
        отчета."""
        return Map._wrap(self._shadow.map())

    @property
    def center(self) -> Pnt:
        """Центр карты в координатах карты."""
        return Pnt.from_qt(self._shadow.get_center())

    @center.setter
    def center(self, c: Pnt) -> None:
        self._shadow.set_center(Pnt._point_value_to_qt(c))

    @property
    def scale(self) -> float:
        """Текущее значение масштаба карты."""
        return self._shadow.get_scale()

    @scale.setter
    def scale(self, s: float) -> None:
        self._shadow.set_scale(s)

    @property
    def map_rect(self) -> Rect:
        """Прямоугольник карты в единицах измерения карты."""
        return Rect.from_qt(self._shadow.rectMap())

    def show_all(self) -> None:
        """
        Меняет масштаб карты чтобы показать ее полностью.

        Пример замены масштаба для всех элементов отчета::

            for item in reportView.report.items:
                if isinstance(item, MapReportItem):
                    item.show_all()
        """
        self._shadow.show_all()

    @property
    def coordsystem(self) -> CoordSystem:
        """Система координат карты."""
        return cast(CoordSystem, CoordSystem._wrap(self._shadow.coordsystem()))

    @coordsystem.setter
    def coordsystem(self, new_coordsystem: CoordSystem) -> None:
        self._shadow.setCoordSystem(new_coordsystem._shadow)

    @cached_property
    def _clip(self) -> ClipGeometry:
        return ClipGeometry._wrap(self._shadow)

    @property
    def clip(self) -> ClipGeometry:
        """
        Геометрия обрезки карты. Устанавливается геометрия, в рамках которой будет
        отрисована карта. За пределами отрисовка производиться не будет. Обрабатываются
        только площадные объекты. Так-же допустимо устанавливать коллекции.

        Пример установки обрезки по заданной геометрии::

            reportview = axipy.view_manager.active
            if isinstance(reportview, axipy.ReportView):
                rectangle = axipy.Rectangle(axipy.Rect(-50, -50, 50, 50))
                if isinstance(reportview.report.items[0], axipy.MapReportItem):
                    reportview.report.items[0].clip.geometry = rectangle

        Если установить None, то режим сбрасывается::

            axipy.view_manager.active.report.items[0].clip.geometry = None

        Если необходимо сохранить объект, то можно просто отключить режим::

            axipy.view_manager.active.report.items[0].clip.status = False
        """
        return self._clip


class RasterReportItem(ReportItem):
    """
    Элемент отчета, основанный на растре.

    Note:
        В качестве источника может быть как локальный файл, расположенный в файловой системе,
        так и базе растра, размещенного на Web ресурсе.

    Args:
        rect: Размер элемента отчета в единицах измерения отчета.
        data: Путь к растровому файлу или его URL.

    .. literalinclude:: /../../tests/doc_examples/render/test_example_report.py
        :caption: Пример элемента на базе URL.
        :pyobject: test_run_example_report_raster_url
        :lines: 3-
        :dedent: 4

    .. literalinclude:: /../../tests/doc_examples/render/test_example_report.py
        :caption: Пример элемента на базе локального файла.
        :pyobject: test_run_example_report_raster_file
        :lines: 4-
        :dedent: 4
    """

    if TYPE_CHECKING:
        _shadow: ShadowRasterReportItem

    def __init__(self, rect: Union[Rect, QRectF], data: str):
        self._shadow = ShadowRasterReportItem(Rect._rect_value_to_qt(rect), data, _shadow_manager.core)

    @property
    def preserve_aspect_ratio(self) -> bool:
        """Сохранять пропорции при изменении размеров элемента."""
        return self._shadow.get_preserveAspectRatio()

    @preserve_aspect_ratio.setter
    def preserve_aspect_ratio(self, v: bool) -> None:
        self._shadow.set_preserveAspectRatio(v)


class TableReportItem(ReportItem):
    """
    Элемент отчета табличного представления данных.

    Note:
        Позволяет отображать как таблицу целиком, так и накладывая дополнительные ограничения при отображении.

    Args:
        rect: Размер элемента отчета в единицах измерения отчета.
        table: Таблица.

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

    if TYPE_CHECKING:
        _shadow: ShadowTableReportItem

    def __init__(self, rect: Union[Rect, QRectF], table: Table) -> None:
        self._shadow = ShadowTableReportItem(Rect._rect_value_to_qt(rect), table._shadow)

    @property
    def columns(self) -> List[str]:
        """
        Перечень наименований для отображения.

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

    @columns.setter
    def columns(self, value: List[str]) -> None:
        self._shadow.set_selectedColumns(value)

    # noinspection PyPep8Naming
    def refreshValues(self) -> None:
        """Обновление данных из таблицы."""
        self._shadow.refreshValues()

    @property
    def row_from(self) -> int:
        """Номер первой строки из таблицы или запроса."""
        return self._shadow.get_rowFrom()

    @row_from.setter
    def row_from(self, value: int) -> None:
        self._shadow.set_rowFrom(value)

    @property
    def row_count(self) -> int:
        """
        Количество записей.

        Если указано -1, то берутся все оставшиеся записи.
        """
        return self._shadow.get_rowCount()

    @row_count.setter
    def row_count(self, value: int) -> None:
        self._shadow.set_rowCount(value)

    @property
    def start_number(self) -> int:
        """
        Нумерация записей.

        Порядковый номер первой записи.
        """
        return self._shadow.get_startNumber()

    @start_number.setter
    def start_number(self, value: int) -> None:
        self._shadow.set_startNumber(value)

    @property
    def show_row_number(self) -> bool:
        """Показывать ли номера строк."""
        return self._shadow.get_showRowNumber()

    @show_row_number.setter
    def show_row_number(self, v: bool) -> None:
        self._shadow.set_showRowNumber(v)

    def table(self) -> Table:
        """Базовая таблица или запрос."""
        return cast(Table, DataObject._wrap(self._shadow.table()))


class LegendReportItem(ReportItem):
    """
    Элемент отчета, основанный на легенде векторного или тематического слоя.

    Args:
        rect: Размер элемента отчета в единицах измерения отчета.
        legend: Предварительно созданная легенда. Она может относиться как к векторному, так и к тематическому слою.

    .. literalinclude:: /../../tests/doc_examples/render/test_example_report.py
        :caption: Пример создания легенды для тематического слоя.
        :pyobject: test_run_example_report_legend
        :lines: 4-
        :dedent: 4
    """

    if TYPE_CHECKING:
        _shadow: ShadowLegendReportItem

    def __init__(self, rect: Union[Rect, QRectF], legend: Legend):
        self._shadow = ShadowLegendReportItem(Rect._rect_value_to_qt(rect), legend._shadow)

    @property
    def legend(self) -> Legend:
        """Легенда на базе которой создан элемент отчета."""
        return Legend._wrap(ShadowLegendReportItem.legend(self._shadow))


class ScaleBarReportItem(ReportItem):
    """Элемент отчета - масштабная линейка для карты.

    .. literalinclude:: /../../tests/doc_examples/render/test_example_report.py
        :caption: Пример создания масштабной линейки на базе существующего элемента - карты.
        :pyobject: test_run_example_report_map
        :start-after: # start scalebar
        :end-before: # finish scalebar
        :dedent: 4
    """

    if TYPE_CHECKING:
        _shadow: ShadowScaleBarReportItem

    # noinspection PyShadowingBuiltins
    def __init__(self, rect: Union[Rect, QRectF], map: MapReportItem):
        self._shadow = ShadowScaleBarReportItem(Rect._rect_value_to_qt(rect), map._shadow)


class PrototypeReportItem(ReportItem):
    if TYPE_CHECKING:
        _shadow: ShadowClicheReportItem

    def __init__(self, rect: Union[Rect, QRectF]):
        self._shadow = ShadowClicheReportItem(Rect._rect_value_to_qt(rect))
