# Updated for 2.8 jan 5 2019

# ##### 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 2
#  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, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# <pep8 compliant>

bl_info = {
    "name": "F2",
    "author": "Bart Crouch, Alexander Nedovizin, Paul Kotelevets "
              "(concept design), Adrian Rutkowski",
    "version": (1, 8, 4),
    "blender": (2, 80, 0),
    "location": "Editmode > F",
    "warning": "",
    "description": "Extends the 'Make Edge/Face' functionality",
    "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
                "Scripts/Modeling/F2",
    "category": "Mesh",
}

# ref: https://github.com/Cfyzzz/Other-scripts/blob/master/f2.py

import bmesh
import bpy
import itertools
import mathutils
import math
from mathutils import Vector
from bpy_extras import view3d_utils


# returns a custom data layer of the UV map, or None
def get_uv_layer(ob, bm, mat_index):
    uv = None
    uv_layer = None
    if ob.material_slots:
        me = ob.data
        if me.uv_layers:
            uv = me.uv_layers.active.name
    # 'material_slots' is deprecated (Blender Internal)
    # else:
    #     mat = ob.material_slots[mat_index].material
    #     if mat is not None:
    #         slot = mat.texture_slots[mat.active_texture_index]
    #         if slot and slot.uv_layer:
    #             uv = slot.uv_layer
    #         else:
    #             for tex_slot in mat.texture_slots:
    #                 if tex_slot and tex_slot.uv_layer:
    #                     uv = tex_slot.uv_layer
    #                     break
    if uv:
        uv_layer = bm.loops.layers.uv.get(uv)

    return (uv_layer)


# create a face from a single selected edge
def quad_from_edge(bm, edge_sel, context, event):
    addon_prefs = context.preferences.addons[__name__].preferences
    ob = context.active_object
    region = context.region
    region_3d = context.space_data.region_3d

    # find linked edges that are open (<2 faces connected) and not part of
    # the face the selected edge belongs to
    all_edges = [[edge for edge in edge_sel.verts[i].link_edges if \
                  len(edge.link_faces) < 2 and edge != edge_sel and \
                  sum([face in edge_sel.link_faces for face in edge.link_faces]) == 0] \
                 for i in range(2)]
    if not all_edges[0] or not all_edges[1]:
        return

    # determine which edges to use, based on mouse cursor position
    mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y])
    optimal_edges = []
    for edges in all_edges:
        min_dist = False
        for edge in edges:
            vert = [vert for vert in edge.verts if not vert.select][0]
            world_pos = ob.matrix_world @ vert.co.copy()
            screen_pos = view3d_utils.location_3d_to_region_2d(region,
                                                               region_3d, world_pos)
            dist = (mouse_pos - screen_pos).length
            if not min_dist or dist < min_dist[0]:
                min_dist = (dist, edge, vert)
        optimal_edges.append(min_dist)

    # determine the vertices, which make up the quad
    v1 = edge_sel.verts[0]
    v2 = edge_sel.verts[1]
    edge_1 = optimal_edges[0][1]
    edge_2 = optimal_edges[1][1]
    v3 = optimal_edges[0][2]
    v4 = optimal_edges[1][2]

    # normal detection
    flip_align = True
    normal_edge = edge_1
    if not normal_edge.link_faces:
        normal_edge = edge_2
        if not normal_edge.link_faces:
            normal_edge = edge_sel
            if not normal_edge.link_faces:
                # no connected faces, so no need to flip the face normal
                flip_align = False
    if flip_align:  # there is a face to which the normal can be aligned
        ref_verts = [v for v in normal_edge.link_faces[0].verts]
        if v3 in ref_verts:
            va_1 = v3
            va_2 = v1
        elif normal_edge == edge_sel:
            va_1 = v1
            va_2 = v2
        else:
            va_1 = v2
            va_2 = v4
        if (va_1 == ref_verts[0] and va_2 == ref_verts[-1]) or \
                (va_2 == ref_verts[0] and va_1 == ref_verts[-1]):
            # reference verts are at start and end of the list -> shift list
            ref_verts = ref_verts[1:] + [ref_verts[0]]
        if ref_verts.index(va_1) > ref_verts.index(va_2):
            # connected face has same normal direction, so don't flip
            flip_align = False

    # material index detection
    ref_faces = edge_sel.link_faces
    if not ref_faces:
        ref_faces = edge_sel.verts[0].link_faces
    if not ref_faces:
        ref_faces = edge_sel.verts[1].link_faces
    if not ref_faces:
        mat_index = False
        smooth = False
    else:
        mat_index = ref_faces[0].material_index
        smooth = ref_faces[0].smooth

    if addon_prefs.quad_from_e_mat:
        mat_index = bpy.context.object.active_material_index

    # create quad
    try:
        if v3 == v4:
            # triangle (usually at end of quad-strip
            verts = [v3, v1, v2]
        else:
            # normal face creation
            verts = [v3, v1, v2, v4]
        if flip_align:
            verts.reverse()
        face = bm.faces.new(verts)
        if mat_index:
            face.material_index = mat_index
        face.smooth = smooth
    except:
        # face already exists
        return

    # change selection
    edge_sel.select = False
    for vert in edge_sel.verts:
        vert.select = False
    for edge in face.edges:
        if edge.index < 0:
            edge.select = True
    v3.select = True
    v4.select = True

    # adjust uv-map
    if __name__ != '__main__':
        if addon_prefs.adjustuv:
            uv_layer = get_uv_layer(ob, bm, mat_index)
            if uv_layer:
                uv_ori = {}
                for vert in [v1, v2, v3, v4]:
                    for loop in vert.link_loops:
                        if loop.face.index > -1:
                            uv_ori[loop.vert.index] = loop[uv_layer].uv
                if len(uv_ori) == 4 or len(uv_ori) == 3:
                    for loop in face.loops:
                        if loop.vert.index in uv_ori:
                            loop[uv_layer].uv = uv_ori[loop.vert.index]

    # toggle mode, to force correct drawing
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.mode_set(mode='EDIT')


# create a face from a single selected vertex, if it is an open vertex
def quad_from_vertex(bm, vert_sel, context, event):
    addon_prefs = context.preferences.addons[__name__].preferences
    ob = context.active_object
    me = ob.data
    region = context.region
    region_3d = context.space_data.region_3d

    # find linked edges that are open (<2 faces connected)
    edges = [edge for edge in vert_sel.link_edges if len(edge.link_faces) < 2]
    if len(edges) < 2:
        return

    # determine which edges to use, based on mouse cursor position
    min_dist = False
    mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y])
    for a, b in itertools.combinations(edges, 2):
        other_verts = [vert for edge in [a, b] for vert in edge.verts \
                       if not vert.select]
        mid_other = (other_verts[0].co.copy() + other_verts[1].co.copy()) \
                    / 2
        new_pos = 2 * (mid_other - vert_sel.co.copy()) + vert_sel.co.copy()
        world_pos = ob.matrix_world @ new_pos
        screen_pos = view3d_utils.location_3d_to_region_2d(region, region_3d,
                                                           world_pos)
        dist = (mouse_pos - screen_pos).length
        if not min_dist or dist < min_dist[0]:
            min_dist = (dist, (a, b), other_verts, new_pos)

    # create vertex at location mirrored in the line, connecting the open edges
    edges = min_dist[1]
    other_verts = min_dist[2]
    new_pos = min_dist[3]
    vert_new = bm.verts.new(new_pos)

    # normal detection
    flip_align = True
    normal_edge = edges[0]
    if not normal_edge.link_faces:
        normal_edge = edges[1]
        if not normal_edge.link_faces:
            # no connected faces, so no need to flip the face normal
            flip_align = False
    if flip_align:  # there is a face to which the normal can be aligned
        ref_verts = [v for v in normal_edge.link_faces[0].verts]
        if other_verts[0] in ref_verts:
            va_1 = other_verts[0]
            va_2 = vert_sel
        else:
            va_1 = vert_sel
            va_2 = other_verts[1]
        if (va_1 == ref_verts[0] and va_2 == ref_verts[-1]) or \
                (va_2 == ref_verts[0] and va_1 == ref_verts[-1]):
            # reference verts are at start and end of the list -> shift list
            ref_verts = ref_verts[1:] + [ref_verts[0]]
        if ref_verts.index(va_1) > ref_verts.index(va_2):
            # connected face has same normal direction, so don't flip
            flip_align = False

    # material index detection
    ref_faces = vert_sel.link_faces
    if not ref_faces:
        mat_index = False
        smooth = False
    else:
        mat_index = ref_faces[0].material_index
        smooth = ref_faces[0].smooth

    if addon_prefs.quad_from_v_mat:
        mat_index = bpy.context.object.active_material_index

    # create face between all 4 vertices involved
    verts = [other_verts[0], vert_sel, other_verts[1], vert_new]
    if flip_align:
        verts.reverse()
    face = bm.faces.new(verts)
    if mat_index:
        face.material_index = mat_index
    face.smooth = smooth

    # change selection
    vert_new.select = True
    vert_sel.select = False

    # adjust uv-map
    if __name__ != '__main__':
        if addon_prefs.adjustuv:
            uv_layer = get_uv_layer(ob, bm, mat_index)
            if uv_layer:
                uv_others = {}
                uv_sel = None
                uv_new = None
                # get original uv coordinates
                for i in range(2):
                    for loop in other_verts[i].link_loops:
                        if loop.face.index > -1:
                            uv_others[loop.vert.index] = loop[uv_layer].uv
                            break
                if len(uv_others) == 2:
                    mid_other = (list(uv_others.values())[0] +
                                 list(uv_others.values())[1]) / 2
                    for loop in vert_sel.link_loops:
                        if loop.face.index > -1:
                            uv_sel = loop[uv_layer].uv
                            break
                    if uv_sel:
                        uv_new = 2 * (mid_other - uv_sel) + uv_sel

                # set uv coordinates for new loops
                if uv_new:
                    for loop in face.loops:
                        if loop.vert.index == -1:
                            x, y = uv_new
                        elif loop.vert.index in uv_others:
                            x, y = uv_others[loop.vert.index]
                        else:
                            x, y = uv_sel
                        loop[uv_layer].uv = (x, y)

    # toggle mode, to force correct drawing
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.mode_set(mode='EDIT')


def expand_vert(self, context, event):
    addon_prefs = context.preferences.addons[__name__].preferences
    ob = context.active_object
    obj = bpy.context.object
    me = obj.data
    bm = bmesh.from_edit_mesh(me)
    region = context.region
    region_3d = context.space_data.region_3d
    rv3d = context.space_data.region_3d

    for v in bm.verts:
        if v.select:
            v_active = v

    try:
        depth_location = v_active.co
    except:
        return {'CANCELLED'}
    # create vert in mouse cursor location

    mouse_pos = Vector((event.mouse_region_x, event.mouse_region_y))
    location_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, depth_location)

    c_verts = []
    # find and select linked edges that are open (<2 faces connected) add those edge verts to c_verts list
    linked = v_active.link_edges
    for edges in linked:
        if len(edges.link_faces) < 2:
            edges.select = True
            for v in edges.verts:
                if v is not v_active:
                    c_verts.append(v)

    # Compare distance in 2d between mouse and edges middle points
    screen_pos_va = view3d_utils.location_3d_to_region_2d(region, region_3d,
                                                          ob.matrix_world @ v_active.co)
    screen_pos_v1 = view3d_utils.location_3d_to_region_2d(region, region_3d,
                                                          ob.matrix_world @ c_verts[0].co)
    screen_pos_v2 = view3d_utils.location_3d_to_region_2d(region, region_3d,
                                                          ob.matrix_world @ c_verts[1].co)

    mid_pos_v1 = Vector(((screen_pos_va[0] + screen_pos_v1[0]) / 2, (screen_pos_va[1] + screen_pos_v1[1]) / 2))
    mid_pos_V2 = Vector(((screen_pos_va[0] + screen_pos_v2[0]) / 2, (screen_pos_va[1] + screen_pos_v2[1]) / 2))

    dist1 = math.log10(pow((mid_pos_v1[0] - mouse_pos[0]), 2) + pow((mid_pos_v1[1] - mouse_pos[1]), 2))
    dist2 = math.log10(pow((mid_pos_V2[0] - mouse_pos[0]), 2) + pow((mid_pos_V2[1] - mouse_pos[1]), 2))

    bm.normal_update()
    bm.verts.ensure_lookup_table()

    # Deselect not needed point and create new face
    if dist1 < dist2:
        c_verts[1].select = False
        lleft = c_verts[0].link_faces

    else:
        c_verts[0].select = False
        lleft = c_verts[1].link_faces

    lactive = v_active.link_faces
    # lverts = lactive[0].verts

    mat_index = lactive[0].material_index
    smooth = lactive[0].smooth

    for faces in lactive:
        if faces in lleft:
            cface = faces
            if len(faces.verts) == 3:
                bm.normal_update()
                bmesh.update_edit_mesh(obj.data)
                bpy.ops.mesh.select_all(action='DESELECT')
                v_active.select = True
                bpy.ops.mesh.rip_edge_move('INVOKE_DEFAULT')
                return {'FINISHED'}

    lverts = cface.verts

    # create triangle with correct normal orientation
    # if You looking at that part - yeah... I know. I still dont get how blender calculates normals...

    # from L to R
    if dist1 < dist2:
        if (lverts[0] == v_active and lverts[3] == c_verts[0]) \
                or (lverts[2] == v_active and lverts[1] == c_verts[0]) \
                or (lverts[1] == v_active and lverts[0] == c_verts[0]) \
                or (lverts[3] == v_active and lverts[2] == c_verts[0]):
            v_new = bm.verts.new(v_active.co)
            face_new = bm.faces.new((c_verts[0], v_new, v_active))

        elif (lverts[1] == v_active and lverts[2] == c_verts[0]) \
                or (lverts[0] == v_active and lverts[1] == c_verts[0]) \
                or (lverts[3] == v_active and lverts[0] == c_verts[0]) \
                or (lverts[2] == v_active and lverts[3] == c_verts[0]):
            v_new = bm.verts.new(v_active.co)
            face_new = bm.faces.new((v_active, v_new, c_verts[0]))

        else:
            pass
    # from R to L
    else:
        if (lverts[2] == v_active and lverts[3] == c_verts[1]) \
                or (lverts[0] == v_active and lverts[1] == c_verts[1]) \
                or (lverts[1] == v_active and lverts[2] == c_verts[1]) \
                or (lverts[3] == v_active and lverts[0] == c_verts[1]):
            v_new = bm.verts.new(v_active.co)
            face_new = bm.faces.new((v_active, v_new, c_verts[1]))

        elif (lverts[0] == v_active and lverts[3] == c_verts[1]) \
                or (lverts[2] == v_active and lverts[1] == c_verts[1]) \
                or (lverts[1] == v_active and lverts[0] == c_verts[1]) \
                or (lverts[3] == v_active and lverts[2] == c_verts[1]):
            v_new = bm.verts.new(v_active.co)
            face_new = bm.faces.new((c_verts[1], v_new, v_active))

        else:
            pass

    # set smooth and mat based on starting face
    if addon_prefs.tris_from_v_mat:
        face_new.material_index = bpy.context.object.active_material_index
    else:
        face_new.material_index = mat_index
    face_new.smooth = smooth

    # update normals
    bpy.ops.mesh.select_all(action='DESELECT')
    v_new.select = True
    bm.select_history.add(v_new)

    bm.normal_update()
    bmesh.update_edit_mesh(obj.data)
    bpy.ops.transform.translate('INVOKE_DEFAULT')


def checkforconnected(conection):
    obj = bpy.context.object
    me = obj.data
    bm = bmesh.from_edit_mesh(me)

    # Checks for number of edes or faces connected to selected vertex
    for v in bm.verts:
        if v.select:
            v_active = v
    if conection == 'faces':
        linked = v_active.link_faces
    elif conection == 'edges':
        linked = v_active.link_edges

    bmesh.update_edit_mesh(obj.data)
    return len(linked)


# autograb preference in addons panel
class F2AddonPreferences(bpy.types.AddonPreferences):
    bl_idname = __name__
    adjustuv : bpy.props.BoolProperty(
        name="Adjust UV",
        description="Automatically update UV unwrapping",
        default=False)
    autograb : bpy.props.BoolProperty(
        name="Auto Grab",
        description="Automatically puts a newly created vertex in grab mode",
        default=True)
    extendvert : bpy.props.BoolProperty(
        name="Enable Extend Vert",
        description="Anables a way to build tris and quads by adding verts",
        default=False)
    quad_from_e_mat : bpy.props.BoolProperty(
        name="Quad From Edge",
        description="Use active material for created face instead of close one",
        default=True)
    quad_from_v_mat : bpy.props.BoolProperty(
        name="Quad From Vert",
        description="Use active material for created face instead of close one",
        default=True)
    tris_from_v_mat : bpy.props.BoolProperty(
        name="Tris From Vert",
        description="Use active material for created face instead of close one",
        default=True)
    ngons_v_mat : bpy.props.BoolProperty(
        name="Ngons",
        description="Use active material for created face instead of close one",
        default=True)

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

        col = layout.column()
        col.label(text="behaviours:")
        col.prop(self, "autograb")
        col.prop(self, "adjustuv")
        col.prop(self, "extendvert")

        col = layout.column()
        col.label(text="use active material when creating:")
        col.prop(self, "quad_from_e_mat")
        col.prop(self, "quad_from_v_mat")
        col.prop(self, "tris_from_v_mat")
        col.prop(self, "ngons_v_mat")


class MeshF2(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "mesh.f2"
    bl_label = "Make Edge/Face"
    bl_description = "Extends the 'Make Edge/Face' functionality"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        # check we are in mesh editmode
        ob = context.active_object
        return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')

    def usequad(self, bm, sel, context, event):
        quad_from_vertex(bm, sel, context, event)
        if __name__ != '__main__':
            addon_prefs = context.preferences.addons[__name__].preferences
            if addon_prefs.autograb:
                bpy.ops.transform.translate('INVOKE_DEFAULT')

    def invoke(self, context, event):
        bm = bmesh.from_edit_mesh(context.active_object.data)
        sel = [v for v in bm.verts if v.select]
        if len(sel) > 2:
            # original 'Make Edge/Face' behaviour
            try:
                bpy.ops.mesh.edge_face_add('INVOKE_DEFAULT')
                addon_prefs = context.preferences.addons[__name__].preferences
                if addon_prefs.ngons_v_mat:
                    bpy.ops.object.material_slot_assign()
            except:
                return {'CANCELLED'}
        elif len(sel) == 1:
            # single vertex selected -> mirror vertex and create new face
            addon_prefs = context.preferences.addons[__name__].preferences
            if addon_prefs.extendvert:
                if checkforconnected('faces') in [2]:
                    if checkforconnected('edges') in [3]:
                        expand_vert(self, context, event)
                    else:
                        self.usequad(bm, sel[0], context, event)

                elif checkforconnected('faces') in [1]:
                    if checkforconnected('edges') in [2]:
                        expand_vert(self, context, event)
                    else:
                        self.usequad(bm, sel[0], context, event)
                else:
                    self.usequad(bm, sel[0], context, event)
            else:
                self.usequad(bm, sel[0], context, event)
        elif len(sel) == 2:
            edges_sel = [ed for ed in bm.edges if ed.select]
            if len(edges_sel) != 1:
                # 2 vertices selected, but not on the same edge
                bpy.ops.mesh.edge_face_add()
            else:
                # single edge selected -> new face from linked open edges
                quad_from_edge(bm, edges_sel[0], context, event)

        return {'FINISHED'}


# registration
classes = [MeshF2, F2AddonPreferences]
addon_keymaps = []


def register():
    # add operator
    for c in classes:
        bpy.utils.register_class(c)

    # add keymap entry
    kcfg = bpy.context.window_manager.keyconfigs.addon
    if kcfg:
        km = kcfg.keymaps.new(name='Mesh', space_type='EMPTY')
        kmi = km.keymap_items.new("mesh.f2", 'F', 'PRESS')
        addon_keymaps.append((km, kmi.idname))


def unregister():
    # remove keymap entry
    for km, kmi_idname in addon_keymaps:
        for kmi in km.keymap_items:
            if kmi.idname == kmi_idname:
                km.keymap_items.remove(kmi)
    addon_keymaps.clear()

    # remove operator and preferences
    for c in reversed(classes):
        bpy.utils.unregister_class(c)


if __name__ == "__main__":
    register()
