import itertools
from typing import List, Optional

from axipy import ObserverManager
from axipy.cs import CoordTransformer
from axipy.da import (
    Feature,
    Geometry,
    GeometryCollection,
    GeometryType,
    Line,
    LineString,
    LineStyle,
    Polygon,
    Table,
)
from axipy.gui import MapTool, MapView
from axipy.utl import Pnt, Rect
from PySide2.QtCore import Qt
from PySide2.QtGui import QKeyEvent
from PySide2.QtWidgets import QMessageBox

from ..helper import print_exc_

supported_types = [GeometryType.Line, GeometryType.LineString, GeometryType.Polygon, GeometryType.GeometryCollection]
supported_types_string = ["Линия", "Полилиния", "Полигон", "Коллекция"]


def show_warning_no_supported_geometry():
    title = "Ошибка"
    message = "Геометрия не найдена или не поддерживается."
    mbox = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Close)
    mbox.setDetailedText(f'Список поддерживаемых геометрий: "{", ".join(supported_types_string)}".')
    mbox.exec()


def is_allowed(geometry):
    return geometry is not None and geometry.type in supported_types or issubclass(type(geometry), GeometryCollection)


def to_points(geometry) -> List[Pnt]:
    geom_type = type(geometry)
    if geom_type is Line:
        return [geometry.begin, geometry.end]
    elif geom_type is LineString:
        return geometry.points
    return []


def find_segment_impl(points: List[Pnt], geometry: Geometry, box: Polygon) -> Optional[LineString]:
    for i in range(len(points) - 1):
        segment = LineString([points[i], points[i + 1]], cs=geometry.coordsystem)
        if box.intersects(segment):
            return segment
    return None


def find_in_polygon(polygon: Polygon, bbox: Polygon) -> Optional[LineString]:
    segment = find_segment_impl(polygon.points, polygon, bbox)
    if segment is not None:
        return segment
    for hole in polygon.holes:
        segment = find_segment_impl(hole, polygon, bbox)
        if segment is not None:
            return segment
    return None


def find_in_single_geometry(geometry: Geometry, bbox: Polygon) -> Optional[LineString]:
    if isinstance(geometry, Polygon):
        return find_in_polygon(geometry, bbox)
    return find_segment_impl(to_points(geometry), geometry, bbox)


def find_segment(geometry: Geometry, bbox: Polygon) -> Optional[LineString]:
    if issubclass(type(geometry), GeometryCollection):
        for sub_geometry in geometry:
            sub_geometry.coordsystem = geometry.coordsystem
            segment = find_in_single_geometry(sub_geometry, bbox)
            if segment is not None:
                return segment
    else:
        return find_in_single_geometry(geometry, bbox)


def project(segment, p):
    p0 = segment.points[0]
    p1 = segment.points[1]
    dx = p1.x - p0.x
    dy = p1.y - p0.y
    len2 = dx * dx + dy * dy
    r = ((p.x - p0.x) * dx + (p.y - p0.y) * dy) / len2
    return Pnt(p0.x + r * (p1.x - p0.x), p0.y + r * (p1.y - p0.y))


class SegmentContainer:
    def __init__(self, found_geom, segment) -> None:
        self.segment: LineString = segment
        self.found_geom: Geometry = found_geom


def find_in_table(table: Table, scene_box: Rect) -> SegmentContainer:
    features = table.items(bbox=scene_box)
    geoms = map(lambda f: f.geometry, features)
    allowed_geoms = filter(is_allowed, geoms)
    layer_box = Polygon.from_rect(scene_box, table.coordsystem)  # type: Polygon
    geoms = map(lambda g: (g, find_segment(g, layer_box)), allowed_geoms)
    intersected_geoms = filter((lambda pair: pair[1] is not None), geoms)
    found_geom, segment = next(intersected_geoms, (None, None))
    return SegmentContainer(found_geom, segment)


def find_in_map_view(map_view: MapView, bbox: Rect) -> SegmentContainer:
    for layer in itertools.chain([map_view.map.cosmetic], map_view.map.layers):
        layer_bbox = Rect(xmin=bbox.xmin, xmax=bbox.xmax, ymin=bbox.ymin, ymax=bbox.ymax)
        if not issubclass(type(layer.data_object), Table):
            continue
        if map_view.coordsystem != layer.coordsystem:
            try:
                layer_bbox = CoordTransformer(map_view.coordsystem, layer.coordsystem).transform(layer_bbox)
            except (Exception,):
                print_exc_()
                continue
        container = find_in_table(layer.data_object, layer_bbox)  # type: SegmentContainer
        if container.segment is not None and container.found_geom is not None:
            return container
    return SegmentContainer(None, None)


class PerpendicularTool(MapTool):
    # enable_on = ObserverManager.Editable

    def __init__(self):
        super().__init__()

        self._start_scene_point = None
        self._end_device_point = None

        self._reset()

    def _reset(self):
        self._start_scene_point = None
        self._end_device_point = None

    def paintEvent(self, event, painter):
        if not self.is_drawing() or self._end_device_point is None:
            return
        painter.drawPolyline([self.to_device(self._start_scene_point), self.snap_device(self._end_device_point)])

    def mousePressEvent(self, event):
        if event.button() != Qt.LeftButton:
            return self.PassEvent
        self._reset()
        self._start_scene_point = self.snap(self.to_scene(event.pos()))
        return self.BlockEvent

    def mouseReleaseEvent(self, event):
        if event.button() != Qt.LeftButton:
            return self.PassEvent
        if not self.is_drawing():
            return self.PassEvent
        scene_box = self.get_select_rect(event.pos(), 30)  # type: Rect
        view_cs = self.view.coordsystem
        container = find_in_map_view(self.view, scene_box)
        found_geom = container.found_geom
        segment = container.segment
        if found_geom is None:
            self._reset()
            show_warning_no_supported_geometry()
            return self.PassEvent
        # Начальная точка для перпендикуляра сохранена в КС карты, значит
        # и сегмент в котором мы будем искать перпендикуляр тоже должен быть
        # в КС карты
        if segment.coordsystem != view_cs:
            segment = segment.reproject(view_cs)
        projection_point = project(segment, self._start_scene_point)
        self.insert(self._start_scene_point, projection_point)
        self._reset()
        return self.BlockEvent

    def mouseMoveEvent(self, event):
        if not self.is_drawing():
            return
        self._end_device_point = event.pos()
        self.redraw()

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

    def insert(self, start, end):
        line = Line(start, end, self.view.coordsystem)
        # target_cs = self.view.editable_layer.data_object.coordsystem
        feature = Feature(geometry=line, style=LineStyle())
        table = self.view.editable_layer.data_object
        if isinstance(table, Table):
            table.insert(feature)

    def is_drawing(self):
        return self._start_scene_point is not None

    def editable_table(self):
        return self.view.editable_layer.data_object
