import enum
import math
import traceback
from typing import Callable, List, Optional, Tuple, Union

import axipy
from axipy import Notifications, Plugin
from axipy.cs import CoordSystem
from axipy.da import Feature, LineString, Polygon, Style
from axipy.gui import MapView, View, view_manager
from axipy.gui.map_tool import ViewPointsHelper
from axipy.utl import Pnt
from PySide2.QtCore import QPoint, QPointF

from ...helper import editable_table_from_view
from .data_types import AngleType

HALF_LINE_SIZE = 5  # полуразмер перекрестия
SMALL_CROSS_SIZE = 3.0  # малое перекрестье которое внутри большого
BIG_CROSS_SIZE = 5.0  # длина большого перекрестья
MIN_LINESTRING_POINTS_COUNT = 2  # минимальное число точек для создания полилинии
MIN_POLYGON_POINTS_COUNT = 3  # минимальное число точек для создания полигона
MIN_NORMALIZE_ANGLE = -1000
MAX_NORMALIZE_ANGLE = 1000


def count_of_decimals(number: float) -> int:
    """
    Возвращает колличество знаков после запятой
    """
    if number % 1 == 0:
        return 0
    count = 1
    while number * pow(10, count) % 1:
        count += 1
    return count


def save_precision(func: Callable[[], float], source_number):
    """
    Точность результата выполнения функции будет такой же как и
    у переданного числа
    """
    precision = count_of_decimals(source_number)
    res = func()
    if precision > 0:
        return round(res, precision)
    else:
        return res


class NormalizeStrategy:
    ToPositive = 0
    StayInRange = 1


def _normalize_impl(angle: float, limit: int = 360):
    if 0 > angle:
        while 0 > angle:
            angle += limit
    else:
        while angle >= limit:
            angle -= limit
    return angle


def check_bounds(angle: float, limit) -> Tuple[bool, float]:
    def print_err_message():
        print(
            axipy.tr(f"Значение {angle} слишком большое. Используейте значение в диапазоне: ")
            + f"-{limit} =< angle <= {limit}"
        )

    if angle > MAX_NORMALIZE_ANGLE:
        print_err_message()
        return False, math.fmod(limit + angle, limit)
    if angle < MIN_NORMALIZE_ANGLE:
        print_err_message()
        return False, math.fmod(limit - abs(angle), limit)
    return True, angle


def normalize_angle(angle: float, limit: int = 360, strategy=NormalizeStrategy.StayInRange) -> float:
    """
    Возвращает угол привидённый к границам -limit =< angle <= +limit

    В отличии от math.fmod  не даёт ошбку округления. Если угол маленький то
    дополнительные накладные расходы на прокрутку while не существенны.
    """
    in_bound, angle = check_bounds(angle, limit)
    # Защитимся от очень больших/маленьких значений угла
    if not in_bound:
        return angle
    if strategy == NormalizeStrategy.StayInRange:
        return _normalize_impl(angle, limit)
    elif strategy == NormalizeStrategy.ToPositive:
        normalized = _normalize_impl(angle, limit)
        if normalized < 0:
            return limit - abs(normalized)
        return normalized


def normalize_point(view: View, point: Union[QPointF, QPoint, Pnt]) -> Union[QPointF, Pnt]:
    """Нормализует точку в соотвествии с КС и масштабом карты"""
    normalized_point = ViewPointsHelper(view).normalize_point(point)
    if isinstance(point, Pnt):
        return Pnt.from_qt(normalized_point)
    else:
        return normalized_point


class AngleConverter:
    """
    Преобразует углы переданные в разных форматах к единому
    формату значений используемому в Аксиоме для решения
    геодезической задачи (0 справа, значения растут против часовой стрелки)

    TODO: Название методов нужно зарефаторить
    """

    def __init__(self, cs: CoordSystem, angles: List[float]) -> None:
        self.__cs = cs
        self.__angles = angles
        self.__view_angles = None

    @staticmethod
    def __axi_angle_to_nord_latlong(value: float) -> float:
        return normalize_angle(value)

    @staticmethod
    def __nord_to_axi_angle_non_earth(angle: float) -> float:
        return normalize_angle(90 - angle)

    @staticmethod
    def __axi_angle_to_nord_non_earth(angle: float) -> float:
        return normalize_angle(90 - angle)

    def set_view_angles(self, angles: List[float]):
        self.__view_angles = angles

    @staticmethod
    def __axi_angle_to_east_normal(angle: float) -> float:
        # Для расчёта координаты принимаются как азимут на север. Соответственно
        # нужно пересчитать из направления на восток к северному направлению
        return normalize_angle(90 - angle)

    def __from_axi_angle_impl_plan_schema(
        self, index: int, angle: float, angle_type: AngleType, prev_value: float
    ) -> float:
        if angle_type == AngleType.ClockWiseTop:
            return self.__axi_angle_to_nord_non_earth(angle)
        elif angle_type == AngleType.CounterClockWiseRight:
            return angle
        elif angle_type == AngleType.Relate:
            fixed_angle = self.__axi_angle_to_nord_non_earth(angle)
            fixed_prev_value = self.__axi_angle_to_nord_non_earth(prev_value)
            if index == 1:
                # Угол для первого индекс никакого смысла не имеет. Сдедовательно
                # не следует его учитывать при вычислении относительного угла для
                # второй точки. Этот угол всегда является углом на север.
                return fixed_angle
            return fixed_angle - fixed_prev_value

    def __from_axi_angle_normal(self, angle_type: AngleType, value: float, prev_value: float) -> float:
        if angle_type == AngleType.ClockWiseTop:
            return value
        elif angle_type == AngleType.CounterClockWiseRight:
            return self.__axi_angle_to_east_normal(value)
        elif angle_type == AngleType.Relate:
            return value - prev_value

    def from_axi_angle(
        self, index: int, angle: float, prev_angle: float, angle_type: AngleType, cs: CoordSystem
    ) -> float:
        """
        Преобразует угол из внутреннего представление в запрощенный формат
        """

        def evaluate():
            if cs is not None and cs.non_earth:
                return self.__from_axi_angle_impl_plan_schema(
                    index=index, angle=angle, angle_type=angle_type, prev_value=prev_angle
                )
            else:
                return self.__from_axi_angle_normal(angle_type=angle_type, value=angle, prev_value=prev_angle)

        return save_precision(evaluate, angle)

    def __axi_angle_to_relate_normal(self, angle: float, current_index: int) -> float:
        azimuth = 0
        if 0 < current_index < len(self.__angles):
            azimuth = self.__angles[current_index - 1]
        current_angle = angle
        return normalize_angle(azimuth + current_angle)

    def __to_axi_angle_normal(self, angle: float, angle_type: AngleType, current_index: int) -> float:
        if angle_type == AngleType.ClockWiseTop:
            return self.__axi_angle_to_nord_latlong(angle)
        elif angle_type == AngleType.CounterClockWiseRight:
            return self.__axi_angle_to_east_normal(angle)
        elif angle_type == AngleType.Relate:
            return self.__axi_angle_to_relate_normal(angle, current_index)
        return angle

    def __to_axi_angle_non_earh(self, angle: float, angle_type: AngleType, current_index: int) -> float:
        if angle_type == AngleType.ClockWiseTop:
            return self.__nord_to_axi_angle_non_earth(angle)
        elif angle_type == AngleType.CounterClockWiseRight:
            return angle
        elif angle_type == AngleType.Relate:
            axi_angle = 0
            for index in range(0, current_index):
                sub_angle = self.__view_angles[index]
                axi_angle += sub_angle
            axi_angle += angle
            return self.__nord_to_axi_angle_non_earth(axi_angle)
        return angle

    def to_axi_angle(self, angle: float, angle_type: AngleType, current_index: int) -> float:
        """
        Преобразует угол введённый пользователем во внутреннее представление
        """

        # ClockWiseTop имеет 0 вверху и растёт по часовой стрелке.
        #
        # CounterClockWiseRight имеет 0 справа и растёт против часовой стрелки.
        #
        # Relate (Угол относительно текущего полжения) всегда заворачивает налево относительно
        # последнего отрезка. Т.е. имеет 0 по направлению последнего отрезка и
        # растёт против часовой стрелки.
        #
        def evaluate():
            if self.__cs is None or self.__cs.non_earth:
                return self.__to_axi_angle_non_earh(angle, angle_type, current_index)
            else:
                return self.__to_axi_angle_normal(angle, angle_type, current_index)

        return save_precision(evaluate, angle)


class NotifyContext:
    def __init__(self, title: str, iface: Plugin) -> None:
        self.title = title
        self.iface = iface


def print_points_to_log(context: NotifyContext, points: List[Pnt]):
    points_str = "Следующие точки не удалось сохранить: "
    for p_index in range(0, len(points)):
        points_str += f"{p_index + 1}) {points[p_index]} "
    print(f"{context.title}: " + points_str)
    Notifications.push(context.title, points_str, Notifications.Critical)


def __notify_no_points(context: NotifyContext, geom_name: str):
    Notifications.push(context.title, axipy.tr(f"Недостаточно точек для создания {geom_name}"), Notifications.Warning)


def __notify_about_finish(context: NotifyContext, len_: int):
    Notifications.push(context.title, axipy.tr(f"Полилиния из {len_} узлов была создана"), Notifications.Success)


def __notify_no_editable_layer(context: NotifyContext):
    Notifications.push(context.title, axipy.tr("Отсутствует редактируемый слой"), Notifications.Critical)


def __notify_no_editable_table(context: NotifyContext):
    Notifications.push(context.title, axipy.tr("Отсутствует редактируемая таблица"), Notifications.Critical)


def make_final_geometry(points: List[Pnt], cs: CoordSystem, make_polygon: bool):
    if make_polygon:
        points.append(points[0])
        return Polygon(points, cs=cs)
    return LineString(points, cs=cs)


def try_save_points(points: List[Pnt], view: MapView, context: NotifyContext, make_polygon: bool) -> bool:
    points_size = len(points)
    if points_size < MIN_LINESTRING_POINTS_COUNT:
        __notify_no_points(context, axipy.tr("полилинии"))
        return False
    if make_polygon and points_size < MIN_POLYGON_POINTS_COUNT:
        __notify_no_points(context, axipy.tr("полигона"))
        return False
    if view.editable_layer is None:
        __notify_no_editable_layer(context)
        return False
    table = editable_table_from_view(view)
    if table is None:
        __notify_no_editable_table(context)
        return False
    geometry = make_final_geometry(points, view.coordsystem, make_polygon)
    style = Style.for_geometry(geometry)
    table.insert(Feature(geometry=geometry, style=style))
    __notify_about_finish(context, points_size)
    return True


def try_save_points_on_deactivation_impl(points: List[Pnt], view: MapView, context: NotifyContext, make_polygon: bool):
    is_saved = False
    try:
        is_saved = try_save_points(points, view, context, make_polygon)
    except (Exception,):
        traceback.print_exc()
    if not is_saved:
        print_points_to_log(context, points)


def find_map_view_ref(origin_view: MapView) -> MapView:
    for view in view_manager.mapviews:
        if view.widget == origin_view.widget:
            return view


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


def clone_cs(cs: CoordSystem) -> Optional[CoordSystem]:
    if cs is None:
        return None

    return CoordSystem._wrap(getattr(cs, "_shadow"))


class DeactivateAction(enum.Enum):
    Reject = 0
    Deactivate = 1
