from __future__ import annotations

import itertools
from collections.abc import Iterable, Iterator, Generator
from enum import Enum, auto
from pathlib import Path
from typing import cast, Final, TYPE_CHECKING

import axipy
from PySide2.QtCore import QSize


class SchemaKeysVertices:
    VERTEX_NUMBER: Final[str] = "NODE_NUMBER"
    X: Final[str] = "X"
    Y: Final[str] = "Y"


class SchemaKeysContours:
    CONTOUR_NUMBER: Final[str] = "CONTOUR_NUMBER"


class WorkType(Enum):
    VERTICES = auto()
    CONTOURS = auto()


GROUP_NAME: Final[str] = "Каталог точек"

if TYPE_CHECKING:
    NUMBERED_PNT = tuple[int, axipy.Pnt]
    NUMBERED_POINT = tuple[int, axipy.Point]


# noinspection PyMethodMayBeStatic
class Worker(axipy.DialogTask):

    def __init__(
            self,
            source_table: axipy.Table,
            nodes_path: Path,
            nodes_style: axipy.PointFontStyle,
            number_each_hole: bool,
            number_each_collection_item: bool,
            nodes_starting_number: int,
            is_contours_checked: bool = False,
            contours_path: Path | None = None,
            number_each_hole_contours: bool = False,
            number_each_collection_item_contours: bool = False,
            contours_starting_number: int | None = None,
    ) -> None:
        super().__init__(self.do_work, cancelable=True)

        self._source_table: axipy.Table = source_table
        self._nodes_path: Path = nodes_path
        self._nodes_style: axipy.PointFontStyle = nodes_style
        self._nodes_number_each_hole: bool = number_each_hole
        self._nodes_number_each_collection_item: bool = number_each_collection_item
        self._nodes_starting_number: int = nodes_starting_number

        self._is_contours_checked: bool = is_contours_checked
        self._contours_path: Path | None = contours_path
        self._contours_style: axipy.PointFontStyle = axipy.PointFontStyle(symbol=32)
        self._contours_number_each_hole: bool = number_each_hole_contours
        self._contours_number_each_collection_item: bool = number_each_collection_item_contours
        self._contours_starting_number: int = contours_starting_number
        self._contours_current_number: int = self._contours_starting_number

    def do_work(self) -> None:
        self.title = "Построение таблицы узлов"
        self.message = self.title
        self._create_vertices_table()
        if self._is_contours_checked:
            self.title = "Построение таблицы контуров"
            self.message = self.title
            self._create_contours_table()

    def _items_to_geom_generator(self, items: Iterator[axipy.Feature]) -> Generator[axipy.Geometry]:
        for f in items:
            if self.is_canceled:
                return None
            self.value += 1
            g = f.geometry
            if isinstance(g, axipy.Geometry):
                yield g

    def _process_items_vertices(self, items: Iterator[axipy.Feature]) -> Generator[axipy.Feature]:
        geometries: Generator[axipy.Geometry] = self._items_to_geom_generator(items)

        for g in geometries:
            for numbered_point in self._geometry_to_numbered_pnt_vertices(g):
                yield self._process_numbered_pnt_vertices(numbered_point)

    def _process_items_contours(self, items: Iterator[axipy.Feature]) -> Generator[axipy.Feature]:
        geometries: Generator[axipy.Geometry] = self._items_to_geom_generator(items)

        for g in geometries:
            for numbered_point in self._geometry_to_points_contours(g):
                yield self._process_numbered_point_contours(numbered_point)

    def _create_vertices_table(self) -> None:
        table = self._source_table
        items = table.items()
        self.max = table.count()

        feature_generator: Generator[axipy.Feature] = self._process_items_vertices(items)

        vertices_schema = axipy.Schema(
            axipy.Attribute.integer(SchemaKeysVertices.VERTEX_NUMBER),
            axipy.Attribute.float(SchemaKeysVertices.X),
            axipy.Attribute.float(SchemaKeysVertices.Y),
            coordsystem=table.coordsystem
        )

        table = self._create_table(self._nodes_path, vertices_schema, feature_generator)

        axipy.run_in_gui(self._add_table_on_map_vertices, table)

    def _create_contours_table(self) -> None:
        table = self._source_table
        items = table.items()
        self.max = table.count()

        feature_generator: Generator[axipy.Feature] = self._process_items_contours(items)

        contours_schema = axipy.Schema(
            axipy.Attribute.integer(SchemaKeysContours.CONTOUR_NUMBER),
            coordsystem=table.coordsystem
        )

        table = self._create_table(self._contours_path, contours_schema, feature_generator)

        axipy.run_in_gui(self._add_table_on_map_contours, table)

    def _create_table(self, path: Path, schema: axipy.Schema, features: Iterable[axipy.Feature]) -> axipy.Table:
        table = cast(axipy.Table, axipy.provider_manager.create_open(
            {"src": str(path), "schema": schema, "override": True}))
        table.insert(features)
        table.commit()
        return table

    def _add_table_on_map_vertices(self, table: axipy.Table) -> None:
        list_layers = self._get_layers_group_ensured()
        new_layer = cast(axipy.VectorLayer, axipy.Layer.create(table))
        new_layer.label.visible = True
        new_layer.label.placementPolicy = axipy.LabelOverlap.OtherPosition
        list_layers.append(new_layer)

    def _add_table_on_map_contours(self, table: axipy.Table) -> None:
        list_layers = self._get_layers_group_ensured()
        new_layer = cast(axipy.VectorLayer, axipy.Layer.create(table))

        p_layout = new_layer.label.pointLayout
        p_layout.offset = QSize(0, 0)
        new_layer.label.pointLayout = p_layout

        new_layer.label.visible = True
        list_layers.append(new_layer)

    def _get_active_map_view_ensured(self) -> axipy.MapView:
        active_view = axipy.view_manager.active
        if not isinstance(active_view, axipy.MapView):
            raise RuntimeError("No active MapView.")
        return cast(axipy.MapView, active_view)

    def _get_layers_group_ensured(self) -> axipy.ListLayers:
        active_map_view = self._get_active_map_view_ensured()

        layers = active_map_view.map.layers

        group = None
        for elem in layers:
            if isinstance(elem, axipy.ListLayers) and elem.title == GROUP_NAME:
                group = elem

        if group is None:
            layers.add_group(GROUP_NAME)
            group = layers.at(0)

        if not isinstance(group, axipy.ListLayers):
            raise RuntimeError("Can't find or create layers group.")
        return group

    def _process_numbered_pnt_vertices(self, numbered_point: NUMBERED_PNT) -> axipy.Feature:
        number, pnt = numbered_point[0], numbered_point[1]
        x, y = pnt.x, pnt.y
        f = axipy.Feature({
            SchemaKeysVertices.VERTEX_NUMBER: number,
            SchemaKeysVertices.X: x,
            SchemaKeysVertices.Y: y,
        },
            geometry=axipy.Point(x, y),
            style=self._nodes_style
        )
        return f

    def _process_numbered_point_contours(self, numbered_point: NUMBERED_POINT) -> axipy.Feature:
        number, pnt = numbered_point[0], numbered_point[1]
        x, y = pnt.x, pnt.y

        f = axipy.Feature({
            SchemaKeysContours.CONTOUR_NUMBER: number,
        },
            geometry=axipy.Point(x, y),
            style=self._contours_style
        )
        return f

    def _collect_pnt_from_g_col_vertices(self, g: axipy.GeometryCollection) -> Generator[axipy.Pnt]:
        for elem in g:
            yield from self._geometry_to_pnt_vertices(elem)

    def enumerate_vertices(self, vertices, starting_number: int):
        dict_ = dict.fromkeys(((elem.x, elem.y) for elem in vertices))
        vertices = (axipy.Pnt(elem_[0], elem_[1]) for elem_ in dict_)
        return enumerate(vertices, starting_number)

    def _geometry_to_numbered_pnt_vertices(self, g: axipy.Geometry) -> Generator[NUMBERED_PNT]:
        points: Iterable[axipy.Pnt] = ()
        if isinstance(g, axipy.Line):
            points = g.begin, g.end
        elif isinstance(g, axipy.LineString):
            points = g.points
        elif isinstance(g, axipy.Polygon):
            polygon_points = list(g.points)[:-1]
            polygon_holes = (list(elem)[:-1] for elem in g.holes)

            if not self._nodes_number_each_hole:
                points = itertools.chain(polygon_points, itertools.chain.from_iterable(polygon_holes))
            else:
                for elem in self.enumerate_vertices(polygon_points, self._nodes_starting_number):
                    yield elem
                for holes in polygon_holes:
                    for numbered_hole in self.enumerate_vertices(holes, self._nodes_starting_number):
                        yield numbered_hole

        elif isinstance(g, axipy.Rectangle):
            points = (
                axipy.Pnt(g.xmin, g.ymin),
                axipy.Pnt(g.xmax, g.ymin),
                axipy.Pnt(g.xmax, g.ymax),
                axipy.Pnt(g.xmin, g.ymax),
            )
        elif isinstance(g, axipy.GeometryCollection):
            if self._nodes_number_each_collection_item:
                for elem in g:
                    yield from self._geometry_to_numbered_pnt_vertices(elem)
            else:
                yield from self.enumerate_vertices(self._collect_pnt_from_g_col_vertices(g),
                                                   self._nodes_starting_number)

        for elem in self.enumerate_vertices(points, self._nodes_starting_number):
            yield elem

    def _geometry_to_pnt_vertices(self, g: axipy.Geometry) -> Generator[axipy.Pnt]:
        points: Iterable[axipy.Pnt] = ()
        if isinstance(g, axipy.Line):
            points = g.begin, g.end
        elif isinstance(g, axipy.LineString):
            points = g.points
        elif isinstance(g, axipy.Polygon):
            polygon_points = list(g.points)[:-1]
            polygon_holes = (list(elem)[:-1] for elem in g.holes)

            if not self._nodes_number_each_hole:
                points = itertools.chain(polygon_points, itertools.chain.from_iterable(polygon_holes))
            else:
                for elem in polygon_points:
                    yield elem
                for holes in polygon_holes:
                    for numbered_hole in holes:
                        yield numbered_hole

        elif isinstance(g, axipy.Rectangle):
            points = (
                axipy.Pnt(g.xmin, g.ymin),
                axipy.Pnt(g.xmax, g.ymin),
                axipy.Pnt(g.xmax, g.ymax),
                axipy.Pnt(g.xmin, g.ymax),
            )

        for elem in points:
            yield elem

    def _geometry_to_points_contours(self, g: axipy.Geometry) -> Generator[NUMBERED_POINT]:
        if isinstance(g, axipy.Polygon):
            yield from self._polygon_to_numbered_points_contours(g)
        elif isinstance(g, axipy.GeometryCollection):
            if self._contours_number_each_collection_item:
                yield from enumerate(
                    self._filter_point_number(self._g_col_to_numbered_points_contours(g)),
                    self._contours_starting_number
                )
            else:
                yield from self._g_col_to_numbered_points_contours(g)

    def _filter_point_number(self, gen: Generator[NUMBERED_POINT]) -> Generator[axipy.Point]:
        for _i, p in gen:
            yield p

    def _polygon_to_numbered_points_contours(self, g: axipy.Polygon) -> Generator[NUMBERED_POINT]:
        g_centroid = g.centroid()
        if isinstance(g_centroid, axipy.Point):
            i = self._contours_current_number
            self._contours_current_number += 1
            yield i, g_centroid

        if self._contours_number_each_hole:
            yield from enumerate(self._holes_to_points_contours(g.holes), self._contours_starting_number)
        else:
            yield from self._holes_to_numbered_points_contours(g.holes)

    def _holes_to_points_contours(self, holes: axipy.ListHoles) -> Generator[axipy.Point]:
        for hole in holes:
            h_centroid = axipy.Polygon(*hole).centroid()
            if isinstance(h_centroid, axipy.Point):
                yield h_centroid

    def _holes_to_numbered_points_contours(self, holes: axipy.ListHoles) -> Generator[NUMBERED_POINT]:
        for hole in holes:
            h_centroid = axipy.Polygon(*hole).centroid()
            if isinstance(h_centroid, axipy.Point):
                i = self._contours_current_number
                self._contours_current_number += 1
                yield i, h_centroid

    def _g_col_to_numbered_points_contours(self, g: axipy.GeometryCollection) -> Generator[NUMBERED_POINT]:
        for elem in g:
            if isinstance(elem, axipy.Polygon):
                yield from self._polygon_to_numbered_points_contours(elem)
