# ##### 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; version 2
#  of the License.
#
#  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 #####

# based upon the functionality of Mesh to wall by luxuy_BlenderCN
# thanks to meta-androcto

bl_info = {
    "name": "Edge Floor Plan",
    "author": "lijenstina",
    "version": (0, 2),
    "blender": (2, 78, 0),
    "location": "View3D > EditMode > Mesh",
    "description": "Make a Floor Plan from Edges",
    "doc_url": "",
    "category": "Mesh",
}

import bpy
import bmesh
from bpy.types import Operator
from bpy.props import (
        BoolProperty,
        EnumProperty,
        FloatProperty,
        FloatVectorProperty,
        IntProperty,
        )


# Handle error notifications
def error_handlers(self, error, reports="ERROR"):
    if self and reports:
        self.report({'WARNING'}, reports + " (See Console for more info)")

    print("\n[mesh.edges_floor_plan]\nError: {}\n".format(error))


class MESH_OT_edges_floor_plan(Operator):
    bl_idname = "mesh.edges_floor_plan"
    bl_label = "Edges Floor Plan"
    bl_description = "Top View, Extrude Flat Along Edges"
    bl_options = {'REGISTER', 'UNDO'}

    wid: FloatProperty(
            name="Wall width:",
            description="Set the width of the generated walls\n",
            default=0.1,
            min=0.001, max=30000
            )
    depth: FloatProperty(
            name="Inner height:",
            description="Set the height of the inner wall edges",
            default=0.0,
            min=0, max=10
            )
    connect_ends: BoolProperty(
            name="Connect Ends",
            description="Connect the ends of the boundary Edge loops",
            default=False
            )
    repeat_cleanup: IntProperty(
            name="Recursive Prepare",
            description="Number of times that the preparation phase runs\n"
                        "at the start of the script\n"
                        "If parts of the mesh are not modified, increase this value",
            min=1, max=20,
            default=1
            )
    fill_items = [
            ('EDGE_NET', "Edge Net",
             "Edge Net Method for mesh preparation - Initial Fill\n"
             "The filled in faces will be Inset individually\n"
             "Supports simple 3D objects"),
            ('SINGLE_FACE', "Single Face",
             "Single Face Method for mesh preparation - Initial Fill\n"
             "The produced face will be Triangulated before Inset Region\n"
             "Good for edges forming a circle, avoid 3D objects"),
            ('SOLIDIFY', "Solidify",
             "Extrude and Solidify Method\n"
             "Useful for complex meshes, however works best on flat surfaces\n"
             "as the extrude direction has to be defined")
            ]
    fill_type: EnumProperty(
            name="Fill Type",
            items=fill_items,
            description="Choose the method for creating geometry",
            default='SOLIDIFY'
            )
    keep_faces: BoolProperty(
            name="Keep Faces",
            description="Keep or not the fill faces\n"
                        "Can depend on Remove Ngons state",
            default=False
            )
    tri_faces: BoolProperty(
            name="Triangulate Faces",
            description="Triangulate the created fill faces\n"
                        "Sometimes can lead to unsatisfactory results",
            default=False
            )
    initial_extrude: FloatVectorProperty(
            name="Initial Extrude",
            description="",
            default=(0.0, 0.0, 0.1),
            min=-20.0, max=20.0,
            subtype='XYZ',
            precision=3,
            size=3
            )
    remove_ngons: BoolProperty(
            name="Remove Ngons",
            description="Keep or not the Ngon Faces\n"
                        "Note about limitations:\n"
                        "Sometimes the kept Faces could be Ngons\n"
                        "Removing the Ngons can lead to no geometry created",
            default=True
            )
    offset: FloatProperty(
            name="Wall Offset:",
            description="Set the offset for the Solidify modifier",
            default=0.0,
            min=-1.0, max=1.0
            )
    only_rim: BoolProperty(
            name="Rim Only",
            description="Solidify Fill Rim only option",
            default=False
            )

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

    def check_edge(self, context):
        bpy.ops.object.mode_set(mode='OBJECT')
        bpy.ops.object.mode_set(mode='EDIT')
        obj = bpy.context.object
        me_check = obj.data
        if len(me_check.edges) < 1:
            return False

        return True

    @staticmethod
    def ensure(bm):
        if bm:
            bm.verts.ensure_lookup_table()
            bm.edges.ensure_lookup_table()
            bm.faces.ensure_lookup_table()

    def solidify_mod(self, context, ob, wid, offset, only_rim):
        try:
            mods = ob.modifiers.new(
                        name="_Mesh_Solidify_Wall", type='SOLIDIFY'
                        )
            mods.thickness = wid
            mods.use_quality_normals = True
            mods.offset = offset
            mods.use_even_offset = True
            mods.use_rim = True
            mods.use_rim_only = only_rim
            mods.show_on_cage = True

            bpy.ops.object.modifier_apply(
                        modifier="_Mesh_Solidify_Wall"
                        )
        except Exception as e:
            error_handlers(self, e,
                           reports="Adding a Solidify Modifier failed")
            pass

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

        box = layout.box()
        box.label(text="Choose Method:", icon="NONE")
        box.prop(self, "fill_type")

        col = box.column(align=True)

        if self.fill_type == 'EDGE_NET':
            col.prop(self, "repeat_cleanup")
            col.prop(self, "remove_ngons", toggle=True)

        elif self.fill_type == 'SOLIDIFY':
            col.prop(self, "offset", slider=True)
            col.prop(self, "initial_extrude")

        else:
            col.prop(self, "remove_ngons", toggle=True)
            col.prop(self, "tri_faces", toggle=True)

        box = layout.box()
        box.label(text="Settings:", icon="NONE")

        col = box.column(align=True)
        col.prop(self, "wid")

        if self.fill_type != 'SOLIDIFY':
            col.prop(self, "depth")
            col.prop(self, "connect_ends", toggle=True)
            col.prop(self, "keep_faces", toggle=True)
        else:
            col.prop(self, "only_rim", toggle=True)

    def execute(self, context):
        if not self.check_edge(context):
            self.report({'WARNING'},
                        "Operation Cancelled. Needs a Mesh with at least one edge")
            return {'CANCELLED'}

        wid = self.wid * 0.1
        depth = self.depth * 0.1
        offset = self.offset * 0.1
        store_selection_mode = context.tool_settings.mesh_select_mode
        # Note: the remove_doubles called after bmesh creation would make
        # blender crash with certain meshes - keep it in mind for the future
        bpy.ops.mesh.remove_doubles(threshold=0.003)
        bpy.ops.object.mode_set(mode='OBJECT')
        bpy.ops.object.mode_set(mode='EDIT')
        ob = bpy.context.object

        me = ob.data
        bm = bmesh.from_edit_mesh(me)

        bmesh.ops.delete(bm, geom=bm.faces, context='FACES_ONLY')
        self.ensure(bm)
        context.tool_settings.mesh_select_mode = (False, True, False)
        original_edges = [edge.index for edge in bm.edges]
        original_verts = [vert.index for vert in bm.verts]
        self.ensure(bm)
        bpy.ops.mesh.select_all(action='DESELECT')

        if self.fill_type == 'EDGE_NET':
            for i in range(self.repeat_cleanup):
                bmesh.ops.edgenet_prepare(bm, edges=bm.edges)
                self.ensure(bm)
            bmesh.ops.edgenet_fill(bm, edges=bm.edges, mat_nr=0, use_smooth=True, sides=0)
            self.ensure(bm)
            if self.remove_ngons:
                ngons = [face for face in bm.faces if len(face.edges) > 4]
                self.ensure(bm)
                bmesh.ops.delete(bm, geom=ngons, context='FACES')  # 5 - delete faces
                del ngons
                self.ensure(bm)

        elif self.fill_type == 'SOLIDIFY':
            for vert in bm.verts:
                vert.normal_update()
            self.ensure(bm)
            bmesh.ops.extrude_edge_only(
                    bm, edges=bm.edges, use_select_history=False
                    )
            self.ensure(bm)
            verts_extrude = [vert for vert in bm.verts if vert.index in original_verts]
            self.ensure(bm)
            bmesh.ops.translate(
                bm,
                verts=verts_extrude,
                vec=(self.initial_extrude)
                )
            self.ensure(bm)
            del verts_extrude
            self.ensure(bm)

            for edge in bm.edges:
                if edge.is_boundary:
                    edge.select = True

            bm = bmesh.update_edit_mesh(ob.data, loop_triangles=True, destructive=True)

            bpy.ops.object.mode_set(mode='OBJECT')
            self.solidify_mod(context, ob, wid, offset, self.only_rim)

            bpy.ops.object.mode_set(mode='EDIT')

            context.tool_settings.mesh_select_mode = store_selection_mode

            return {'FINISHED'}

        else:
            bm.faces.new(bm.verts)
            self.ensure(bm)

            if self.tri_faces:
                bmesh.ops.triangle_fill(
                        bm, use_beauty=True, use_dissolve=False, edges=bm.edges
                        )
                self.ensure(bm)

        if self.remove_ngons and self.fill_type != 'EDGE_NET':
            ngons = [face for face in bm.faces if len(face.edges) > 4]
            self.ensure(bm)
            bmesh.ops.delete(bm, geom=ngons, context='FACES')  # 5 - delete faces
            del ngons
            self.ensure(bm)

        del_boundary = [edge for edge in bm.edges if edge.index not in original_edges]
        self.ensure(bm)

        del original_edges
        self.ensure(bm)

        if self.fill_type == 'EDGE_NET':
            extrude_inner = bmesh.ops.inset_individual(
                    bm, faces=bm.faces, thickness=wid, depth=depth,
                    use_even_offset=True, use_interpolate=False,
                    use_relative_offset=False
                    )
        else:
            extrude_inner = bmesh.ops.inset_region(
                    bm, faces=bm.faces, faces_exclude=[], use_boundary=True,
                    use_even_offset=True, use_interpolate=False,
                    use_relative_offset=False, use_edge_rail=False,
                    thickness=wid, depth=depth, use_outset=False
                    )
        self.ensure(bm)

        del_faces = [faces for faces in bm.faces if faces not in extrude_inner["faces"]]
        self.ensure(bm)
        del extrude_inner
        self.ensure(bm)

        if not self.keep_faces:
            bmesh.ops.delete(bm, geom=del_faces, context='FACES')  # 5 delete faces
        del del_faces
        self.ensure(bm)

        face_del = set()
        for face in bm.faces:
            for edge in del_boundary:
                if isinstance(edge, bmesh.types.BMEdge):
                    if edge in face.edges:
                        face_del.add(face)
        self.ensure(bm)
        face_del = list(face_del)
        self.ensure(bm)

        del del_boundary
        self.ensure(bm)

        if not self.connect_ends:
            bmesh.ops.delete(bm, geom=face_del, context='FACES')
            self.ensure(bm)

        del face_del
        self.ensure(bm)

        for edge in bm.edges:
            if edge.is_boundary:
                edge.select = True

        bm = bmesh.update_edit_mesh(ob.data, loop_triangles=True, destructive=True)

        context.tool_settings.mesh_select_mode = store_selection_mode

        return {'FINISHED'}


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


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


if __name__ == "__main__":
    register()