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

import axipy
from axipy._internal._decorator import _deprecated_by, _experimental
from axipy._internal._utils import _NoInitTypeError
from axipy.cpp_common import ShadowCalcMode
from axipy.cpp_gui import (
    ShadowAlignedView,
    ShadowDrawableView,
    ShadowLegendView,
    ShadowMapView,
    ShadowReportView,
    ShadowTableView,
    ShadowView,
)
from axipy.cs import CoordSystem, LinearUnit
from axipy.da import DataObject, Table
from axipy.render import Layer, Legend, Map, Report, ReportItems, VectorLayer
from axipy.render.clip import ClipGeometry
from axipy.utl import CalcMode, Pnt, Rect
from PySide2.QtCore import QObject, QRect, QRectF, Signal
from PySide2.QtGui import QTransform
from PySide2.QtPrintSupport import QPrinter
from PySide2.QtWidgets import QMainWindow, QMdiSubWindow, QTableView, QWidget

__all__: List[str] = [
    "View",
    "TableView",
    "DrawableView",
    "ReportView",
    "MapView",
    "ListLegend",
    "LegendView",
]


class View:
    """Базовый класс для отображения данных в окне."""

    if TYPE_CHECKING:
        _shadow: ShadowView

    SHOW_NORMAL: Final[int] = 1
    SHOW_MINIMIZED: Final[int] = 2
    SHOW_MAXIMIZED: Final[int] = 3

    def __init__(self) -> None:
        raise _NoInitTypeError

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

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

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

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

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

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

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

        obj_type: Type[View]
        if isinstance(shadow, ShadowTableView):
            obj_type = TableView
        elif isinstance(shadow, ShadowMapView):
            obj_type = MapView
        elif isinstance(shadow, ShadowReportView):
            obj_type = ReportView
        elif isinstance(shadow, ShadowLegendView):
            obj_type = LegendView
        else:
            return None
        return obj_type.__wrap_typed(shadow)

    @property
    def widget(self) -> QWidget:
        """
        Виджет, соответствующий содержимому окна.

        Return:
            Qt5 виджет содержимого.
        """
        return self._shadow.widget()

    @property
    def title(self) -> str:
        """Заголовок окна просмотра."""
        return self._shadow.widget().windowTitle()

    @title.setter
    def title(self, v: str) -> None:
        self._shadow.widget().setWindowTitle(v)

    def reset_parent(self, parent: QWidget) -> None:
        """
        Сбрасывает окно контейнера для карты или таблицы просмотра, если он существует и
        закрывает его. Это требуется когда окно карты или таблица просмотра необходимо
        встроить в другое окно.

        Args:
            parent: Окно - новый родитель

        Пример встраивания окно карты в диалог::

            table = provider_manager.openfile('world.tab')
            layer = Layer.create(table)
            m = Map([layer])
            view = view_manager.create_mapview(m)

            # Диалог, в который будет встраиваться
            class MyDialog(QDialog):

                def __init__(self, parent ):
                    super().__init__(parent)
                    # Инициализируем менеджер компоновки
                    self.layout = QGridLayout()
                    self.setLayout(self.layout)

                def set_mapview(self, view):
                    # Устанавливаем окно карты как контент данного диалога
                    view.reset_parent(self)
                    self.layout.addWidget(view.widget)
                    view.show()

            dialog = MyDialog(view_manager.global_parent)
            dialog.resize(500,300)
            dialog.set_mapview(view)
            dialog.show()
        """
        container = self._base_widget()
        if isinstance(self.widget, QMainWindow):
            tw = self.widget.findChild(QTableView)
            if tw is not None:
                tw.setParent(parent)
        else:
            self.widget.setParent(parent)
        if container:
            container.close()

    def _base_widget(self) -> Union[QWidget, QMdiSubWindow]:
        w: Union[QObject, None] = self.widget
        while w is not None:
            if isinstance(w, QMdiSubWindow):
                return w
            w = w.parent()
        return self.widget

    @property
    def position(self) -> QRect:
        """Размер и положение окна."""
        w = self._base_widget()
        if w is not None:
            return w.geometry()
        return QRect()

    @position.setter
    def position(self, v: QRect) -> None:
        w = self._base_widget()
        if w is not None:
            rect = Rect._rect_value_to_qt(v).toRect()
            w.setGeometry(rect)
            """Move нужен, чтобы переместилась вся рамка виджета, включая заголовок,
            функция setGeometry не учитывает размер заголовка, при размещении виджета на
            экране, в результате, если запускать виджет без аксиомы, под Windows, при
            position = Rect(10, 10, 500, 500), виджет уходит за видимую область
            экрана."""
            w.move(rect.x(), rect.y())

    # noinspection PyShadowingBuiltins
    def show(self, type: int = SHOW_NORMAL) -> None:
        """
        Показывает окно в соответствие с приведенным типом.

        .. csv-table:: Допустимые значения:
            :header: "Константа", "Значение", "Описание"
            :align: left
            :widths: 10, 5, 40

            SHOW_NORMAL, 1, "Обычный показ окна (по умолчанию)."
            SHOW_MINIMIZED, 2, "Показ окна в режиме минимизации."
            SHOW_MAXIMIZED, 3, "Показ окна в режиме распахивания."
        """
        w = self._base_widget()
        if w is not None:
            if type == self.SHOW_MAXIMIZED:
                w.showMaximized()
            elif type == self.SHOW_MINIMIZED:
                w.showMinimized()
            else:
                w.showNormal()

    def close(self) -> None:
        """
        Закрывает окно.

        Note:
            Если при закрытии окна выдается запрос о сохранении данных перед закрытием и необходимо изменить поведение,
            то это можно сделать через соответствующие типу окна настройки или же через свойство
            :attr:`axipy.CurrentSettings.SilentCloseWidget`
        """
        w = self._base_widget()
        if w is not None:
            w.close()

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

        Подробнее см. :meth:`show`
        """
        w = self._base_widget()
        if w is not None:
            if w.isMaximized():
                return self.SHOW_MAXIMIZED
            elif w.isMinimized():
                return self.SHOW_MINIMIZED
        return self.SHOW_NORMAL

    @property
    @_experimental()
    def _id(self) -> int:
        """
        Уникальный идентификатор окна в рамках сессии.

        See also:

            Подробнее об использовании см. :meth:`axipy.ViewManager._find_by_id`.
        """
        return self._shadow.id()

    @property
    def is_active(self) -> bool:
        return axipy.view_manager._shadow.is_active(self._shadow)

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

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

    def __eq__(self, other: object) -> bool:
        if isinstance(other, View):
            return self._id == other._id
        return NotImplemented


class TableView(View):
    """
    Таблица просмотра атрибутивной информации.

    Для создания экземпляра необходимо использовать :meth:`axipy.ViewManager.create_tableview`
    через экземпляр :attr:`axipy.view_manager`.
    """

    if TYPE_CHECKING:
        _shadow: ShadowTableView

    @property
    def data_object(self) -> Table:
        """
        Таблица, на основании которой создается данное окно просмотра.

        Return:
            Таблица.
        """
        return cast(Table, DataObject._wrap(self._shadow.dataobject()))

    @property
    def table_view(self) -> QTableView:
        """
        Ссылка на объект таблицы просмотра.

        Пример установки сортировки для таблицы в текущем окне по второй колонке по возрастанию::

            if isinstance(view_manager.active, TableView):
                view_manager.active.table_view.sortByColumn(2, Qt.AscendingOrder)
        """
        return self._shadow.table_view()


class DrawableView(View):
    """Базовый класс для визуализации геометрических данных."""

    if TYPE_CHECKING:
        _shadow: ShadowDrawableView

    @property
    def snap_mode(self) -> bool:
        """Включает режим привязки координат при редактировании геометрии в окне карты
        или отчета."""
        return self._shadow.isSnapMode()

    @snap_mode.setter
    def snap_mode(self, v: bool) -> None:
        self._shadow.setSnapMode(v)

    def scale_with_center(self, scale: float, center: Pnt) -> None:
        """
        Установка нового центра с заданным масштабированием.

        Args:
            scale: Коэффициент масштабирования по отношению к текущему.
            center: Устанавливаемый центр.
        """
        self._shadow.scaleWithCenter(scale, Pnt._point_value_to_qt(center))

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

        :rtype: Signal[]
        """

        return self._shadow.sceneChanged

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

        :rtype: Signal[]

        Пример::

            view = axipy.view_manager.active
            if view:
                view._selection_changed.connect(lambda: print('Selection changed'))
        """
        return self._shadow.selectionChanged

    @property
    @_experimental()
    def _data_changed(self) -> Signal:
        """
        Сигнал об изменении данных в окне.

        :rtype: Signal[]

        Пример::

        view = axipy.view_manager.active
        if view:
            view._data_changed.connect(lambda: print('Data changed'))
        """
        return self._shadow.dataChanged

    @property
    @_experimental()
    def _snap_status_changed(self) -> Signal:
        """
        Сигнал об изменении статуса режима привязки координат в окне.

        :rtype: Signal[]

        Пример::

            view = axipy.view_manager.active
            if view:
                view._snap_status_changed.connect(lambda status: print(f'Snap status changed: {status}'))
        """
        return self._shadow.snapStatusChanged

    @property
    def is_modified(self) -> bool:
        """Есть ли изменения в окне."""
        return self._shadow.hasModified()

    @property
    def can_undo(self) -> bool:
        """Возможен ли откат на один шаг назад."""
        return self._shadow.canRollBack()

    def undo(self) -> None:
        """Производит откат на один шаг назад."""
        if self.can_undo:
            self._shadow.rollBack()

    @property
    def can_redo(self) -> bool:
        """Возможен ли откат на один шаг вперед."""
        return self._shadow.canRollForward()

    def redo(self) -> None:
        """
        Производит откат на один шаг вперед.

        При этом возвращается состояние до последней отмены.
        """
        if self.can_redo:
            self._shadow.rollForward()

    def offset(self, dx: float, dy: float) -> None:
        """
        Производит сдвиг окна карты или отчета. Особенностью является то, что при этом
        сохраняется прежний центр (актуально для карты).

        Args:
            dx: Смещение по горизонтали в координатах экрана (пикселях)
            dy: Смещение по вертикали в координатах экрана (пикселях)
        """
        self._shadow.offset(dx, dy)

    @property
    def scene_to_device_transform(self) -> QTransform:
        """Объект трансформации из координат карты в координаты окна.

        Return:
            Объект трансформации.
        """
        return self._shadow.sceneToDeviceTransform()

    @property
    def device_to_scene_transform(self) -> QTransform:
        """Объект трансформации из координат окна в координаты карты.

        Return:
            Объект трансформации.
        """
        return self._shadow.deviceToSceneTransform()


class Guidelines:
    if TYPE_CHECKING:
        _shadow: ShadowAlignedView
        _GuidelinesT = TypeVar("_GuidelinesT", bound="Guidelines")

    def __init__(self) -> None:
        raise _NoInitTypeError

    @classmethod
    def _wrap(cls: Type["_GuidelinesT"], shadow: "ShadowAlignedView") -> "_GuidelinesT":
        result = cls.__new__(cls)
        result._shadow = shadow
        return result


class XGuidelines(Guidelines):

    def append(self, v: float) -> None:
        self._shadow.addXGuideline(v)

    def __getitem__(self, idx: int) -> float:
        return self._shadow.xGuidelines()[idx]

    def __setitem__(self, idx: int, v: float) -> None:
        self._shadow.setXGuideline(idx, v)

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


class YGuidelines(Guidelines):

    def append(self, v: float) -> None:
        self._shadow.addYGuideline(v)

    def __getitem__(self, idx: int) -> float:
        return self._shadow.yGuidelines()[idx]

    def __setitem__(self, idx: int, v: float) -> None:
        self._shadow.setYGuideline(idx, v)

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


class ReportView(DrawableView):
    """
    Окно с планом отчета. Для создания экземпляра необходимо использовать
    :meth:`axipy.ViewManager.create_reportview`. через экземпляр
    :attr:`axipy.view_manager`. Параметры отчета :class:`axipy.Report` можно получить
    через свойство :meth:`ReportView.report`.

    Пример создания отчета::

        reportview = view_manager.create_reportview()

        # Добавим полигон
        geomItem = GeometryReportItem()
        geomItem.geometry = Polygon((10,10), (10, 100), (100, 100), (10, 10))
        geomItem.style = PolygonStyle(45, Qt.red)
        reportview.report.items.add(geomItem)

        # Установим текущий масштаб
        reportview.view_scale = 33
    """

    if TYPE_CHECKING:
        _shadow: ShadowReportView

    @property
    def report(self) -> Report:
        """
        Объект отчета.

        Return:
            Отчет.
        """
        return Report._wrap(ShadowReportView.report(self._shadow))

    @property
    def show_mesh(self) -> bool:
        """Показывать сетку привязки."""
        return self._shadow.showMesh()

    @show_mesh.setter
    def show_mesh(self, v: bool) -> None:
        self._shadow.setShowMesh(v)

    @property
    def snap_to_mesh(self) -> bool:
        """Включение режима притяжения элементов отчета к узлам сетки."""
        return self._shadow.snapToMesh()

    @snap_to_mesh.setter
    def snap_to_mesh(self, v: bool) -> None:
        self._shadow.setSnapToMesh(v)

    @property
    def snap_to_guidelines(self) -> bool:
        """Включение режима притяжения элементов отчета к направляющим."""
        return self._shadow.snapToGuidelines()

    @snap_to_guidelines.setter
    def snap_to_guidelines(self, v: bool) -> None:
        self._shadow.setSnapToGuidelines(v)

    @cached_property
    def x_guidelines(self) -> XGuidelines:
        """
        Вертикальные направляющие. Значения содержатся в единицах измерения отчета.

        Рассмотрим на примере::

            # Добавление вертикальной направляющей
            reportview.x_guidelines.append(20)
            # Изменение значения направляющей по индексу
            reportview.x_guidelines[0] = 80
            # Удаление всех направляющих.
            reportview.clear_guidelines()
        """
        return XGuidelines._wrap(self._shadow)

    @cached_property
    def y_guidelines(self) -> YGuidelines:
        """
        Горизонтальные направляющие.

        Работа с ними производится по аналогии с вертикальными направляющими.
        """
        return YGuidelines._wrap(self._shadow)

    def clear_guidelines(self) -> None:
        """Удаляет все направляющие."""
        self._shadow.clearGuidelines()

    def clear_selected_guidelines(self) -> None:
        """Очищает выбранные направляющие."""
        self._shadow.removeSelectedGuideline()

    def fill_on_pages(self) -> None:
        """Наиболее эффективно заполняет пространство отчета масштабированием его
        элементов."""
        self._shadow.fillOnPages()

    @property
    def view_scale(self) -> float:
        """Текущий масштаб."""
        return self._shadow.viewScale()

    @view_scale.setter
    def view_scale(self, v: float) -> None:
        self._shadow.setViewScale(v)

    @property
    def show_borders(self) -> bool:
        """Показывать границы страниц."""
        return self._shadow.showLayoutBorders()

    @show_borders.setter
    def show_borders(self, v: bool) -> None:
        self._shadow.setShowLayoutBorders(v)

    @property
    def show_ruler(self) -> bool:
        """Показывать линейку по краям."""
        return self._shadow.showLayoutRuler()

    @show_ruler.setter
    def show_ruler(self, v: bool) -> None:
        self._shadow.setShowLayoutRuler(v)

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

    @show_elements_size.setter
    def show_elements_size(self, v: bool) -> None:
        self._shadow.setShowElementsSize(v)

    @property
    def mesh_size(self) -> float:
        """Размер ячейки сетки."""
        return self._shadow.meshSize()

    @mesh_size.setter
    def mesh_size(self, v: float) -> None:
        self._shadow.setMeshSize(v)

    @property
    def mouse_moved(self) -> Signal:
        """
        Сигнал при смещении курсора мыши. Возвращает значения в координатах отчета.

        :rtype: Signal[float, float]

        Пример::

            reportview.mouse_moved.connect(lambda x,y: print('Coords: {} {}'.format(x, y)))
        """
        return self._shadow.mouseMoved

    def get_printer(self) -> QPrinter:
        """
        Ссылка на используемый текущий принтер. Для того, чтобы изменить настройки,
        нужно запросить существующий объект, поменять необходимые значения и снова
        назначить посредством :meth:`ReportView.set_printer`. Или же установить другой
        объект :class:`PySide2.QtPrintSupport.QPrinter`.

        .. literalinclude:: /../../tests/doc_examples/render/test_example_report.py
            :caption: Пример смены свойств у текущего окна отчета
            :pyobject: test_run_example_report_printer
            :lines: 3-
            :dedent: 4
        """
        return self._shadow.get_printer()

    def set_printer(self, printer: QPrinter) -> None:
        """
        Устанавливает для отчета объект :class:`PySide2.QtPrintSupport.QPrinter`

        Args:
            printer: Новое значение принтера или измененное запрошенное ранее через :meth:`ReportView.get_printer`
        """
        self._shadow.set_printer(printer)

    def save_template(self, file_name: str) -> None:
        """
        Сохраняет отчет в виде шаблона как файл `*.tmpl`

        Args:
            file_name: Наименование выходного файла.
        """
        self._shadow.saveToFile(file_name)

    @_experimental()
    def _print(self) -> None:
        """Вызов диалога печати отчета."""
        self._shadow.print()

    @_experimental()
    def _print_preview(self) -> None:
        """Предварительный просмотр перед печатью отчета."""
        self._shadow.print_preview()

    @cached_property
    def _selected_items_cached(self) -> "ReportItems":
        return axipy.ReportItems._wrap(ShadowReportView.get_selected_items(self._shadow))

    @property
    @_experimental()
    def _selected_items(self) -> "ReportItems":
        """
        Выделенные в окне отчета элементы.

        Примеры использования::

            # Добавление к выборке первого элемента отчета
            rv = axipy.view_manager.active
            if rv and len(rv.report.items):
                rv._selected_items.add(rv.report.items[0])

            # Удаление первого элемента из выборки
            rv = axipy.view_manager.active
            if rv and len(rv.report.items):
                rv._selected_items.remove(0)
        """
        return self._selected_items_cached


class MapView(DrawableView):
    """
    Окно просмотра карты. Используется для проведения различных манипуляций с картой.
    Для создания экземпляра необходимо использовать
    :meth:`axipy.ViewManager.create_mapview` через экземпляр `view_manager` (пример см.
    ниже).

    Note:
        При создании 'MapView' посредством :meth:`axipy.ViewManager.create_mapview`
        производится клонирование экземпляра карты :class:`axipy.Map`
        и для последующей работы при доступе к данному объекту необходимо использовать свойство :attr:`map`.

    Свойство :attr:`device_rect` определяет размер самого окна карты, а свойство :attr:`scene_rect`
    - прямоугольную область, которая умещается в этом окне в СК карты.

    Преобразование между этими двумя прямоугольниками производится с помощью матриц трансформации
    :attr:`scene_to_device_transform` и :attr:`device_to_scene_transform`.

    К параметрам самой карты можно получить доступ через свойство :attr:`map`.
    Единицы измерения координат (:attr:`unit`) также берутся из наиболее подходящей СК,
    но при желании они могут быть изменены.
    К примеру, вместо метров могут быть установлены километры.

    Рассмотрим пример создания карты с последующим помещением ее в окно ее просмотра. Далее, попробуем преобразовать
    объект типа полигон из координат окна экрана в координаты СК слоя посредством
    :meth:`axipy.Geometry.affine_transform`.

    ::

        # Откроем таблицу, создадим на ее базе слой и добавим в карту
        >>> import axipy
        >>> table_world = axipy.provider_manager.openfile('world.tab')
        >>> world = Layer.create(table_world)
        >>> map_ = Map([world])
        # Для полученной карты создадим окно просмотра
        >>> mapview = axipy.view_manager.create_mapview(map_)
        # Выведем полученные параметры отображения
        >>> print('Прямоугольник экрана:', mapview.device_rect)
        Прямоугольник экрана: (0.0 0.0) (300.0 200.0)
        >>> print('Прямоугольник карты:', mapview.scene_rect)
        Прямоугольник карты: (-16194966.287183324 -8621185.324024437) (16789976.633236416 8326222.646170927)
        # Установим ширину карты и ее центр
        >>> mapview.center = (1000000, 1000000)
        >>> mapview.set_zoom(10e6)

    ::

        # Создадим геометрический объект полигон в координатах экрана и преобразуем его в СК карты
        >>> poly_device = axipy.Polygon([(100,100), (100,150), (150, 150), (150,100)])
        # Используя матрицу трансформации, преобразуем его в координаты карты.
        >>> poly_scene = poly_device.affine_transform(mapview.device_to_scene_transform)
        # Для контроля выведем полученный полигон в виде WKT
        >>> print('WKT:', poly_scene.to_wkt())
        WKT: POLYGON ((-5199985.31371008 -147481.338926755, -5199985.31371008 -4384333.3314756, 297505.173026545 -4384333.3314756, 297505.173026545 -147481.338926755, -5199985.31371008 -147481.338926755))
    """

    if TYPE_CHECKING:
        _shadow: ShadowMapView

    @property
    def map(self) -> Map:
        """
        Объект карты.

        Return:
            Карта.
        """
        return Map._wrap(ShadowMapView.map(self._shadow))

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

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

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

        Return:
            Редактируемый слой. Если не определен, возвращает None.
        """
        return Layer._wrap(ShadowMapView.editableLayer(self._shadow))

    @editable_layer.setter
    def editable_layer(self, layer: VectorLayer) -> None:
        self.map.editable_layer = layer

    @property
    def selected_layer(self) -> Optional[VectorLayer]:
        """
        Выделенный слой на карте.

        Return:
            Выделенный слой. Если выделение отсутствует, возвращает None.
        """
        return Layer._wrap(ShadowMapView.selectionLayer(self._shadow))

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

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

    @property
    def scene_rect(self) -> Rect:
        """
        Видимая область в координатах карты (в единицах измерения СК).

        Return:
            Прямоугольник в координатах карты.
        """
        return Rect.from_qt(self._shadow.sceneRect())

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

    @property
    def device_rect(self) -> Rect:
        """
        Видимая область в координатах окна (пиксели).

        Return:
            Прямоугольник в координатах окна.
        """
        return Rect.from_qt(self._shadow.deviceRect())

    @device_rect.setter
    def device_rect(self, rect: Union[Rect, QRect, QRectF]) -> None:
        r_qt: QRectF = Rect._rect_value_to_qt(rect)
        if r_qt.isValid():
            r_geom: QRect = self._base_widget().geometry()
            r_device: QRectF = self._shadow.deviceRect()
            dx: float = r_geom.width() - r_device.width()
            dy: float = r_geom.height() - r_device.height()
            r_geom_new = QRect(r_geom.left(), r_geom.top(), cast(int, r_qt.width() + dx), cast(int, r_qt.height() + dy))
            self._base_widget().setGeometry(r_geom_new)

    @property
    def editable_layer_changed(self) -> Signal:
        """Сигнал о том, что редактируемый слой сменился."""
        return self._shadow.editableLayerChanged

    def zoom(self, unit: Optional[LinearUnit] = None) -> float:
        """
        Ширина окна карты.

        Args:
            unit: Единицы измерения. Если не заданы, берутся текущие для карты.
        """
        return self._shadow.zoom(unit._shadow if unit is not None else None)

    def set_zoom(self, zoom: float, unit: Optional[LinearUnit] = None) -> None:
        """
        Задание ширины окна карты.

        Args:
            zoom: Значение ширины карты.
            unit: Единицы измерения. Если не заданы, берутся текущие для карты.
        """
        self._shadow.setZoom(zoom, unit._shadow if unit is not None else None)

    def set_zoom_and_center(self, zoom: float, center: Pnt, unit: Optional[LinearUnit] = None) -> None:
        """
        Задает новый центр и ширину окна карты.

        Args:
            zoom: Значение ширины карты.
            center: Центр карты.
            unit: Единицы измерения. Если не заданы, берутся текущие для карты.
        """
        self._shadow.setZoomAndCenter(
            zoom,
            Pnt._point_value_to_qt(center),
            unit._shadow if unit is not None else None,
        )

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

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

    @property
    def scale(self) -> float:
        """Масштаб карты."""
        return self._shadow.scale()

    @scale.setter
    def scale(self, v: float) -> None:
        self._shadow.setScale(v)

    def show_all(self) -> None:
        """Полностью показывает все слои карты."""
        self._shadow.showAll()

    def show_selection(self) -> None:
        """Перемещает карту к группе выделенных объектов, максимально увеличивая
        масштаб, но так, чтобы все объекты попадали."""
        self._shadow.showSelection()

    @property
    def coordsystem_changed(self) -> Signal:
        """
        Сигнал о том, что система координат изменилась.

        Пример::
            >>> import axipy
            >>> layer_world = axipy.Layer.create(table_world)
            >>> map_ = axipy.Map([layer_world])
            >>> mapview = axipy.view_manager.create_mapview(map_)
            >>> mapview.coordsystem_changed.connect(lambda: print('СК была изменена'))
            >>> csLL = axipy.CoordSystem.from_prj("1, 104")
            >>> mapview.coordsystem = csLL
            СК была изменена
        """
        return self._shadow.coordSystemChanged

    @property
    def mouse_moved(self) -> Signal:
        """
        Сигнал при смещении курсора мыши. Возвращает значения в СК карты.

        :rtype: Signal[float, float]

        Пример::

            mapview.mouse_moved.connect(lambda x,y: print('Coords: {} {}'.format(x, y)))

            >> Coords: -5732500.0 958500.0
            >> Coords: -5621900.0 847900.0
        """

        return self._shadow.mouseMoved

    @property
    def coordsystem_visual(self) -> CoordSystem:
        """
        Система координат карты с учетом поправки цены градуса по широте.

        Отличается от :attr:`coordsystem` только лишь в случае, когда основная система
        координат - это широта/долгота и широта имеет ненулевое значение.
        При этом в диапазоне широты (-70...70) градусов вводится поправочный коэффициент,
        растягивающий изображение по широте и равный 1/cos(y).
        """
        return cast(CoordSystem, CoordSystem._wrap(self._shadow.coordSystemVisual()))

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

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

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

            rectangle = axipy.Rectangle(axipy.Rect(-50, -50, 50, 50))
            axipy.view_manager.active.clip.geometry = rectangle

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

            axipy.view_manager.active.clip.geometry = None

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

            axipy.view_manager.active.clip.status = False
        """
        return self._clip

    @property
    def calc_mode(self) -> CalcMode:
        """
        Устанавливает или возвращает режим расчета для карты.

        Пример установки для активной карты::

            if isinstance(view_manager.active, MapView):
                view_manager.active.calc_mode = CalcMode.ELLIPSOID
        """
        return CalcMode(self._shadow.calc_mode())

    @calc_mode.setter
    def calc_mode(self, v: CalcMode) -> None:
        self._shadow.set_calc_mode(ShadowCalcMode(v))


class ListLegend:
    """Список добавленных в окно легенд."""

    if TYPE_CHECKING:
        _shadow: ShadowLegendView

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

    def append(self, legend: Legend) -> None:
        self._shadow.append(legend._shadow)

    def __getitem__(self, idx: int) -> Legend:
        return Legend._wrap(ShadowLegendView.at(self._shadow, idx))

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

    def remove(self, idx: int) -> None:
        self._shadow.remove(idx)

    def __iter__(self) -> Iterator:
        return (self.__getitem__(idx) for idx in range(self.__len__()))


class LegendView(View):
    """
    Легенда для карты.
    Для создания экземпляра необходимо использовать :meth:`axipy.ViewManager.create_legendview`
    через экземпляр `view_manager`.
    В качестве параметра передается открытое ранее окно с картой::

        legendView = view_manager.create_legendview(map_view)

    Список легенд доступен через свойство :attr:`legends`::

        for legend in legendView.legends:
           print(legend.caption)

    Состав может меняться посредством вызова соответствующих методов свойства :attr:`legends`.

    Добавление легенды для слоя карты::

        legend = Legend(map_view.map.layers[0])
        legend.caption = 'Легенда слоя'
        legendView.legends.append(legend)
        legendView.arrange()

    Доступ к элементу по индексу.
    Поменяем описание четвертого оформления у первой легенды :class:`axipy.Legend` окна::

        legend = legendView.legends[1]
        item = legend.items[3]
        item.title = 'Описание'
        legend.items[3] = item

    Удаление первой легенды из окна::

        legendView.legends.remove(0)

    """

    if TYPE_CHECKING:
        _shadow: ShadowLegendView

    @cached_property
    def legends(self) -> ListLegend:
        """Перечень добавленных в окно легенд."""
        return ListLegend._wrap(self._shadow)

    def arrange(self) -> None:
        """Упорядочивает легенды с целью устранения наложений легенд друг на друга."""
        self._shadow.arrange()


def _apply_deprecated() -> None:
    @_deprecated_by("axipy.View.position")
    def rect_get(self: View) -> QRect:
        return self.position

    @_deprecated_by("axipy.View.position")
    def rect_set(self: View, v: QRect) -> None:
        self.position = v

    setattr(View, "rect", property(rect_get, rect_set))


_apply_deprecated()
