import abc
import copy
import enum
import typing
from typing import List, Optional

import axipy
from axipy import CoordSystem, FloatCoord, Geometry, LinearUnit, Pnt, Unit, UnitValue
from PySide2.QtCore import QAbstractTableModel, QModelIndex, QObject, Qt, Signal
from PySide2.QtWidgets import QAction, QUndoCommand, QUndoStack

from ...helper import clone_cs
from .cartesian_utils import CartesianAngleConverter
from .data_types import (
    AngleType,
    CPAngle,
    CPModelMode,
    CPNode,
    CSPoint,
    IndexRange,
    RawNodeData,
)
from .utils import AngleConverter, NormalizeStrategy, normalize_angle


def sphere_converter(model: "CPModel") -> AngleConverter:
    """
    Фабричный метод упрощающий создание конвертера для углов
    """
    source_angles = [copy.deepcopy(node.angle) for node in model._source_nodes]
    converter = AngleConverter(model.coordsystem, source_angles)
    view_angles = [copy.deepcopy(node.angle) for node in model._view_nodes]
    converter.set_view_angles(view_angles)
    return converter


def cartesian_converter(model: "CPModel") -> CartesianAngleConverter:
    """
    Фабричный метод упрощающий создание конвертера для углов
    """
    converter = CartesianAngleConverter()
    view_angles = [copy.deepcopy(node.angle) for node in model._view_nodes]
    converter.set_view_angles(view_angles)
    return converter


class Column(enum.Enum):
    """
    Список колонок для таблицы координат точек
    """

    angle = 0
    distance = 1
    x = 2
    y = 3
    last = y
    first = angle


class SetDataCommand(QUndoCommand):
    """
    Комманда редактирования данных через таблицу (Model::setData)
    """

    def __init__(self, index: QModelIndex, value: typing.Any, role: int, model: "CPModel") -> None:
        super().__init__()
        self.setText(axipy.tr("редактирование параметров точек"))
        self.__model = model
        self.__value = value
        self.__row = index.row()
        self.__column = index.column()
        self.__role = role
        tmp_copy = copy.deepcopy(model._source_nodes)
        self.__original_points = tmp_copy[index.row() :]

    def undo(self):
        count = len(self.__original_points)
        self.__model._set_source_nodes(self.__row, count, copy.deepcopy(self.__original_points))

    def redo(self):
        index = self.__model.index(self.__row, self.__column)
        self.__model._set_data_impl(index, self.__value, self.__role)


class InsertCommand(QUndoCommand):

    def __init__(self, node: CPNode, index: int, model: "CPModel") -> None:
        super().__init__()
        self.setText(axipy.tr("добавление точки"))
        self.__node = copy.deepcopy(node)
        self.__model = model
        self.__adding_row = index

    def redo(self):
        self.__model._insert_point_impl(copy.deepcopy(self.__node), self.__adding_row)

    def undo(self):
        self.__model._remove_rows_impl(self.__adding_row, 1)


class RemoveCommand(QUndoCommand):

    def __init__(self, row: int, count: int, parent: QModelIndex, model: "CPModel") -> None:
        super().__init__()
        self.setText(axipy.tr("удаление точки"))
        self.__row = row
        self.__count = count
        self.__model = model
        self.__parent = parent
        self.__removed_nodes = copy.deepcopy(model._source_nodes[row : row + count])
        self.__next_src_node = None
        self.__next_view_node = None
        self.__next_row = None
        # Угл и расстояние не применимы для первой точки поэтому они зануляются.
        # Однако, если удалить первую точку и затем отменить удаление, получится,
        # что точка, следовавшая за нулевой, останется с нулевыми координатами.
        # Чтобы такого не проиходило, в комманду удаления записывается информация
        # о следующей точке.
        if row == 0 and self.__model.size() >= 2:
            self.__next_row = row + 1
            self.__next_src_node = copy.deepcopy(model._source_nodes[self.__next_row])
            self.__next_view_node = copy.deepcopy(model._view_nodes[self.__next_row])

    def redo(self):
        self.__model._remove_rows_impl(copy.deepcopy(self.__row), copy.deepcopy(self.__count), self.__parent)

    def undo(self):
        nodes = copy.deepcopy(self.__removed_nodes)
        self.__model._insert_source_nodes(copy.deepcopy(self.__row), nodes)
        if self.__next_view_node is not None and self.__next_src_node is not None:
            view_node = copy.deepcopy(self.__next_view_node)
            src_node = copy.deepcopy(self.__next_src_node)
            index = copy.deepcopy(self.__next_row)
            self.__model._silent_update_neighbor_nodes(index=index, source_node=src_node, view_node=view_node)


def cs_name(cs: CoordSystem) -> str:
    if cs is None:
        return "None"
    return cs.name


class SetCSCommand(QUndoCommand):

    def __init__(self, cs: CoordSystem, model: "CPModel"):
        super().__init__()
        self.setText(axipy.tr(f"установка координатной системы: {cs.name}"))
        self.__cs = clone_cs(cs)
        self.__cs_old = clone_cs(model.coordsystem)
        self.__model = model

    def redo(self):
        self.__model._set_coordsystem_impl(clone_cs(self.__cs))

    def undo(self):
        self.__model._set_coordsystem_impl(clone_cs(self.__cs_old))


class SetAngleTypeCommand(QUndoCommand):

    def __init__(self, angle_type: AngleType, model: "CPModel") -> None:
        super().__init__()
        self.setText(axipy.tr("смену направления угла: " + AngleType.to_str(angle_type)))
        self.__angle_type = copy.deepcopy(angle_type)
        self.__old_angle_type = copy.deepcopy(model.angle_type)
        self.__model = model

    def redo(self):
        self.__model._set_angle_type_impl(copy.deepcopy(self.__angle_type))

    def undo(self):
        self.__model._set_angle_type_impl(copy.deepcopy(self.__old_angle_type))


class SetUnitCommand(QUndoCommand):

    def __init__(self, unit: LinearUnit, model: "CPModel") -> None:
        super().__init__()
        self.setText(axipy.tr(f"установка единиц измерения: {unit.description}"))
        self.__model = model
        self.__old_unit = self.__copy_unit(model.unit)
        self.__unit = self.__copy_unit(unit)

    @staticmethod
    def __copy_unit(copy_unit: LinearUnit):
        # TODO: implement clone() for Units.
        return LinearUnit._wrap(getattr(copy_unit, "_shadow"))

    def redo(self):
        self.__model._set_unit_impl(self.__copy_unit(self.__unit))

    def undo(self):
        self.__model._set_unit_impl(self.__copy_unit(self.__old_unit))


class MapViewCSChanged(QUndoCommand):

    def __init__(self, cs: CoordSystem, old_cs: CoordSystem, model: "CPModel") -> None:
        super().__init__()
        self.setText(axipy.tr(f"установка КС карты {cs.name}"))
        self.__cs = clone_cs(cs)
        self.__old_cs = clone_cs(old_cs)
        self.__model = model

    def redo(self):
        self.__model._set_map_view_cs_impl(clone_cs(self.__cs))

    def undo(self):
        self.__model._set_map_view_cs_impl(clone_cs(self.__old_cs))


class ClearAllCommand(QUndoCommand):

    def __init__(self, model: "CPModel") -> None:
        super().__init__()
        self.setText(axipy.tr("удаление всех точек"))
        self.__model = model
        self.__source_nodes = copy.deepcopy(self.__model._source_nodes)

    def redo(self):
        self.__model.beginResetModel()
        self.__model._remove_rows_impl(0, self.__model.size())
        self.__model.endResetModel()

    def undo(self):
        self.__model.beginResetModel()
        nodes = copy.deepcopy(self.__source_nodes)
        self.__model._set_source_nodes(0, len(self.__source_nodes), nodes)
        self.__model.endResetModel()


class ChangeWorkingModeCommand(QUndoCommand):

    def __init__(self, mode: CPModelMode, model: "CPModel") -> None:
        super().__init__()
        self.__mode = copy.deepcopy(mode)
        self.__prev_mode = copy.deepcopy(model.mode)
        self.__model = model
        self.setText(axipy.tr("изменение режима расчета углов и расстояний"))
        self.__source_nodes = copy.deepcopy(self.__model._source_nodes)
        # Исходим из предположения, что пользователь вводил углы и расстояния
        # и потом понял, что выбрал не тот режим. Внутренние форматы хранения углов
        # для двух режимов отличаются, поэтому нужно сконвертировать из одного в
        # другой так, как будто бы это делал пользователь
        self.__dest_source_nodes = copy.deepcopy(self.__model._source_nodes)
        view_nodes = copy.deepcopy(self.__model._view_nodes)  # type: List[CPNode]
        converter = cartesian_converter(self.__model)
        if self.__mode == CPModelMode.Sphere:
            converter = sphere_converter(self.__model)
        for index, node in enumerate(view_nodes):
            dest_angle = converter.to_axi_angle(node.angle, node.angle_type, index)
            self.__dest_source_nodes[index].angle = dest_angle

    def redo(self):
        self.__model._set_source_nodes(0, len(self.__dest_source_nodes), copy.deepcopy(self.__dest_source_nodes))
        self.__model._set_mode(copy.deepcopy(self.__mode))

    def undo(self):
        source_nodes = copy.deepcopy(self.__source_nodes)
        self.__model._set_source_nodes(0, len(self.__source_nodes), source_nodes)
        self.__model._set_mode(copy.deepcopy(self.__prev_mode))


class CPModelImpl(abc.ABC):
    """
    Интерфейс для классов отвечающих за внутренее поведения модели
    для разных режимов работы инструмента
    """

    def __init__(self, model: "CPModel") -> None:
        self.__model = model

    @property
    def model(self) -> "CPModel":
        return self.__model

    @abc.abstractmethod
    def _update_neighbor_coordinates(self, from_row: int, to_row: int) -> IndexRange:
        pass

    @abc.abstractmethod
    def _fill_all_node_param(self, raw_node_data: RawNodeData) -> CPNode:
        """
        Создаёт ноду для переданной точки и заполняет значения азимута и расстояния
        до предыдущей точки.
        """
        pass

    def __fill_only_node_coordinate(self, raw_node_data: RawNodeData) -> CPNode:

        if self.__model.is_empty:
            node = CPNode(raw_node_data.point, CPAngle(0, AngleType.ClockWiseTop), UnitValue(0, Unit.m))
        else:
            cs = raw_node_data.point.coordsystem
            points = self.__model.points_in_cs(cs)
            cs_point = CSPoint(points[self.__model.size() - 1], cs)
            node = CPNode(cs_point, CPAngle(0, AngleType.ClockWiseTop), UnitValue(0, Unit.m))
        return node

    def _node_from_raw_data(self, raw_node_data: RawNodeData) -> CPNode:
        if raw_node_data.evaluate_other:
            return self._fill_all_node_param(raw_node_data)
        else:
            return self.__fill_only_node_coordinate(raw_node_data)

    @abc.abstractmethod
    def _change_coordinate(self, index: QModelIndex, value: typing.Any, role: int) -> IndexRange:
        pass

    @abc.abstractmethod
    def _change_angle_or_distance(self, index: QModelIndex, value: typing.Any, role: int) -> IndexRange:
        pass

    @abc.abstractmethod
    def _update_view_angles(self):
        pass

    @abc.abstractmethod
    def _update_view_distances(self):
        pass

    @abc.abstractmethod
    def _update_view_coordinates(self):
        pass


class CPModel(QAbstractTableModel):
    map_view_coordsystem_chagned = Signal(CoordSystem)
    coordsystem_changed = Signal(CoordSystem)
    override_coordsystem_chagned = Signal(CoordSystem)
    point_was_added = Signal(QModelIndex)
    point_was_deleted = Signal(int)
    angle_type_changed = Signal(AngleType)
    distance_unit_changed = Signal(LinearUnit)

    class CartesianModelImpl(CPModelImpl):
        """
        Модель для работы с точками в декартовой системе координат
        """

        def _fill_all_node_param(self, raw_node_data: RawNodeData) -> CPNode:
            if self.model.is_empty:
                return CPNode(
                    point=raw_node_data.point, angle=CPAngle(0, AngleType.ClockWiseTop), distance=UnitValue(0, Unit.m)
                )
            else:
                # Если добавлена только одна точка то индексом предыдущей точки будет 0
                cs = raw_node_data.point.coordsystem
                prev_point_index = max(self.model.size() - 1, 0)
                p1 = self.model.points_in_cs(cs)[prev_point_index]
                p2 = raw_node_data.point.to_pnt()
                length, angle = Geometry.distance_by_points(p1, p2, None)
                angle = normalize_angle(angle, strategy=NormalizeStrategy.ToPositive)
                # angle это азимут в градусах, следовательно тип угла AngleType.ClockWiseTop
                cad_angle = CPAngle(angle, AngleType.ClockWiseTop)
                return CPNode(point=raw_node_data.point, angle=cad_angle, distance=UnitValue(length, Unit.m))

        def _change_angle_or_distance(self, index: QModelIndex, value: typing.Any, role: int) -> Optional[IndexRange]:
            edited_source_node = self.model._source_nodes[index.row()]
            if index.column() == Column.angle.value:
                # Обновление угла для первой точки точки смысла не имеет и в текущей реализации невозможно
                # FIXME: Подобную проверку нужно вынести в отдельный метод
                if index.row() <= 0:
                    return None
                angle = normalize_angle(value)
                if self.model.angle_type == AngleType.Relate:
                    return self.__change_angle_for_relate_angle_type(index, angle)
                else:
                    edited_source_node.angle = cartesian_converter(self.model).to_axi_angle(
                        angle, self.model.angle_type, index.row()
                    )
                    edited_source_node.angle_type = AngleType.ClockWiseTop
                    index_range = self._update_neighbor_coordinates(index.row(), self.model.size())
                    return index_range
            elif index.column() == Column.distance.value:
                edited_source_node.distance = value
                edited_source_node.distance_type = self.model.unit
                self._update_neighbor_coordinates(index.row(), self.model.size())
            return IndexRange(
                self.model.index(index.row(), Column.first.value), self.model.index(index.row(), Column.last.value)
            )

        def __change_angle_for_relate_angle_type(self, index: QModelIndex, user_view_value: float):
            view_angles = [copy.deepcopy(node.angle) for node in self.model._view_nodes]
            view_angles[index.row()] = user_view_value
            # начинаем со следующего т.к. параметры редактируемого узла уже расчитанны
            for node_index in range(index.row(), self.model.size()):
                source_node = self.model._source_nodes[node_index]
                # относительные углы уже подсчитаны, просто берём те значения, которые
                # видим в таблице
                view_relate_angle = self.model._view_nodes[node_index].angle
                # внутренее представлениее для план-схемы отличается от не декартовых
                # проекций, поэтому перессчитываем значение угла через конвертер
                converter = CartesianAngleConverter()
                converter.set_view_angles(view_angles)
                relate_angle = converter.to_axi_angle(view_relate_angle, self.model.angle_type, node_index)
                source_node.angle = relate_angle
                source_node.angle_type = AngleType.ClockWiseTop
            return self._update_neighbor_coordinates(index.row(), self.model.size())

        def _update_neighbor_coordinates(self, from_row: int, to_row: int) -> IndexRange:
            """
            Диапазон индексов для которых нужно пересчитать координаты точек опираясь на углы
            Индексы в диапазоне: [from_row, to_row)
            """
            for i in range(from_row, to_row):
                if i == 0:
                    # Менять значение угла и расстояния для самой первой точки бесмыслленно
                    continue
                prev = self.model._source_nodes[i - 1]
                current = self.model._source_nodes[i]
                prev_pnt = prev.cs_point.reproject(self.model.coordsystem).to_pnt()
                distance_in_meter = current.distance_type.to_unit(Unit.m, current.distance)
                angle = cartesian_converter(self.model).from_axi_angle(
                    index=i,
                    angle=current.angle,
                    prev_angle=prev.angle,
                    angle_type=AngleType.Math,
                )
                current_new_pnt = Geometry.point_by_azimuth(prev_pnt, angle, distance_in_meter, None)
                current.cs_point = CSPoint(current_new_pnt, self.model.coordsystem)
            top_left = self.model.index(from_row, Column.first.value)
            bottom_right = self.model.index(to_row - 1, Column.last.value)
            return IndexRange(top_left, bottom_right)

        def _update_view_angles(self):
            # обновляем отображение углов
            converter = cartesian_converter(self.model)
            for i in range(self.model.size()):
                node = self.model._source_nodes[i]
                prev_value = 0
                if i > 0:
                    prev_value = self.model._source_nodes[i - 1].angle
                angle = converter.from_axi_angle(
                    index=i,
                    angle=node.angle,
                    prev_angle=prev_value,
                    angle_type=self.model.angle_type,
                    # cs=self.model.map_view_cs,
                )
                view_node = self.model._view_nodes[i]
                view_node.angle = angle
                view_node.angle_type = copy.deepcopy(self.model.angle_type)

        def _update_view_distances(self):
            # обновляем отображение расстояний
            for view, source in zip(self.model._view_nodes, self.model._source_nodes):
                view.distance = source.distance_type.to_unit(self.model.unit, source.distance)
                view.distance_type = self.model.unit

        def _update_view_coordinates(self):
            # обновляем значения координат
            self.model._view_nodes = [p.reproject(self.model.coordsystem) for p in self.model._source_nodes]

        def _change_coordinate(self, index: QModelIndex, value: typing.Any, role: int) -> IndexRange:
            # Когда пользователь редактирует координаты точки он хочет сохранить именно
            # то что видит, поэтому берём self.model._view_nodes
            edited_node = copy.deepcopy(self.model._view_nodes[index.row()])
            if index.column() == Column.x.value:
                edited_node.cs_point = CSPoint(Pnt(value, edited_node.y), self.model.coordsystem)
                self.model._source_nodes[index.row()] = edited_node
            elif index.column() == Column.y.value:
                edited_node.cs_point = CSPoint(Pnt(edited_node.x, value), self.model.coordsystem)
                self.model._source_nodes[index.row()] = edited_node
            # Обновляем значения углов и расстояний для соседних точек
            return self.__update_angle_and_distance(index)

        def __update_angle_and_distance(self, index: QModelIndex) -> IndexRange:
            index_edited_node = index.row()
            if self.model.size() <= 1:
                # Если это единиственный узел ничего не обновляем
                return IndexRange(index, index)
            index_next_node = index_edited_node + 1
            index_prev_node = index_edited_node - 1

            def is_valid(index_arg: int):
                return index_arg != index_edited_node and 0 <= index_arg < self.model.size()

            if is_valid(index_prev_node):
                self.__update_angle_and_distance_impl(
                    index.row(), first_index=index_prev_node, second_index=index.row(), cs=self.model.coordsystem
                )
            if is_valid(index_next_node):
                self.__update_angle_and_distance_impl(
                    index_next_node, first_index=index.row(), second_index=index_next_node, cs=self.model.coordsystem
                )
            bottom_right_index = self.model.index(index_edited_node, Column.last.value)
            return IndexRange(index, bottom_right_index)

        def __update_angle_and_distance_impl(
            self, edited_index: int, first_index: int, second_index: int, cs: CoordSystem
        ):
            # Исходные точки могут быть в разных КС, поэтому для корректности расчётов
            # нужно их привести к общей КС.
            first_cs_pnt = self.model._source_nodes[first_index].cs_point.reproject(cs)
            second_cs_pnt = self.model._source_nodes[second_index].cs_point.reproject(cs)
            distance, azimuth = Geometry.distance_by_points(first_cs_pnt.to_pnt(), second_cs_pnt.to_pnt(), None)
            # избавляемся от отрицательных значений углов
            azimuth = normalize_angle(azimuth, strategy=NormalizeStrategy.ToPositive)
            edited_node = self.model._source_nodes[edited_index]
            edited_node.angle = azimuth
            edited_node.angle_type = AngleType.ClockWiseTop
            edited_node.distance = distance
            edited_node.distance_type = Unit.m

    class SphereModelImpl(CPModelImpl):
        """
        Модель для работы с точками на сфере
        """

        def _fill_all_node_param(self, raw_node_data: RawNodeData) -> CPNode:
            if self.model.is_empty:
                return CPNode(
                    point=raw_node_data.point, angle=CPAngle(0, AngleType.ClockWiseTop), distance=UnitValue(0, Unit.m)
                )
            else:
                # Если добавлена только одна точка то за предыдущую точку берём её же
                cs = raw_node_data.point.coordsystem
                prev_point_index = max(self.model.size() - 1, 0)
                p1 = self.model.points_in_cs(cs)[prev_point_index]
                p2 = raw_node_data.point.to_pnt()
                distance, angle = Geometry.distance_by_points(p1, p2, cs)
                angle = normalize_angle(angle, strategy=NormalizeStrategy.ToPositive)
                # angle это азимут в градусах, следовательно тип угла AngleType.ClockWiseTop
                cad_angle = CPAngle(angle, AngleType.ClockWiseTop)
                return CPNode(point=raw_node_data.point, angle=cad_angle, distance=UnitValue(distance, Unit.m))

        def _change_angle_or_distance(self, index: QModelIndex, value: typing.Any, role: int) -> IndexRange:
            edited_source_node = self.model._source_nodes[index.row()]
            if index.column() == Column.angle.value:

                def update_angle(node_index: int, relative_angle):
                    # Конвертируем угол заданный пользователем во внутреннее представление
                    source_angles = [copy.deepcopy(node.angle) for node in self.model._source_nodes]
                    converter = AngleConverter(self.model.coordsystem, source_angles)
                    view_angles = [copy.deepcopy(node.angle) for node in self.model._view_nodes]
                    converter.set_view_angles(view_angles)
                    angle = converter.to_axi_angle(relative_angle, self.model.angle_type, node_index)
                    edited_source_node.angle = angle
                    edited_source_node.angle_type = AngleType.ClockWiseTop

                normalized_angle = normalize_angle(value, strategy=NormalizeStrategy.ToPositive)
                update_angle(index.row(), normalized_angle)
                if self.model.angle_type == AngleType.Relate:
                    return self.__change_angle_for_relate_angle_type(index, normalized_angle)
                return self._update_neighbor_coordinates(index.row(), self.model.size())
            elif index.column() == Column.distance.value:
                edited_source_node.distance = value
                edited_source_node.distance_type = self.model.unit
                self._update_neighbor_coordinates(index.row(), self.model.size())
            return IndexRange(
                self.model.index(index.row(), Column.first.value), self.model.index(index.row(), Column.last.value)
            )

        def __change_angle_for_relate_angle_type(self, index: QModelIndex, user_view_value: float):
            edited_source_node = self.model._source_nodes[index.row()]
            edited_node_angle = copy.deepcopy(edited_source_node.angle)
            view_angles = [copy.deepcopy(node.angle) for node in self.model._view_nodes]
            view_angles[index.row()] = user_view_value
            # начинаем со следующего т.к. параметры редактируемого узла уже расчитанны
            for node_index in range(index.row() + 1, self.model.size()):
                source_node = self.model._source_nodes[node_index]
                # относительные углы уже подсчитаны, просто берём те значения, которые
                # видим на экране
                view_relate_angle = self.model._view_nodes[node_index].angle
                if self.model.map_view_cs.non_earth:
                    # внутренее представлениее для план-схемы отличается от не декартовых
                    # проекций, поэтому перессчитываем значение угла через конвертер
                    source_angles = [copy.deepcopy(node.angle) for node in self.model._source_nodes]
                    converter = AngleConverter(self.model.coordsystem, source_angles)
                    converter.set_view_angles(view_angles)
                    relate_angle = converter.to_axi_angle(view_relate_angle, self.model.angle_type, node_index)
                    source_node.angle = relate_angle
                    source_node.angle_type = AngleType.ClockWiseTop
                else:
                    edited_node_angle += view_relate_angle
                    source_node.angle = edited_node_angle
                    source_node.angle_type = AngleType.ClockWiseTop
            return self._update_neighbor_coordinates(index.row(), self.model.size())

        def _update_neighbor_coordinates(self, from_row: int, to_row: int) -> IndexRange:
            """
            Диапазон индексов для которых нужно пересчитать координаты точек опираясь на углы
            Индексы в диапазоне: [from_row, to_row)
            """
            for i in range(from_row, to_row):
                if i == 0:
                    # Менять значение угла и расстояния для самой первой точки бесмыслленно
                    continue
                prev = self.model._source_nodes[i - 1]
                current = self.model._source_nodes[i]
                prev_pnt = prev.cs_point.reproject(self.model.coordsystem).to_pnt()
                distance_in_meter = current.distance_type.to_unit(Unit.m, current.distance)
                current_new_pnt = Geometry.point_by_azimuth(
                    prev_pnt, current.angle, distance_in_meter, self.model.coordsystem
                )
                current.cs_point = CSPoint(current_new_pnt, self.model.coordsystem)
            top_left = self.model.index(from_row, Column.first.value)
            bottom_right = self.model.index(to_row - 1, Column.last.value)
            return IndexRange(top_left, bottom_right)

        def _update_view_angles(self):
            # обновляем отображение углов
            angles = [copy.deepcopy(node.angle) for node in self.model._source_nodes]
            converter = AngleConverter(self.model.coordsystem, angles)
            for i in range(self.model.size()):
                node = self.model._source_nodes[i]
                prev_value = 0
                if i > 0:
                    prev_value = self.model._source_nodes[i - 1].angle
                angle = converter.from_axi_angle(
                    i, node.angle, prev_value, self.model.angle_type, cs=self.model.map_view_cs
                )
                view_node = self.model._view_nodes[i]
                view_node.angle = angle
                view_node.angle_type = copy.deepcopy(self.model.angle_type)

        def _update_view_distances(self):
            # обновляем отображение расстояний
            for view, source in zip(self.model._view_nodes, self.model._source_nodes):
                view.distance = source.distance_type.to_unit(self.model.unit, source.distance)
                view.distance_type = self.model.unit

        def _update_view_coordinates(self):
            # обновляем значения координат
            self.model._view_nodes = [p.reproject(self.model.coordsystem) for p in self.model._source_nodes]

        def _change_coordinate(self, index: QModelIndex, value: typing.Any, role: int) -> IndexRange:
            # Когда пользователь редактирует координаты точки он хочет сохранить именно
            # то что видит, поэтому берём self.model._view_nodes
            edited_node = copy.deepcopy(self.model._view_nodes[index.row()])
            if index.column() == Column.x.value:
                edited_node.cs_point = CSPoint(Pnt(value, edited_node.y), self.model.coordsystem)
                self.model._source_nodes[index.row()] = edited_node
            elif index.column() == Column.y.value:
                edited_node.cs_point = CSPoint(Pnt(edited_node.x, value), self.model.coordsystem)
                self.model._source_nodes[index.row()] = edited_node
            # Обновляем значения углов и расстояний для соседних точек
            return self.__update_angle_and_distance(index)

        def __update_angle_and_distance(self, index: QModelIndex) -> IndexRange:
            index_edited_node = index.row()
            if self.model.size() <= 1:
                # Если это единиственный узел ничего не обновляем
                return IndexRange(index, index)
            index_next_node = index_edited_node + 1
            index_prev_node = index_edited_node - 1

            def is_valid(index_arg: int):
                return index_arg != index_edited_node and 0 <= index_arg < self.model.size()

            if is_valid(index_prev_node):
                self.__update_angle_and_distance_impl(
                    index.row(), first_index=index_prev_node, second_index=index.row(), cs=self.model.coordsystem
                )
            if is_valid(index_next_node):
                self.__update_angle_and_distance_impl(
                    index_next_node, first_index=index.row(), second_index=index_next_node, cs=self.model.coordsystem
                )
            bottom_right_index = self.model.index(index_edited_node, Column.last.value)
            return IndexRange(index, bottom_right_index)

        def __update_angle_and_distance_impl(
            self, edited_index: int, first_index: int, second_index: int, cs: CoordSystem
        ):
            # Исходные точки могут быть в разных КС, поэтому для корректности расчётов
            # нужно их привести к общей КС.
            first_cs_pnt = self.model._source_nodes[first_index].cs_point.reproject(cs)
            second_cs_pnt = self.model._source_nodes[second_index].cs_point.reproject(cs)
            distance, azimuth = Geometry.distance_by_points(first_cs_pnt.to_pnt(), second_cs_pnt.to_pnt(), cs)
            azimuth = normalize_angle(azimuth, strategy=NormalizeStrategy.ToPositive)
            edited_node = self.model._source_nodes[edited_index]
            edited_node.angle = azimuth
            edited_node.angle_type = AngleType.ClockWiseTop
            edited_node.distance = distance
            edited_node.distance_type = Unit.m

    class CPModelFactory:

        @staticmethod
        def make_model(mode: CPModelMode, model: "CPModel") -> CPModelImpl:
            if mode == CPModelMode.Cartesian:
                return CPModel.CartesianModelImpl(model)
            else:
                return CPModel.SphereModelImpl(model)

    def __init__(self, angle_type: AngleType, cs: CoordSystem = None, parent: QObject = None) -> None:
        super().__init__(parent)
        # self.iface = iface
        # Координатная система (КС) в которой отображаются данные
        self.__view_cs = cs
        # Рельная текущая КС карты
        self.__map_view_cs = cs
        # КС редактируемого слоя
        self.__editable_layer_cs = None
        self.__header_titles = {
            Column.x.value: "X",
            Column.y.value: "Y",
            Column.angle.value: axipy.tr("Угол"),
            Column.distance.value: axipy.tr("Расстояние"),
        }
        # Два списка с точками нужны чтобы избежать накопления ошибки трансформации.
        # При смене КС карты внутренний список точек пересчитывается, а после обратной
        # трансформации точки уже немного другие. Поэтому список оригинальных точек
        # которые задал пользователь храниться отдельно от списка используемого для
        # отображения и отрисовки. Точки в обоих списках могут совпадать.
        self.__source_nodes = []  # type: List[CPNode]
        self.__view_nodes = []  # type: List[CPNode]
        # self.__angle_type = AngleType.ClockWiseTop # type: AngleType
        self.__angle_type = angle_type
        self.__distance_unit = Unit.m  # type: LinearUnit
        self.__undo_stack = QUndoStack(self)
        self.__undo_action = self.__undo_stack.createUndoAction(self)
        self.__redo_action = self.__undo_stack.createRedoAction(self)
        self.__mode = CPModelMode.Cartesian
        self.__impl = CPModel.CPModelFactory.make_model(self.__mode, self)  # type: CPModelImpl

    @property
    def mode(self) -> CPModelMode:
        return self.__mode

    @mode.setter
    def mode(self, mode: CPModelMode):
        self.__undo_stack.push(ChangeWorkingModeCommand(mode, self))

    def _set_mode(self, mode: CPModelMode):
        """
        Внутренняя функция, для смены режима расчётов углов и расстояний используейте mode.
        """
        self.__mode = mode
        self.__change_model_impl(mode)
        index_range = self.__impl._update_neighbor_coordinates(from_row=0, to_row=self.size())
        self.__full_view_update()
        if index_range is not None:
            self.dataChanged.emit(index_range.top_left, index_range.bottom_right)

    def __change_model_impl(self, mode: CPModelImpl):
        self.__impl = CPModel.CPModelFactory.make_model(mode, self)

    @property
    def editable_layer_cs(self) -> Optional[CoordSystem]:
        return self.__editable_layer_cs

    @editable_layer_cs.setter
    def editable_layer_cs(self, cs: CoordSystem):
        self.__editable_layer_cs = cs

    @property
    def map_view_cs(self) -> CoordSystem:
        return self.__map_view_cs

    def can_push_map_view_change_cs_command(self, cs) -> bool:
        return cs != self.__map_view_cs

    def set_map_view_cs(self, cs: CoordSystem, old_cs: CoordSystem):
        if not self.can_push_map_view_change_cs_command(cs):
            return
        self.__undo_stack.push(MapViewCSChanged(cs, old_cs, self))

    def _set_map_view_cs_impl(self, cs: CoordSystem):
        self.__map_view_cs = cs
        self.map_view_coordsystem_chagned.emit(clone_cs(cs))

    @property
    def undo_action(self) -> QAction:
        return self.__undo_action

    @property
    def redo_action(self) -> QAction:
        return self.__redo_action

    def start_commands_recording(self):
        self.__undo_stack.clear()

    def begin_command_macro(self, text: str):
        self.__undo_stack.beginMacro(text)

    def end_commamnd_macro(self):
        self.__undo_stack.endMacro()

    @property
    def _view_nodes(self) -> List[CPNode]:
        return self.__view_nodes

    @_view_nodes.setter
    def _view_nodes(self, view_nodes: List[CPNode]):
        self.__view_nodes = view_nodes

    @property
    def _source_nodes(self) -> List[CPNode]:
        return self.__source_nodes

    def _set_source_nodes(self, start_index: int, count: int, source_nodes: List[CPNode]):
        self.__source_nodes[start_index:] = source_nodes[:]
        self.__full_view_update()
        top_left = self.index(start_index, Column.first.value)
        bottom_right = self.index(start_index + count, Column.last.value)
        self.dataChanged.emit(top_left, bottom_right)

    def _silent_update_neighbor_nodes(self, index, source_node: CPNode, view_node: CPNode):
        self.__source_nodes[index].angle = source_node.angle
        self.__source_nodes[index].distance = source_node.distance
        self.__view_nodes[index].angle = view_node.angle
        self.__view_nodes[index].distance = view_node.distance
        top_left = self.index(index, Column.first.value)
        bottom_right = self.index(index, Column.last.value)
        self.dataChanged.emit(top_left, bottom_right)

    def _insert_source_nodes(self, start_index: int, source_nodes: List[CPNode]):
        for index, node in enumerate(source_nodes):
            insert_index = start_index + index
            self.beginInsertRows(QModelIndex(), insert_index, insert_index)
            self.__source_nodes.insert(insert_index, node)
            self.endInsertRows()
        self.__full_view_update()
        top_left = self.index(start_index, Column.first.value)
        bottom_right = self.index(start_index + len(source_nodes), Column.last.value)
        self.dataChanged.emit(top_left, bottom_right)

    def edit_data(self, index: QModelIndex) -> Optional[float]:
        point = self.__view_nodes[index.row()]
        if index.column() == Column.x.value:
            return point.x
        elif index.column() == Column.y.value:
            return point.y
        elif index.column() == Column.angle.value:
            return point.angle
        elif index.column() == Column.distance.value:
            return point.distance
        else:
            return None

    def display_data(self, index: QModelIndex) -> str:
        if index.column() == Column.x.value:
            # return FloatFormatter.to_localized_string(self.__view_nodes[index.row()].x)
            return FloatCoord(self.__view_nodes[index.row()].x).as_string()
        elif index.column() == Column.y.value:
            # return FloatFormatter.to_localized_string(self.__view_nodes[index.row()].y)
            return FloatCoord(self.__view_nodes[index.row()].y).as_string()
        elif index.column() == Column.angle.value:
            # return FloatFormatter.to_localized_string(self.__view_nodes[index.row()].angle)
            return FloatCoord(self.__view_nodes[index.row()].angle).as_string()
        elif index.column() == Column.distance.value:
            # return FloatFormatter.to_localized_string(self.__view_nodes[index.row()].distance)
            return FloatCoord(self.__view_nodes[index.row()].distance).as_string()

    def is_last_row(self, index: QModelIndex) -> bool:
        return index.row() == self.size()

    @staticmethod
    def is_first_non_editable_cells(index: QModelIndex) -> bool:
        return index.row() == 0 and (index.column() == Column.angle.value or index.column() == Column.distance.value)

    def data(self, index: QModelIndex, role: int = None):
        if not index.isValid():
            return None
        if self.is_last_row(index):
            return None
        if self.is_first_non_editable_cells(index):
            return None
        if role == Qt.EditRole:
            return self.edit_data(index)
        if role == Qt.DisplayRole:
            return self.display_data(index)

    def setData(self, index: QModelIndex, value: typing.Any, role: int = None) -> bool:
        if not index.isValid():
            return False
        if role != Qt.EditRole:
            return False
        current_value = self.data(index, Qt.EditRole)
        if current_value == value:
            return False
        self.__undo_stack.push(SetDataCommand(index, value, role, self))
        return True

    def _set_data_impl(self, index: QModelIndex, value: typing.Any, role: int):
        if index.column() == Column.x.value or index.column() == Column.y.value:
            index_range = self._change_coordinate(index, value, role)
        else:
            index_range = self._change_angle_or_distance(index, value, role)
        if index_range is None:
            return False
        self.__full_view_update()
        self.dataChanged.emit(index_range.top_left, index_range.bottom_right)

    def reset(self):
        """Удаляет все точки и очищает историю комманд"""
        self.beginResetModel()
        self._remove_rows_impl(0, self.size())
        self.__undo_stack.clear()
        self.endResetModel()

    def _change_angle_or_distance(self, index: QModelIndex, value: typing.Any, role: int) -> IndexRange:
        return self.__impl._change_angle_or_distance(index, value, role)

    def _update_view_angles(self):
        self.__impl._update_view_angles()

    def _update_view_distances(self):
        self.__impl._update_view_distances()

    def _update_view_coordinates(self):
        self.__impl._update_view_coordinates()

    def __update_first_node(self):
        # Значения угла и расстояния бессмысленны при рассмотрении первой точки
        # в цепочке
        if len(self.__source_nodes) > 0:
            self.__source_nodes[0].angle = 0
            self.__source_nodes[0].distance = 0
        if len(self.__view_nodes) > 0:
            self.__view_nodes[0].angle = 0
            self.__view_nodes[0].distance = 0

    def __full_view_update(self):
        # Важно занулить угол и расстояние для первой точки перед началом
        # обновления, дабы избежать ошибок отображения
        self.__update_first_node()
        self._update_view_coordinates()
        self._update_view_angles()
        self._update_view_distances()
        self.__update_first_node()

    def _change_coordinate(self, index: QModelIndex, value: typing.Any, role: int) -> IndexRange:
        return self.__impl._change_coordinate(index, value, role)

    def flags(self, index: QModelIndex) -> Qt.ItemFlags:
        is_angle_or_dist_column = index.column() == Column.angle.value or index.column() == Column.distance.value
        if index.row() == 0 and is_angle_or_dist_column:
            return super().flags(index)
        if self.is_last_row(index):
            return super().flags(index)
        return Qt.ItemIsEditable | super().flags(index)

    def headerData(self, section, orientation, role: int = None):
        if orientation == Qt.Vertical and section == self.size():
            return "*"
        if orientation == Qt.Vertical or role != Qt.DisplayRole:
            return super().headerData(section, orientation, role)
        if section > self.columnCount():
            return None
        title = self.__header_titles[section]
        return title

    def rowCount(self, parent: QModelIndex = None, *args, **kwargs) -> int:
        # if parent is None:
        #     parent = QModelIndex()
        return self.size() + 1

    def columnCount(self, parent: QModelIndex = None, *args, **kwargs) -> int:
        return len(Column)

    def size(self) -> int:
        return len(self.__source_nodes)

    def update_coordinates(self, index: int, cs_point: CSPoint):
        self.__undo_stack.beginMacro(axipy.tr("обновление координат с помощью мыши"))
        self.setData(self.index(index, Column.x.value), cs_point.x, Qt.EditRole)
        self.setData(self.index(index, Column.y.value), cs_point.y, Qt.EditRole)
        self.__undo_stack.endMacro()

    def add_point(self, node_data: RawNodeData, index: int = None):
        if index is None:
            index = self.size()
        # последняя реальная строчка зарезервирована
        index = min(index, self.size())
        self.insert_point(node_data, index)

    def insert_point(self, node_data: RawNodeData, index: int):
        node = self.__impl._node_from_raw_data(node_data)
        self.__undo_stack.push(InsertCommand(node, index, self))

    def _insert_point_impl(self, node: CPNode, index: int):
        self.beginInsertRows(QModelIndex(), index, index)
        self.__source_nodes.insert(index, node)
        self.__view_nodes.insert(index, copy.deepcopy(node))
        self.endInsertRows()
        top_left = self.index(index, Column.x.value, QModelIndex())
        bottom_right = self.index(index, Column.y.value, QModelIndex())
        self.__full_view_update()
        # При изменении данных модели нужно испустить соответствующие сигналы
        self.dataChanged.emit(top_left, bottom_right)
        self.point_was_added.emit(top_left)

    def __can_remove(self, row: int, count: int) -> bool:
        """
        Проверяем можем ли мы удалить точку. Необходимо, чтобы не удалить
        последнюю неудаляемую строку
        """
        start_row = row
        end_row = row + count
        last_row = self.rowCount() - 1
        if last_row in range(start_row, end_row):
            if row == last_row and count == 1:
                return False
            if row + count == last_row:
                # TODO: Если нужно будет удалять сразу несколько строк,
                # то нужно будет возвращать исправленное значение count
                end_row -= 1
            else:
                print(f"You haven't delete thid row: {row} count: {count}!")
                return False
        return True

    def removeRows(self, row: int, count: int, parent: QModelIndex = None, *args, **kwargs) -> bool:
        if parent is None:
            parent = QModelIndex()
        if not self.__can_remove(row, count):
            return False
        self.__undo_stack.push(RemoveCommand(row, count, parent, self))
        return True

    def _remove_rows_impl(self, row: int, count: int, parent: QModelIndex = QModelIndex()):
        super().beginRemoveRows(parent, row, row + count - 1)
        for row_index in reversed(range(row, row + count)):
            self.__view_nodes.pop(row_index)
            self.__source_nodes.pop(row_index)
        self.__full_view_update()
        super().endRemoveRows()
        self.dataChanged.emit(QModelIndex(), QModelIndex())
        self.point_was_deleted.emit(row)

    def clear_all(self):
        """
        Удалаяет все точки
        """
        if self.size() == 0:
            return
        self.__undo_stack.push(ClearAllCommand(self))

    def __can_push_cs_command(self, cs: CoordSystem) -> bool:
        if cs is None:
            return False
        if cs == self.coordsystem:
            return False
        return True

    def points_in_cs(self, cs: CoordSystem) -> List[Pnt]:
        return [p.reproject(cs).to_pnt() for p in self.__source_nodes]

    @property
    def coordsystem(self) -> CoordSystem:
        return clone_cs(self.__view_cs)

    @coordsystem.setter
    def coordsystem(self, cs: CoordSystem):
        cs = clone_cs(cs)
        if not self.__can_push_cs_command(cs):
            return
        self.__undo_stack.push(SetCSCommand(cs, self))

    @property
    def unit(self) -> LinearUnit:
        return self.__distance_unit

    @unit.setter
    def unit(self, unit: LinearUnit):
        if unit == self.__distance_unit:
            return
        self.__undo_stack.push(SetUnitCommand(unit, self))

    def _set_unit_impl(self, unit: LinearUnit):
        self.__distance_unit = unit
        super().beginResetModel()
        self.__full_view_update()
        self.distance_unit_changed.emit(unit)
        super().endResetModel()

    @property
    def angle_type(self) -> AngleType:
        return self.__angle_type

    @angle_type.setter
    def angle_type(self, angle_type: AngleType):
        if angle_type.value == self.__angle_type.value:
            return
        command = SetAngleTypeCommand(angle_type, self)
        self.__undo_stack.push(command)

    def _set_angle_type_impl(self, angle_type: AngleType):
        super().beginResetModel()
        self.__angle_type = angle_type
        self.__full_view_update()
        self.angle_type_changed.emit(angle_type)
        super().endResetModel()

    def _set_coordsystem_impl(self, cs: CoordSystem):
        if self.__view_cs is None:
            # Если кс устанавливается в первый раз
            self.__view_cs = cs
            return
        self.beginResetModel()
        self.__view_cs = cs
        self.__full_view_update()
        self.endResetModel()
        self.coordsystem_changed.emit(self.__view_cs)

    @property
    def is_empty(self) -> bool:
        return self.size() == 0
