# SPDX-License-Identifier: GPL-2.0-or-later

# <pep8 compliant>
#
# ----------------------------------------------------------
# Author: Zeffii
# Modified by: Alan Odom (Clockmender) & Rune Morling (ermo)
# ----------------------------------------------------------
#
import bpy
import bmesh
from mathutils.geometry import intersect_line_line as LineIntersect
import itertools
from collections import defaultdict
from . import pdt_cad_module as cm
from .pdt_functions import oops
from .pdt_msg_strings import (
    PDT_ERR_EDOB_MODE
)


def order_points(edge, point_list):
    """Order these edges from distance to v1, then sandwich the sorted list with v1, v2."""
    v1, v2 = edge

    def dist(coord):
        """Measure distance between two coordinates."""

        return (v1 - coord).length

    point_list = sorted(point_list, key=dist)
    return [v1] + point_list + [v2]


def remove_permutations_that_share_a_vertex(bm, permutations):
    """Get useful Permutations.

    Args:
        bm: Object's Bmesh
        permutations: Possible Intersection Edges as a list

    Returns:
        List of Edges.
    """

    final_permutations = []
    for edges in permutations:
        raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges)
        if len(set(raw_vert_indices)) < 4:
            continue

        # reaches this point if they do not share.
        final_permutations.append(edges)

    return final_permutations


def get_valid_permutations(bm, edge_indices):
    """Get useful Permutations.

    Args:
        bm: Object's Bmesh
        edge_indices: List of indices of Edges to consider

    Returns:
        List of suitable Edges.
    """

    raw_permutations = itertools.permutations(edge_indices, 2)
    permutations = [r for r in raw_permutations if r[0] < r[1]]
    return remove_permutations_that_share_a_vertex(bm, permutations)


def can_skip(closest_points, vert_vectors):
    """Check if the intersection lies on both edges and return True
    when criteria are not met, and thus this point can be skipped.

    Args:
        closest_points: List of Coordinates of points to consider
        vert_vectors: List of Coordinates of vertices to consider

    Returns:
        Boolean.
    """

    if not closest_points:
        return True
    if not isinstance(closest_points[0].x, float):
        return True
    if cm.num_edges_point_lies_on(closest_points[0], vert_vectors) < 2:
        return True

    # if this distance is larger than than 1.0e-5, we can skip it.
    cpa, cpb = closest_points
    return (cpa - cpb).length > 1.0e-5


def get_intersection_dictionary(bm, edge_indices):
    """Return a dictionary of edge indices and points found on those edges.

    Args:
        bm, Object's Bmesh
        edge_indices: List of Edge Indices

    Returns:
        Dictionary of Vectors.
    """

    bm.verts.ensure_lookup_table()
    bm.edges.ensure_lookup_table()

    permutations = get_valid_permutations(bm, edge_indices)

    list_k = defaultdict(list)
    list_d = defaultdict(list)

    for edges in permutations:
        raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges)
        vert_vectors = cm.vectors_from_indices(bm, raw_vert_indices)

        points = LineIntersect(*vert_vectors)

        # some can be skipped.    (NaN, None, not on both edges)
        if can_skip(points, vert_vectors):
            continue

        # reaches this point only when an intersection happens on both edges.
        [list_k[edge].append(points[0]) for edge in edges]

    # list_k will contain a dict of edge indices and points found on those edges.
    for edge_idx, unordered_points in list_k.items():
        tv1, tv2 = bm.edges[edge_idx].verts
        v1 = bm.verts[tv1.index].co
        v2 = bm.verts[tv2.index].co
        ordered_points = order_points((v1, v2), unordered_points)
        list_d[edge_idx].extend(ordered_points)

    return list_d


def update_mesh(bm, int_dict):
    """Make new geometry (delete old first).

    Args:
        bm, Object's Bmesh
        int_dict: Dictionary of Indices of Vertices

    Returns:
        Nothing.
    """

    orig_e = bm.edges
    orig_v = bm.verts

    new_verts = []
    collect = new_verts.extend
    for _, point_list in int_dict.items():
        num_edges_to_add = len(point_list) - 1
        for i in range(num_edges_to_add):
            coord_a = orig_v.new(point_list[i])
            coord_b = orig_v.new(point_list[i + 1])
            orig_e.new((coord_a, coord_b))
            bm.normal_update()
            collect([coord_a, coord_b])

    bmesh.ops.delete(bm, geom=[edge for edge in bm.edges if edge.select], context="EDGES")
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)


def unselect_nonintersecting(bm, d_edges, edge_indices):
    """Deselects Non-Intersection Edges.

    Args:
        bm, Object's Bmesh
        d_edges: List of Intersecting Edges
        edge_indices: List of Edge Indices to consider

    Returns:
        Nothing.
    """

    if len(edge_indices) > len(d_edges):
        reserved_edges = set(edge_indices) - set(d_edges)
        for edge in reserved_edges:
            bm.edges[edge].select = False


def intersect_all(context):
    """Computes All intersections with Crossing Geometry.

    Note:
        Deletes original edges and replaces with new intersected edges

    Args:
        context: Blender bpy.context instance.

    Returns:
        Status Set.
    """

    pg = context.scene.pdt_pg
    obj = context.active_object
    if all([bool(obj), obj.type == "MESH", obj.mode == "EDIT"]):
        # must force edge selection mode here
        bpy.context.tool_settings.mesh_select_mode = (False, True, False)


        if obj.mode == "EDIT":
            bm = bmesh.from_edit_mesh(obj.data)

            selected_edges = [edge for edge in bm.edges if edge.select]
            edge_indices = [i.index for i in selected_edges]

            int_dict = get_intersection_dictionary(bm, edge_indices)

            unselect_nonintersecting(bm, int_dict.keys(), edge_indices)
            update_mesh(bm, int_dict)

            bmesh.update_edit_mesh(obj.data)
        else:
            pg.error = f"{PDT_ERR_EDOB_MODE},{obj.mode})"
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return

        return
    else:
        pg.error = f"{PDT_ERR_EDOB_MODE},{obj.mode})"
        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
        return

class PDT_OT_IntersectAllEdges(bpy.types.Operator):
    """Cut Selected Edges at All Intersections"""

    bl_idname = "pdt.intersectall"
    bl_label = "Intersect All Edges"
    bl_options = {"REGISTER", "UNDO"}

    @classmethod
    def poll(cls, context):
        """Check to see object is in correct condition.

        Args:
            context: Blender bpy.context instance.

        Returns:
            Boolean
        """
        obj = context.active_object
        if obj is None:
            return False
        return obj is not None and obj.type == "MESH" and obj.mode == "EDIT"

    def execute(self, context):
        """Computes All intersections with Crossing Geometry.

        Note:
            Deletes original edges and replaces with new intersected edges

        Args:
            context: Blender bpy.context instance.

        Returns:
            Status Set.
        """

        pg = context.scene.pdt_pg
        pg.command = f"intall"
        return {"FINISHED"}
