#!BPY
""" 
Name: 'MilkShape3D (.ms3d)...'
Blender: 245
Group: 'Import'
Tooltip: 'Import from MilkShape3D file format (.ms3d)'
"""
# 
# Author: Markus Ilmola
# Email: markus.ilmola@pp.inet.fi
#
# 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#

# import needed stuff
import os.path
import math
from math import *
import struct
import Blender
from Blender import Mathutils
from Blender.Mathutils import *


# trims a string by removing ending 0 and everything after it
def uku(s):
	try:
		return s[:s.index('\0')]
	except:
		return s


# Converts ms3d euler angles to a rotation matrix
def RM(a):
	sy = sin(a[2])
	cy = cos(a[2])
	sp = sin(a[1])
	cp = cos(a[1])
	sr = sin(a[0])
	cr = cos(a[0])
	return Matrix([cp*cy, cp*sy, -sp], [sr*sp*cy+cr*-sy, sr*sp*sy+cr*cy, sr*cp],[cr*sp*cy+-sr*-sy, cr*sp*sy+-sr*cy, cr*cp])


# Converts ms3d euler angles to a quaternion
def RQ(a):
	angle = a[2] * 0.5;
	sy = sin(angle);
	cy = cos(angle);
	angle = a[1] * 0.5;
	sp = sin(angle);
	cp = cos(angle);
	angle = a[0] * 0.5;
	sr = sin(angle);
	cr = cos(angle);
	return Quaternion(cr*cp*cy+sr*sp*sy, sr*cp*cy-cr*sp*sy, cr*sp*cy+sr*cp*sy, cr*cp*sy-sr*sp*cy)


# takes a texture filename and tries to load it
def loadImage(path, filename):
	image = None
	try:
		image = Blender.Image.Load(os.path.abspath(filename))
	except IOError:
		print "Warning: Failed to load image: " + filename + ". Trying short path instead...\n"
		try:
			image = Blender.Image.Load(os.path.dirname(path) + "/" + os.path.basename(filename))
		except IOError:
			print "Warning: Failed to load image: " + os.path.basename(filename) + "!\n"
	return image


# imports a ms3d file to the current scene
def import_ms3d(path):
	# get scene
	scn = Blender.Scene.GetCurrent()
	if scn == None:
		return "No scene to import to!"

	# open the file
	try:
		file = open(path, 'rb')
	except IOError:
		return "Failed to open the file!"

	# get the file size
	file.seek(0, os.SEEK_END);
	fileSize = file.tell();
	file.seek(0, os.SEEK_SET);

	# read id to check if the file is a MilkShape3D file
	id = file.read(10)
	if id!="MS3D000000":
		return "The file is not a MS3D file!"

	# read version
	version = struct.unpack("i", file.read(4))[0]
	if version!=4:
		return "The file has invalid version!"

	# Create the mesh
	scn.objects.selected = []
	mesh = Blender.Mesh.New("MilkShape3D Mesh")
	meshOb = scn.objects.new(mesh)

	# read the number of vertices
	numVertices = struct.unpack("H", file.read(2))[0]

	# read vertices
	coords = []
	boneIds = []
	for i in xrange(numVertices):
		# skip flags
		file.read(1)

		# read coords
		coords.append(struct.unpack("fff", file.read(3*4)))

		# read bone ids 
		boneIds.append(struct.unpack("b", file.read(1))[0])

		# skip refcount		
		file.read(1)

	# add the vertices to the mesh
	mesh.verts.extend(coords)

	# read number of triangles
	numTriangles = struct.unpack("H", file.read(2))[0]
	
	# read triangles
	faces = []
	uvs = []
	for i in xrange(numTriangles):
		# skip flags
		file.read(2)

		# read indices (faces)
		faces.append(struct.unpack("HHH", file.read(3*2)))

		# read normals
		normals = struct.unpack("fffffffff", file.read(3*3*4))

		# read texture coordinates
		s = struct.unpack("fff", file.read(3*4))
		t = struct.unpack("fff", file.read(3*4))

		# store texture coordinates
		uvs.append([[s[0], 1-t[0]], [s[1], 1-t[1]], [s[2], 1-t[2]]])
		
		if faces[-1][2] == 0: # Cant have zero at the third index
			faces[-1] = faces[-1][1], faces[-1][2], faces[-1][0]
			uvs[-1] = uvs[-1][1], uvs[-1][2], uvs[-1][0]
		
		# skip smooth group
		file.read(1)

		# skip group
		file.read(1)

	# add the faces to the mesh
	mesh.faces.extend(faces)

	# set texture coordinates
	for i in xrange(numTriangles):
		mesh.faces[i].uv = [Vector(uvs[i][0]), Vector(uvs[i][1]), Vector(uvs[i][2])]

	# read number of groups
	numGroups = struct.unpack("H", file.read(2))[0]

	# read groups
	for i in xrange(numGroups):
		# skip flags
		file.read(1)

		# skip name
		file.read(32)

		# read the number of triangles in the group
		numGroupTriangles = struct.unpack("H", file.read(2))[0]

		# read the group triangles
		if numGroupTriangles > 0:
			triangleIndices = struct.unpack(str(numGroupTriangles) + "H", file.read(2*numGroupTriangles));

		# read material
		material = struct.unpack("b", file.read(1))[0]
		if material>=0:
			for j in xrange(numGroupTriangles):
				mesh.faces[triangleIndices[j]].mat = material

	# read the number of materials
	numMaterials = struct.unpack("H", file.read(2))[0]

	# read materials
	for i in xrange(numMaterials):
		# read name
		name = uku(file.read(32))

		# create the material
		mat = Blender.Material.New(name)
		mesh.materials += [mat]

		# read ambient color
		ambient = struct.unpack("ffff", file.read(4*4))[0:3]
		mat.setAmb((ambient[0]+ambient[1]+ambient[2])/3)

		# read diffuse color
		diffuse = struct.unpack("ffff", file.read(4*4))[0:3]
		mat.setRGBCol(diffuse)

		# read specular color
		specular = struct.unpack("ffff", file.read(4*4))[0:3]
		mat.setSpecCol(specular)

		# read emissive color
		emissive = struct.unpack("ffff", file.read(4*4))[0:3]
		mat.setEmit((emissive[0]+emissive[1]+emissive[2])/3)

		# read shininess
		shininess = struct.unpack("f", file.read(4))[0]

		# read transparency		
		transparency = struct.unpack("f", file.read(4))[0]
		mat.setAlpha(transparency)
		if transparency < 1:
			 mat.mode |= Blender.Material.Modes.ZTRANSP

		# read mode
		mode = struct.unpack("B", file.read(1))[0]

		# read texturemap
		texturemap = uku(file.read(128))
		if len(texturemap)>0:
			colorTexture = Blender.Texture.New(name + "_texture")
			colorTexture.setType('Image')
			colorTexture.setImage(loadImage(path, texturemap))
			mat.setTexture(0, colorTexture, Blender.Texture.TexCo.UV, Blender.Texture.MapTo.COL)

		# read alphamap
		alphamap = uku(file.read(128))
		if len(alphamap)>0:
			alphaTexture = Blender.Texture.New(name + "_alpha")
			alphaTexture.setType('Image') 
			alphaTexture.setImage(loadImage(path, alphamap))
			mat.setTexture(1, alphaTexture, Blender.Texture.TexCo.UV, Blender.Texture.MapTo.ALPHA)		

	# read animation
	fps = struct.unpack("f", file.read(4))[0]
	time = struct.unpack("f", file.read(4))[0]
	frames = struct.unpack("i", file.read(4))[0]

	# read the number of joints
	numJoints = struct.unpack("H", file.read(2))[0]

	# create the armature
	armature = 0
	armOb = 0
	if numJoints > 0:
		armOb = Blender.Object.New('Armature', "MilkShape3D Skeleton")
		armature = Blender.Armature.New("MilkShape3D Skeleton")
		armature.drawType = Blender.Armature.STICK
		armOb.link(armature)
		scn.objects.link(armOb)
		armOb.makeParentDeform([meshOb])
		armature.makeEditable()

	# read joints
	joints = []
	rotKeys = {}
	posKeys = {}
	for i in xrange(numJoints):
		# skip flags
		file.read(1)

		# read name
		name = uku(file.read(32))
		joints.append(name)

		# create the bone
		bone = Blender.Armature.Editbone()
		armature.bones[name] = bone

		# read parent
		parent = uku(file.read(32))
		if len(parent)>0:
			bone.parent = armature.bones[parent]

		# read orientation
		rot = struct.unpack("fff", file.read(3*4))

		# read position
		pos = struct.unpack("fff", file.read(3*4))
		
		# set head
		if bone.hasParent():
			bone.head =  Vector(pos) * bone.parent.matrix + bone.parent.head
			tempM = RM(rot) * bone.parent.matrix
			tempM.transpose;
			bone.matrix = tempM
		else:
			bone.head = Vector(pos)
			bone.matrix = RM(rot)

		# set tail
		bvec = bone.tail - bone.head
		bvec.normalize()
		bone.tail = bone.head + 0.01 * bvec

		# Create vertex group for this bone
		mesh.addVertGroup(name)
		vgroup = []
		for index, v in enumerate(boneIds):
			if v==i:
				vgroup.append(index)
		mesh.assignVertsToGroup(name, vgroup, 1.0, 1)
	
		# read the number of rotation keys
		numKeyFramesRot = struct.unpack("H", file.read(2))[0]
			
		# read the number of postions keys
		numKeyFramesPos = struct.unpack("H", file.read(2))[0]

		# read rotation keys
		rotKeys[name] = []		
		for j in xrange(numKeyFramesRot):
			# read time
			time = fps * struct.unpack("f", file.read(4))[0]
			# read data
			rotKeys[name].append([time, struct.unpack("fff", file.read(3*4))])

		# read position keys
		posKeys[name] = []
		for j in xrange(numKeyFramesPos):
			# read time
			time = fps * struct.unpack("f", file.read(4))[0]
			# read data
			posKeys[name].append([time, struct.unpack("fff", file.read(3*4))])
	
	# create action and pose
	action = 0
	pose = 0
	if armature!=0:
		armature.update()
		pose = armOb.getPose()
		action = armOb.getAction()
		if not action:
			action = Blender.Armature.NLA.NewAction()
			action.setActive(armOb)

	# create animation key frames
	for name, pbone in pose.bones.items():
		# create position keys
		for key in posKeys[name]:
			pbone.loc = Vector(key[1])
			pbone.insertKey(armOb, int(key[0]+0.5), Blender.Object.Pose.LOC, True)

		# create rotation keys
		for key in rotKeys[name]:
			pbone.quat = RQ(key[1])
			pbone.insertKey(armOb, int(key[0]+0.5), Blender.Object.Pose.ROT, True)

	# The old format ends here. If there is more data then the file is newer version

	# check to see if there are any comments
	if file.tell()<fileSize:

		# read sub version
		subVersion = struct.unpack("i", file.read(4))[0]

		# Is the sub version a supported one
		if subVersion==1:

			# Group comments
			numComments = struct.unpack("i", file.read(4))[0]
			for i in range(numComments):
				file.read(4) # index
				size = struct.unpack("i", file.read(4))[0] # comment size
				if size>0:
					print "Group comment: " + file.read(size)

			# Material comments
			numComments = struct.unpack("i", file.read(4))[0]
			for i in range(numComments):
				file.read(4) # index
				size = struct.unpack("i", file.read(4))[0] # comment size
				if size>0:
					print "Material comment: " + file.read(size)

			# Joint comments
			numComments = struct.unpack("i", file.read(4))[0]
			for i in range(numComments):
				file.read(4) # index
				size = struct.unpack("i", file.read(4))[0] # comment size
				if size>0:
					print "Joint comment: " + file.read(size)

			# Model comments
			numComments = struct.unpack("i", file.read(4))[0]
			for i in range(numComments):
				file.read(4) # index
				size = struct.unpack("i", file.read(4))[0] # comment size
				if size>0:
					print "Model comment: " + file.read(size)

		# Unknown version give a warning
		else:
			print "Warning: Unknown version!"

		
	# check to see if there is any extra vertex data
	if file.tell()<fileSize:
		
		# read the subversion
		subVersion = struct.unpack("i", file.read(4))[0]

		# is the version supported
		if subVersion==2:
			# read the extra data for each vertex
			for i in xrange(numVertices):
				# bone ids
				ids = struct.unpack("bbb", file.read(3))
				# weights
				weights = struct.unpack("BBB", file.read(3))
				# extra
				extra = struct.unpack("I", file.read(4))
				# add extra vertices with weights to deform groups
				if ids[0]>=0 or ids[1]>=0 or ids[2]>=0:
					mesh.assignVertsToGroup(joints[boneIds[i]], [i], 0.01*weights[0], 1)
					if ids[0]>=0:
						mesh.assignVertsToGroup(joints[ids[0]], [i], 0.01*weights[1], 1)
					if ids[1]>=0:
						mesh.assignVertsToGroup(joints[ids[1]], [i], 0.01*weights[2], 1)
					if ids[2]>=0:
						mesh.assignVertsToGroup(joints[ids[2]], [i], 0.01*(100-(weights[0]+weights[1]+weights[2])), 1)	
					
		elif subVersion==1:
			# read extra data for each vertex
			for i in xrange(numVertices):
				# bone ids
				ids = struct.unpack("bbb", file.read(3))
				# weights
				weights = struct.unpack("BBB", file.read(3))
				# add extra vertices with weights to deform groups
				if ids[0]>=0 or ids[1]>=0 or ids[2]>=0:
					mesh.assignVertsToGroup(joints[boneIds[i]], [i], 0.01*weights[0], 1)
					if ids[0]>=0:
						mesh.assignVertsToGroup(joints[ids[0]], [i], 0.01*weights[1], 1)
					if ids[1]>=0:
						mesh.assignVertsToGroup(joints[ids[1]], [i], 0.01*weights[2], 1)
					if ids[2]>=0:
						mesh.assignVertsToGroup(joints[ids[2]], [i], 0.01*(100-(weights[0]+weights[1]+weights[2])), 1)	

		# non supported subversion give a warning
		else:
			print "Warning: Unknown subversion!"
	
	# rest of the extra data in the file is not imported/used
	
	# refresh the view
	Blender.Redraw()
	
	# close the file
	file.close()

	# succes return empty error string
	return ""


# load the model
def fileCallback(filename):
	error = import_ms3d(filename)
	if error!="":
		Blender.Draw.PupMenu("An error occured during import: " + error + "|Not all data might have been imported succesfully.", 2)

Blender.Window.FileSelector(fileCallback, 'Import')
