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

# Generic helper functions, to be used by any modules.

import bmesh
import array


def bmesh_copy_from_object(obj, transform=True, triangulate=True, apply_modifiers=False):
    """
    Returns a transformed, triangulated copy of the mesh
    """

    assert(obj.type == 'MESH')

    if apply_modifiers and obj.modifiers:
        import bpy
        me = obj.to_mesh(bpy.context.scene, True, 'PREVIEW', calc_tessface=False)
        bm = bmesh.new()
        bm.from_mesh(me)
        bpy.data.meshes.remove(me)
        del bpy
    else:
        me = obj.data
        if obj.mode == 'EDIT':
            bm_orig = bmesh.from_edit_mesh(me)
            bm = bm_orig.copy()
        else:
            bm = bmesh.new()
            bm.from_mesh(me)

    # TODO. remove all customdata layers.
    # would save ram

    if transform:
        bm.transform(obj.matrix_world)

    if triangulate:
        bmesh.ops.triangulate(bm, faces=bm.faces, use_beauty=True)

    return bm


def bmesh_from_object(obj):
    """
    Object/Edit Mode get mesh, use bmesh_to_object() to write back.
    """
    me = obj.data
    is_editmode = (obj.mode == 'EDIT')
    if is_editmode:
        bm = bmesh.from_edit_mesh(me)
    else:
        bm = bmesh.new()
        bm.from_mesh(me)
    return bm


def bmesh_to_object(obj, bm):
    """
    Object/Edit Mode update the object.
    """
    me = obj.data
    is_editmode = (obj.mode == 'EDIT')
    if is_editmode:
        bmesh.update_edit_mesh(me, True)
    else:
        bm.to_mesh(me)
    # grr... cause an update
    if me.vertices:
        me.vertices[0].co[0] = me.vertices[0].co[0]


def bmesh_calc_volume(bm):
    """
    Calculate the volume of a triangulated bmesh.
    """
    def tri_signed_volume(p1, p2, p3):
        return p1.dot(p2.cross(p3)) / 6.0
    return abs(sum((tri_signed_volume(*(v.co for v in f.verts))
                    for f in bm.faces)))


def bmesh_calc_volume_signed(bm):
    """
    Calculate the volume of a triangulated bmesh.
    """
    def tri_signed_volume(p1, p2, p3):
        return p1.dot(p2.cross(p3)) / 6.0
    return sum((tri_signed_volume(*(v.co for v in f.verts))
                for f in bm.faces))


def bmesh_calc_area(bm):
    """
    Calculate the surface area.
    """
    return sum(f.calc_area() for f in bm.faces)


def bmesh_check_self_intersect_object(obj):
    """
    Check if any faces self intersect

    returns an array of edge index values.
    """
    import bpy

    # Heres what we do!
    #
    # * Take original Mesh.
    # * Copy it and triangulate it (keeping list of original edge index values)
    # * Move the BMesh into a temp Mesh.
    # * Make a temp Object in the scene and assign the temp Mesh.
    # * For every original edge - ray-cast on the object to find which intersect.
    # * Report all edge intersections.

    # Triangulate
    bm = bmesh_copy_from_object(obj, transform=False, triangulate=False)
    face_map_index_org = {f: i for i, f in enumerate(bm.faces)}
    ret = bmesh.ops.triangulate(bm, faces=bm.faces, use_beauty=False)
    face_map = ret["face_map"]
    # map new index to original index
    face_map_index = {i: face_map_index_org[face_map.get(f, f)] for i, f in enumerate(bm.faces)}
    del face_map_index_org
    del ret

    # Create a real mesh (lame!)
    scene = bpy.context.scene
    me_tmp = bpy.data.meshes.new(name="~temp~")
    bm.to_mesh(me_tmp)
    bm.free()
    obj_tmp = bpy.data.objects.new(name=me_tmp.name, object_data=me_tmp)
    scene.objects.link(obj_tmp)
    scene.update()
    ray_cast = obj_tmp.ray_cast

    faces_error = set()

    EPS_NORMAL = 0.000001
    EPS_CENTER = 0.01  # should always be bigger

    for ed in me_tmp.edges:
        v1i, v2i = ed.vertices
        v1 = me_tmp.vertices[v1i]
        v2 = me_tmp.vertices[v2i]

        # setup the edge with an offset
        co_1 = v1.co.copy()
        co_2 = v2.co.copy()
        co_mid = (co_1 + co_2) * 0.5
        no_mid = (v1.normal + v2.normal).normalized() * EPS_NORMAL
        co_1 = co_1.lerp(co_mid, EPS_CENTER) + no_mid
        co_2 = co_2.lerp(co_mid, EPS_CENTER) + no_mid

        co, no, index = ray_cast(co_1, co_2)
        if index != -1:
            faces_error.add(face_map_index[index])

    scene.objects.unlink(obj_tmp)
    bpy.data.objects.remove(obj_tmp)
    bpy.data.meshes.remove(me_tmp)

    scene.update()

    return array.array('i', faces_error)


def bmesh_face_points_random(f, num_points=1, margin=0.05):
    import random
    from random import uniform
    uniform_args = 0.0 + margin, 1.0 - margin

    # for pradictable results
    random.seed(f.index)

    vecs = [v.co for v in f.verts]

    for i in range(num_points):
        u1 = uniform(*uniform_args)
        u2 = uniform(*uniform_args)
        u_tot = u1 + u2

        if u_tot > 1.0:
            u1 = 1.0 - u1
            u2 = 1.0 - u2

        side1 = vecs[1] - vecs[0]
        side2 = vecs[2] - vecs[0]

        yield vecs[0] + u1 * side1 + u2 * side2


def bmesh_check_thick_object(obj, thickness):

    import bpy

    # Triangulate
    bm = bmesh_copy_from_object(obj, transform=True, triangulate=False)
    # map original faces to their index.
    face_index_map_org = {f: i for i, f in enumerate(bm.faces)}
    ret = bmesh.ops.triangulate(bm, faces=bm.faces, use_beauty=False)
    face_map = ret["face_map"]
    del ret
    # old edge -> new mapping

    # Convert new/old map to index dict.

    # Create a real mesh (lame!)
    scene = bpy.context.scene
    me_tmp = bpy.data.meshes.new(name="~temp~")
    bm.to_mesh(me_tmp)
    # bm.free()  # delay free
    obj_tmp = bpy.data.objects.new(name=me_tmp.name, object_data=me_tmp)
    scene.objects.link(obj_tmp)
    scene.update()
    ray_cast = obj_tmp.ray_cast

    EPS_BIAS = 0.0001

    faces_error = set()

    bm_faces_new = bm.faces[:]

    for f in bm_faces_new:
        no = f.normal
        no_sta = no * EPS_BIAS
        no_end = no * thickness
        for p in bmesh_face_points_random(f, num_points=6):
            # Cast the ray backwards
            p_a = p - no_sta
            p_b = p - no_end

            co, no, index = ray_cast(p_a, p_b)

            if index != -1:
                # Add the face we hit
                for f_iter in (f, bm_faces_new[index]):
                    # if the face wasn't triangulated, just use existing
                    f_org = face_map.get(f_iter, f_iter)
                    f_org_index = face_index_map_org[f_org]
                    faces_error.add(f_org_index)

    # finished with bm
    bm.free()

    scene.objects.unlink(obj_tmp)
    bpy.data.objects.remove(obj_tmp)
    bpy.data.meshes.remove(me_tmp)

    scene.update()

    return array.array('i', faces_error)



def object_merge(context, objects):
    """
    Caller must remove.
    """

    import bpy

    def cd_remove_all_but_active(seq):
        tot = len(seq)
        if tot > 1:
            act = seq.active_index
            for i in range(tot - 1, -1, -1):
                if i != act:
                    seq.remove(seq[i])

    scene = context.scene

    # deselect all
    for obj in scene.objects:
        obj.select = False

    # add empty object
    mesh_base = bpy.data.meshes.new(name="~tmp~")
    obj_base = bpy.data.objects.new(name="~tmp~", object_data=mesh_base)
    base_base = scene.objects.link(obj_base)
    scene.objects.active = obj_base
    obj_base.select = True

    # loop over all meshes
    for obj in objects:
        if obj.type != 'MESH':
            continue

        # convert each to a mesh
        mesh_new = obj.to_mesh(scene=scene,
                               apply_modifiers=True,
                               settings='PREVIEW',
                               calc_tessface=False)

        # remove non-active uvs/vcols
        cd_remove_all_but_active(mesh_new.vertex_colors)
        cd_remove_all_but_active(mesh_new.uv_textures)

        # join into base mesh
        obj_new = bpy.data.objects.new(name="~tmp-new~", object_data=mesh_new)
        base_new = scene.objects.link(obj_new)
        obj_new.matrix_world = obj.matrix_world

        fake_context = context.copy()
        fake_context["active_object"] = obj_base
        fake_context["selected_editable_bases"] = [base_base, base_new]

        bpy.ops.object.join(fake_context)
        del base_new, obj_new

        # remove object and its mesh, join does this
        #~ scene.objects.unlink(obj_new)
        #~ bpy.data.objects.remove(obj_new)

        bpy.data.meshes.remove(mesh_new)

    scene.update()

    # return new object
    return base_base