# ##### 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 #####

# ToDo and ideas list:
# Could test if an edge.is_boundary instead of testing the number of connected faces (this would not allow for floating wires, though)
# Should probably add a check to see if the final results has a length greater than 0.  If it doesn't, don't write an empty file.  Show a warning instead?
# Tried to sort the results but it didn't work as I expected.  I'm guessing python's sort function considers length to be prioritized above alphabetical.
# Instead of grabbing all meshes in the scene, only grab meshes that match the naming convention of cross-section_xxxx

bl_info = {
    "name": "Measure Tools",
    "author": "Christopher Kohl",
    "version": (0, 0, 3),
    "blender": (2, 80, 0),
    "location": "View3D > Properties",
    "description": "Track previous selection when new selection made",
    "wiki_url": "https://blenderartists.org/t/how-to-measure-the-sum-of-the-length-of-all-edges-of-an-object/1224932/2",
    "tracker_url": "",
    "category": ""
}

import bpy
import bmesh
import os
from bpy.types import Panel

classes = []

class KO_OT_Measure_Perimeters(bpy.types.Operator):
    bl_idname = "ko.measure_perimeters"
    bl_label = "Write perimeters to file"
    bl_description = "Measure perimeters of selected edge loops and write them to a text file"
    bl_options = {"UNDO"}

    @classmethod
    def poll(cls, context):
        return context.scene is not None

    def execute(self, context):
        print("---Beginning Measurement---")
        # Start in object mode.
        bpy.ops.object.mode_set(mode='OBJECT')
        prefs = context.preferences.addons[__name__].preferences
        
        # If preference is to use the same DIRECTORY of the .blend file.
        if prefs.use_blend_path:
            filepath = bpy.data.filepath
            path = os.path.dirname(filepath)
        # Otherwise get user supplied directory.
        elif not prefs.use_blend_path:
            if prefs.user_file_path != '': # Make sure the text field isn't empty
                get_path = bpy.path.abspath(prefs.user_file_path)
                path = os.path.dirname(get_path)
            else: 
                self.report({'ERROR_INVALID_INPUT'}, 'ERROR: Please choose a directory first.')
                return {'CANCELLED'}
        
        # If preference is to use the same FILE NAME of the .blend file.
        if prefs.use_blend_name:
            file = bpy.path.basename(bpy.context.blend_data.filepath)
            name = os.path.splitext(file)[0] + ".csv"
        # Otherwise get user supplied name.
        elif not prefs.use_blend_name:
            if prefs.user_file_name != '': # Make sure the text field isn't empty
                user_name = os.path.dirname(prefs.user_file_name)
                name = os.path.splitext(prefs.user_file_name)[0] + ".csv"
            else:
                self.report({'ERROR_INVALID_INPUT'}, 'ERROR: Please set a file name first.')
                return {'CANCELLED'}
        
        complete_filepath = os.path.join(path, name)
        
        # Must apply scale transform otherwise the measurement will be wrong.
        # However, you could probably calculate the scale delta(?) and adjust the numbers as needed
        # without forcibly applying scale (If you were smarter than me)
        bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)

        context = bpy.context
        scene_objects = [o for o in context.scene.objects if o.type == 'MESH' and o.name != prefs.scale_reference]
        
#        def sortfunc(o):
#            return o.name
        
        # Sort objects in ascending order for a nicer .csv output.
#        scene_objects.sort(key=sortfunc)
        
        result = ""
        possible_error_objects = []
        
        # Select everything.
        for obj in scene_objects:
                obj.select_set(True)
        
        # Make sure a mesh is the active object, otherwise we can't do the next step of entering the correct edit mode.
        context.view_layer.objects.active = scene_objects[0]
        
        # Measurement has to be done in edit mode.
        bpy.ops.object.mode_set(mode='EDIT')

        for obj in scene_objects:
            me = obj.data
            bm = bmesh.from_edit_mesh(me)
            object_edges = [e for e in bm.edges]
            possible_error = False
            
            perimeter_length = 0.0
            for e in object_edges:
                if len(e.link_faces) < 2:
                    # Measure length of e with calc_length
                    perimeter_length = perimeter_length + e.calc_length()
                elif len(e.link_faces) > 1:
                    possible_error = True
            if possible_error == True:
                possible_error_objects.append(obj)
                print("Error in " + str (obj.name) + "?")
            elif possible_error == False:
                print(str(obj.name) + " length is: " + str(perimeter_length))
                result = result + (str(obj.name) + "," + str(perimeter_length) + "\n")
        
        # Write result to file
        # Ensure all folders of the path exist. Make the directory if it doesn't exist.
        os.makedirs(path, exist_ok=True)

        # Write data out (THIS WILL OVERWRITE THE FILE IF IT ALREADY EXISTS)
        with open(complete_filepath, "w") as file:
            file.write(result)
            file.close()

        # Finally return back to object mode
        bpy.ops.object.mode_set(mode='OBJECT')
        
        if len(possible_error_objects) > 0:
            self.report({'WARNING'}, 'Connected faces detected in one or more objects.  Only works with stand-alone loops of edges or single unconnected faces and n-gons.')
            bpy.ops.object.select_all(action='DESELECT')
            for obj in possible_error_objects:
                obj.select_set(True)
            context.view_layer.objects.active = possible_error_objects[0]
            bpy.ops.view3d.localview()
        
        print("---Measurement Complete---")
        return {'FINISHED'}
classes.append(KO_OT_Measure_Perimeters)

class MeasureToolsPreferences(bpy.types.AddonPreferences):
    # this must match the addon name, use '__package__'
    # when defining this in a submodule of a python package.
    bl_idname = __name__

    use_blend_path: bpy.props.BoolProperty(
        name="Use .blend directory", 
        description="Output text file will be saved in the same directory as the .blend file",
        default=True)
    
    use_blend_name: bpy.props.BoolProperty(
        name="Use .blend file name", 
        description="Output .csv file will be named the same as the .blend file. e.g. results from 'filename.blend' will output to 'filename.csv'",
        default=True)
    
    user_file_path: bpy.props.StringProperty(
        name = "",
        description="Choose a directory to save the output file",
        default="",
        maxlen=1023,
        subtype='DIR_PATH')
    
    user_file_name: bpy.props.StringProperty(
        name = "",
        description="Choose a name for the output file.  IF FILE EXISTS IT WILL BE OVERWRITTEN, NOT APPENDED. (fragile; may not work with more than one period in a file name; may or may not work with special characters or accents)",
        default="",
        maxlen=1023,
        subtype='FILE_NAME')
    
    scale_reference: bpy.props.StringProperty(
        name = "",
        description="Name of scale reference object",
        default="SCALE 4.2x6.3",
        maxlen=1023,
        subtype='NONE')
    
    def draw(self, context):
        layout = self.layout
        layout.label(text="Name of scale reference:")
        layout.prop(self, "scale_reference")
        layout.prop(self, "use_blend_path")
        if not self.use_blend_path:
            layout.label(text="Set output directory:")
            layout.prop(self, "user_file_path")
        layout.prop(self, "use_blend_name")
        if not self.use_blend_name:
            layout.label(text="Set file name:")
            layout.prop(self, "user_file_name")
classes.append(MeasureToolsPreferences)

class PanelMeasureTools(Panel):
    bl_idname = "VIEW3D_PT_measure_tools"
    bl_label = "Measure Tools"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = 'Measure Tools'
    
    def draw(self, context):
        prefs = bpy.context.preferences.addons[__name__].preferences
        layout = self.layout
        layout.label(text="Name of scale reference:")
        layout.prop(prefs, "scale_reference")
        layout.prop(prefs, "use_blend_path")
        if not prefs.use_blend_path:
            layout.label(text="Set output directory:")
            layout.prop(prefs, "user_file_path")
        layout.prop(prefs, "use_blend_name")
        if not prefs.use_blend_name:
            layout.label(text="Set file name:")
            layout.prop(prefs, "user_file_name")
        layout.label(text="Measure Perimeters:")
        layout.operator("ko.measure_perimeters", text="Make it so")
classes.append(PanelMeasureTools)
        

def register():
    for every_class in classes:
        bpy.utils.register_class(every_class)

def unregister():
    for every_class in classes:
        bpy.utils.unregister_class(every_class)

if __name__ == "__main__":
    register()