# gpl author: Giuseppe De Marco [BlenderLab] inspired by NirenYang

bl_info = {
    "name": "Set edges length",
    "description": "Edges length",
    "author": "Giuseppe De Marco [BlenderLab] inspired by NirenYang",
    "version": (0, 1, 0),
    "blender": (2, 80, 0),
    "location": "Toolbar > Tools > Mesh Tools: set Length(Shit+Alt+E)",
    "warning": "",
    "doc_url": "",
    "category": "Mesh",
}

import bpy
import bmesh
from mathutils import Vector
from bpy.types import Operator
from bpy.props import (
        FloatProperty,
        EnumProperty,
        )

# GLOBALS
edge_length_debug = False
_error_message = "Please select at least one edge to fill select history"
_error_message_2 = "Edges with shared vertices are not allowed. Please, use scale instead"

# Note : Refactor - removed all the operators apart from LengthSet
#        and merged the other ones as options of length (lijenstina)


def get_edge_vector(edge):
    verts = (edge.verts[0].co, edge.verts[1].co)
    vector = verts[1] - verts[0]

    return vector


def get_selected(bmesh_obj, geometry_type):
    # geometry type should be edges, verts or faces
    selected = []

    for i in getattr(bmesh_obj, geometry_type):
        if i.select:
            selected.append(i)
    return tuple(selected)


def get_center_vector(verts):
    # verts = [Vector((x,y,z)), Vector((x,y,z))]

    center_vector = Vector((((verts[1][0] + verts[0][0]) / 2.),
                           ((verts[1][1] + verts[0][1]) / 2.),
                           ((verts[1][2] + verts[0][2]) / 2.)))
    return center_vector


class LengthSet(Operator):
    bl_idname = "object.mesh_edge_length_set"
    bl_label = "Set edge length"
    bl_description = ("Change one selected edge length by a specified target,\n"
                      "existing length and different modes\n"
                      "Note: works only with Edges that not share a vertex")
    bl_options = {'REGISTER', 'UNDO'}

    old_length: FloatProperty(
            name="Original length",
            options={'HIDDEN'},
            )
    set_length_type: EnumProperty(
            items=[
                ('manual', "Manual",
                 "Input manually the desired Target Length"),
                ('existing', "Existing Length",
                 "Use existing geometry Edges' characteristics"),
            ],
            name="Set Type of Input",
            )
    target_length: FloatProperty(
            name="Target Length",
            description="Input a value for an Edges Length target",
            default=1.00,
            unit='LENGTH',
            precision=5
            )
    existing_length: EnumProperty(
            items=[
                ('min', "Shortest",
                 "Set all to shortest Edge of selection"),
                ('max', "Longest",
                 "Set all to the longest Edge of selection"),
                ('average', "Average",
                 "Set all to the average Edge length of selection"),
                ('active', "Active",
                 "Set all to the active Edge's one\n"
                 "Needs a selection to be done in Edge Select mode"),
            ],
            name="Existing length"
            )
    mode: EnumProperty(
            items=[
                ('fixed', "Fixed", "Fixed"),
                ('increment', "Increment", "Increment"),
                ('decrement', "Decrement", "Decrement"),
            ],
            name="Mode"
            )
    behaviour: EnumProperty(
            items=[
                ('proportional', "Proportional",
                 "Move vertex locations proportionally to the center of the Edge"),
                ('clockwise', "Clockwise",
                "Compute the Edges' vertex locations in a clockwise fashion"),
                ('unclockwise', "Counterclockwise",
                "Compute the Edges' vertex locations in a counterclockwise fashion"),
            ],
            name="Resize behavior"
            )

    originary_edge_length_dict = {}
    edge_lengths = []
    selected_edges = ()

    @classmethod
    def poll(cls, context):
        return (context.edit_object and context.object.type == 'MESH')

    def check(self, context):
        return True

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

        layout.label(text="Original Active length is: {:.3f}".format(self.old_length))

        layout.label(text="Input Mode:")
        layout.prop(self, "set_length_type", expand=True)
        if self.set_length_type == 'manual':
            layout.prop(self, "target_length")
        else:
            layout.prop(self, "existing_length", text="")

        layout.label(text="Mode:")
        layout.prop(self, "mode", text="")

        layout.label(text="Resize Behavior:")
        layout.prop(self, "behaviour", text="")

    def get_existing_edge_length(self, bm):
        if self.existing_length != "active":
            if self.existing_length == "min":
                return min(self.edge_lengths)
            if self.existing_length == "max":
                return max(self.edge_lengths)
            elif self.existing_length == "average":
                return sum(self.edge_lengths) / float(len(self.selected_edges))
        else:
            bm.edges.ensure_lookup_table()
            active_edge_length = None

            for elem in reversed(bm.select_history):
                if isinstance(elem, bmesh.types.BMEdge):
                    active_edge_length = elem.calc_length()
                    break
            return active_edge_length

        return 0.0

    def invoke(self, context, event):
        wm = context.window_manager

        obj = context.edit_object
        bm = bmesh.from_edit_mesh(obj.data)

        bpy.ops.mesh.select_mode(type="EDGE")
        self.selected_edges = get_selected(bm, 'edges')

        if self.selected_edges:
            vertex_set = []

            for edge in self.selected_edges:
                vector = get_edge_vector(edge)

                if edge.verts[0].index not in vertex_set:
                    vertex_set.append(edge.verts[0].index)
                else:
                    self.report({'ERROR_INVALID_INPUT'}, _error_message_2)
                    return {'CANCELLED'}

                if edge.verts[1].index not in vertex_set:
                    vertex_set.append(edge.verts[1].index)
                else:
                    self.report({'ERROR_INVALID_INPUT'}, _error_message_2)
                    return {'CANCELLED'}

                # warning, it's a constant !
                verts_index = ''.join((str(edge.verts[0].index), str(edge.verts[1].index)))
                self.originary_edge_length_dict[verts_index] = vector
                self.edge_lengths.append(vector.length)
                self.old_length = vector.length
        else:
            self.report({'ERROR'}, _error_message)
            return {'CANCELLED'}

        if edge_length_debug:
            self.report({'INFO'}, str(self.originary_edge_length_dict))

        self.target_length = vector.length

        return wm.invoke_props_dialog(self)

    def execute(self, context):

        bpy.ops.mesh.select_mode(type="EDGE")
        self.context = context

        obj = context.edit_object
        bm = bmesh.from_edit_mesh(obj.data)

        self.selected_edges = get_selected(bm, 'edges')

        if not self.selected_edges:
            self.report({'ERROR'}, _error_message)
            return {'CANCELLED'}

        for edge in self.selected_edges:
            vector = get_edge_vector(edge)
            # what we should see in original length dialog field
            self.old_length = vector.length

            if self.set_length_type == 'manual':
                vector.length = abs(self.target_length)
            else:
                get_lengths = self.get_existing_edge_length(bm)
                # check for edit mode
                if not get_lengths:
                    self.report({'WARNING'},
                                "Operation Cancelled. "
                                "Active Edge could not be determined (needs selection in Edit Mode)")
                    return {'CANCELLED'}

                vector.length = get_lengths

            if vector.length == 0.0:
                self.report({'ERROR'}, "Operation cancelled. Target length is set to zero")
                return {'CANCELLED'}

            center_vector = get_center_vector((edge.verts[0].co, edge.verts[1].co))

            verts_index = ''.join((str(edge.verts[0].index), str(edge.verts[1].index)))

            if edge_length_debug:
                self.report({'INFO'},
                            ' - '.join(('vector ' + str(vector),
                                        'originary_vector ' +
                                        str(self.originary_edge_length_dict[verts_index])
                                        )))
            verts = (edge.verts[0].co, edge.verts[1].co)

            if edge_length_debug:
                self.report({'INFO'},
                            '\n edge.verts[0].co ' + str(verts[0]) +
                            '\n edge.verts[1].co ' + str(verts[1]) +
                            '\n vector.length' + str(vector.length))

            # the clockwise direction have v1 -> v0, unclockwise v0 -> v1
            if self.target_length >= 0:
                if self.behaviour == 'proportional':
                    edge.verts[1].co = center_vector + vector / 2
                    edge.verts[0].co = center_vector - vector / 2

                    if self.mode == 'decrement':
                        edge.verts[0].co = (center_vector + vector / 2) - \
                                            (self.originary_edge_length_dict[verts_index] / 2)
                        edge.verts[1].co = (center_vector - vector / 2) + \
                                            (self.originary_edge_length_dict[verts_index] / 2)

                    elif self.mode == 'increment':
                        edge.verts[1].co = (center_vector + vector / 2) + \
                                            self.originary_edge_length_dict[verts_index] / 2
                        edge.verts[0].co = (center_vector - vector / 2) - \
                                            self.originary_edge_length_dict[verts_index] / 2

                elif self.behaviour == 'unclockwise':
                    if self.mode == 'increment':
                        edge.verts[1].co = \
                                verts[0] + (self.originary_edge_length_dict[verts_index] + vector)
                    elif self.mode == 'decrement':
                        edge.verts[0].co = \
                                verts[1] - (self.originary_edge_length_dict[verts_index] - vector)
                    else:
                        edge.verts[1].co = verts[0] + vector

                else:
                    # clockwise
                    if self.mode == 'increment':
                        edge.verts[0].co = \
                                verts[1] - (self.originary_edge_length_dict[verts_index] + vector)
                    elif self.mode == 'decrement':
                        edge.verts[1].co = \
                                verts[0] + (self.originary_edge_length_dict[verts_index] - vector)
                    else:
                        edge.verts[0].co = verts[1] - vector


            if edge_length_debug:
                self.report({'INFO'},
                            '\n edge.verts[0].co' + str(verts[0]) +
                            '\n edge.verts[1].co' + str(verts[1]) +
                            '\n vector' + str(vector) + '\n v1 > v0:' + str((verts[1] >= verts[0]))
                            )
            bmesh.update_edit_mesh(obj.data, True)

        return {'FINISHED'}


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


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


if __name__ == "__main__":
    register()
