# -*- indent-tabs-mode: t -*-
# $Id: bcdata.py 426 2006-10-03 15:04:31Z cubicool $

# The purpose of this module is to provide methods by which the caller can easily
# create sequences of bcobject instances for their own use. These don't necessarily
# have to be used in a way specific to Cal3D.

# TODO: I'm using too many list traversals here. It could probably be condensed
# down into "one interation to rule them all."

import Blender
import blendercal
import bcobject
import bcconf
import os

def __yieldBlenderObj(t, objlist=None):
	# This function yields the Blender Object and the result of getData() on
	# that Object for each instance that is a type of t. Futher, you can specify
	# a list of objects to traverse rather than Blender.Object.Get().
	
	for obj in objlist or Blender.Object.Get():
		# Ignore anything that begins with an underscore as a kind
		# of "agreed-upon" technique.
		if obj.getType() == t and not obj.getName().startswith("_"):
			yield obj, obj.getData()

def SkeletonData():
	# This function returns a single blendercal.bcobject.Skeleton instance
	# and sets blendercal.bcobject.Skeleton.ARMATURE to the appropriate Blender
	# object. For the time being we ony support a single skeleton (although, this
	# could be changed later), so it only makes sense to return a single Skeleton.
	# The ARMATURE variable is set for use later in retrieving the animations
	# tied to the Armature.

	skeleton = bcobject.Skeleton()

	# A recursive function that operates on a single root bone and creates
	# bcobject.Bone instances appropriately. It might be possible to totally
	# get rid of this function and just use bone.getAllChildren() instead.
	def __recurseBone(bone, matrix, parent=None):
		cbone = bcobject.Bone(skeleton, parent, bone, matrix)

		bone.hasChildren() and [__recurseBone(c, matrix, cbone) for c in bone.children]

	for obj, data in __yieldBlenderObj("Armature"):
		rootbones = [b for b in data.bones.values() if not b.hasParent()]

		for rootbone in rootbones: __recurseBone(rootbone, obj.getMatrix())
		
		# Set the ARMATURE variable for use later in Animations.
		bcobject.Skeleton.ARMATURE = obj
	
	return skeleton

SkeletonData = blendercal.exception(SkeletonData)

#@blendercal.exception
def MeshData():
	# This function returns a list of blender.bcobject.Mesh objects, one for
	# each mesh in your Blender scene. The Cal3D notion of a Mesh is actually more
	# like a container whose purpose it is to hold other SubMeshes, which are
	# what hold the vertex, weight, and material data. As it is now, there is always
	# one Mesh+SubMesh combo per item in the list, though it would also be possible
	# to have all meshes in the scene be SubMeshes of a single Mesh returned
	# by this function.

	meshes = []

	# This class serves as a kind of "temporary" vertex class that we will use
	# to create both the vertex list and the index list for later iteration. The
	# only reason we have to do this is due to the fact that Blender doesn't
	# pre-sanitize vertex data such that each vertex is TRULY unique, i.e. in 
	# location, normal, and UV coords.
	#
	# TODO: There are special methods (__hash__, __cmp__, and __eq__) we could
	# use instead for implicit comparison rather than having to call MakeKey()
	# directly. I'll need to research that a bit...
	class HashableVertex(object):
		def __init__(self, index, co, no, uvx, uvy):
			object.__init__(self)

			self.index = index
			self.co    = co
			self.no    = no
			self.uvco  = uvx, uvy

		# We will use the return value of this function as keys in a dictionary
		# to make sure we only have one occurance of each unique vertex.
		def MakeKey(self):
			return self.index, self.uvco[0], self.uvco[1]

	# A function to split our faces into triangles and convert the vertices into
	# our HashableVert class.
	def __triFaces(f, hasUV):
		nulluv = lambda n: [(None, None)] * n
	
		if len(f) == 4:
			verts = f.v[ : 3] + f.v[2 : ] + [f.v[0]]
			uvs   = hasUV and f.uv[ : 3] + f.uv[2 : ] + [f.uv[0]] or nulluv(6)
			
		else:
			verts = f.v
			uvs   = hasUV and f.uv or nulluv(3)

		if hasUV: uvs = [(faceuv[0], 1.0 - faceuv[1]) for faceuv in uvs]
		
		return [HashableVertex(
			facevert.index,
			facevert.co,
			facevert.no,
			faceuv[0],
			faceuv[1]
		) for facevert, faceuv in zip(verts, uvs)]
	
	# This will return two lists: the first is a list of indices (in triangles) of
	# faces. The second is the actual vertices the list refers to. These is still
	# data in the style of Blender; that is, nothing has been converted to Cal3D
	# specific formats yet.
	def __createIndexVertexLists(faces, hasUV):
		lookup     = {}
		vertices   = []
		indices    = []

		duplookup  = {}
		dupindices = {}

		# - A vertex needs to be appended (and then it's index) if it doesn't
		#   appear in the lookup dictionary.
		# - An index HAS to be appended.
		trifaces = [__triFaces(f, hasUV) for f in faces]
		if trifaces:
			for vert in reduce(lambda x, y: x + y, trifaces):
				key = vert.MakeKey()

				# If we can't find the "key" in the lookup dictionary, add
				# it to the vertices list and put it's index back into the
				# lookup table.
				if not key in lookup:
					lookup[key] = i = len(vertices)

					vertices.append(vert)
			
				# Otherwise, just get the index to be appended to the indices
				# list, to be handled later.
				else:
					i = lookup[key]

				if not vert.index in duplookup:
					duplookup[vert.index] = vert

				else:
					if vert.uvco != duplookup[vert.index].uvco:
						dupindices[vert.index] = True
						dupindices[i] = True

				indices.append(i)

		return indices, vertices, dupindices

	def __createSubMeshes(mesh, objlist):
		for obj, data in __yieldBlenderObj("Mesh", objlist):
			# Put mesh into worldspace
			objmatrix   = Blender.Mathutils.Matrix(obj.getMatrix('worldspace'))
			localmatrix = Blender.Mathutils.Matrix(obj.getMatrix('localspace'))
			
			obj.setMatrix(Blender.Mathutils.Matrix().identity())
			
			data.transform(objmatrix, True)

			# Handle all of the materials associated with this Mesh.
			firstmaterial   = True
			submeshmaterial = None 
			
			for m in data.materials:
				if not m.name in bcobject.Material.MATERIALS:
					mapnames = []
					ambient  = [255] * 3
					
					diffuse = [
						m.R * 255,
						m.G * 255,
						m.B * 255,
						m.getRef() * 255
					]
					
					specular = [
						m.specR * 255,
						m.specG * 255,
						m.specB * 255,
						m.spec  * 127.5
					]

					ambient.append(m.getAmb() * 255)
					
					for mtex in m.getTextures():
						if mtex and mtex.tex.getType() == "Image":
							filename = mtex.tex.image.getFilename()
							filename = os.path.split(filename)[1]
							
							mapnames.append(filename)

					material = bcobject.Material(
						m.name,
						ambient,
						diffuse,
						specular,
						mapnames
					)
				else:
					material = bcobject.Material.MATERIALS[m.name]

				if firstmaterial:
					submeshmaterial = material
					
				firstmaterial = False
				
			submesh = bcobject.SubMesh(mesh, submeshmaterial)
			
			ilist, vlist, dupdict = __createIndexVertexLists(
				data.faces,
				data.hasFaceUV()
			)

			# At this points we should have a list of indices and a list of
			# vertices which the index list refers to. We could iterate over
			# both sequences and create the bcobject.{Face,Vertex} objects
			# accordingly.

			vertices = [bcobject.Vertex(
				submesh,
				v.co,
				v.no,
				dupdict.has_key(v.index),
				[v.uvco] * len(submesh.material.mapnames)
			) for v in vlist]

			# We really need to have references to the actual vertices,
			# otherwise, changing the vertex order during LOD becomes much
			# too difficult.
			faces = []

			for v in zip(*[iter(ilist)] * 3):
				verts = vertices[v[0]], vertices[v[1]], vertices[v[2]]
				face  = bcobject.Face(submesh, *verts)
				
				faces.append(face)
			
			# TODO: *sigh*, this used to be so clean. :)
			# faces = [bcobject.Face(submesh, *v) for v in zip(*[iter(ilist)] * 3)]

			# Here we parse data.verts (again) to get the weight values
			# of each Vertex. For some reason, Blender doesn't guarantee
			# that the max weight will be 1.0, so we keep our own sum().
			#
			# TODO: This could probably be moved up into the first loops,
			# preventing another iteration of all the vertices here.
			for i, v in enumerate(vlist):
				weights = data.getVertexInfluences(v.index)
				total   = sum([w for b, w in weights])
				if total == 0: total = 1
				
				for b, w in weights:
					vertices[i].influences.append(bcobject.Influence(
						bcobject.Bone.BONES[b.replace(".", "_")],
						w / total
					))

			# Put object matrix and mesh back to normal
			obj.setMatrix(Blender.Mathutils.Matrix(localmatrix))

			data.transform(objmatrix.invert(), True)

			if bcconf.LOD:
				submesh.LOD()

	# Automatic choice. If any Groups exist, we export each Group as a mesh
	# and each Object in that group as a submesh. Otherwise, we just create
	# one mesh with all objects of the file as submeshes.
	#
	# TODO: Perhaps make this choice user-configurable?
	if bcconf.SUBMESHMODE == "object":
		for obj, data in __yieldBlenderObj("Mesh"):
			mesh = bcobject.Mesh(obj.name)
			meshes.append(mesh)
			__createSubMeshes(mesh, [obj])

	elif len(Blender.Group.Get()):
		for group in Blender.Group.Get():
			mesh = bcobject.Mesh(group.name)
			
			meshes.append(mesh)
			
			__createSubMeshes(mesh, group.objects)
	
	else:
		mesh = bcobject.Mesh(Blender.Scene.GetCurrent().name)
		
		meshes.append(mesh)
		
		__createSubMeshes(mesh, Blender.Object.Get())

	return meshes

MeshData = blendercal.exception(MeshData)

def AnimationData():
	# This function demonstrates a new way of parsing and retrieving animation
	# data in Blender. With version 242 and above, users can call the the
	# Object.evaluatePose and Action.setActive methods to set the poses and read
	# the rotation data directly from the pose bones.
	
	animations = []

	for actionname, action in Blender.Armature.NLA.GetActions().iteritems():
		animation = bcobject.Animation(actionname)

		action.setActive(bcobject.Skeleton.ARMATURE)

		name2ipo = {}
		frames   = []
		
		# Grab the "times" at which this action has keyframes. There has
		# GOT to be a better way than this.
		for iponame, ipo in action.getAllChannelIpos().iteritems():
			for curve in ipo.getCurves():
				for point in curve.getPoints():
					frame = int(point.pt[0])
					
					frame not in frames and frames.append(frame)
			
				name2ipo[iponame] = ipo

		if not frames: continue
		frames.sort()
    
		animation.duration = frames[-1]

		# Move the armature to the keyframe "times" and grab the data we
		# need to export our animations.
		for iponame, ipo in name2ipo.iteritems():
			bonename = iponame.replace(".", "_")
			bone     = bcobject.Bone.BONES[bonename]
			track    = bcobject.Track(animation, bone)
			
			animation.tracks[bonename] = track
      
			blenderbone = bcobject.Skeleton.ARMATURE.getPose().bones[iponame]
      
			# Before versions 242 you would build Loc/Quat values using
			# Ipo.evaluate(); no need for that now.
			for curframe in frames:
				bcobject.Skeleton.ARMATURE.evaluatePose(curframe)
				
				matrix = blenderbone.quat.toMatrix()
				
				matrix.resize4x4()
				
				matrix[3] = Blender.Mathutils.Vector(*blenderbone.loc).resize4D()
				matrix    = blendercal.MATRIX2GL(matrix * bone.local)
      
				bcobject.KeyFrame(
					track,
					(curframe - 1) / bcconf.ANIMFPS,
					matrix.translationPart(),
					matrix.toQuat()
				)

		animation.duration /= bcconf.ANIMFPS

		animations.append(animation)

	return animations

AnimationData = blendercal.exception(AnimationData)

def ExportData(filename, skeldata, meshdata, animdata, prefixfiles=False):
	dirname  = os.path.dirname(filename)
	basename = os.path.splitext(os.path.basename(filename))[0]
	
	try:
		os.makedirs(dirname)
	
	except OSError, e:
		pass
	
	cfg = file(os.path.join(dirname, "%s.cfg" % basename), "w")
	
	if prefixfiles:
		prefix = "%s_" % basename
	
	else:
		prefix = ""
	
	print >> cfg, "# Cal3D model exported from Blender with cal3d-export.py"
	print >> cfg, "skeleton=%s.xsf" % basename
	
	print >> file(os.path.join(dirname, "%s.xsf" % basename), "w"), skeldata
	
	for animation in animdata:
		if not animation.name.startswith("_"):
			animfile = "%s%s.xaf" % (prefix, animation.name)
				
			print >> file(os.path.join(dirname, animfile), "w"), animation
				
			print >> cfg, "animation=%s" % animfile

	for mesh in meshdata:
		if not mesh.name.startswith("_"):
			meshfile = "%s%s.xmf" % (prefix, mesh.name)
		
			print >> file(os.path.join(dirname, meshfile), "w"), mesh
			
			print >> cfg, "mesh=%s" % meshfile
	
	materials = bcobject.Material.MATERIALS.values()
	
	materials.sort(lambda a, b: cmp(a.id, b.id))
	
	for material in materials:
		matfile = "%s.xrf" % material.name
		
		print >> file(os.path.join(dirname, matfile), "w"), material
		
		print >> cfg, "material=%s" % matfile

ExportData = blendercal.exception(ExportData)
