# SPDX-FileCopyrightText: 2019-2022 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later

bl_info = {
    "name": "FilletPlus",
    "author": "Gert De Roost - original by zmj100",
    "version": (0, 4, 3),
    "blender": (2, 80, 0),
    "location": "View3D > Tool Shelf",
    "description": "",
    "warning": "",
    "doc_url": "",
    "category": "Mesh",
}


import bpy
from bpy.props import (
        FloatProperty,
        IntProperty,
        BoolProperty,
        )
from bpy.types import Operator
import bmesh
from mathutils import Matrix
from math import (
        cos, pi, sin,
        degrees, tan,
        )


def list_clear_(l):
    if l:
        del l[:]
    return l


def get_adj_v_(list_):
    tmp = {}
    for i in list_:
        try:
            tmp[i[0]].append(i[1])
        except KeyError:
            tmp[i[0]] = [i[1]]
        try:
            tmp[i[1]].append(i[0])
        except KeyError:
            tmp[i[1]] = [i[0]]
    return tmp

class f_buf():
    # one of the angles was not 0 or 180
    check = False


def fillets(list_0, startv, vertlist, face, adj, n, out, flip, radius):
    try:
        dict_0 = get_adj_v_(list_0)
        list_1 = [[dict_0[i][0], i, dict_0[i][1]] for i in dict_0 if (len(dict_0[i]) == 2)][0]
        list_3 = []
        for elem in list_1:
            list_3.append(bm.verts[elem])
        list_2 = []

        p_ = list_3[1]
        p = (list_3[1].co).copy()
        p1 = (list_3[0].co).copy()
        p2 = (list_3[2].co).copy()

        vec1 = p - p1
        vec2 = p - p2

        ang = vec1.angle(vec2, any)
        check_angle = round(degrees(ang))

        if check_angle == 180 or check_angle == 0.0:
            return False
        else:
            f_buf.check = True

        opp = adj

        if radius is False:
            h = adj * (1 / cos(ang * 0.5))
            adj_ = adj
        elif radius is True:
            h = opp / sin(ang * 0.5)
            adj_ = opp / tan(ang * 0.5)

        p3 = p - (vec1.normalized() * adj_)
        p4 = p - (vec2.normalized() * adj_)
        rp = p - ((p - ((p3 + p4) * 0.5)).normalized() * h)

        vec3 = rp - p3
        vec4 = rp - p4

        axis = vec1.cross(vec2)

        if out is False:
            if flip is False:
                rot_ang = vec3.angle(vec4)
            elif flip is True:
                rot_ang = vec1.angle(vec2)
        elif out is True:
            rot_ang = (2 * pi) - vec1.angle(vec2)

        for j in range(n + 1):
            new_angle = rot_ang * j / n
            mtrx = Matrix.Rotation(new_angle, 3, axis)
            if out is False:
                if flip is False:
                    tmp = p4 - rp
                    tmp1 = mtrx @ tmp
                    tmp2 = tmp1 + rp
                elif flip is True:
                    p3 = p - (vec1.normalized() * opp)
                    tmp = p3 - p
                    tmp1 = mtrx @ tmp
                    tmp2 = tmp1 + p
            elif out is True:
                p4 = p - (vec2.normalized() * opp)
                tmp = p4 - p
                tmp1 = mtrx @ tmp
                tmp2 = tmp1 + p

            v = bm.verts.new(tmp2)
            list_2.append(v)

        if flip is True:
            list_3[1:2] = list_2
        else:
            list_2.reverse()
            list_3[1:2] = list_2

        list_clear_(list_2)

        n1 = len(list_3)

        for t in range(n1 - 1):
            bm.edges.new([list_3[t], list_3[(t + 1) % n1]])

            v = bm.verts.new(p)
            bm.edges.new([v, p_])

        bm.edges.ensure_lookup_table()

        if face is not None:
            for l in face.loops:
                if l.vert == list_3[0]:
                    startl = l
                    break
            vertlist2 = []

            if startl.link_loop_next.vert == startv:
                l = startl.link_loop_prev
                while len(vertlist) > 0:
                    vertlist2.insert(0, l.vert)
                    vertlist.pop(vertlist.index(l.vert))
                    l = l.link_loop_prev
            else:
                l = startl.link_loop_next
                while len(vertlist) > 0:
                    vertlist2.insert(0, l.vert)
                    vertlist.pop(vertlist.index(l.vert))
                    l = l.link_loop_next

            for v in list_3:
                vertlist2.append(v)
            bm.faces.new(vertlist2)
        if startv.is_valid:
            bm.verts.remove(startv)
        else:
            print("\n[Function fillets Error]\n"
                  "Starting vertex (startv var) couldn't be removed\n")
            return False
        bm.verts.ensure_lookup_table()
        bm.edges.ensure_lookup_table()
        bm.faces.ensure_lookup_table()
        list_3[1].select = 1
        list_3[-2].select = 1
        bm.edges.get([list_3[0], list_3[1]]).select = 1
        bm.edges.get([list_3[-1], list_3[-2]]).select = 1
        bm.verts.index_update()
        bm.edges.index_update()
        bm.faces.index_update()

        me.update(calc_edges=True, calc_loop_triangles=True)
        bmesh.ops.recalc_face_normals(bm, faces=bm.faces)

    except Exception as e:
        print("\n[Function fillets Error]\n{}\n".format(e))
        return False


def do_filletplus(self, pair):
    is_finished = True
    try:
        startv = None
        global inaction
        global flip
        list_0 = [list([e.verts[0].index, e.verts[1].index]) for e in pair]

        vertset = set([])
        bm.verts.ensure_lookup_table()
        bm.edges.ensure_lookup_table()
        bm.faces.ensure_lookup_table()
        vertset.add(bm.verts[list_0[0][0]])
        vertset.add(bm.verts[list_0[0][1]])
        vertset.add(bm.verts[list_0[1][0]])
        vertset.add(bm.verts[list_0[1][1]])

        v1, v2, v3 = vertset

        if len(list_0) != 2:
            self.report({'WARNING'}, "Two adjacent edges must be selected")
            is_finished = False
        else:
            inaction = 1
            vertlist = []
            found = 0
            for f in v1.link_faces:
                if v2 in f.verts and v3 in f.verts:
                    found = 1
            if not found:
                for v in [v1, v2, v3]:
                    if v.index in list_0[0] and v.index in list_0[1]:
                        startv = v
                face = None
            else:
                for f in v1.link_faces:
                    if v2 in f.verts and v3 in f.verts:
                        for v in f.verts:
                            if not(v in vertset):
                                vertlist.append(v)
                            if (v in vertset and v.link_loops[0].link_loop_prev.vert in vertset and
                               v.link_loops[0].link_loop_next.vert in vertset):
                                startv = v
                        face = f
            if out is True:
                flip = False
            if startv:
                fills = fillets(list_0, startv, vertlist, face, adj, n, out, flip, radius)
                if not fills:
                    is_finished = False
            else:
                is_finished = False
    except Exception as e:
        print("\n[Function do_filletplus Error]\n{}\n".format(e))
        is_finished = False
    return is_finished


def check_is_not_coplanar(bm_data):
    from mathutils import Vector
    check = False
    angles, norm_angle = 0, 0
    z_vec = Vector((0, 0, 1))
    try:
        bm_data.faces.ensure_lookup_table()

        for f in bm_data.faces:
            norm_angle = f.normal.angle(z_vec)
            if angles == 0:
                angles = norm_angle
            if angles != norm_angle:
                check = True
                break
    except Exception as e:
        print("\n[Function check_is_not_coplanar Error]\n{}\n".format(e))
        check = True
    return check


#  Operator

class MESH_OT_fillet_plus(Operator):
    bl_idname = "mesh.fillet_plus"
    bl_label = "Fillet Plus"
    bl_description = ("Fillet adjoining edges\n"
                      "Note: Works on a mesh whose all faces share the same normal")
    bl_options = {"REGISTER", "UNDO"}

    adj: FloatProperty(
            name="",
            description="Size of the filleted corners",
            default=0.1,
            min=0.00001, max=100.0,
            step=1,
            precision=3
            )
    n: IntProperty(
            name="",
            description="Subdivision of the filleted corners",
            default=3,
            min=1, max=50,
            step=1
            )
    out: BoolProperty(
            name="Outside",
            description="Fillet towards outside",
            default=False
            )
    flip: BoolProperty(
            name="Flip",
            description="Flip the direction of the Fillet\n"
                        "Only available if Outside option is not active",
            default=False
            )
    radius: BoolProperty(
            name="Radius",
            description="Use radius for the size of the filleted corners",
            default=False
            )

    @classmethod
    def poll(cls, context):
        obj = context.active_object
        return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')

    def draw(self, context):
        layout = self.layout

        if f_buf.check is False:
            layout.label(text="Angle is equal to 0 or 180", icon="INFO")
            layout.label(text="Can not fillet", icon="BLANK1")
        else:
            layout.prop(self, "radius")
            if self.radius is True:
                layout.label(text="Radius:")
            elif self.radius is False:
                layout.label(text="Distance:")
            layout.prop(self, "adj")
            layout.label(text="Number of sides:")
            layout.prop(self, "n")

            if self.n > 1:
                row = layout.row(align=False)
                row.prop(self, "out")
                if self.out is False:
                    row.prop(self, "flip")

    def execute(self, context):
        global inaction
        global bm, me, adj, n, out, flip, radius, f_buf

        adj = self.adj
        n = self.n
        out = self.out
        flip = self.flip
        radius = self.radius

        inaction = 0
        f_buf.check = False

        ob_act = context.active_object
        try:
            me = ob_act.data
            bm = bmesh.from_edit_mesh(me)
            warn_obj = bool(check_is_not_coplanar(bm))
            if warn_obj is False:
                tempset = set([])
                bm.verts.ensure_lookup_table()
                bm.edges.ensure_lookup_table()
                bm.faces.ensure_lookup_table()
                for v in bm.verts:
                    if v.select and v.is_boundary:
                        tempset.add(v)
                for v in tempset:
                    edgeset = set([])
                    for e in v.link_edges:
                        if e.select and e.is_boundary:
                            edgeset.add(e)
                        if len(edgeset) == 2:
                            is_finished = do_filletplus(self, edgeset)
                            if not is_finished:
                                break

                if inaction == 1:
                    bpy.ops.mesh.select_all(action="DESELECT")
                    for v in bm.verts:
                        if len(v.link_edges) == 0:
                            bm.verts.remove(v)
                    bpy.ops.object.editmode_toggle()
                    bpy.ops.object.editmode_toggle()
                else:
                    self.report({'WARNING'}, "Filletplus operation could not be performed")
                    return {'CANCELLED'}
            else:
                self.report({'WARNING'}, "Mesh is not a coplanar surface. Operation cancelled")
                return {'CANCELLED'}
        except:
            self.report({'WARNING'}, "Filletplus operation could not be performed")
            return {'CANCELLED'}

        return {'FINISHED'}

# define classes for registration
classes = (
    MESH_OT_fillet_plus,
    )

# registering and menu integration
def register():
    for cls in classes:
        bpy.utils.register_class(cls)


# unregistering and removing menus
def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()
