from typing import (
    TYPE_CHECKING,
    Any,
    Final,
    Generator,
    Iterable,
    List,
    Optional,
    Tuple,
    cast,
)

import axipy
import numpy as np
from axipy import Notifications
from PySide2.QtCore import QLineF, QPoint, Qt, Slot
from PySide2.QtGui import QMouseEvent, QPainter, QPaintEvent, QPen

from .utils import (
    _items_to_row_generator,
    _list_points_generator,
    _modify_from_point_row,
)

if TYPE_CHECKING:
    from . import ChooseStartingPointToolButton
    from .observer import ChooseStartingPointObserver

HALF_LINE_SIZE: Final[int] = 5
LINE_SIZE: Final[int] = HALF_LINE_SIZE * 2

SMALL_CROSS_SIZE: Final[float] = 3.0
BIG_CROSS_SIZE: Final[float] = 5.0


# noinspection PyMethodMayBeStatic
class ChooseStartingPointMapTool(axipy.MapTool):

    def __init__(self, tb: "ChooseStartingPointToolButton") -> None:
        self.tb: "ChooseStartingPointToolButton" = tb

        self._points: List[axipy.Pnt] = []
        self._first_points: List[axipy.Pnt] = []

        self.big_white_pen: QPen = QPen(Qt.white, BIG_CROSS_SIZE)
        self.small_red_pen: QPen = QPen(Qt.red, SMALL_CROSS_SIZE)
        self.small_blue_pen: QPen = QPen(Qt.blue, SMALL_CROSS_SIZE)

        super().__init__()

    def _init_points_in_task(self, dt: axipy.DialogTask) -> None:
        dt.title = "Подготовка точек контуров"
        dt.message = dt.title
        table = axipy.data_manager.selection
        if table is None:
            axipy.run_in_gui(
                lambda: Notifications.push(
                    self.tb.title,
                    self.tb.plugin.tr("Отсутствует таблица выборки"),
                    Notifications.Critical,
                )
            )
            self.reset()
            raise RuntimeError("Selection Table is missing.")

        dt.max = table.count()
        for list_point in _list_points_generator(table.items(), dt):
            first_point = list_point[0]
            self._first_points.append(first_point)
            other = (list_point[i] for i in range(1, len(list_point) - 1))
            self._points.extend(other)

        self.__reproject_self_points_list_in_place(dt)

    def __reproject_self_points_list_in_place(self, dt: axipy.DialogTask) -> None:
        cs_from = self.__observer.selection_table().coordsystem
        cs_to = self.__observer.active_map_view().coordsystem
        if cs_from == cs_to:
            return

        dt.message = "Перепроецировние геометрии"
        dt.infinite_progress = True
        coord_transformer = axipy.CoordTransformer(cs_from, cs_to)
        self._points = coord_transformer.transform(self._points)
        self._first_points = coord_transformer.transform(self._first_points)

    def _init_points(self) -> None:
        self._points = []
        self._first_points = []

        dt = axipy.DialogTask(self._init_points_in_task)
        dt.run_and_get()

    def load(self) -> None:
        # axipy.selection_manager.changed.connect(self.reset)
        axipy.view_manager.active_changed.connect(self.reset)
        self._init_points()
        self.redraw()

    def unload(self) -> None:
        # axipy.selection_manager.changed.disconnect(self.reset)
        axipy.view_manager.active_changed.disconnect(self.reset)
        self.redraw()

    def draw_cross_lines(self, painter: QPainter, pen: QPen, lines: List[QLineF]) -> None:
        painter.setPen(pen)
        painter.drawLines(lines)

    def draw_points(self, painter: QPainter, points: Iterable[QPoint], secondary_pen: QPen) -> None:
        lines: List[QLineF] = []

        for p in points:
            line_h = QLineF(p.x() - HALF_LINE_SIZE, p.y(), p.x() + HALF_LINE_SIZE, p.y())
            line_v = QLineF(p.x(), p.y() - HALF_LINE_SIZE, p.x(), p.y() + HALF_LINE_SIZE)
            lines.append(line_h)
            lines.append(line_v)

        self.draw_cross_lines(painter, self.big_white_pen, lines)
        self.draw_cross_lines(painter, secondary_pen, lines)

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

        data = self.filter_data_in_bounds(
            (self.to_device(elem) for elem in self._points),
        )
        self.draw_points(painter, data, self.small_blue_pen)

        data = self.filter_data_in_bounds(
            (self.to_device(elem) for elem in self._first_points),
        )
        self.draw_points(painter, data, self.small_red_pen)

        painter.restore()
        super().paintEvent(event, painter)

    def filter_data_in_bounds(
        self,
        points: Iterable[QPoint],
    ) -> Generator[QPoint, Any, None]:

        data = np.array(tuple((point.x(), point.y()) for point in points))
        # Параметры для фильтрации
        bbox = axipy.Rect.from_qt(self.__observer.active_map_view().position)
        a, b = bbox.xmin, bbox.xmax  # Условия для колонки 0
        c, d = bbox.ymin, bbox.ymax  # Условия для колонки 1

        # Применяем фильтрацию сразу ко всему массиву
        filtered_data = data[(data[:, 0] >= a) & (data[:, 0] <= b) & (data[:, 1] >= c) & (data[:, 1] <= d)]
        # Проверяем, есть ли подходящие строки и выбираем первую
        if filtered_data.shape[0]:  # Проверка на наличие строк
            for row in filtered_data:
                yield QPoint(row[0], row[1])

    def find_point_row_in_task(
        self, task: axipy.DialogTask, bbox: axipy.Rect
    ) -> Optional[Tuple[float, float, float, float, float, float]]:
        task.title = "Изменение первой точки"
        task.message = task.title

        table = self.__observer.selection_table()
        x = tuple(_items_to_row_generator(table.items(), task))
        data = np.array(x)
        # Параметры для фильтрации
        a, b = bbox.xmin, bbox.xmax  # Условия для колонки 0
        c, d = bbox.ymin, bbox.ymax  # Условия для колонки 1

        # Применяем фильтрацию сразу ко всему массиву
        filtered_data = data[(data[:, 0] >= a) & (data[:, 0] <= b) & (data[:, 1] >= c) & (data[:, 1] <= d)]
        # Проверяем, есть ли подходящие строки и выбираем первую
        if filtered_data.shape[0]:  # Проверка на наличие строк
            selected_row = filtered_data[0]
            return cast(Tuple[float, float, float, float, float, float], tuple(selected_row))
        return None

    def check_if_any_items_in_bbox(self, table: axipy.Table, bbox: axipy.Rect) -> bool:
        items_bbox = table.items(bbox=bbox)
        try:
            next(items_bbox)
        except StopIteration:
            return False
        return True

    @property
    def __observer(self) -> "ChooseStartingPointObserver":
        return self.tb.choose_starting_point_observer

    def __ensure_bbox_reprojected(self, bbox: axipy.Rect) -> axipy.Rect:
        cs_from = self.__observer.active_map_view().coordsystem
        cs_to = self.__observer.selection_table().coordsystem
        if cs_from == cs_to:
            return bbox
        coord_transformer = axipy.CoordTransformer(cs_from, cs_to)
        bbox = coord_transformer.transform(bbox)
        return bbox

    def mouseReleaseEvent(self, event: QMouseEvent) -> bool:
        if event.button() != Qt.LeftButton:
            return self.PassEvent

        if self.is_snapped():
            event_pos = cast(QPoint, self.snap_device())
        else:
            event_pos = event.pos()

        bbox = self.get_select_rect(event_pos, LINE_SIZE * 2)
        bbox = self.__ensure_bbox_reprojected(bbox)

        table = self.__observer.selection_table()
        if not self.check_if_any_items_in_bbox(table, bbox):
            return self.BlockEvent

        dt = axipy.DialogTask(self.find_point_row_in_task)
        point_row = dt.run_and_get(bbox)
        if point_row is None:
            return self.BlockEvent

        _modify_from_point_row(table, point_row)
        self._init_points()
        self.redraw()

        return self.BlockEvent

    @Slot()
    def reset(self) -> None:
        super().reset()
