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

bl_info = {
    "name": "Add Wetted Mesh",
    "author": "freejack",
    "version": (0, 2, 1),
    "blender": (2, 58, 0),
    "location": "View3D > Tool Shelf > Wetted Mesh Panel",
    "description": "Adds separated fluid, dry and wetted mesh for selected pair.",
    "warning": "",
    "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
        "Scripts/Mesh/Wetted_Mesh",
    "tracker_url": "https://developer.blender.org/T27156",
    "category": "Mesh"}


import bpy
import collections
import math

### Tool Panel ###
class VIEW3D_PT_tools_WettedMesh(bpy.types.Panel):
    """Wetted Mesh Tool Panel"""
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    bl_label = 'Wetted Mesh'
    bl_context = 'objectmode'
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)
        slcnt = len(context.selected_objects)

        if slcnt != 2:
            col.label(text = 'Select two mesh objects')
            col.label(text = 'to generate separated')
            col.label(text = 'fluid, dry and wetted')
            col.label(text = 'meshes.')
        else:
            (solid, fluid) = getSelectedPair(context)
            col.label(text = 'solid = '+solid.name)
            col.label(text = 'fluid = '+fluid.name)
            col.operator('mesh.primitive_wetted_mesh_add', text='Generate Meshes')

### Operator ###
class AddWettedMesh(bpy.types.Operator):
    """Add wetted mesh for selected mesh pair"""
    bl_idname = "mesh.primitive_wetted_mesh_add"
    bl_label = "Add Wetted Mesh"
    bl_options = {'REGISTER', 'UNDO'}
    statusMessage = ''

    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)
        col.label(text = self.statusMessage)

    def execute(self, context):
        # make sure a pair of objects is selected
        if len(context.selected_objects) != 2:
            # should not happen if called from tool panel
            self.report({'WARNING'}, "no mesh pair selected, operation cancelled")
            return {'CANCELLED'}

        print("add_wetted_mesh begin")

        # super-selected object is solid, other object is fluid
        (solid, fluid) = getSelectedPair(context)
        print("   solid = "+solid.name)
        print("   fluid = "+fluid.name)

        # make a copy of fluid object, convert to mesh if required
        print("   copy fluid")
        bpy.ops.object.select_all(action='DESELECT')
        fluid.select = True
        context.scene.objects.active = fluid
        bpy.ops.object.duplicate()
        bpy.ops.object.convert(target='MESH', keep_original=False)
        bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
        fluidCopy = context.object

        # substract solid from fluidCopy
        print("   bool: fluidCopy DIFFERENCE solid")
        bpy.ops.object.modifier_add(type='BOOLEAN')
        bop = fluidCopy.modifiers.items()[0]
        bop[1].operation = 'DIFFERENCE'
        bop[1].object = solid
        bpy.ops.object.modifier_apply(apply_as='DATA', modifier=bop[0])
        fluidMinusSolid = fluidCopy
        fluidMinusSolid.name = "fluidMinusSolid"

        # make a second copy of fluid object
        print("   copy fluid")
        bpy.ops.object.select_all(action='DESELECT')
        fluid.select = True
        context.scene.objects.active = fluid
        bpy.ops.object.duplicate()
        bpy.ops.object.convert(target='MESH', keep_original=False)
        bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
        fluidCopy = context.object

        # make union from fluidCopy and solid
        print("   bool: fluidCopy UNION solid")
        bpy.ops.object.modifier_add(type='BOOLEAN')
        bop = fluidCopy.modifiers.items()[0]
        bop[1].operation = 'UNION'
        bop[1].object = solid
        bpy.ops.object.modifier_apply(apply_as='DATA', modifier=bop[0])
        fluidUnionSolid = fluidCopy
        fluidUnionSolid.name = "fluidUnionSolid"

        # index meshes
        print("   KDTree index fluidMinusSolid")
        fluidMinusSolidKDT = KDTree(3, fluidMinusSolid.data.vertices)
        print("   KDTree index fluidUnionSolid")
        fluidUnionSolidKDT = KDTree(3, fluidUnionSolid.data.vertices)
        kdtrees = (fluidMinusSolidKDT, fluidUnionSolidKDT)

        # build mesh face sets
        faceDict = { }
        vertDict = { }

        print("   processing fluidMinusSolid faces")
        cacheDict = { }
        setFMSfaces = set()
        numFaces = len(fluidUnionSolid.data.faces)
        i = 0
        for f in fluidMinusSolid.data.faces:
            if i % 500 == 0:
                print("      ", i, " / ", numFaces)
            i += 1
            fuid = unifiedFaceId(kdtrees, f, fluidMinusSolid.data.vertices, \
                                 faceDict, vertDict, cacheDict)
            setFMSfaces.add(fuid)

        print("   processing fluidUnionSolid faces")
        cacheDict = { }
        setFUSfaces = set()
        numFaces = len(fluidUnionSolid.data.faces)
        i = 0
        for f in fluidUnionSolid.data.faces:
            if i % 500 == 0:
                print("      ", i, " / ", numFaces)
            i += 1
            fuid = unifiedFaceId(kdtrees, f, fluidUnionSolid.data.vertices, \
                                 faceDict, vertDict, cacheDict)
            setFUSfaces.add(fuid)

        # remove boolean helpers
        print("   delete helper objects")
        bpy.ops.object.select_all(action='DESELECT')
        fluidUnionSolid.select = True
        fluidMinusSolid.select = True
        bpy.ops.object.delete()

        # wetted = FMS - FUS
        print("   set operation FMS diff FUS")
        setWetFaces = setFMSfaces.difference(setFUSfaces)
        print("   build wetted mesh")
        verts, faces = buildMesh(setWetFaces, faceDict, vertDict)
        print("   create wetted mesh")
        wetted = createMesh("Wetted", verts, faces)

        # fluid = FMS x FUS
        print("   set operation FMS intersect FUS")
        setFluidFaces = setFMSfaces.intersection(setFUSfaces)
        print("   build fluid mesh")
        verts, faces = buildMesh(setFluidFaces, faceDict, vertDict)
        print("   create fluid mesh")
        fluid = createMesh("Fluid", verts, faces)

        # solid = FUS - FMS
        print("   set operation FUS diff FMS")
        setSolidFaces = setFUSfaces.difference(setFMSfaces)
        print("   build solid mesh")
        verts, faces = buildMesh(setSolidFaces, faceDict, vertDict)
        print("   create solid mesh")
        solid = createMesh("Solid", verts, faces)

        # parent wetted mesh
        print("   parent mesh")
        bpy.ops.object.add(type='EMPTY')
        wettedMesh = context.object
        solid.select = True
        fluid.select = True
        wetted.select = True
        wettedMesh.select = True
        bpy.ops.object.parent_set(type='OBJECT')
        wettedMesh.name = 'WettedMesh'

        print("add_wetted_mesh done")
        self.statusMessage = 'created '+wettedMesh.name

        return {'FINISHED'}


### Registration ###
def register():
    bpy.utils.register_class(VIEW3D_PT_tools_WettedMesh)
    bpy.utils.register_class(AddWettedMesh)


def unregister():
    bpy.utils.unregister_class(VIEW3D_PT_tools_WettedMesh)
    bpy.utils.unregister_class(AddWettedMesh)

if __name__ == "__main__":
    register()


#
# KD tree (used to create a geometric index of mesh vertices)
#

def distance(a, b):
    return (a-b).length

Node = collections.namedtuple("Node", 'point axis label left right')

class KDTree(object):
    """A tree for nearest neighbor search in a k-dimensional space.

    For information about the implementation, see
    http://en.wikipedia.org/wiki/Kd-tree

    Usage:
    objects is an iterable of (co, index) tuples (so MeshVertex is useable)
    k is the number of dimensions (=3)

    t = KDTree(k, objects)
    point, label, distance = t.nearest_neighbor(destination)
    """

    def __init__(self, k, objects=[]):

        def build_tree(objects, axis=0):

            if not objects:
                return None

            objects.sort(key=lambda o: o.co[axis])
            median_idx = len(objects) // 2
            median_point = objects[median_idx].co
            median_label = objects[median_idx].index

            next_axis = (axis + 1) % k
            return Node(median_point, axis, median_label,
                        build_tree(objects[:median_idx], next_axis),
                        build_tree(objects[median_idx + 1:], next_axis))

        self.root = build_tree(list(objects))
        self.size = len(objects)


    def nearest_neighbor(self, destination):

        best = [None, None, float('inf')]
        # state of search: best point found, its label,
        # lowest distance

        def recursive_search(here):

            if here is None:
                return
            point, axis, label, left, right = here

            here_sd = distance(point, destination)
            if here_sd < best[2]:
                best[:] = point, label, here_sd

            diff = destination[axis] - point[axis]
            close, away = (left, right) if diff <= 0 else (right, left)

            recursive_search(close)
            if math.fabs(diff) < best[2]:
                recursive_search(away)

        recursive_search(self.root)
        return best[0], best[1], best[2]


#
# helper functions
#

# get super-selected object and other object from selected pair
def getSelectedPair(context):
    objA = context.object
    objB = context.selected_objects[0]
    if objA == objB:
        objB = context.selected_objects[1]
    return (objA, objB)

# get a unified vertex id for given coordinates
def unifiedVertexId(kdtrees, location, vertDict):
    eps = 0.0001
    offset = 0
    for t in kdtrees:
        co, index, d = t.nearest_neighbor(location)
        if d < eps:
            uvid = offset + index
            if uvid not in vertDict:
                vertDict[uvid] = co
            return uvid
        offset += t.size
    return -1

# get a unified face id tuple
#    Stores the ordered face id tuple in faceDict
#    and the used coordinates for vertex id in vertDict.
#    cacheDict caches the unified vertex id (lookup in kdtree is expensive).
#    For each mesh (where the face belongs to) a separate cacheDict is expected.
def unifiedFaceId(kdtrees, face, vertices, faceDict, vertDict, cacheDict):
    fids = [ ]
    for v in face.vertices:
        uvid = cacheDict.get(v)
        if uvid == None:
            uvid = unifiedVertexId(kdtrees, vertices[v].co, vertDict)
            cacheDict[v] = uvid
        fids.append(uvid)
    ofids = tuple(fids)
    fids.sort()
    fuid = tuple(fids)
    if fuid not in faceDict:
        faceDict[fuid] = ofids
    return fuid

# build vertex and face array from unified face sets
def buildMesh(unifiedFaceSet, faceDict, vertDict):
    verts = [ ]
    nextV = 0
    myV = { }
    faces = [ ]
    for uf in unifiedFaceSet:
        of = faceDict[uf]
        myf = [ ]
        for uV in of:
            v = myV.get(uV)
            if v == None:
                v = nextV
                myV[uV] = nextV
                verts.append(vertDict[uV])
                nextV += 1
            myf.append(v)
        faces.append(myf)
    return verts, faces

# create mesh object and link to scene
def createMesh(name, verts, faces):
    me = bpy.data.meshes.new(name+"Mesh")
    ob = bpy.data.objects.new(name, me)
    ob.show_name = True
    bpy.context.scene.objects.link(ob)
    me.from_pydata(verts, [], faces)
    me.update(calc_edges=True)
    return ob
