# -*- coding:utf-8 -*-

# ##### 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>

# ----------------------------------------------------------
# Author: Stephen Leger (s-leger)
#
# ----------------------------------------------------------
import bpy
import bmesh


class BmeshEdit():
    @staticmethod
    def _start(context, o):
        """
            private, start bmesh editing of active object
        """
        o.select_set(state=True)
        context.view_layer.objects.active = o
        bpy.ops.object.mode_set(mode='EDIT')
        bm = bmesh.from_edit_mesh(o.data)
        bm.verts.ensure_lookup_table()
        bm.faces.ensure_lookup_table()
        return bm

    @staticmethod
    def bmesh_join(context, o, list_of_bmeshes, normal_update=False):
        """
            takes as input a list of bm references and outputs a single merged bmesh
            allows an additional 'normal_update=True' to force _normal_ calculations.
        """
        bm = BmeshEdit._start(context, o)

        add_vert = bm.verts.new
        add_face = bm.faces.new
        add_edge = bm.edges.new

        for bm_to_add in list_of_bmeshes:
            offset = len(bm.verts)

            for v in bm_to_add.verts:
                add_vert(v.co)

            bm.verts.index_update()
            bm.verts.ensure_lookup_table()

            if bm_to_add.faces:
                layer = bm_to_add.loops.layers.uv.verify()
                dest = bm.loops.layers.uv.verify()
                for face in bm_to_add.faces:
                    f = add_face(tuple(bm.verts[i.index + offset] for i in face.verts))
                    f.material_index = face.material_index
                    for j, loop in enumerate(face.loops):
                        f.loops[j][dest].uv = loop[layer].uv
                bm.faces.index_update()

            if bm_to_add.edges:
                for edge in bm_to_add.edges:
                    edge_seq = tuple(bm.verts[i.index + offset] for i in edge.verts)
                    try:
                        add_edge(edge_seq)
                    except ValueError:
                        # edge exists!
                        pass
                bm.edges.index_update()

        # cleanup
        for old_bm in list_of_bmeshes:
            old_bm.free()

        if normal_update:
            bm.normal_update()

        BmeshEdit._end(bm, o)

    @staticmethod
    def _end(bm, o):
        """
            private, end bmesh editing of active object
        """
        bm.normal_update()
        bmesh.update_edit_mesh(o.data, True)
        bpy.ops.object.mode_set(mode='OBJECT')
        bm.free()

    @staticmethod
    def _matids(bm, matids):
        for i, matid in enumerate(matids):
            bm.faces[i].material_index = matid

    @staticmethod
    def _uvs(bm, uvs):
        layer = bm.loops.layers.uv.verify()
        l_i = len(uvs)
        for i, face in enumerate(bm.faces):
            if i > l_i:
                raise RuntimeError("Missing uvs for face {}".format(i))
            l_j = len(uvs[i])
            for j, loop in enumerate(face.loops):
                if j > l_j:
                    raise RuntimeError("Missing uv {} for face {}".format(j, i))
                loop[layer].uv = uvs[i][j]

    @staticmethod
    def _verts(bm, verts):
        for i, v in enumerate(verts):
            bm.verts[i].co = v

    @staticmethod
    def buildmesh(context, o, verts, faces,
            matids=None, uvs=None, weld=False,
            clean=False, auto_smooth=True, temporary=False):

        if temporary:
            bm = bmesh.new()
        else:
            bm = BmeshEdit._start(context, o)
            bm.clear()

        for v in verts:
            bm.verts.new(v)
        bm.verts.index_update()
        bm.verts.ensure_lookup_table()

        for f in faces:
            bm.faces.new([bm.verts[i] for i in f])
        bm.faces.index_update()
        bm.faces.ensure_lookup_table()

        if matids is not None:
            BmeshEdit._matids(bm, matids)

        if uvs is not None:
            BmeshEdit._uvs(bm, uvs)

        if temporary:
            return bm

        if weld:
            bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
        BmeshEdit._end(bm, o)
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='SELECT')
        if auto_smooth:
            bpy.ops.mesh.faces_shade_smooth()
            o.data.use_auto_smooth = True
        else:
            bpy.ops.mesh.faces_shade_flat()
        if clean:
            bpy.ops.mesh.delete_loose()
        bpy.ops.object.mode_set(mode='OBJECT')

    @staticmethod
    def addmesh(context, o, verts, faces, matids=None, uvs=None, weld=False, clean=False, auto_smooth=True):
        bm = BmeshEdit._start(context, o)
        nv = len(bm.verts)
        nf = len(bm.faces)

        for v in verts:
            bm.verts.new(v)

        bm.verts.ensure_lookup_table()

        for f in faces:
            bm.faces.new([bm.verts[nv + i] for i in f])

        bm.faces.ensure_lookup_table()

        if matids is not None:
            for i, matid in enumerate(matids):
                bm.faces[nf + i].material_index = matid

        if uvs is not None:
            layer = bm.loops.layers.uv.verify()
            for i, face in enumerate(bm.faces[nf:]):
                for j, loop in enumerate(face.loops):
                    loop[layer].uv = uvs[i][j]

        if weld:
            bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
        BmeshEdit._end(bm, o)
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='SELECT')
        if auto_smooth:
            bpy.ops.mesh.faces_shade_smooth()
            o.data.use_auto_smooth = True
        else:
            bpy.ops.mesh.faces_shade_flat()
        if clean:
            bpy.ops.mesh.delete_loose()
        bpy.ops.object.mode_set(mode='OBJECT')

    @staticmethod
    def bevel(context, o,
            offset,
            offset_type='OFFSET',
            segments=1,
            profile=0.5,
            # vertex_only=False,
            clamp_overlap=True,
            material=-1,
            use_selection=True):
        """
        /* Bevel offset_type slot values */
        enum {
          BEVEL_AMT_OFFSET,
          BEVEL_AMT_WIDTH,
          BEVEL_AMT_DEPTH,
          BEVEL_AMT_PERCENT
        };
        """
        bm = bmesh.new()
        bm.from_mesh(o.data)
        bm.verts.ensure_lookup_table()
        if use_selection:
            geom = [v for v in bm.verts if v.select]
            geom.extend([ed for ed in bm.edges if ed.select])
        else:
            geom = bm.verts[:]
            geom.extend(bm.edges[:])

        bmesh.ops.bevel(bm,
            geom=geom,
            offset=offset,
            offset_type=offset_type,
            segments=segments,
            profile=profile,
            # vertex_only=vertex_only,
            clamp_overlap=clamp_overlap,
            material=material)

        bm.to_mesh(o.data)
        bm.free()

    @staticmethod
    def bissect(context, o,
            plane_co,
            plane_no,
            dist=0.001,
            use_snap_center=False,
            clear_outer=True,
            clear_inner=False
            ):

        bm = bmesh.new()
        bm.from_mesh(o.data)
        bm.verts.ensure_lookup_table()
        geom = bm.verts[:]
        geom.extend(bm.edges[:])
        geom.extend(bm.faces[:])

        bmesh.ops.bisect_plane(bm,
            geom=geom,
            dist=dist,
            plane_co=plane_co,
            plane_no=plane_no,
            use_snap_center=False,
            clear_outer=clear_outer,
            clear_inner=clear_inner
            )

        bm.to_mesh(o.data)
        bm.free()

    @staticmethod
    def solidify(context, o, amt, floor_bottom=False, altitude=0):
        bm = bmesh.new()
        bm.from_mesh(o.data)
        bm.verts.ensure_lookup_table()
        geom = bm.faces[:]
        bmesh.ops.solidify(bm, geom=geom, thickness=amt)
        if floor_bottom:
            for v in bm.verts:
                if not v.select:
                    v.co.z = altitude
        bm.to_mesh(o.data)
        bm.free()

    @staticmethod
    def verts(context, o, verts):
        """
            update vertex position of active object
        """
        bm = BmeshEdit._start(context, o)
        BmeshEdit._verts(bm, verts)
        BmeshEdit._end(bm, o)

    @staticmethod
    def aspect(context, o, matids, uvs):
        """
            update material id and uvmap of active object
        """
        bm = BmeshEdit._start(context, o)
        BmeshEdit._matids(bm, matids)
        BmeshEdit._uvs(bm, uvs)
        BmeshEdit._end(bm, o)
