# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 3
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# ##### END GPL LICENSE BLOCK #####

# Contact for more information about the Addon:
# Email:    germano.costa@ig.com.br
# Twitter:  wii_mano @mano_wii

bl_info = {
    "name": "Extrude and Reshape",
    "author": "Germano Cavalcante",
    "version": (0, 8, 1),
    "blender": (2, 80, 0),
    "location": "View3D > UI > Tools > Mesh Tools > Add: > Extrude Menu (Alt + E)",
    "description": "Extrude face and merge edge intersections "
                   "between the mesh and the new edges",
    "wiki_url": "http://blenderartists.org/forum/"
                "showthread.php?376618-Addon-Push-Pull-Face",
    "category": "Mesh"}

import bpy
import bmesh
from mathutils.geometry import intersect_line_line
from bpy.types import Operator


class BVHco():
    i = 0
    c1x = 0.0
    c1y = 0.0
    c1z = 0.0
    c2x = 0.0
    c2y = 0.0
    c2z = 0.0


def edges_BVH_overlap(bm, edges, epsilon=0.0001):
    bco = set()
    for e in edges:
        bvh = BVHco()
        bvh.i = e.index
        b1 = e.verts[0]
        b2 = e.verts[1]
        co1 = b1.co.x
        co2 = b2.co.x
        if co1 <= co2:
            bvh.c1x = co1 - epsilon
            bvh.c2x = co2 + epsilon
        else:
            bvh.c1x = co2 - epsilon
            bvh.c2x = co1 + epsilon
        co1 = b1.co.y
        co2 = b2.co.y
        if co1 <= co2:
            bvh.c1y = co1 - epsilon
            bvh.c2y = co2 + epsilon
        else:
            bvh.c1y = co2 - epsilon
            bvh.c2y = co1 + epsilon
        co1 = b1.co.z
        co2 = b2.co.z
        if co1 <= co2:
            bvh.c1z = co1 - epsilon
            bvh.c2z = co2 + epsilon
        else:
            bvh.c1z = co2 - epsilon
            bvh.c2z = co1 + epsilon
        bco.add(bvh)
    del edges
    overlap = {}
    oget = overlap.get
    for e1 in bm.edges:
        by = bz = True
        a1 = e1.verts[0]
        a2 = e1.verts[1]
        c1x = a1.co.x
        c2x = a2.co.x
        if c1x > c2x:
            tm = c1x
            c1x = c2x
            c2x = tm
        for bvh in bco:
            if c1x <= bvh.c2x and c2x >= bvh.c1x:
                if by:
                    by = False
                    c1y = a1.co.y
                    c2y = a2.co.y
                    if c1y > c2y:
                        tm = c1y
                        c1y = c2y
                        c2y = tm
                if c1y <= bvh.c2y and c2y >= bvh.c1y:
                    if bz:
                        bz = False
                        c1z = a1.co.z
                        c2z = a2.co.z
                        if c1z > c2z:
                            tm = c1z
                            c1z = c2z
                            c2z = tm
                    if c1z <= bvh.c2z and c2z >= bvh.c1z:
                        e2 = bm.edges[bvh.i]
                        if e1 != e2:
                            overlap[e1] = oget(e1, set()).union({e2})
    return overlap


def intersect_edges_edges(overlap, precision=4):
    epsilon = .1**precision
    fpre_min = -epsilon
    fpre_max = 1 + epsilon
    splits = {}
    sp_get = splits.get
    new_edges1 = set()
    new_edges2 = set()
    targetmap = {}
    for edg1 in overlap:
        # print("***", ed1.index, "***")
        for edg2 in overlap[edg1]:
            a1 = edg1.verts[0]
            a2 = edg1.verts[1]
            b1 = edg2.verts[0]
            b2 = edg2.verts[1]

            # test if are linked
            if a1 in {b1, b2} or a2 in {b1, b2}:
                # print('linked')
                continue

            aco1, aco2 = a1.co, a2.co
            bco1, bco2 = b1.co, b2.co
            tp = intersect_line_line(aco1, aco2, bco1, bco2)
            if tp:
                p1, p2 = tp
                if (p1 - p2).to_tuple(precision) == (0, 0, 0):
                    v = aco2 - aco1
                    f = p1 - aco1
                    x, y, z = abs(v.x), abs(v.y), abs(v.z)
                    max1 = 0 if x >= y and x >= z else\
                           1 if y >= x and y >= z else 2
                    fac1 = f[max1] / v[max1]

                    v = bco2 - bco1
                    f = p2 - bco1
                    x, y, z = abs(v.x), abs(v.y), abs(v.z)
                    max2 = 0 if x >= y and x >= z else\
                           1 if y >= x and y >= z else 2
                    fac2 = f[max2] / v[max2]

                    if fpre_min <= fac1 <= fpre_max:
                        # print(edg1.index, 'can intersect', edg2.index)
                        ed1 = edg1

                    elif edg1 in splits:
                        for ed1 in splits[edg1]:
                            a1 = ed1.verts[0]
                            a2 = ed1.verts[1]

                            vco1 = a1.co
                            vco2 = a2.co

                            v = vco2 - vco1
                            f = p1 - vco1
                            fac1 = f[max1] / v[max1]
                            if fpre_min <= fac1 <= fpre_max:
                                # print(e.index, 'can intersect', edg2.index)
                                break
                        else:
                            # print(edg1.index, 'really does not intersect', edg2.index)
                            continue
                    else:
                        # print(edg1.index, 'not intersect', edg2.index)
                        continue

                    if fpre_min <= fac2 <= fpre_max:
                        # print(ed1.index, 'actually intersect', edg2.index)
                        ed2 = edg2

                    elif edg2 in splits:
                        for ed2 in splits[edg2]:
                            b1 = ed2.verts[0]
                            b2 = ed2.verts[1]

                            vco1 = b1.co
                            vco2 = b2.co

                            v = vco2 - vco1
                            f = p2 - vco1
                            fac2 = f[max2] / v[max2]
                            if fpre_min <= fac2 <= fpre_max:
                                # print(ed1.index, 'actually intersect', e.index)
                                break
                        else:
                            # print(ed1.index, 'really does not intersect', ed2.index)
                            continue
                    else:
                        # print(ed1.index, 'not intersect', edg2.index)
                        continue

                    new_edges1.add(ed1)
                    new_edges2.add(ed2)

                    if abs(fac1) <= epsilon:
                        nv1 = a1
                    elif fac1 + epsilon >= 1:
                        nv1 = a2
                    else:
                        ne1, nv1 = bmesh.utils.edge_split(ed1, a1, fac1)
                        new_edges1.add(ne1)
                        splits[edg1] = sp_get(edg1, set()).union({ne1})

                    if abs(fac2) <= epsilon:
                        nv2 = b1
                    elif fac2 + epsilon >= 1:
                        nv2 = b2
                    else:
                        ne2, nv2 = bmesh.utils.edge_split(ed2, b1, fac2)
                        new_edges2.add(ne2)
                        splits[edg2] = sp_get(edg2, set()).union({ne2})

                    if nv1 != nv2:  # necessary?
                        targetmap[nv1] = nv2

    return new_edges1, new_edges2, targetmap


class ER_OT_Extrude_and_Reshape(Operator):
    bl_idname = "mesh.extrude_reshape"
    bl_label = "Extrude and Reshape"
    bl_description = "Push and pull face entities to sculpt 3d models"
    bl_options = {'REGISTER', 'GRAB_CURSOR', 'BLOCKING'}

    @classmethod
    def poll(cls, context):
        if context.mode=='EDIT_MESH':
            return True

    def modal(self, context, event):
        if self.confirm:
            sface = self.bm.faces.active
            if not sface:
                for face in self.bm.faces:
                    if face.select is True:
                        sface = face
                        break
                else:
                    return {'FINISHED'}
            # edges to intersect
            edges = set()
            [[edges.add(ed) for ed in v.link_edges] for v in sface.verts]

            overlap = edges_BVH_overlap(self.bm, edges, epsilon=0.0001)
            overlap = {k: v for k, v in overlap.items() if k not in edges}  # remove repetition
            """
            print([e.index for e in edges])
            for a, b in overlap.items():
                print(a.index, [e.index for e in b])
            """
            new_edges1, new_edges2, targetmap = intersect_edges_edges(overlap)
            pos_weld = set()
            for e in new_edges1:
                v1, v2 = e.verts
                if v1 in targetmap and v2 in targetmap:
                    pos_weld.add((targetmap[v1], targetmap[v2]))
            if targetmap:
                bmesh.ops.weld_verts(self.bm, targetmap=targetmap)
            """
            print([e.is_valid for e in new_edges1])
            print([e.is_valid for e in new_edges2])
            sp_faces1 = set()
            """
            for e in pos_weld:
                v1, v2 = e
                lf1 = set(v1.link_faces)
                lf2 = set(v2.link_faces)
                rlfe = lf1.intersection(lf2)
                for f in rlfe:
                    try:
                        nf = bmesh.utils.face_split(f, v1, v2)
                        # sp_faces1.update({f, nf[0]})
                    except:
                        pass

            # sp_faces2 = set()
            for e in new_edges2:
                lfe = set(e.link_faces)
                v1, v2 = e.verts
                lf1 = set(v1.link_faces)
                lf2 = set(v2.link_faces)
                rlfe = lf1.intersection(lf2)
                for f in rlfe.difference(lfe):
                    nf = bmesh.utils.face_split(f, v1, v2)
                    # sp_faces2.update({f, nf[0]})

            bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
            return {'FINISHED'}
        if self.cancel:
            return {'FINISHED'}
        self.cancel = event.type in {'ESC', 'NDOF_BUTTON_ESC'}
        self.confirm = event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER'}
        return {'PASS_THROUGH'}

    def execute(self, context):
        self.mesh = context.object.data
        self.bm = bmesh.from_edit_mesh(self.mesh)
        try:
            selection = self.bm.select_history[-1]
        except:
            for face in self.bm.faces:
                if face.select is True:
                    selection = face
                    break
            else:
                return {'FINISHED'}
        if not isinstance(selection, bmesh.types.BMFace):
            bpy.ops.mesh.extrude_region_move('INVOKE_DEFAULT')
            return {'FINISHED'}
        else:
            face = selection
            # face.select = False
            bpy.ops.mesh.select_all(action='DESELECT')
            geom = []
            for edge in face.edges:
                if abs(edge.calc_face_angle(0) - 1.5707963267948966) < 0.01:  # self.angle_tolerance:
                    geom.append(edge)

            ret_dict = bmesh.ops.extrude_discrete_faces(self.bm, faces=[face])

            for face in ret_dict['faces']:
                self.bm.faces.active = face
                face.select = True
                sface = face
            dfaces = bmesh.ops.dissolve_edges(
                        self.bm, edges=geom, use_verts=True, use_face_split=False
                        )
            bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True)
            bpy.ops.transform.translate(
                        'INVOKE_DEFAULT', constraint_axis=(False, False, True),
                        orient_type='NORMAL', release_confirm=True
                        )

        context.window_manager.modal_handler_add(self)

        self.cancel = False
        self.confirm = False
        return {'RUNNING_MODAL'}


def operator_draw(self, context):
    layout = self.layout
    col = layout.column(align=True)
    col.operator("mesh.extrude_reshape")


def register():
    bpy.utils.register_class(ER_OT_Extrude_and_Reshape)


def unregister():
    bpy.utils.unregister_class(ER_OT_Extrude_and_Reshape)


if __name__ == "__main__":
    register()
