# NUT Blender Export Script
#
# Author:		Nathaniel Meyer
# Website:		http://www.nutty.ca
#
# Notes:
# 1. Built with Blender version 2.63
#	The original Blender 2.58 script was updated to use the new "tessface" proprty of the mesh.
#
# 2. To install this script, place it in your <BlenderHome>/<BlenderVersion>/scripts/addons/ folder.
#	Open the "User Preferences" window, select Add-Ons, and search for the script
#	"Import-Export: NUT Scene Format(.nut)".
#
# 3. This script is provided as-is. Do whatever you want with it. Use it, modify it, call it your own,
#	sell it, gift wrap it for your mother, whatever. It's 100.0% free, with no added GNU.


bl_info = {
	"name": "NUT Scene Format (.nut)",
	"author": "Nathaniel Meyer",
	"version": (1, 0),
	"blender": (2, 6, 3),
	"location": "File > Export > Nut (.nut)",
	"description": "Export NUT Scene Format (.nut)",
	"warning": "",
	"wiki_url": "http://www.nutty.ca",
	"tracker_url": "http://www.nutty.ca",
	"category": "Import-Export"}


# Blender and Python Imports
import bpy
from bpy.props import StringProperty, BoolProperty
from bpy_extras.io_utils import ExportHelper
from mathutils import *
import math

# Convert RAD to degrees
RAD = 180.0 / math.pi


# Summary:
# Defines the structure for a single vertex.
class Vertex():
	# Stores the (x,y,z) coordinate
	#point = None
	
	# Stores the (x,y,z) vertex normal
	#normal = None
	
	# Stores the (u,v) texture coodinate
	#uv = None
	
	# Collection of group weights for this vertex. First index stores the group index,
	# second number stores the vertex weight [[g,w],...].
	#groups = None
	
	# Collection of shared vertices. A shared vertex has the same point, normal, and
	# bone weights, but a different UV coordinate. This collection contains the index
	# into the vertex array where this shared vertex is located.
	#shared = None

	# Constructor
	def __init__(self):
		self.point = None
		self.normal = None
		self.uv = None
		self.groups = None
		self.shared = []


# Summary:
# Export cameras.
#
# @param file
#   File stream to output.
def ExportCameras(file):
	if bpy.data.cameras and (len(bpy.data.cameras) > 0):
		file.write("\t<cameras>\n")
		for camera in bpy.data.cameras:
			file.write("\t\t<camera name=\"%s\" type=\"%s\">\n" % (camera.name, "perspective" if camera.type == "PERSP" else "orthographic"))
			file.write("\t\t\t<resolution x=\"%d\" y=\"%d\" />\n" % (bpy.data.scenes[0].render.resolution_x, bpy.data.scenes[0].render.resolution_y))
			file.write("\t\t\t<angle>%s</angle>\n" % (camera.angle_y * RAD))
			file.write("\t\t\t<clip near=\"%f\" far=\"%f\" />\n" % (camera.clip_start, camera.clip_end))
			file.write("\t\t\t<lens>%s</lens>\n" % camera.lens)
			file.write("\t\t</camera>\n")
		file.write("\t</cameras>\n")


# Summary:
# Export lights. Blender calls them "lamps".
#
# @param file
#   File stream to output.
def ExportLights(file):
	if bpy.data.lamps and (len(bpy.data.lamps) > 0):
		file.write("\t<lights>\n")
		for lamp in bpy.data.lamps:
			file.write("\t\t<light name=\"%s\" type=\"%s\">\n" % (lamp.name, "point" if lamp.type == "POINT" else "ambient" if lamp.type == "SUN" else "directional" if lamp.type == "SPOT" else "area" if lamp.type == "AREA" else ""))
			file.write("\t\t\t<colour r=\"%f\" g=\"%f\" b=\"%f\" />\n" % (lamp.color[0], lamp.color[1], lamp.color[2]))
			try:
				if lamp.falloff_type == "INVERSE_LINEAR":
					file.write("\t\t\t<attenuation l=\"%f\" q=\"%f\" />\n" % (1.0 / lamp.distance, 0.0))
				elif lamp.falloff_type == "INVERSE_SQUARE":
					file.write("\t\t\t<attenuation l=\"%f\" q=\"%f\" />\n" % (0.0, 1.0 / (lamp.distance * lamp.distance)))
				elif lamp.falloff_type == "LINEAR_QUADRATIC_WEIGHTE":
					file.write("\t\t\t<attenuation l=\"%f\" q=\"%f\" />\n" % (lamp.linear_attenuation, lamp.quadratic_attenuation))
			except:
				pass
			file.write("\t\t</light>\n")
		file.write("\t</lights>\n")


# Summary:
# Export images. Images are used by textures.
#
# @param file
#   File stream to output.
def ExportImages(file):
	if bpy.data.images and (len(bpy.data.images) > 0):
		file.write("\t<images>\n")
		for image in bpy.data.images:
			file.write("\t\t<image name=\"%s\" url=\"%s\" />\n" % (image.name, image.filepath.replace("\\", "/")))
		file.write("\t</images>\n")


# Summary:
# Export textures. Only image textures are supported.
#
# @param file
#   File stream to output.
def ExportTextures(file):
	if bpy.data.textures and (len(bpy.data.textures) > 0):
		file.write("\t<textures>\n")
		for texture in bpy.data.textures:
			file.write("\t\t<texture name=\"%s\" type=\"%s\">\n" % (texture.name, texture.type.lower()))
			if (texture.type == "IMAGE") and texture.image:
				file.write("\t\t\t<image>%s</image>\n" % texture.image.name)
				file.write("\t\t\t<filter interpolate=\"%s\" mipmap=\"%s\" wrap=\"%s\" />\n" % ("true" if texture.use_interpolation else "false", "true" if texture.use_mipmap else "false", "repeat" if texture.extension == "REPEAT" else "clamp"))
			file.write("\t\t</texture>\n")
		file.write("\t</textures>\n")


# Summary:
# Export materials.
#
# @param file
#   File stream to output.
def ExportMaterials(file):
	if bpy.data.materials and (len(bpy.data.materials) > 0):
		file.write("\t<materials>\n")
		for material in bpy.data.materials:
			file.write("\t\t<material name=\"%s\">\n" % material.name)
			if material.use_shadeless:
				file.write("\t\t\t<ambient r=\"%f\" g=\"%f\" b=\"%f\" />\n" % (material.diffuse_color[0], material.diffuse_color[1], material.diffuse_color[2]))
				file.write("\t\t\t<diffuse kd=\"0\" r=\"0\" g=\"0\" b=\"0\" />\n")
				file.write("\t\t\t<specular ks=\"0\" r=\"0\" g=\"0\" b=\"0\" />\n")
			else:
				file.write("\t\t\t<diffuse kd=\"%f\" r=\"%f\" g=\"%f\" b=\"%f\" />\n" % (material.diffuse_intensity, material.diffuse_color[0], material.diffuse_color[1], material.diffuse_color[2]))
				file.write("\t\t\t<specular ks=\"%f\" r=\"%f\" g=\"%f\" b=\"%f\" />\n" % (material.specular_intensity, material.specular_color[0], material.specular_color[1], material.specular_color[2]))
				file.write("\t\t\t<shininess>%f</shininess>\n" % material.specular_hardness)
			file.write("\t\t\t<alpha>%f</alpha>\n" % material.alpha)
			
			if (material.raytrace_mirror != None) and material.raytrace_mirror.use:
				file.write("\t\t\t<reflection reflectivity=\"%f\" fresnel=\"%f\" />\n" % (material.raytrace_mirror.reflect_factor, material.raytrace_mirror.fresnel * material.raytrace_mirror.fresnel_factor))
			
			for texSlot in material.texture_slots:
				if texSlot:
					file.write("\t\t\t<texture name=\"%s\">\n" % texSlot.texture.name)
					file.write("\t\t\t\t<offset x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (texSlot.offset[0], texSlot.offset[1], texSlot.offset[2]))
					file.write("\t\t\t\t<scale x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (texSlot.scale[0], texSlot.scale[1], texSlot.scale[2]))
					file.write("\t\t\t</texture>\n")
			file.write("\t\t</material>\n")
		file.write("\t</materials>\n")


# Summary:
# Export curves. Blender curves may either use bezier paths or nurbs.
# This exporter only handles bezier paths.
#
# @param file
#   File stream to output.
def ExportCurves(file):
	if bpy.data.curves and (len(bpy.data.curves) > 0):
		file.write("\t<curves>\n")
		for curve in bpy.data.curves:
			file.write("\t\t<curve name=\"%s\">\n" % curve.name)
			for spline in curve.splines:
				if spline.bezier_points and (len(spline.bezier_points) > 0):
					file.write("\t\t\t<path>\n")
					for point in spline.bezier_points:
						file.write("\t\t\t\t<point x=\"%f\" y=\"%f\" z=\"%f\">\n" % (point.co[0], point.co[1], point.co[2]))
						file.write("\t\t\t\t\t<left x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (point.handle_left[0], point.handle_left[1], point.handle_left[2]))
						file.write("\t\t\t\t\t<right x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (point.handle_right[0], point.handle_right[1], point.handle_right[2]))
						file.write("\t\t\t\t\t<radius x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (point.radius, point.radius, point.radius))
						file.write("\t\t\t\t</point>\n")
					file.write("\t\t\t</path>\n")
			file.write("\t\t</curve>\n")
		file.write("\t</curves>\n")


# Summary:
# Export geometries. Blender stores UV coordinates per-index, not
# per-vertex. If you have 4 vertices and 6 face indices, there are 6 UV
# coordinates. If you wish to convert from per-index to per-vertex,
# you must reconstruct the index list with unshared vertices. This
# takes away render efficiency.
#
# @param file
#   File stream to output.
# @param exportVertexUv
#	True if the mesh should be reconstructed so that UVs are exported per-vertex
#	instead of per-face.
# @param exportVertexNormals
#	True if the vertex normals of the mesh should be exported
def ExportMeshes(file, exportVertexUv, exportVertexNormals):
	if bpy.data.meshes and (len(bpy.data.meshes) > 0):
		file.write("\t<geometries>\n")
		for mesh in bpy.data.meshes:
			file.write("\t\t<geometry name=\"%s\">\n" % mesh.name)
			
			# Note: Blender 2.63 introduced a new BMesh type and has changed the old API.
			# To access tesselated data, use the new mesh.tessface* properties.
			# Before you can do that, you need to make sure blender creates it first.
			mesh.update(calc_tessface=True)
			
			if ( exportVertexUv and (len(mesh.tessface_uv_textures) > 0) ):
				# Create duplicate vertices that have multiple UVs and update the face indices.
				# Normals and bone weights must also be duplicated.
				
				faceCount = len(mesh.tessfaces)
				vertCount = len(mesh.vertices)
				
				vertData = []
				indexData = []
				maxBonePerVertex = 0
				
				# Step 1: Copy vertex data
				for i in range(vertCount):
					vertex = mesh.vertices[i]
					newVertex = Vertex()
					
					# Coordiantes
					newVertex.point = [vertex.co[0], vertex.co[1], vertex.co[2]]
					
					# Normal
					newVertex.normal = [vertex.normal[0], vertex.normal[1], vertex.normal[2]]
					
					# Group weights
					if vertex.groups and (len(vertex.groups) > 0):
						newVertex.groups = []
						groupCount = len(vertex.groups)
						for j in range(groupCount):
							newVertex.groups.append([vertex.groups[j].group, vertex.groups[j].weight])
						
						if groupCount > maxBonePerVertex:
							maxBonePerVertex = groupCount
					
					vertData.append(newVertex);
				
				# Step 2: Copy index data
				for i in range(faceCount):
					indices = []
					vertCount = len(mesh.tessfaces[i].vertices)
					for j in range(vertCount):
						indices.append(mesh.tessfaces[i].vertices[j])
					indexData.append(indices);
				
				# Step 3: Iterate indices and assign UV coordinates to each vertex. If a vertex
				# has more than one unique UV coordinate, create a duplicate entry
				tessUv = mesh.tessface_uv_textures[0];
				for i in range(faceCount):
					vertCount = len(indexData[i])
					for j in range(vertCount):
						index = indexData[i][j]
						vertex = vertData[index]
						uv = [tessUv.data[i].uv[j][0], tessUv.data[i].uv[j][1]]
						
						if ( vertex.uv == None ):
							# Assign first UV to vertex and move on
							vertex.uv = uv;
						else:
							# Unique UV?
							if ( vertex.uv != uv ):
								# Scan the shared list
								newIndex = -1
								sharedCount = len(vertex.shared)
								for k in range(sharedCount):
									sharedIndex = vertex.shared[k]
									sharedVertex = vertData[sharedIndex]
									if ( sharedVertex.uv == uv ):
										newIndex = sharedIndex
										break
								
								if ( newIndex == -1 ):
									# Duplicate vertex
									newVertex = Vertex()
									newVertex.point = vertex.point
									newVertex.normal = vertex.normal
									newVertex.uv = uv
									newVertex.groups = vertex.groups
									newIndex = len(vertData)
									
									vertData.append(newVertex)
									vertex.shared.append(newIndex)
								
								# Update face index
								indexData[i][j] = newIndex;
				
				# Vertices
				vertCount = len(vertData)
				file.write("\t\t\t<vertices count=\"%d\">" % vertCount)
				for i in range(vertCount):
					if i > 0:
						file.write(" ")
					file.write("%f %f %f" % (vertData[i].point[0], vertData[i].point[1], vertData[i].point[2]))
				file.write("</vertices>\n")
				
				# UVs
				file.write("\t\t\t<uv count=\"%d\">" % vertCount)
				for i in range(vertCount):
					if i > 0:
						file.write(" ")
					if ( vertData[i].uv == None ):
						file.write("0.0 0.0")
					else:
						file.write("%f %f" % (vertData[i].uv[0], vertData[i].uv[1]))
				file.write("</uv>\n")
				
				# Normals
				if ( exportVertexNormals == True ):
					file.write("\t\t\t<normals count=\"%d\">" % vertCount);
					for i in range(vertCount):
						if i > 0:
							file.write(" ")
						file.write("%f %f %f" % (vertData[i].normal[0], vertData[i].normal[1], vertData[i].normal[2]))
					file.write("</normals>\n")
				
				# Indices
				file.write("\t\t\t<indices count=\"%d\">" % faceCount);
				for i in range(faceCount):
					if i > 0:
						file.write(",")
						
					vertCount = len(indexData[i])
					for j in range(vertCount):
						file.write("%d" % indexData[i][j])
						if j != (vertCount - 1):
							file.write(" ")
				file.write("</indices>\n")
				
				# Bone groups and weights
				vertCount = len(vertData)
				if maxBonePerVertex > 0:
					file.write("\t\t\t<bones count=\"%d\" maxBonePerVertex=\"%d\">" % (vertCount, maxBonePerVertex))
					for i in range(vertCount):
						if i > 0:
							file.write(",")
						if vertData[i].groups and (len(vertData[i].groups) > 0):
							groupCount = len(vertData[i].groups)
							for j in range(groupCount):
								file.write("%d %f" % (vertData[i].groups[j][0], vertData[i].groups[j][1]))
								if (j + 1) < groupCount:
									file.write(" ")
					file.write("</bones>\n")
					
					# Find the object with this mesh and extract the vertex groups from it
					for object in bpy.data.objects:
						if object.data == mesh:
							# Vertex Groups
							if object.vertex_groups and (len(object.vertex_groups) > 0):
								file.write("\t\t\t<groups>\n")
								for vertexGroup in object.vertex_groups:
									file.write("\t\t\t\t<group name=\"%s\" index=\"%d\" />\n" % (vertexGroup.name, vertexGroup.index))
								file.write("\t\t\t</groups>\n")
								
								break;
			else:
				# Vertices
				maxBonePerVertex = 0
				vertCount = len(mesh.vertices)
				file.write("\t\t\t<vertices count=\"%d\">" % vertCount)
				for i in range(vertCount):
					if i > 0:
						file.write(" ")
					file.write("%f %f %f" % (mesh.vertices[i].co[0], mesh.vertices[i].co[1], mesh.vertices[i].co[2]))
					if mesh.vertices[i].groups and (len(mesh.vertices[i].groups) > 0):
						groupCount = len(mesh.vertices[i].groups);
						if groupCount > maxBonePerVertex:
							maxBonePerVertex = groupCount
				file.write("</vertices>\n")
				
				# UV Coordinates
				#for uvTexture in mesh.uv_textures: (Blender 2.62)
				for uvTexture in mesh.tessface_uv_textures:
					uvDataCount = len(uvTexture.data)
					file.write("\t\t\t<uv count=\"%d\">" % uvDataCount)
					for i in range(uvDataCount):
						# UV per face index, not per vertex
						if i > 0:
							file.write(" ")
						uvCount = len(uvTexture.data[i].uv)
						for j in range(uvCount):
							if j > 0:
								file.write(" ")
							file.write("%f %f" % (uvTexture.data[i].uv[j][0], uvTexture.data[i].uv[j][1]))
					file.write("</uv>\n")
				
				# Normals
				if ( exportVertexNormals == True ):
					file.write("\t\t\t<normals count=\"%d\">" % vertCount);
					for i in range(vertCount):
						if i > 0:
							file.write(" ")
						file.write("%f %f %f" % (mesh.vertices[i].normal[0], mesh.vertices[i].normal[1], mesh.vertices[i].normal[2]))
					file.write("</normals>\n")
				
				# Indices. Polygons are separated by commas. It is intuitive to know when you're
				# dealing with a triangle or a quad.
				#faceCount = len(mesh.faces); (Blender 2.62)
				faceCount = len(mesh.tessfaces);
				file.write("\t\t\t<indices count=\"%d\">" % faceCount);
				for face in range(faceCount):
					if face > 0:
						file.write(",")
					#count = len(mesh.faces[face].vertices) (Blender 2.62)
					count = len(mesh.tessfaces[face].vertices)
					for i in range(count):
						#file.write("%d" % mesh.faces[face].vertices[i]) (Blender 2.62)
						file.write("%d" % mesh.tessfaces[face].vertices[i])
						if i != (count - 1):
							file.write(" ")
				file.write("</indices>\n")
				
				# Bone groups and weights. Bone groups and weights are separated by commas and represent
				# the bone groups and weights for a particular vertex.
				# Ex: 0 0.5 3 0.4 2 0.1, ...  <- This means the first vertex has 3 bones that affect it.
				# They are groups 0, 3, and 2. The weights for each of these bones are listed beside them,
				# 0.5, 0.4, and 0.1 respectively. Some vertices may have more or fewer bones than others.
				# maxBonePerVertex attribute tells you what the maximum number of bones are for any vertex.
				if maxBonePerVertex > 0:
					file.write("\t\t\t<bones count=\"%d\" maxBonePerVertex=\"%d\">" % (vertCount, maxBonePerVertex))
					for i in range(vertCount):
						if i > 0:
							file.write(",")
						if mesh.vertices[i].groups and (len(mesh.vertices[i].groups) > 0):
							groupCount = len(mesh.vertices[i].groups)
							for j in range(groupCount):
								file.write("%d %f" % (mesh.vertices[i].groups[j].group, mesh.vertices[i].groups[j].weight))
								if (j + 1) < groupCount:
									file.write(" ")
					file.write("</bones>\n")
					
					# Find the object with this mesh and extract the vertex groups from it
					for object in bpy.data.objects:
						if object.data == mesh:
							# Vertex Groups
							if object.vertex_groups and (len(object.vertex_groups) > 0):
								file.write("\t\t\t<groups>\n")
								for vertexGroup in object.vertex_groups:
									file.write("\t\t\t\t<group name=\"%s\" index=\"%d\" />\n" % (vertexGroup.name, vertexGroup.index))
								file.write("\t\t\t</groups>\n")
								
								break;
			
			file.write("\t\t</geometry>\n")
		file.write("\t</geometries>\n")


# Summary:
# This method will traverse up the bone's parent until the root bone is found.
#
# @param bone
#   Bone to traverse up until the root bone is found.
# @return
#   Reference to the root bone.
def GetRootBone(bone):
	while bone:
		if bone.parent:
			bone = bone.parent;
		else:
			break;
	return bone


# Summary:
# Export bones. Called directly by ExportSkeletons.
# A bone has two matrices:
#	1. matrix (3x3) - Rotation matrix relative to its parent bone.
#	2. matrix_local (4x4) - Tranformation matrix relative to the armature.
#
# @param file
#   File stream to output.
# @param bone
#   Current bone to output.
# @param relative
#   Set true to output relative coordiantes, otherwise false for absolute coordinates.
# @param tab
#   String to store XML tabbing to keep things ordered.
def ExportBones(file, bone, relative, tab):
	file.write("%s<bone name=\"%s\">\n" % (tab, bone.name))
	
	# Output bone translation (aka: offset)
	if relative and bone.parent:
		# Calculate position relative to parent bone
		x = (bone.head_local[0] - bone.parent.tail_local[0]) + (bone.parent.tail_local[0] - bone.parent.head_local[0])
		y = (bone.head_local[1] - bone.parent.tail_local[1]) + (bone.parent.tail_local[1] - bone.parent.head_local[1])
		z = (bone.head_local[2] - bone.parent.tail_local[2]) + (bone.parent.tail_local[2] - bone.parent.head_local[2])
		quat = bone.matrix.to_quaternion()
		
		file.write("%s\t<translation x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (tab, x, y, z))
	else:
		# Calculate position relative to skeleton / armature
		quat = bone.matrix_local.to_quaternion();
		file.write("%s\t<translation x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (tab, bone.head_local[0], bone.head_local[1], bone.head_local[2]))
	
	# Output bone rotation
	file.write("%s\t<rotation x=\"%f\" y=\"%f\" z=\"%f\" w=\"%f\" />\n" % (tab, quat.x, quat.y, quat.z, quat.w))

	# Process bone children
	if bone.children and (len(bone.children) > 0):
		for childBone in bone.children:
			ExportBones(file, childBone, relative, tab + "\t");
	file.write("%s</bone>\n" % tab)


# Summary:
# Export skeletons. Blender calls them armatures.
# 1. Blender armatures can have multiple root bones (need to find them).
# 2. bone.head_local is relative to armature.
# 3. bone.head is relative to parent, but doesn't appear correct.
#
# @param file
#   File stream to output.
def ExportSkeletons(file):
	if bpy.data.armatures and (len(bpy.data.armatures) > 0):
		file.write("\t<skeletons>\n")
		for armature in bpy.data.armatures:
			file.write("\t\t<skeleton name=\"%s\">\n" % armature.name)
			
			# Store a list of processed root bones
			rootBoneList = list()
			
			# Find root bones
			# Blender's bone list is a flat array, so we need to reconstruct the hierarchy.
			if armature.bones and (len(armature.bones) > 0):
				for bone in armature.bones:
					# Get the root bone using recursion
					rootBone = GetRootBone(bone);
					
					# Process only unprocessed root bones, avoid duplicates.
					if rootBone not in rootBoneList:
						rootBoneList.append(rootBone);
						ExportBones(file, rootBone, True, "\t\t\t");
			file.write("\t\t</skeleton>\n")
		file.write("\t</skeletons>\n")


# Summary:
# Export animations. Blender calls them actions.
#
# Animations are broken down into Actions.
# Each action has a set of channels.
# A channel represents the keyframes for a single property (x, y, or z axis) along the timeline.
# A channel contains a number of keyframe points.
# Keyframe points are represented as a bezier path. X-axis represents time, Y-axis represents value.
#
# Blender animates per property, not per object. So a rotation has 4 separate channels rather than one
# channel with 4 separate values for the quaternion (or 3 channels with euler angles). This exporter
# makes assumptions about this data format. Channels are ordered by (x,y,z) for euler angles or (w,x,y,z)
# for quaternions.
#
# Blender action groups store the actual keyframe values specified by the user during posing; however this
# is not what we want. If a bone uses inverse kinematics or has constraints, we will not get the actual bone
# transformation matrix. Instead, we must find the armature object, set it to use the active action, then
# iterate over the scene's timeline and extract the bone transformations.
#
# Blender uses different structures for skeletal animations.
#	1. Armature and Bones - Required to extract bone groups and rest pose position.
#	2. Objects and Pose - Required for extracting bone pose transformations.
#		A PoseBone has 3 matrices
#			1. matrix (4x4) - Final 4x4 matrix after constraints and drivers are applied (object space).
#			2. matrix_basis - Alternative access to location/scale/rotation relative to the parent and own rest bone.
#			3. matrix_channel - 4x4 matrix, before constraints.
#
# @param file
#   File stream to output.
def ExportAnimations(file, exportBezier):
	if bpy.data.actions and (len(bpy.data.actions) > 0):
		file.write("\t<animations>\n")
		for action in bpy.data.actions:
			# An action is a separate entity. We must find the skeleton that belongs to this action by
			# searching the armature group for the skeleton that has all of the bones defined in the
			# action's group. The action's group name will be equal to the bone name.
			skeleton = None
			skeletonObject = None
			if bpy.data.armatures and (len(bpy.data.armatures) > 0):
				for armature in bpy.data.armatures:
					hasBone = False
					for group in action.groups:
						hasBone = False
						for bone in armature.bones:
							if group.name == bone.name:
								hasBone = True
								break;
						if not hasBone:
							break;
					if hasBone:
						skeleton = armature;
						break;
			
			file.write("\t\t<animation name=\"%s\"" % action.name)
			if skeleton != None:
				file.write(" skeleton=\"%s\"" % skeleton.name)
				
				# Find an object using this skeleton. We will use this to extract PoseBone data
				if bpy.data.objects and (len(bpy.data.objects) > 0):
					for object in bpy.data.objects:
						armature = object.find_armature();
						if (armature != None) and (armature.name == skeleton.name):
							skeletonObject = armature;
							break;
			file.write(">\n")
			
			# Get the scene object to modify the active frame
			scene = bpy.data.scenes[0]
			currentFrame = scene.frame_current
			
			for group in action.groups:
				file.write("\t\t\t<group name=\"%s\">\n" % group.name)
				if group.channels and (len(group.channels) > 0):
					channelCount = len(group.channels)
					i = 0
					while i < channelCount:
						if group.channels[i].keyframe_points and (len(group.channels[i].keyframe_points) > 0):
							keyframeCount = len(group.channels[i].keyframe_points)
							
							# Supported channel types:
							# location, rotation_euler, rotation_quaternion, scale
							isQuat = False
							isEuler = False
							isTranslation = False
							isScale = False
							if group.channels[i].data_path.endswith("location"):
								file.write("\t\t\t\t<keyframes type=\"translation\">\n")
								isTranslation = True
							elif group.channels[i].data_path.endswith("rotation_quaternion"):
								file.write("\t\t\t\t<keyframes type=\"rotation\">\n")
								isQuat = True
							elif group.channels[i].data_path.endswith("rotation_euler"):
								file.write("\t\t\t\t<keyframes type=\"rotation\">\n")
								isEuler = True
							elif group.channels[i].data_path.endswith("scale"):
								file.write("\t\t\t\t<keyframes type=\"scale\">\n")
								isScale = True
							
							# Blender keyframes are frame based, not time based. I use time anyway, so make sure to apply a proper
							# FPS to this value. I don't force a time value here because it's really custom.
							for j in range(keyframeCount):
								frame = group.channels[i].keyframe_points[j].co[0]
								x = 0
								y = 0
								z = 0
								w = 0
								
								if (exportBezier == False) and (skeletonObject != None):
									# Set the scene's frame number and extract pose data
									poseBone = skeletonObject.pose.bones[group.name]
									if poseBone != None:
										scene.frame_set(frame)
										
										# Get bone pose matrix
										#matrix = poseBone.matrix
										#if poseBone.parent != None:
										#	matrix = (poseBone.parent.matrix.inverted() * matrix)
										#matrix = poseBone.matrix_basis;
										
										if isQuat:
											#quat = matrix.to_quaternion()
											quat = poseBone.rotation_quaternion
											x = quat.x
											y = quat.y
											z = quat.z
											w = quat.w
										elif isEuler:
											#euler = matrix.to_euler()
											euler = poseBone.rotation_euler
											x = euler.x
											y = euler.y
											z = euler.z
										elif isTranslation:
											#translation = matrix.to_translation()
											translation = poseBone.location
											x = translation.x
											y = translation.y
											z = translation.z
										else:
											#scale = matrix.to_scale()
											scale = poseBone.scale
											x = scale.x
											y = scale.y
											z = scale.z
								else:
									# The following extracts keyframe data directly from the channel; however this does not
									# account for IK movement or bone constraints. Instead, extract the animation data from
									# the PoseBone object in the armature's object.
									if isQuat:
										x = group.channels[i + 1].keyframe_points[j].co[1]
										y = group.channels[i + 2].keyframe_points[j].co[1]
										z = group.channels[i + 3].keyframe_points[j].co[1]
										w = group.channels[i].keyframe_points[j].co[1]
									else:
										x = group.channels[i].keyframe_points[j].co[1]
										y = group.channels[i + 1].keyframe_points[j].co[1]
										z = group.channels[i + 2].keyframe_points[j].co[1]
								if isEuler:
									x = x * RAD
									y = y * RAD
									z = z * RAD
							
								file.write("\t\t\t\t\t<keyframe time=\"%f\"" % frame)
								
								if exportBezier:
									file.write(">\n")
									file.write("\t\t\t\t\t\t<bezier x=\"%f\" lx=\"%f\" ly=\"%f\" rx=\"%f\" ry=\"%f\" />\n" % (x, group.channels[i].keyframe_points[j].handle_left[0], group.channels[i].keyframe_points[j].handle_left[1], group.channels[i].keyframe_points[j].handle_right[0], group.channels[i].keyframe_points[j].handle_right[1]))
									file.write("\t\t\t\t\t\t<bezier y=\"%f\" lx=\"%f\" ly=\"%f\" rx=\"%f\" ry=\"%f\" />\n" % (y, group.channels[i + 1].keyframe_points[j].handle_left[0], group.channels[i + 1].keyframe_points[j].handle_left[1], group.channels[i + 1].keyframe_points[j].handle_right[0], group.channels[i + 1].keyframe_points[j].handle_right[1]))
									file.write("\t\t\t\t\t\t<bezier z=\"%f\" lx=\"%f\" ly=\"%f\" rx=\"%f\" ry=\"%f\" />\n" % (z, group.channels[i + 2].keyframe_points[j].handle_left[0], group.channels[i + 2].keyframe_points[j].handle_left[1], group.channels[i + 2].keyframe_points[j].handle_right[0], group.channels[i + 2].keyframe_points[j].handle_right[1]))
									if isQuat:
										file.write("\t\t\t\t\t\t<Bezier w=\"\" lx=\"%f\" ly=\"%f\" rx=\"%f\" ry=\"%f\" />\n" % (w, group.channels[i + 3].keyframe_points[j].handle_left[0], group.channels[i + 3].keyframe_points[j].handle_left[1], group.channels[i + 3].keyframe_points[j].handle_right[0], group.channels[i + 3].keyframe_points[j].handle_right[1]))
									file.write("\t\t\t\t\t</keyframe>\n")
								else:
									file.write(" x=\"%f\" y=\"%f\" z=\"%f\"" % (x, y, z))
									if isQuat:
										file.write(" w=\"%f\" />\n" % w)
									else:
										file.write(" />\n");
							if isQuat:
								i += 4
							else:
								i += 3
							file.write("\t\t\t\t</keyframes>\n")
						else:
							i += 1;
					# Restore the scene's active frame
					bpy.data.scenes[0].frame_set(frame)
				file.write("\t\t\t</group>\n")
			file.write("\t\t</animation>\n")
			
			# Restore the scene's last frame
			scene.frame_set(currentFrame)
		file.write("\t</animations>\n")


# Summary:
# Export sounds.
#
# @param file
#   File stream to output.
def ExportSounds(file):
	# Sound files
	if bpy.data.sounds and (len(bpy.data.sounds) > 0):
		file.write("\t<sounds>\n")
		for sound in bpy.data.sounds:
			if sound.filepath != None:
				file.write("\t\t<sound name=\"%s\" url=\"%s\" />\n" % (sound.name, sound.filepath))
		file.write("\t</sounds>\n")
	
	# Speakers
	if bpy.data.speakers and (len(bpy.data.speakers) > 0):
		file.write("\t<speakers>\n")
		for speaker in bpy.data.speakers:
			file.write("\t\t<speaker name=\"%s\" " % speaker.name)
			if speaker.sound != None:
				file.write("sound=\"%s\">\n" % speaker.sound.name)
			else:
				file.write(">\n")
			file.write("\t\t\t<attenuation>%f</attenuation>\n" % speaker.attenuation)
			file.write("\t\t\t<cone inner=\"%f\" outer=\"%f\" volume=\"%f\" />\n" % (speaker.cone_angle_inner, speaker.cone_angle_outer, speaker.cone_volume_outer))
			file.write("\t\t\t<distance max=\"%f\" reference=\"%f\" />\n" % (speaker.distance_max, speaker.distance_reference))
			file.write("\t\t\t<pitch>%f</pitch>\n" % speaker.pitch)
			file.write("\t\t\t<volume min=\"%f\" max=\"%f\" volume=\"%f\" />\n" % (speaker.volume_min, speaker.volume_max, speaker.volume))
			file.write("\t\t</speaker>\n")
		file.write("\t</speakers>\n")


# Summary:
# Export scenes. A scene contains many objects, which reference cameras,
# lights, and geometries.
#
# @param file
#   File stream to output.
def ExportScenes(file):
	if bpy.data.scenes and (len(bpy.data.scenes) > 0):
		file.write("\t<scenes>\n")
		for scene in bpy.data.scenes:
			file.write("\t\t<scene name=\"%s\">\n" % scene.name)
			for object in scene.objects:
				if object.data:
					type = object.type.lower();
					if type == "lamp":
						type = "light"
					elif type == "armature":
						type = "skeleton"
					
					if object.parent:
						file.write("\t\t\t<object name=\"%s\" data=\"%s\" type=\"%s\" parent=\"%s\">\n" % (object.name, object.data.name, type, object.parent.name))
					else:
						file.write("\t\t\t<object name=\"%s\" data=\"%s\" type=\"%s\">\n" % (object.name, object.data.name, type))
					armature = object.find_armature();
					if armature:
						file.write("\t\t\t\t<skeleton name=\"%s\" />\n" % armature.name)
					
					# Relative to parent
					translation = object.matrix_local.to_translation()
					rotation = object.matrix_local.to_euler()
					scale = object.matrix_local.to_scale()
					
					file.write("\t\t\t\t<position x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (translation.x, translation.y, translation.z))
					file.write("\t\t\t\t<rotation x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (rotation.x * RAD, rotation.y * RAD, rotation.z * RAD))
					file.write("\t\t\t\t<scale x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (scale.x, scale.y, scale.z))
					
					# Relative to world
					#file.write("\t\t\t\t<position x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (object.location[0], object.location[1], object.location[2]))
					#file.write("\t\t\t\t<rotation x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (object.rotation_euler[0] * RAD, object.rotation_euler[1] * RAD, object.rotation_euler[2] * RAD))
					#use rotation_quaternion[0..3] if you want quaternions
					#file.write("\t\t\t\t<scale x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (object.scale[0], object.scale[1], object.scale[2]))
					
					# Materials
					if object.material_slots and (len(object.material_slots)) > 0:
						for matSlot in object.material_slots:
							if matSlot.material:
								file.write("\t\t\t\t<material name=\"%s\" />\n" % matSlot.material.name)
					
					# Custom Properties
					# First property is always _RNA_UI
					customProperties = object.items();
					count = len(customProperties);
					for i in range(count):
						if ( customProperties[i][0] != "_RNA_UI" ):
							file.write("\t\t\t\t<property name=\"%s\" value=\"%s\" />\n" % (customProperties[i][0], customProperties[i][1]))
					
					file.write("\t\t\t</object>\n")
			file.write("\t\t</scene>\n")
		file.write("\t</scenes>\n")


# Summary:
# Export data in XML format.
#
# @param context
#   Blender context.
# @param filepath
#   Location of the file to write out.
# @param settings
#   Export settings.
def ExportXML(context, filepath, settings):
	print("Exporting %s\n" % filepath)
	
	file = open(filepath, 'w', encoding="utf8", newline="\n")
	
	# XML Declaration
	file.write("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n\n")
	
	# Output XML
	file.write("<nut>\n")
	
	if settings.ExportCameras:
		ExportCameras(file)
	if settings.ExportLights:
		ExportLights(file)
	if settings.ExportImages:
		ExportImages(file)
	if settings.ExportTextures:
		ExportTextures(file)
	if settings.ExportMaterials:
		ExportMaterials(file)
	if settings.ExportCurves:
		ExportCurves(file)
	if settings.ExportMeshes:
		ExportMeshes(file, settings.ExportVertexUv, settings.ExportVertexNormals)
	if settings.ExportSkeletons:
		ExportSkeletons(file)
	if settings.ExportAnimations:
		ExportAnimations(file, settings.ExportBezierKeyframes)
	if settings.ExportSounds:
		ExportSounds(file)
	if settings.ExportScenes:
		ExportScenes(file)
	
	file.write("</nut>")
	file.close()

	return {'FINISHED'}


# Summary:
# Class invoked by Blender when the user clicks on the "Export NUT" menu item.
class ExportNut(bpy.types.Operator, ExportHelper):
	bl_idname = "export.nut"
	bl_label = "Export NUT File"

	# ExportHelper mixin class uses this
	filename_ext = ".nut"

	filter_glob = StringProperty(default="*.nut", options={'HIDDEN'})

	# List of export properties
	ExportCameras = BoolProperty(name="Export Cameras", description="Export all cameras", default=True)
	ExportLights = BoolProperty(name="Export Lights", description="Export all lights", default=True)
	ExportImages = BoolProperty(name="Export Images", description="Export all images", default=True)
	ExportTextures = BoolProperty(name="Export Textures", description="Export all textures", default=True)
	ExportMaterials = BoolProperty(name="Export Materials", description="Export all materials", default=True)
	ExportCurves = BoolProperty(name="Export Curves", description="Export all curves", default=True)
	ExportMeshes = BoolProperty(name="Export Meshes", description="Export all meshes", default=True)
	ExportVertexUv = BoolProperty(name="- Export Vertex Uv", description="Export UVs per-vertex instead of per-face", default=True)
	ExportVertexNormals = BoolProperty(name="- Export Normals", description="Export vertex normals", default=False)
	ExportSkeletons = BoolProperty(name="Export Skeletons", description="Export all skeletons (armatures)", default=True)
	ExportAnimations = BoolProperty(name="Export Animations", description="Export all animations (actions)", default=True)
	ExportBezierKeyframes = BoolProperty(name="Export Bezier Keyframes", description="Export all bezier curves with keyframes", default=False)
	ExportSounds = BoolProperty(name="Export Sounds", description="Export all sounds", default=True)
	ExportScenes = BoolProperty(name="Export Scenes", description="Export all scenes", default=True)

	@classmethod
	def poll(cls, context):
		return context.active_object != None

	def execute(self, context):
		return ExportXML(context, self.filepath, self)


# Only needed if you want to add into a dynamic menu
def menu_func_export(self, context):
	self.layout.operator(ExportNut.bl_idname, text="NUT (.nut)")


def register():
#	bpy.utils.register_class(ExportNut)
	bpy.utils.register_module(__name__)
	bpy.types.INFO_MT_file_export.append(menu_func_export)


def unregister():
#	bpy.utils.unregister_class(ExportNut)
	bpy.utils.unregister_module(__name__)
	bpy.types.INFO_MT_file_export.remove(menu_func_export)


if __name__ == "__main__":
	register()
