"""
Вспомогательные функции для построения окружности и дуги
"""

from __future__ import annotations

import math
from abc import abstractmethod
from typing import Iterable, List, Optional, Tuple, overload

import axipy
import numpy as np
from axipy.gui.map_tool import ViewPointsHelper
from PySide2.QtCore import QLineF, QPoint, QPointF, Qt
from PySide2.QtGui import (
    QKeyEvent,
    QMouseEvent,
    QPainter,
    QPaintEvent,
    QPen,
    QWheelEvent,
)

from .helper import ensure_editable_table


@overload
def ensure_util_geom_visual(
    view: axipy.MapView, util_geom: Iterable[axipy.Pnt], inverse: bool = False
) -> list[axipy.Pnt]: ...


@overload
def ensure_util_geom_visual(view: axipy.MapView, util_geom: axipy.Rect, inverse: bool = False) -> axipy.Rect: ...


def ensure_util_geom_visual(view, util_geom, inverse=False):
    """
    Поправка для долготы/широты.
    """
    cs = view.coordsystem
    cs_visual = view.coordsystem_visual

    if cs == cs_visual:
        return util_geom

    if not inverse:
        transformer = axipy.CoordTransformer(cs, cs_visual)
    else:
        transformer = axipy.CoordTransformer(cs_visual, cs)

    if isinstance(util_geom, axipy.Rect):
        visual_rect = transformer.transform(util_geom)
        return visual_rect
    else:
        visual_points = [transformer.transform(pnt) for pnt in util_geom]
        return visual_points


def __find_center_and_radius(points: tuple[axipy.Pnt, axipy.Pnt, axipy.Pnt]) -> tuple[axipy.Pnt, float]:
    """
    Находит центр и радиус окружности, проходящей через три точки.
    """
    p1, p2, p3 = points
    x1, y1 = p1.x, p1.y
    x2, y2 = p2.x, p2.y
    x3, y3 = p3.x, p3.y

    # Создаем матрицу для системы уравнений
    # noinspection PyPep8Naming
    A = np.array([[2 * (x2 - x1), 2 * (y2 - y1)], [2 * (x3 - x1), 2 * (y3 - y1)]])

    # Создаем вектор правой части системы
    b = np.array([x2**2 - x1**2 + y2**2 - y1**2, x3**2 - x1**2 + y3**2 - y1**2])

    # Решаем систему уравнений для нахождения центра (a, b)
    a, b = np.linalg.solve(A, b)

    # Находим радиус окружности
    r = np.sqrt((x1 - a) ** 2 + (y1 - b) ** 2)

    return axipy.Pnt(a, b), r


@overload
def center_and_radius_of_circle(points: tuple[axipy.Pnt, axipy.Pnt, axipy.Pnt]) -> tuple[axipy.Pnt, float] | None: ...


@overload
def center_and_radius_of_circle(points: tuple[QPoint, QPoint, QPoint]) -> tuple[axipy.Pnt, float] | None: ...


def center_and_radius_of_circle(points):
    if isinstance(points[0], QPoint):
        points = tuple(axipy.Pnt.from_qt(elem) for elem in points)

    try:
        center, radius = __find_center_and_radius(points)
    except Exception:
        return None

    return center, radius


def angle_by_points(center: axipy.Pnt | QPoint, p: axipy.Pnt | QPoint, device=False) -> float:
    """
    Возвращает математический угол в градусах.
    """

    if isinstance(center, QPoint):
        c_x, c_y = center.x(), center.y()
    else:
        c_x, c_y = center.x, center.y

    if isinstance(p, QPoint):
        p_x, p_y = p.x(), p.y()
    else:
        p_x, p_y = p.x, p.y

    dx = p_x - c_x
    dy = p_y - c_y

    if math.isclose(dx, 0, abs_tol=1e-15):
        if dy >= 0:
            degree = 90
        else:
            degree = -90
        return degree

    tan_ = dy / dx
    radians = math.atan(tan_)
    degree = math.degrees(radians)

    if dx < 0:
        degree += 180

    if degree < 0:
        degree += 360

    if device:
        degree = 360 - degree

    return degree


def start_end_angles(points: list[axipy.Pnt | QPoint], center: axipy.Pnt | QPoint, device=False) -> Tuple[float, float]:
    """
    Расчет начального и конечного угла для дуги, по трем точкам и центру.
    """
    p1, p2, p3 = points[0], points[1], points[2]

    if device:
        a1, a2, a3 = (
            angle_by_points(center, p1, device=True),
            angle_by_points(center, p2, device=True),
            angle_by_points(center, p3, device=True),
        )
    else:
        a1, a2, a3 = angle_by_points(center, p1), angle_by_points(center, p2), angle_by_points(center, p3)

    angles = (a2 - a1, a3 - a2, a1 - a3)

    # Создание дуги изначально идет против часовой
    def is_counterclockwise(__angles: Iterable[float]) -> bool:
        lst = [angle > 0 for angle in __angles]
        return max(set(lst), key=lst.count)

    if is_counterclockwise(angles):
        return a1, a3
    else:
        return a3, a1


def draw_lines(q_painter: QPainter, points: List[QPointF | QPoint]):
    """
    Нарисовать круг по точкам, каждые две точки = линия.
    Необходимо минимум две точки.
    """
    n = len(points)
    if n >= 2:
        lines = [QLineF(points[i], points[i + 1]) for i in range(n - 1)]

        pen = q_painter.pen()
        old_pen = QPen(pen)

        pen.setStyle(Qt.DashLine)
        q_painter.setPen(pen)
        q_painter.drawLines(lines)

        q_painter.setPen(old_pen)


def insert_feature(f: axipy.Feature):
    table = ensure_editable_table()
    table_cs = table.coordsystem

    # Перепроецирование вручную, потому что иногда, при выходе за границы КС, получается некорректная геометрия
    # Лучше, если Exception произойдет на перепроецировании, чем на вставке в таблицу.
    g = f.geometry
    if g.coordsystem != table_cs:
        f.geometry = g.reproject(table_cs)

    table.insert(f)


class CircleMapTool(axipy.MapTool):

    def __init__(self, plugin: axipy.Plugin, title: str) -> None:
        super().__init__()
        self.plugin: axipy.Plugin = plugin
        self.title: str = title

        self.points: list[axipy.Pnt] = []
        self.current_point: axipy.Pnt | None = None

    def load(self) -> None:
        axipy.view_manager.active_changed.connect(self.active_changed)

    def unload(self) -> None:
        axipy.view_manager.active_changed.disconnect(self.active_changed)
        self.clear_and_redraw()

    def clear_and_redraw(self) -> None:
        self.points.clear()
        self.current_point = None

        self.redraw()

    def keyPressEvent(self, event: QKeyEvent) -> Optional[bool]:
        if event.key() == Qt.Key_Escape:
            self.reset()
        else:
            return super().keyPressEvent(event)

    def wheelEvent(self, event: QWheelEvent) -> Optional[bool]:
        self.redraw()
        return self.PassEvent

    # Signals

    def active_changed(self) -> None:
        self.clear_and_redraw()

    # Mouse

    def __normalize_point(self, point: axipy.Pnt) -> axipy.Pnt:
        normalized_point = ViewPointsHelper(self.view).normalize_point(point)
        return axipy.Pnt.from_qt(normalized_point)

    def pnt_from_mouse_event(self, event: QMouseEvent) -> axipy.Pnt:
        if self.is_snapped():
            point = self.snap()
        else:
            event_pos = event.pos()
            point = self.to_scene(event_pos)

        return point

    def mouseMoveEvent(self, event: QMouseEvent) -> None:
        if self.points:
            self.current_point = self.pnt_from_mouse_event(event)
            self.redraw()

    def paintEvent(self, event: QPaintEvent, painter: QPainter) -> None:
        painter.save()

        d_points = [self.to_device(point) for point in self.points]
        if self.current_point:
            d_points.append(self.to_device(self.current_point))

        n = len(d_points)

        if n == 2:
            draw_lines(painter, d_points)
        elif n == 3:
            draw_lines(painter, d_points)
            self.prepare_to_draw(painter, d_points)

        painter.restore()

    @abstractmethod
    def prepare_to_draw(self, painter: QPainter, points: List[QPoint]) -> None:
        pass

    def mouseReleaseEvent(self, event: QMouseEvent) -> Optional[bool]:
        if event.button() == Qt.LeftButton:

            pnt = self.pnt_from_mouse_event(event)

            self.points.append(pnt)

            if len(self.points) == 3:
                self.prepare_to_insert()
            else:
                self.redraw()

            return self.BlockEvent
        else:
            return self.PassEvent

    @abstractmethod
    def prepare_to_insert(self) -> None:
        pass
