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

# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
# ------------------------------- version 0.84 ------------------------------- #
#                                                                              #
# Creates duplicates of selected mesh to active morphing the shape according   #
# to target faces.                                                             #
#                                                                              #
#                    (c)  Alessandro Zomparelli                                #
#                             (2017)                                           #
#                                                                              #
# http://www.co-de-it.com/                                                     #
#                                                                              #
# ############################################################################ #


import bpy
from bpy.types import (
        Operator,
        Panel,
        PropertyGroup,
        )
from bpy.props import (
        BoolProperty,
        EnumProperty,
        FloatProperty,
        IntProperty,
        StringProperty,
        PointerProperty
        )
from mathutils import Vector, Quaternion, Matrix
import numpy as np
from math import *
import random, time, copy
import bmesh
from .utils import *

class polyhedra_wireframe(Operator):
    bl_idname = "object.polyhedra_wireframe"
    bl_label = "Tissue Polyhedra Wireframe"
    bl_description = "Generate wireframes around the faces.\
                      \nDoesn't works with boundary edges.\
                      \n(Experimental)"
    bl_options = {'REGISTER', 'UNDO'}

    thickness : FloatProperty(
        name="Thickness", default=0.1, min=0.001, soft_max=200,
        description="Wireframe thickness"
        )

    subdivisions : IntProperty(
        name="Segments", default=1, min=1, soft_max=10,
        description="Max sumber of segments, used for the longest edge"
        )

    #regular_sections : BoolProperty(
    #    name="Regular Sections", default=False,
    #    description="Turn inner loops into polygons"
    #    )

    dissolve_inners : BoolProperty(
        name="Dissolve Inners", default=False,
        description="Dissolve inner edges"
        )

    @classmethod
    def poll(cls, context):
        try:
            #bool_tessellated = context.object.tissue_tessellate.generator != None
            ob = context.object
            return ob.type == 'MESH' and ob.mode == 'OBJECT'# and bool_tessellated
        except:
            return False

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self)

    def execute(self, context):

        merge_dist = self.thickness*0.001

        subs = self.subdivisions

        start_time = time.time()
        ob = context.object
        me = simple_to_mesh(ob)
        bm = bmesh.new()
        bm.from_mesh(me)

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

        # Subdivide edges
        proportional_subs = True
        if subs > 1 and proportional_subs:
            wire_length = [e.calc_length() for e in bm.edges]
            all_edges = list(bm.edges)
            max_segment = max(wire_length)/subs
            split_edges = [[] for i in range(subs+1)]
            for e, l in zip(all_edges, wire_length):
                split_edges[int(l//max_segment)].append(e)
            for i in range(2,subs):
                perc = {}
                for e in split_edges[i]:
                    perc[e]=0.1
                bmesh.ops.bisect_edges(bm, edges=split_edges[i], cuts=i, edge_percents=perc)

        ### Create double faces
        double_faces = []
        double_layer_edge = []
        double_layer_piece = []
        for f in bm.faces:
            verts0 = [v.co for v in f.verts]
            verts1 = [v.co for v in f.verts]
            verts1.reverse()
            double_faces.append(verts0)
            double_faces.append(verts1)

        # Create new bmesh object and data layers
        bm1 = bmesh.new()

        # Create faces and assign Edge Layers
        for verts in double_faces:
            new_verts = []
            for v in verts:
                vert = bm1.verts.new(v)
                new_verts.append(vert)
            bm1.faces.new(new_verts)

        bm1.verts.ensure_lookup_table()
        bm1.edges.ensure_lookup_table()
        bm1.faces.ensure_lookup_table()

        n_faces = len(bm.faces)
        n_doubles = len(bm1.faces)

        polyhedra = []

        for e in bm.edges:
            done = []

            # ERROR: Naked edges
            e_faces = len(e.link_faces)
            if e_faces < 2:
                bm.free()
                bm1.free()
                message = "Naked edges are not allowed"
                self.report({'ERROR'}, message)
                return {'CANCELLED'}

            edge_vec =  e.verts[1].co - e.verts[0].co

            # run first face
            for i1 in range(e_faces-1):
                f1 = e.link_faces[i1]
                #edge_verts1 = [v.index for v in f1.verts if v in e.verts]
                verts1 = [v.index for v in f1.verts]
                va1 = verts1.index(e.verts[0].index)
                vb1 = verts1.index(e.verts[1].index)
                # check if order of the edge matches the order of the face
                dir1 = va1 == (vb1+1)%len(verts1)
                edge_vec1 = edge_vec if dir1 else -edge_vec

                # run second face
                faces2 = []
                normals2 = []
                for i2 in range(i1+1,e_faces):
                #for i2 in range(n_faces):
                    if i1 == i2: continue
                    f2 = e.link_faces[i2]
                    f2.normal_update()
                    #edge_verts2 = [v.index for v in f2.verts if v in e.verts]
                    verts2 = [v.index for v in f2.verts]
                    va2 = verts2.index(e.verts[0].index)
                    vb2 = verts2.index(e.verts[1].index)
                    # check if order of the edge matches the order of the face
                    dir2 = va2 == (vb2+1)%len(verts2)
                    # check for normal consistency
                    if dir1 != dir2:
                        # add face
                        faces2.append(f2.index+1)
                        normals2.append(f2.normal)
                    else:
                        # add flipped face
                        faces2.append(-(f2.index+1))
                        normals2.append(-f2.normal)



                # find first polyhedra (positive)
                plane_x = f1.normal                     # normal
                plane_y = plane_x.cross(edge_vec1)      # tangent face perp edge
                id1 = (f1.index+1)

                min_angle0 = 10000

                # check consistent faces
                if id1 not in done:
                    id2 = None
                    min_angle = min_angle0
                    for i2, n2 in zip(faces2,normals2):
                        v2 = flatten_vector(-n2, plane_x, plane_y)
                        angle = vector_rotation(v2)
                        if angle < min_angle:
                            id2 = i2
                            min_angle = angle
                    if id2: done.append(id2)
                    new_poly = True
                    # add to existing polyhedron
                    for p in polyhedra:
                        if id1 in p or id2 in p:
                            new_poly = False
                            if id2 not in p: p.append(id2)
                            if id1 not in p: p.append(id1)
                            break
                    # start new polyhedron
                    if new_poly: polyhedra.append([id1, id2])

                # find second polyhedra (negative)
                plane_x = -f1.normal                    # normal
                plane_y = plane_x.cross(-edge_vec1)      # tangent face perp edge
                id1 = -(f1.index+1)

                if id1 not in done:
                    id2 = None
                    min_angle = min_angle0
                    for i2, n2 in zip(faces2, normals2):
                        v2 = flatten_vector(n2, plane_x, plane_y)
                        angle = vector_rotation(v2)
                        if angle < min_angle:
                            id2 = -i2
                            min_angle = angle
                    done.append(id2)
                    add = True
                    for p in polyhedra:
                        if id1 in p or id2 in p:
                            add = False
                            if id2 not in p: p.append(id2)
                            if id1 not in p: p.append(id1)
                            break
                    if add: polyhedra.append([id1, id2])

        for i in range(len(bm1.faces)):
            for j in (False,True):
                if j: id = i+1
                else: id = -(i+1)
                join = []
                keep = []
                for p in polyhedra:
                    if id in p: join += p
                    else: keep.append(p)
                if len(join) > 0:
                    keep.append(list(dict.fromkeys(join)))
                    polyhedra = keep

        for i, p in enumerate(polyhedra):
            for j in p:
                bm1.faces[j].material_index = i

        end_time = time.time()
        print('Tissue: Polyhedra wireframe, found {} polyhedra in {:.4f} sec'.format(len(polyhedra), end_time-start_time))


        delete_faces = []
        wireframe_faces = []
        not_wireframe_faces = []
        flat_faces = []

        bm.free()

        #bmesh.ops.bisect_edges(bm1, edges=bm1.edges, cuts=3)

        end_time = time.time()
        print('Tissue: Polyhedra wireframe, subdivide edges in {:.4f} sec'.format(end_time-start_time))

        bm1.faces.index_update()
        #merge_verts = []
        for p in polyhedra:
            delete_faces_poly = []
            wireframe_faces_poly = []
            faces_id = [(f-1)*2 if f > 0 else (-f-1)*2+1 for f in p]
            faces_id_neg = [(-f-1)*2 if -f > 0 else (f-1)*2+1 for f in p]
            merge_verts = []
            faces = [bm1.faces[f_id] for f_id in faces_id]
            for f in faces:
                delete = False
                if f.index in delete_faces: continue
                '''
                cen = f.calc_center_median()
                for e in f.edges:
                    mid = (e.verts[0].co + e.verts[1].co)/2
                    vec1 = e.verts[0].co - e.verts[1].co
                    vec2 = mid - cen
                    ang = Vector.angle(vec1,vec2)
                    length = vec2.length
                    #length = sin(ang)*length
                    if length < self.thickness/2:
                        delete = True
                '''
                if False:
                    sides = len(f.verts)
                    for i in range(sides):
                        v = f.verts[i].co
                        v0 = f.verts[(i-1)%sides].co
                        v1 = f.verts[(i+1)%sides].co
                        vec0 = v0 - v
                        vec1 = v1 - v
                        ang = (pi - vec0.angle(vec1))/2
                        length = min(vec0.length, vec1.length)*sin(ang)
                        if length < self.thickness/2:
                            delete = True
                            break

                if delete:
                    delete_faces_poly.append(f.index)
                else:
                    wireframe_faces_poly.append(f.index)
                merge_verts += [v for v in f.verts]
            if len(wireframe_faces_poly) < 2:
                delete_faces += faces_id
                not_wireframe_faces += faces_id_neg
            else:
                wireframe_faces += wireframe_faces_poly
                flat_faces += delete_faces_poly

            #wireframe_faces = list(dict.fromkeys(wireframe_faces))
            bmesh.ops.remove_doubles(bm1, verts=merge_verts, dist=merge_dist)
            bm1.edges.ensure_lookup_table()
            bm1.faces.ensure_lookup_table()
            bm1.faces.index_update()


        wireframe_faces = [i for i in wireframe_faces if i not in not_wireframe_faces]
        wireframe_faces = list(dict.fromkeys(wireframe_faces))

        flat_faces = list(dict.fromkeys(flat_faces))

        end_time = time.time()
        print('Tissue: Polyhedra wireframe, merge and delete in {:.4f} sec'.format(end_time-start_time))

        poly_me = me.copy()
        bm1.to_mesh(poly_me)
        poly_me.update()
        new_ob = bpy.data.objects.new("Polyhedra", poly_me)
        context.collection.objects.link(new_ob)

        ############# FRAME #############
        bm1.faces.index_update()
        wireframe_faces = [bm1.faces[i] for i in wireframe_faces]
        original_faces = wireframe_faces
        #bmesh.ops.remove_doubles(bm1, verts=merge_verts, dist=0.001)

        # detect edge loops

        loops = []
        boundaries_mat = []
        neigh_face_center = []
        face_normals = []

        # compute boundary frames
        new_faces = []
        wire_length = []
        vert_ids = []

        # append regular faces

        for f in original_faces:
            loop = list(f.verts)
            loops.append(loop)
            boundaries_mat.append([f.material_index for v in loop])
            f.normal_update()
            face_normals.append([f.normal for v in loop])

        push_verts = []
        inner_loops = []

        for loop_index, loop in enumerate(loops):
            is_boundary = loop_index < len(neigh_face_center)
            materials = boundaries_mat[loop_index]
            new_loop = []
            loop_ext = [loop[-1]] + loop + [loop[0]]

            # calc tangents
            tangents = []
            for i in range(len(loop)):
                # vertices
                vert0 = loop_ext[i]
                vert = loop_ext[i+1]
                vert1 = loop_ext[i+2]
                # edge vectors
                vec0 = (vert0.co - vert.co).normalized()
                vec1 = (vert.co - vert1.co).normalized()
                # tangent
                _vec1 = -vec1
                _vec0 = -vec0
                ang = (pi - vec0.angle(vec1))/2
                normal = face_normals[loop_index][i]
                tan0 = normal.cross(vec0)
                tan1 = normal.cross(vec1)
                tangent = (tan0 + tan1).normalized()/sin(ang)*self.thickness/2
                tangents.append(tangent)

            # calc correct direction for boundaries
            mult = -1
            if is_boundary:
                dir_val = 0
                for i in range(len(loop)):
                    surf_point = neigh_face_center[loop_index][i]
                    tangent = tangents[i]
                    vert = loop_ext[i+1]
                    dir_val += tangent.dot(vert.co - surf_point)
                if dir_val > 0: mult = 1

            # add vertices
            for i in range(len(loop)):
                vert = loop_ext[i+1]
                area = 1
                new_co = vert.co + tangents[i] * mult * area
                # add vertex
                new_vert = bm1.verts.new(new_co)
                new_loop.append(new_vert)
                vert_ids.append(vert.index)
            new_loop.append(new_loop[0])

            # add faces
            #materials += [materials[0]]
            for i in range(len(loop)):
                v0 = loop_ext[i+1]
                v1 = loop_ext[i+2]
                v2 = new_loop[i+1]
                v3 = new_loop[i]
                face_verts = [v1,v0,v3,v2]
                if mult == -1: face_verts = [v0,v1,v2,v3]
                new_face = bm1.faces.new(face_verts)
                # Material by original edges
                piece_id = 0
                new_face.select = True
                new_faces.append(new_face)
                wire_length.append((v0.co - v1.co).length)
            max_segment = max(wire_length)/self.subdivisions
            #for f,l in zip(new_faces,wire_length):
            #    f.material_index = min(int(l/max_segment), self.subdivisions-1)
            bm1.verts.ensure_lookup_table()
            push_verts += [v.index for v in loop_ext]

        # At this point topology han been build, but not yet thickened

        end_time = time.time()
        print('Tissue: Polyhedra wireframe, frames in {:.4f} sec'.format(end_time-start_time))

        bm1.verts.ensure_lookup_table()
        bm1.edges.ensure_lookup_table()
        bm1.faces.ensure_lookup_table()
        bm1.verts.index_update()

        ### Displace vertices ###

        circle_center = [0]*len(bm1.verts)
        circle_normal = [0]*len(bm1.verts)

        smooth_corners = [True] * len(bm1.verts)
        corners = [[] for i in range(len(bm1.verts))]
        normals = [0]*len(bm1.verts)
        vertices = [0]*len(bm1.verts)
        # Define vectors direction
        for f in new_faces:
            v0 = f.verts[0]
            v1 = f.verts[1]
            id = v0.index
            corners[id].append((v1.co - v0.co).normalized())
            normals[id] = v0.normal.copy()
            vertices[id] = v0
            smooth_corners[id] = False
        # Displace vertices
        for i, vecs in enumerate(corners):
            if len(vecs) > 0:
                v = vertices[i]
                nor = normals[i]
                ang = 0
                for vec in vecs:
                    ang += nor.angle(vec)
                ang /= len(vecs)
                div = sin(ang)
                if div == 0: div = 1
                v.co += nor*self.thickness/2/div

        end_time = time.time()
        print('Tissue: Polyhedra wireframe, corners displace in {:.4f} sec'.format(end_time-start_time))

        # Removing original flat faces

        flat_faces = [bm1.faces[i] for i in flat_faces]
        for f in flat_faces:
            f.material_index = self.subdivisions+1
            for v in f.verts:
                if smooth_corners[v.index]:
                    v.co += v.normal*self.thickness/2
                    smooth_corners[v.index] = False
        delete_faces = delete_faces + [f.index for f in original_faces]
        delete_faces = list(dict.fromkeys(delete_faces))
        delete_faces = [bm1.faces[i] for i in delete_faces]
        bmesh.ops.delete(bm1, geom=delete_faces, context='FACES')

        bmesh.ops.remove_doubles(bm1, verts=bm1.verts, dist=merge_dist)
        bm1.faces.ensure_lookup_table()
        bm1.edges.ensure_lookup_table()
        bm1.verts.ensure_lookup_table()

        if self.dissolve_inners:
            bm1.edges.index_update()
            dissolve_edges = []
            for f in bm1.faces:
                e = f.edges[2]
                if e not in dissolve_edges:
                    dissolve_edges.append(e)
            bmesh.ops.dissolve_edges(bm1, edges=dissolve_edges, use_verts=True, use_face_split=True)

        all_lines = [[] for e in me.edges]
        all_end_points = [[] for e in me.edges]
        for v in bm1.verts: v.select_set(False)
        for f in bm1.faces: f.select_set(False)

        _me = me.copy()
        bm1.to_mesh(me)
        me.update()
        new_ob = bpy.data.objects.new("Wireframe", me)
        context.collection.objects.link(new_ob)
        for o in context.scene.objects: o.select_set(False)
        new_ob.select_set(True)
        context.view_layer.objects.active = new_ob
        me = _me

        bm1.free()
        bpy.data.meshes.remove(_me)
        #new_ob.location = ob.location
        new_ob.matrix_world = ob.matrix_world

        end_time = time.time()
        print('Tissue: Polyhedra wireframe in {:.4f} sec'.format(end_time-start_time))
        return {'FINISHED'}
