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

# based completely on addon by zmj100
# added some distance limits to prevent overlap - max12345


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


def edit_mode_out():
    bpy.ops.object.mode_set(mode='OBJECT')


def edit_mode_in():
    bpy.ops.object.mode_set(mode='EDIT')


def angle_rotation(rp, q, axis, angle):
    # returns the vector made by the rotation of the vector q
    # rp by angle around axis and then adds rp

    return (Matrix.Rotation(angle, 3, axis) * (q - rp)) + rp


def face_inset_fillet(bme, face_index_list, inset_amount, distance,
                      number_of_sides, out, radius, type_enum, kp):
    list_del = []

    for faceindex in face_index_list:

        bme.faces.ensure_lookup_table()
        # loops through the faces...
        f = bme.faces[faceindex]
        f.select_set(False)
        list_del.append(f)
        f.normal_update()
        vertex_index_list = [v.index for v in f.verts]
        dict_0 = {}
        orientation_vertex_list = []
        n = len(vertex_index_list)
        for i in range(n):
            # loops through the vertices
            dict_0[i] = []
            bme.verts.ensure_lookup_table()
            p = (bme.verts[vertex_index_list[i]].co).copy()
            p1 = (bme.verts[vertex_index_list[(i - 1) % n]].co).copy()
            p2 = (bme.verts[vertex_index_list[(i + 1) % n]].co).copy()
            # copies some vert coordinates, always the 3 around i
            dict_0[i].append(bme.verts[vertex_index_list[i]])
            # appends the bmesh vert of the appropriate index to the dict
            vec1 = p - p1
            vec2 = p - p2
            # vectors for the other corner points to the cornerpoint
            # corresponding to i / p
            angle = vec1.angle(vec2)

            adj = inset_amount / tan(angle * 0.5)
            h = (adj ** 2 + inset_amount ** 2) ** 0.5
            if round(degrees(angle)) == 180 or round(degrees(angle)) == 0.0:
                # if the corner is a straight line...
                # I think this creates some new points...
                if out is True:
                    val = ((f.normal).normalized() * inset_amount)
                else:
                    val = -((f.normal).normalized() * inset_amount)
                p6 = angle_rotation(p, p + val, vec1, radians(90))
            else:
                # if the corner is an actual corner
                val = ((f.normal).normalized() * h)
                if out is True:
                    # this -(p - (vec2.normalized() * adj))) is just the freaking axis afaik...
                    p6 = angle_rotation(
                                p, p + val,
                                -(p - (vec2.normalized() * adj)),
                                -radians(90)
                                )
                else:
                    p6 = angle_rotation(
                                p, p - val,
                                ((p - (vec1.normalized() * adj)) - (p - (vec2.normalized() * adj))),
                                -radians(90)
                                )

                orientation_vertex_list.append(p6)

        new_inner_face = []
        orientation_vertex_list_length = len(orientation_vertex_list)
        ovll = orientation_vertex_list_length

        for j in range(ovll):
            q = orientation_vertex_list[j]
            q1 = orientation_vertex_list[(j - 1) % ovll]
            q2 = orientation_vertex_list[(j + 1) % ovll]
            # again, these are just vectors between somewhat displaced corner vertices
            vec1_ = q - q1
            vec2_ = q - q2
            ang_ = vec1_.angle(vec2_)

            # the angle between them
            if round(degrees(ang_)) == 180 or round(degrees(ang_)) == 0.0:
                # again... if it's really a line...
                v = bme.verts.new(q)
                new_inner_face.append(v)
                dict_0[j].append(v)
            else:
                # s.a.
                if radius is False:
                    h_ = distance * (1 / cos(ang_ * 0.5))
                    d = distance
                elif radius is True:
                    h_ = distance / sin(ang_ * 0.5)
                    d = distance / tan(ang_ * 0.5)
                # max(d) is vec1_.magnitude * 0.5
                # or vec2_.magnitude * 0.5 respectively

                # only functional difference v
                if d > vec1_.magnitude * 0.5:
                    d = vec1_.magnitude * 0.5

                if d > vec2_.magnitude * 0.5:
                    d = vec2_.magnitude * 0.5
                # only functional difference ^

                q3 = q - (vec1_.normalized() * d)
                q4 = q - (vec2_.normalized() * d)
                # these are new verts somewhat offset from the corners
                rp_ = q - ((q - ((q3 + q4) * 0.5)).normalized() * h_)
                # reference point inside the curvature
                axis_ = vec1_.cross(vec2_)
                # this should really be just the face normal
                vec3_ = rp_ - q3
                vec4_ = rp_ - q4
                rot_ang = vec3_.angle(vec4_)
                cornerverts = []

                for o in range(number_of_sides + 1):
                    # this calculates the actual new vertices
                    q5 = angle_rotation(rp_, q4, axis_, rot_ang * o / number_of_sides)
                    v = bme.verts.new(q5)

                    # creates new bmesh vertices from it
                    bme.verts.index_update()

                    dict_0[j].append(v)
                    cornerverts.append(v)

                cornerverts.reverse()
                new_inner_face.extend(cornerverts)

        if out is False:
            f = bme.faces.new(new_inner_face)
            f.select_set(True)
        elif out is True and kp is True:
            f = bme.faces.new(new_inner_face)
            f.select_set(True)

        n2_ = len(dict_0)
        # these are the new side faces, those that don't depend on cornertype
        for o in range(n2_):
            list_a = dict_0[o]
            list_b = dict_0[(o + 1) % n2_]
            bme.faces.new([list_a[0], list_b[0], list_b[-1], list_a[1]])
            bme.faces.index_update()
        # cornertype 1 - ngon faces
        if type_enum == 'opt0':
            for k in dict_0:
                if len(dict_0[k]) > 2:
                    bme.faces.new(dict_0[k])
                    bme.faces.index_update()
        # cornertype 2 - triangulated faces
        if type_enum == 'opt1':
            for k_ in dict_0:
                q_ = dict_0[k_][0]
                dict_0[k_].pop(0)
                n3_ = len(dict_0[k_])
                for kk in range(n3_ - 1):
                    bme.faces.new([dict_0[k_][kk], dict_0[k_][(kk + 1) % n3_], q_])
                    bme.faces.index_update()

    del_ = [bme.faces.remove(f) for f in list_del]

    if del_:
        del del_


# Operator

class MESH_OT_face_inset_fillet(Operator):
    bl_idname = "mesh.face_inset_fillet"
    bl_label = "Face Inset Fillet"
    bl_description = ("Inset selected and Fillet (make round) the corners \n"
                     "of the newly created Faces")
    bl_options = {"REGISTER", "UNDO"}

    # inset amount
    inset_amount: FloatProperty(
            name="Inset amount",
            description="Define the size of the Inset relative to the selection",
            default=0.04,
            min=0, max=100.0,
            step=1,
            precision=3
            )
    # number of sides
    number_of_sides: IntProperty(
            name="Number of sides",
            description="Define the roundness of the corners by specifying\n"
                        "the subdivision count",
            default=4,
            min=1, max=100,
            step=1
            )
    distance: FloatProperty(
            name="",
            description="Use distance or radius for corners' size calculation",
            default=0.04,
            min=0.00001, max=100.0,
            step=1,
            precision=3
            )
    out: BoolProperty(
            name="Outside",
            description="Inset the Faces outwards in relation to the selection\n"
                        "Note: depending on the geometry, can give unsatisfactory results",
            default=False
            )
    radius: BoolProperty(
            name="Radius",
            description="Use radius for corners' size calculation",
            default=False
            )
    type_enum: EnumProperty(
            items=(('opt0', "N-gon", "N-gon corners - Keep the corner Faces uncut"),
                   ('opt1', "Triangle", "Triangulate corners")),
            name="Corner Type",
            default="opt0"
            )
    kp: BoolProperty(
            name="Keep faces",
            description="Do not delete the inside Faces\n"
                        "Only available if the Out option is checked",
            default=False
            )

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

        layout.label(text="Corner Type:")

        row = layout.row()
        row.prop(self, "type_enum", text="")

        row = layout.row(align=True)
        row.prop(self, "out")

        if self.out is True:
            row.prop(self, "kp")

        row = layout.row()
        row.prop(self, "inset_amount")

        row = layout.row()
        row.prop(self, "number_of_sides")

        row = layout.row()
        row.prop(self, "radius")

        row = layout.row()
        dist_rad = "Radius" if self.radius else "Distance"
        row.prop(self, "distance", text=dist_rad)

    def execute(self, context):
        # this really just prepares everything for the main function
        inset_amount = self.inset_amount
        number_of_sides = self.number_of_sides
        distance = self.distance
        out = self.out
        radius = self.radius
        type_enum = self.type_enum
        kp = self.kp

        edit_mode_out()
        ob_act = context.active_object
        bme = bmesh.new()
        bme.from_mesh(ob_act.data)
        # this
        face_index_list = [f.index for f in bme.faces if f.select and f.is_valid]

        if len(face_index_list) == 0:
            self.report({'WARNING'},
                        "No suitable Face selection found. Operation cancelled")
            edit_mode_in()

            return {'CANCELLED'}

        elif len(face_index_list) != 0:
            face_inset_fillet(bme, face_index_list,
                              inset_amount, distance, number_of_sides,
                              out, radius, type_enum, kp)

        bme.to_mesh(ob_act.data)
        edit_mode_in()

        return {'FINISHED'}
