#include "jassimp.h"

#include <assimp/cimport.h>
#include <assimp/scene.h>


#ifdef JNI_LOG
#define lprintf(...) printf (__VA_ARGS__)
#else
#define lprintf
#endif


static bool createInstance(JNIEnv *env, const char* className, jobject& newInstance)
{
	jclass clazz = env->FindClass(className);

	if (NULL == clazz)
	{
		lprintf("could not find class %s\n", className);
		return false;
	}

	jmethodID ctr_id = env->GetMethodID(clazz, "<init>", "()V");

	if (NULL == ctr_id)
	{
		lprintf("could not find no-arg constructor for class %s\n", className);
		return false;
	}

	newInstance = env->NewObject(clazz, ctr_id);

	if (NULL == newInstance) 
	{
		lprintf("error calling no-arg constructor for class %s\n", className);
		return false;
	}

	return true;
}


static bool createInstance(JNIEnv *env, const char* className, const char* signature, const jvalue* params, jobject& newInstance)
{
	jclass clazz = env->FindClass(className);

	if (NULL == clazz)
	{
		lprintf("could not find class %s\n", className);
		return false;
	}

	jmethodID ctr_id = env->GetMethodID(clazz, "<init>", signature);

	if (NULL == ctr_id)
	{
		lprintf("could not find no-arg constructor for class %s\n", className);
		return false;
	}

	newInstance = env->NewObjectA(clazz, ctr_id, params);

	if (NULL == newInstance) 
	{
		lprintf("error calling  constructor for class %s, signature %s\n", className, signature);
		return false;
	}

	return true;
}


static bool getField(JNIEnv *env, jobject object, const char* fieldName, const char* signature, jobject& field)
{
	jclass clazz = env->GetObjectClass(object);

	if (NULL == clazz)
	{
		lprintf("could not get class for object\n");
		return false;
	}

	jfieldID fieldId = env->GetFieldID(clazz, fieldName, signature);

	if (NULL == fieldId)
	{
		lprintf("could not get field %s with signature %s\n", fieldName, signature);
		return false;
	}

	field = env->GetObjectField(object, fieldId);

	return true;
}


static bool setIntField(JNIEnv *env, jobject object, const char* fieldName, jint value)
{
	jclass clazz = env->GetObjectClass(object);

	if (NULL == clazz)
	{
		lprintf("could not get class for object\n");
		return false;
	}

	jfieldID fieldId = env->GetFieldID(clazz, fieldName, "I");

	if (NULL == fieldId)
	{
		lprintf("could not get field %s with signature I\n", fieldName);
		return false;
	}

	env->SetIntField(object, fieldId, value);

	return true;
}


static bool setFloatField(JNIEnv *env, jobject object, const char* fieldName, jfloat value)
{
	jclass clazz = env->GetObjectClass(object);

	if (NULL == clazz)
	{
		lprintf("could not get class for object\n");
		return false;
	}

	jfieldID fieldId = env->GetFieldID(clazz, fieldName, "F");

	if (NULL == fieldId)
	{
		lprintf("could not get field %s with signature F\n", fieldName);
		return false;
	}

	env->SetFloatField(object, fieldId, value);

	return true;
}


static bool setObjectField(JNIEnv *env, jobject object, const char* fieldName, const char* signature, jobject value)
{
	jclass clazz = env->GetObjectClass(object);

	if (NULL == clazz)
	{
		lprintf("could not get class for object\n");
		return false;
	}

	jfieldID fieldId = env->GetFieldID(clazz, fieldName, signature);

	if (NULL == fieldId)
	{
		lprintf("could not get field %s with signature %s\n", fieldName, signature);
		return false;
	}

	env->SetObjectField(object, fieldId, value);

	return true;
}


static bool getStaticField(JNIEnv *env, const char* className, const char* fieldName, const char* signature, jobject& field)
{
	jclass clazz = env->FindClass(className);

	if (NULL == clazz)
	{
		lprintf("could not find class %s\n", className);
		return false;
	}

	jfieldID fieldId = env->GetFieldID(clazz, fieldName, signature);

	if (NULL == fieldId)
	{
		lprintf("could not get field %s with signature %s\n", fieldName, signature);
		return false;
	}

	field = env->GetStaticObjectField(clazz, fieldId);

	return true;
}


static bool call(JNIEnv *env, jobject object, const char* typeName, const char* methodName, 
	const char* signature, const jvalue* params)
{
	jclass clazz = env->FindClass(typeName);

	if (NULL == clazz)
	{
		lprintf("could not find class %s\n", typeName);
		return false;
	}

	jmethodID mid = env->GetMethodID(clazz, methodName, signature);

	if (NULL == mid)
	{
		lprintf("could not find method %s with signature %s in type %s\n", methodName, signature, typeName);
		return false;
	}

	env->CallVoidMethodA(object, mid, params);

	return true;
}


static bool callStaticObject(JNIEnv *env, const char* typeName, const char* methodName, 
	const char* signature, const jvalue* params, jobject& returnValue)
{
	jclass clazz = env->FindClass(typeName);

	if (NULL == clazz)
	{
		lprintf("could not find class %s\n", typeName);
		return false;
	}

	jmethodID mid = env->GetStaticMethodID(clazz, methodName, signature);

	if (NULL == mid)
	{
		lprintf("could not find method %s with signature %s in type %s\n", methodName, signature, typeName);
		return false;
	}

	returnValue = env->CallStaticObjectMethodA(clazz, mid, params);

	return true;
}


static bool copyBuffer(JNIEnv *env, jobject jMesh, const char* jBufferName, void* cData, size_t size)
{
	jobject jBuffer = NULL;

	if (!getField(env, jMesh, jBufferName, "Ljava/nio/ByteBuffer;", jBuffer))
	{
		return false;
	}

	if (env->GetDirectBufferCapacity(jBuffer) != size)
	{
		lprintf("invalid direct buffer, expected %u, got %u\n", size, env->GetDirectBufferCapacity(jBuffer));
		return false;
	}

	void* jBufferPtr = env->GetDirectBufferAddress(jBuffer);

	if (NULL == jBufferPtr)
	{
		lprintf("could not access direct buffer\n");
		return false;
	}

	memcpy(jBufferPtr, cData, size);

	return true;
}


static bool copyBufferArray(JNIEnv *env, jobject jMesh, const char* jBufferName, int index, void* cData, size_t size)
{
	jobject jBufferArray = NULL;

	if (!getField(env, jMesh, jBufferName, "[Ljava/nio/ByteBuffer;", jBufferArray))
	{
		return false;
	}

	jobject jBuffer = env->GetObjectArrayElement((jobjectArray) jBufferArray, index);

	if (env->GetDirectBufferCapacity(jBuffer) != size)
	{
		lprintf("invalid direct buffer, expected %u, got %u\n", size, env->GetDirectBufferCapacity(jBuffer));
		return false;
	}

	void* jBufferPtr = env->GetDirectBufferAddress(jBuffer);

	if (NULL == jBufferPtr)
	{
		lprintf("could not access direct buffer\n");
		return false;
	}

	memcpy(jBufferPtr, cData, size);

	return true;
}



static bool loadMeshes(JNIEnv *env, const aiScene* cScene, jobject& jScene)
{
	for (unsigned int meshNr = 0; meshNr < cScene->mNumMeshes; meshNr++)
	{
		const aiMesh *cMesh = cScene->mMeshes[meshNr];

		lprintf("converting mesh %s ...\n", cMesh->mName.C_Str());

		/* create mesh */
		jobject jMesh = NULL;

		if (!createInstance(env, "jassimp/AiMesh", jMesh)) 
		{
			return false;
		}

		
		/* add mesh to m_meshes java.util.List */
		jobject jMeshes = NULL;

		if (!getField(env, jScene, "m_meshes", "Ljava/util/List;", jMeshes))
		{
			return false;
		}

		jvalue addParams[1];
		addParams[0].l = jMesh;
		if (!call(env, jMeshes, "java/util/Collection", "add", "(Ljava/lang/Object;)Z", addParams))
		{
			return false;
		}


		/* set general mesh data in java */
		jvalue setTypesParams[1];
		setTypesParams[0].i = cMesh->mPrimitiveTypes;
		if (!call(env, jMesh, "jassimp/AiMesh", "setPrimitiveTypes", "(I)V", setTypesParams))
		{
			return false;
		}


		if (!setIntField(env, jMesh, "m_materialIndex", cMesh->mMaterialIndex))
		{
			return false;
		}

		if (!setObjectField(env, jMesh, "m_name", "Ljava/lang/String;", env->NewStringUTF(cMesh->mName.C_Str())))
		{
			return false;
		}


		/* determine face buffer size */
		bool isPureTriangle = cMesh->mPrimitiveTypes == aiPrimitiveType_TRIANGLE;
		size_t faceBufferSize;
		if (isPureTriangle) 
		{
			faceBufferSize = cMesh->mNumFaces * 3 * sizeof(unsigned int);
		}
		else
		{
			int numVertexReferences = 0;
			for (unsigned int face = 0; face < cMesh->mNumFaces; face++)
			{
				numVertexReferences += cMesh->mFaces[face].mNumIndices;
			}

			faceBufferSize = numVertexReferences * sizeof(unsigned int);
		}


		/* allocate buffers - we do this from java so they can be garbage collected */
		jvalue allocateBuffersParams[4];
		allocateBuffersParams[0].i = cMesh->mNumVertices;
		allocateBuffersParams[1].i = cMesh->mNumFaces;
		allocateBuffersParams[2].z = isPureTriangle;
		allocateBuffersParams[3].i = (jint) faceBufferSize;
		if (!call(env, jMesh, "jassimp/AiMesh", "allocateBuffers", "(IIZI)V", allocateBuffersParams))
		{
			return false;
		}


		if (cMesh->mNumVertices > 0)
		{
			/* push vertex data to java */
			if (!copyBuffer(env, jMesh, "m_vertices", cMesh->mVertices, cMesh->mNumVertices * sizeof(aiVector3D)))
			{
				lprintf("could not copy vertex data\n");
				return false;
			}

			lprintf("    with %u vertices\n", cMesh->mNumVertices);
		}


		/* push face data to java */
		if (cMesh->mNumFaces > 0)
		{
			if (isPureTriangle) 
			{
				char* faceBuffer = (char*) malloc(faceBufferSize);

				size_t faceDataSize = 3 * sizeof(unsigned int);
				for (unsigned int face = 0; face < cMesh->mNumFaces; face++)
				{
					memcpy(faceBuffer + face * faceDataSize, cMesh->mFaces[face].mIndices, faceDataSize);
				}

				bool res = copyBuffer(env, jMesh, "m_faces", faceBuffer, faceBufferSize);

				free(faceBuffer);

				if (!res) 
				{
					lprintf("could not copy face data\n");
					return false;
				}
			}
			else
			{
				char* faceBuffer = (char*) malloc(faceBufferSize);
				char* offsetBuffer = (char*) malloc(cMesh->mNumFaces * sizeof(unsigned int));

				size_t faceBufferPos = 0;
				for (unsigned int face = 0; face < cMesh->mNumFaces; face++)
				{
					size_t faceBufferOffset = faceBufferPos / sizeof(unsigned int);
					memcpy(offsetBuffer + face * sizeof(unsigned int), &faceBufferOffset, sizeof(unsigned int));

					size_t faceDataSize = cMesh->mFaces[face].mNumIndices * sizeof(unsigned int);
					memcpy(faceBuffer + faceBufferPos, cMesh->mFaces[face].mIndices, faceDataSize);
					faceBufferPos += faceDataSize;
				}
		
				if (faceBufferPos != faceBufferSize)
				{
					/* this should really not happen */
					lprintf("faceBufferPos %u, faceBufferSize %u\n", faceBufferPos, faceBufferSize);
					env->FatalError("error copying face data");
					exit(-1);
				}


				bool res = copyBuffer(env, jMesh, "m_faces", faceBuffer, faceBufferSize);
				res &= copyBuffer(env, jMesh, "m_faceOffsets", offsetBuffer, cMesh->mNumFaces * sizeof(unsigned int));

				free(faceBuffer);
				free(offsetBuffer);

				if (!res) 
				{
					lprintf("could not copy face data\n");
					return false;
				}
			}

			lprintf("    with %u faces\n", cMesh->mNumFaces);
		}


		/* push normals to java */
		if (cMesh->HasNormals())
		{
			jvalue allocateDataChannelParams[2];
			allocateDataChannelParams[0].i = 0;
			allocateDataChannelParams[1].i = 0;
			if (!call(env, jMesh, "jassimp/AiMesh", "allocateDataChannel", "(II)V", allocateDataChannelParams))
			{
				lprintf("could not allocate normal data channel\n");
				return false;
			}
			if (!copyBuffer(env, jMesh, "m_normals", cMesh->mNormals, cMesh->mNumVertices * 3 * sizeof(float)))
			{
				lprintf("could not copy normal data\n");
				return false;
			}

			lprintf("    with normals\n");
		}


		/* push tangents to java */
		if (cMesh->mTangents != NULL)
		{
			jvalue allocateDataChannelParams[2];
			allocateDataChannelParams[0].i = 1;
			allocateDataChannelParams[1].i = 0;
			if (!call(env, jMesh, "jassimp/AiMesh", "allocateDataChannel", "(II)V", allocateDataChannelParams))
			{
				lprintf("could not allocate tangents data channel\n");
				return false;
			}
			if (!copyBuffer(env, jMesh, "m_tangents", cMesh->mTangents, cMesh->mNumVertices * 3 * sizeof(float)))
			{
				lprintf("could not copy tangents data\n");
				return false;
			}

			lprintf("    with tangents\n");
		}


		/* push bitangents to java */
		if (cMesh->mBitangents != NULL)
		{
			jvalue allocateDataChannelParams[2];
			allocateDataChannelParams[0].i = 2;
			allocateDataChannelParams[1].i = 0;
			if (!call(env, jMesh, "jassimp/AiMesh", "allocateDataChannel", "(II)V", allocateDataChannelParams))
			{
				lprintf("could not allocate bitangents data channel\n");
				return false;
			}
			if (!copyBuffer(env, jMesh, "m_bitangents", cMesh->mBitangents, cMesh->mNumVertices * 3 * sizeof(float)))
			{
				lprintf("could not copy bitangents data\n");
				return false;
			}

			lprintf("    with bitangents\n");
		}


		/* push color sets to java */
		for (int c = 0; c < AI_MAX_NUMBER_OF_COLOR_SETS; c++)
		{
			if (cMesh->mColors[c] != NULL)
			{
				jvalue allocateDataChannelParams[2];
				allocateDataChannelParams[0].i = 3;
				allocateDataChannelParams[1].i = c;
				if (!call(env, jMesh, "jassimp/AiMesh", "allocateDataChannel", "(II)V", allocateDataChannelParams))
				{
					lprintf("could not allocate colorset data channel\n");
					return false;
				}
				if (!copyBufferArray(env, jMesh, "m_colorsets", c, cMesh->mColors[c], cMesh->mNumVertices * 4 * sizeof(float)))
				{
					lprintf("could not copy colorset data\n");
					return false;
				}

				lprintf("    with colorset[%d]\n", c);
			}
		}


		/* push tex coords to java */
		for (int c = 0; c < AI_MAX_NUMBER_OF_TEXTURECOORDS; c++)
		{
			if (cMesh->mTextureCoords[c] != NULL)
			{
				jvalue allocateDataChannelParams[2];

				switch (cMesh->mNumUVComponents[c])
				{
				case 1:
					allocateDataChannelParams[0].i = 4;
					break;
				case 2:
					allocateDataChannelParams[0].i = 5;
					break;
				case 3:
					allocateDataChannelParams[0].i = 6;
					break;
				default:
					return false;
				}

				allocateDataChannelParams[1].i = c;
				if (!call(env, jMesh, "jassimp/AiMesh", "allocateDataChannel", "(II)V", allocateDataChannelParams))
				{
					lprintf("could not allocate texture coordinates data channel\n");
					return false;
				}

				/* gather data */
				size_t coordBufferSize = cMesh->mNumVertices * cMesh->mNumUVComponents[c] * sizeof(float);
				char* coordBuffer = (char*) malloc(coordBufferSize);
				size_t coordBufferOffset = 0;

				for (unsigned int v = 0; v < cMesh->mNumVertices; v++)
				{
					memcpy(coordBuffer + coordBufferOffset, &cMesh->mTextureCoords[c][v], cMesh->mNumUVComponents[c] * sizeof(float));
					coordBufferOffset += cMesh->mNumUVComponents[c] * sizeof(float);
				}

				if (coordBufferOffset != coordBufferSize)
				{
					/* this should really not happen */
					lprintf("coordBufferPos %u, coordBufferSize %u\n", coordBufferOffset, coordBufferSize);
					env->FatalError("error copying coord data");
					exit(-1);
				}

				bool res = copyBufferArray(env, jMesh, "m_texcoords", c, coordBuffer, coordBufferSize);

				free(coordBuffer);

				if (!res)
				{
					lprintf("could not copy texture coordinates data\n");
					return false;
				}

				lprintf("    with %uD texcoord[%d]\n", cMesh->mNumUVComponents[c], c);
			}
		}


		for (unsigned int b = 0; b < cMesh->mNumBones; b++)
		{
			aiBone *cBone = cMesh->mBones[b];

			jobject jBone;
			if (!createInstance(env, "jassimp/AiBone", jBone)) 
			{
				return false;
			}

			/* add bone to bone list */
			jobject jBones = NULL;

			if (!getField(env, jMesh, "m_bones", "Ljava/util/List;", jBones))
			{
				return false;
			}

			jvalue addParams[1];
			addParams[0].l = jBone;
			if (!call(env, jBones, "java/util/Collection", "add", "(Ljava/lang/Object;)Z", addParams))
			{
				return false;
			}

			/* set bone data */
			if (!setObjectField(env, jBone, "m_name", "Ljava/lang/String;", env->NewStringUTF(cBone->mName.C_Str())))
			{
				return false;
			}

			/* add bone weights */
			for (unsigned int w = 0; w < cBone->mNumWeights; w++)
			{
				jobject jBoneWeight;
				if (!createInstance(env, "jassimp/AiBoneWeight", jBoneWeight)) 
				{
					return false;
				}

				/* add boneweight to bone list */
				jobject jBoneWeights = NULL;

				if (!getField(env, jBone, "m_boneWeights", "Ljava/util/List;", jBoneWeights))
				{
					return false;
				}


				/* copy offset matrix */
				jfloatArray jMatrixArr = env->NewFloatArray(16);
				env->SetFloatArrayRegion(jMatrixArr, 0, 16, (jfloat*) &cBone->mOffsetMatrix);

				jvalue wrapParams[1];
				wrapParams[0].l = jMatrixArr;
				jobject jMatrix;
				
				if (!callStaticObject(env, "Ljassimp/Jassimp;", "wrapMatrix", "([F)Ljava/lang/Object;", wrapParams, jMatrix)) 
				{
					return false;
				}

				if (!setObjectField(env, jBone, "m_offsetMatrix", "Ljava/lang/Object;", jMatrix))
				{
					return false;
				}


				jvalue addBwParams[1];
				addBwParams[0].l = jBoneWeight;
				if (!call(env, jBoneWeights, "java/util/Collection", "add", "(Ljava/lang/Object;)Z", addBwParams))
				{
					return false;
				}


				if (!setIntField(env, jBoneWeight, "m_vertexId", cBone->mWeights[w].mVertexId))
				{
					return false;
				}
				
				if (!setFloatField(env, jBoneWeight, "m_weight", cBone->mWeights[w].mWeight))
				{
					return false;
				}
			}
		}
	}

	return true;
}


static bool loadSceneNode(JNIEnv *env, const aiNode *cNode, jobject parent, jobject* loadedNode = NULL) 
{
	lprintf("   converting node %s ...\n", cNode->mName.C_Str());

	/* wrap matrix */
	jfloatArray jMatrixArr = env->NewFloatArray(16);
	env->SetFloatArrayRegion(jMatrixArr, 0, 16, (jfloat*) &cNode->mTransformation);

	jvalue wrapMatParams[1];
	wrapMatParams[0].l = jMatrixArr;
	jobject jMatrix;
				
	if (!callStaticObject(env, "Ljassimp/Jassimp;", "wrapMatrix", "([F)Ljava/lang/Object;", wrapMatParams, jMatrix)) 
	{
		return false;
	}


	/* create mesh references array */
	jintArray jMeshrefArr = env->NewIntArray(cNode->mNumMeshes);
	jint *temp = (jint*) malloc(sizeof(jint) * cNode->mNumMeshes);

	for (unsigned int i = 0; i < cNode->mNumMeshes; i++)
	{
		temp[i] = cNode->mMeshes[i];
	}
	env->SetIntArrayRegion(jMeshrefArr, 0, cNode->mNumMeshes, (jint*) temp);

	free(temp);


	/* convert name */
	jstring jNodeName = env->NewStringUTF(cNode->mName.C_Str());


	/* wrap scene node */
	jvalue wrapNodeParams[4];
	wrapNodeParams[0].l = parent;
	wrapNodeParams[1].l = jMatrix;
	wrapNodeParams[2].l = jMeshrefArr;
	wrapNodeParams[3].l = jNodeName;
	jobject jNode;
	if (!callStaticObject(env, "Ljassimp/Jassimp;", "wrapSceneNode", 
		"(Ljava/lang/Object;Ljava/lang/Object;[ILjava/lang/String;)Ljava/lang/Object;", wrapNodeParams, jNode)) 
	{
		return false;
	}


	/* and recurse */
	for (unsigned int c = 0; c < cNode->mNumChildren; c++)
	{
		if (!loadSceneNode(env, cNode->mChildren[c], jNode))
		{
			return false;
		}
	}

	if (NULL != loadedNode)
	{
		*loadedNode = jNode;
	}

	return true;
}


static bool loadSceneGraph(JNIEnv *env, const aiScene* cScene, jobject& jScene)
{
	lprintf("converting scene graph ...\n");

	if (NULL != cScene->mRootNode)
	{
		jobject jRoot;

		if (!loadSceneNode(env, cScene->mRootNode, NULL, &jRoot))
		{
			return false;
		}

		if (!setObjectField(env, jScene, "m_sceneRoot", "Ljava/lang/Object;", jRoot))
		{
			return false;
		}
	}

	lprintf("converting scene graph finished\n");

	return true;
}


static bool loadMaterials(JNIEnv *env, const aiScene* cScene, jobject& jScene) 
{
	for (unsigned int m = 0; m < cScene->mNumMaterials; m++)
	{
		const aiMaterial* cMaterial = cScene->mMaterials[m];

		lprintf("converting material ...\n", m);

		jobject jMaterial = NULL;

		if (!createInstance(env, "jassimp/AiMaterial", jMaterial))
		{
			return false;
		}

		/* add material to m_materials java.util.List */
		jobject jMaterials = NULL;

		if (!getField(env, jScene, "m_materials", "Ljava/util/List;", jMaterials))
		{
			return false;
		}

		jvalue addMatParams[1];
		addMatParams[0].l = jMaterial;
		if (!call(env, jMaterials, "java/util/Collection", "add", "(Ljava/lang/Object;)Z", addMatParams))
		{
			return false;
		}

		/* set texture numbers */
		for (int ttInd = aiTextureType_DIFFUSE; ttInd < aiTextureType_UNKNOWN; ttInd++) 
		{
			aiTextureType tt = static_cast<aiTextureType>(ttInd);

			unsigned int num = cMaterial->GetTextureCount(tt);

			lprintf("   found %d textures of type %d ...\n", num, ttInd);

			jvalue setNumberParams[2];
			setNumberParams[0].i = ttInd;
			setNumberParams[1].i = num;

			if (!call(env, jMaterial, "jassimp/AiMaterial", "setTextureNumber", "(II)V", setNumberParams))
			{
				return false;
			}
		}


		for (unsigned int p = 0; p < cMaterial->mNumProperties; p++)
		{
			//printf("%s - %u - %u\n", cScene->mMaterials[m]->mProperties[p]->mKey.C_Str(), 
			//	cScene->mMaterials[m]->mProperties[p]->mSemantic,
			//	cScene->mMaterials[m]->mProperties[p]->mDataLength);

			const aiMaterialProperty* cProperty = cMaterial->mProperties[p];

			lprintf("   converting property %s ...\n", cProperty->mKey.C_Str());

			jobject jProperty = NULL;

			jvalue constructorParams[5];
			constructorParams[0].l = env->NewStringUTF(cProperty->mKey.C_Str());
			constructorParams[1].i = cProperty->mSemantic;
			constructorParams[2].i = cProperty->mIndex;
			constructorParams[3].i = cProperty->mType;


			/* special case conversion for color3 */
			if (NULL != strstr(cProperty->mKey.C_Str(), "clr") && 
				cProperty->mType == aiPTI_Float &&
				cProperty->mDataLength == 3 * sizeof(float)) 
			{
				jobject jData = NULL;

				/* wrap color */
				jvalue wrapColorParams[3];
				wrapColorParams[0].f = ((float*) cProperty->mData)[0];
				wrapColorParams[1].f = ((float*) cProperty->mData)[1];
				wrapColorParams[2].f = ((float*) cProperty->mData)[2];
				if (!callStaticObject(env, "Ljassimp/Jassimp;", "wrapColor3", "(FFF)Ljava/lang/Object;", wrapColorParams, jData)) 
				{
					return false;
				}

				constructorParams[4].l = jData;
				if (!createInstance(env, "jassimp/AiMaterial$Property", "(Ljava/lang/String;IIILjava/lang/Object;)V", 
					constructorParams, jProperty))
				{
					return false;
				}
			}
			/* special case conversion for color4 */
			else if (NULL != strstr(cProperty->mKey.C_Str(), "clr") && 
				cProperty->mType == aiPTI_Float &&
				cProperty->mDataLength == 4 * sizeof(float)) 
			{
				jobject jData = NULL;

				/* wrap color */
				jvalue wrapColorParams[4];
				wrapColorParams[0].f = ((float*) cProperty->mData)[0];
				wrapColorParams[1].f = ((float*) cProperty->mData)[1];
				wrapColorParams[2].f = ((float*) cProperty->mData)[2];
				wrapColorParams[3].f = ((float*) cProperty->mData)[3];
				if (!callStaticObject(env, "Ljassimp/Jassimp;", "wrapColor4", "(FFFF)Ljava/lang/Object;", wrapColorParams, jData)) 
				{
					return false;
				}

				constructorParams[4].l = jData;
				if (!createInstance(env, "jassimp/AiMaterial$Property", "(Ljava/lang/String;IIILjava/lang/Object;)V", 
					constructorParams, jProperty))
				{
					return false;
				}
			}
			else if (cProperty->mType == aiPTI_Float && cProperty->mDataLength == sizeof(float)) 
			{
				jobject jData = NULL;

				jvalue newFloatParams[1];
				newFloatParams[0].f = ((float*) cProperty->mData)[0];
				if (!createInstance(env, "java/lang/Float", "(F)V", newFloatParams, jData))
				{
					return false;
				}

				constructorParams[4].l = jData;
				if (!createInstance(env, "jassimp/AiMaterial$Property", "(Ljava/lang/String;IIILjava/lang/Object;)V", 
					constructorParams, jProperty))
				{
					return false;
				}
			}
			else if (cProperty->mType == aiPTI_Integer && cProperty->mDataLength == sizeof(int)) 
			{
				jobject jData = NULL;

				jvalue newIntParams[1];
				newIntParams[0].i = ((int*) cProperty->mData)[0];
				if (!createInstance(env, "java/lang/Integer", "(I)V", newIntParams, jData))
				{
					return false;
				}

				constructorParams[4].l = jData;
				if (!createInstance(env, "jassimp/AiMaterial$Property", "(Ljava/lang/String;IIILjava/lang/Object;)V", 
					constructorParams, jProperty))
				{
					return false;
				}
			}
			else if (cProperty->mType == aiPTI_String) 
			{
				/* skip length prefix */
				jobject jData = env->NewStringUTF(cProperty->mData + 4);

				constructorParams[4].l = jData;
				if (!createInstance(env, "jassimp/AiMaterial$Property", "(Ljava/lang/String;IIILjava/lang/Object;)V", 
					constructorParams, jProperty))
				{
					return false;
				}
			}
			else 
			{
				constructorParams[4].i = cProperty->mDataLength;

				/* generic copy code, uses dump ByteBuffer on java side */
				if (!createInstance(env, "jassimp/AiMaterial$Property", "(Ljava/lang/String;IIII)V", constructorParams, jProperty))
				{
					return false;
				}

				jobject jBuffer = NULL;

				if (!getField(env, jProperty, "m_data", "Ljava/lang/Object;", jBuffer))
				{
					return false;
				}

				if (env->GetDirectBufferCapacity(jBuffer) != cProperty->mDataLength)
				{
					lprintf("invalid direct buffer\n");
					return false;
				}

				void* jBufferPtr = env->GetDirectBufferAddress(jBuffer);

				if (NULL == jBufferPtr)
				{
					lprintf("could not access direct buffer\n");
					return false;
				}

				memcpy(jBufferPtr, cProperty->mData, cProperty->mDataLength);
			}


			/* add property */
			jobject jProperties = NULL;

			if (!getField(env, jMaterial, "m_properties", "Ljava/util/List;", jProperties))
			{
				return false;
			}

			jvalue addPropParams[1];
			addPropParams[0].l = jProperty;
			if (!call(env, jProperties, "java/util/Collection", "add", "(Ljava/lang/Object;)Z", addPropParams))
			{
				return false;
			}
		}
	}

	lprintf("materials finished\n");

	return true;
}


static bool loadAnimations(JNIEnv *env, const aiScene* cScene, jobject& jScene) 
{
	lprintf("converting %d animations ...\n", cScene->mNumAnimations);

	for (unsigned int a = 0; a < cScene->mNumAnimations; a++)
	{
		const aiAnimation *cAnimation = cScene->mAnimations[a];

		lprintf("   converting animation %s ...\n", cAnimation->mName.C_Str());

		jobject jAnimation;
		jvalue newAnimParams[3];
		newAnimParams[0].l = env->NewStringUTF(cAnimation->mName.C_Str());
		newAnimParams[1].d = cAnimation->mDuration;
		newAnimParams[2].d = cAnimation->mTicksPerSecond;

		if (!createInstance(env, "jassimp/AiAnimation", "(Ljava/lang/String;DD)V", newAnimParams, jAnimation))
		{
			return false;
		}

		/* add animation to m_animations java.util.List */
		jobject jAnimations = NULL;

		if (!getField(env, jScene, "m_animations", "Ljava/util/List;", jAnimations))
		{
			return false;
		}

		jvalue addParams[1];
		addParams[0].l = jAnimation;
		if (!call(env, jAnimations, "java/util/Collection", "add", "(Ljava/lang/Object;)Z", addParams))
		{
			return false;
		}


		for (unsigned int c = 0; c < cAnimation->mNumChannels; c++)
		{
			const aiNodeAnim *cNodeAnim = cAnimation->mChannels[c];

			jobject jNodeAnim;
			jvalue newNodeAnimParams[6];
			newNodeAnimParams[0].l = env->NewStringUTF(cNodeAnim->mNodeName.C_Str());
			newNodeAnimParams[1].i = cNodeAnim->mNumPositionKeys;
			newNodeAnimParams[2].i = cNodeAnim->mNumRotationKeys;
			newNodeAnimParams[3].i = cNodeAnim->mNumScalingKeys;
			newNodeAnimParams[4].i = cNodeAnim->mPreState;
			newNodeAnimParams[5].i = cNodeAnim->mPostState;

			if (!createInstance(env, "jassimp/AiNodeAnim", "(Ljava/lang/String;IIIII)V", newNodeAnimParams, jNodeAnim))
			{
				return false;
			}


			/* add nodeanim to m_animations java.util.List */
			jobject jNodeAnims = NULL;

			if (!getField(env, jAnimation, "m_nodeAnims", "Ljava/util/List;", jNodeAnims))
			{
				return false;
			}

			jvalue addParams[1];
			addParams[0].l = jNodeAnim;
			if (!call(env, jNodeAnims, "java/util/Collection", "add", "(Ljava/lang/Object;)Z", addParams))
			{
				return false;
			}

			/* copy keys */
			if (!copyBuffer(env, jNodeAnim, "m_posKeys", cNodeAnim->mPositionKeys, 
				cNodeAnim->mNumPositionKeys * sizeof(aiVectorKey)))
			{
				return false;
			}

			if (!copyBuffer(env, jNodeAnim, "m_rotKeys", cNodeAnim->mRotationKeys, 
				cNodeAnim->mNumRotationKeys * sizeof(aiQuatKey)))
			{
				return false;
			}

			if (!copyBuffer(env, jNodeAnim, "m_scaleKeys", cNodeAnim->mScalingKeys, 
				cNodeAnim->mNumScalingKeys * sizeof(aiVectorKey)))
			{
				return false;
			}
		}
	}

	lprintf("converting animations finished\n");

	return true;
}


static bool loadLights(JNIEnv *env, const aiScene* cScene, jobject& jScene) 
{
	lprintf("converting %d lights ...\n", cScene->mNumLights);

	for (unsigned int l = 0; l < cScene->mNumLights; l++)
	{
		const aiLight *cLight = cScene->mLights[l];

		lprintf("converting light %s ...\n", cLight->mName.C_Str());

		/* wrap color nodes */
		jvalue wrapColorParams[3];
		wrapColorParams[0].f = cLight->mColorDiffuse.r;
		wrapColorParams[1].f = cLight->mColorDiffuse.g;
		wrapColorParams[2].f = cLight->mColorDiffuse.b;
		jobject jDiffuse;
		if (!callStaticObject(env, "Ljassimp/Jassimp;", "wrapColor3", "(FFF)Ljava/lang/Object;", wrapColorParams, jDiffuse)) 
		{
			return false;
		}

		wrapColorParams[0].f = cLight->mColorSpecular.r;
		wrapColorParams[1].f = cLight->mColorSpecular.g;
		wrapColorParams[2].f = cLight->mColorSpecular.b;
		jobject jSpecular;
		if (!callStaticObject(env, "Ljassimp/Jassimp;", "wrapColor3", "(FFF)Ljava/lang/Object;", wrapColorParams, jSpecular)) 
		{
			return false;
		}

		wrapColorParams[0].f = cLight->mColorAmbient.r;
		wrapColorParams[1].f = cLight->mColorAmbient.g;
		wrapColorParams[2].f = cLight->mColorAmbient.b;
		jobject jAmbient;
		if (!callStaticObject(env, "Ljassimp/Jassimp;", "wrapColor3", "(FFF)Ljava/lang/Object;", wrapColorParams, jAmbient)) 
		{
			return false;
		}


		/* wrap vec3 nodes */
		jvalue wrapVec3Params[3];
		wrapVec3Params[0].f = cLight->mPosition.x;
		wrapVec3Params[1].f = cLight->mPosition.y;
		wrapVec3Params[2].f = cLight->mPosition.z;
		jobject jPosition;
		if (!callStaticObject(env, "Ljassimp/Jassimp;", "wrapVec3", "(FFF)Ljava/lang/Object;", wrapVec3Params, jPosition)) 
		{
			return false;
		}

		wrapVec3Params[0].f = cLight->mPosition.x;
		wrapVec3Params[1].f = cLight->mPosition.y;
		wrapVec3Params[2].f = cLight->mPosition.z;
		jobject jDirection;
		if (!callStaticObject(env, "Ljassimp/Jassimp;", "wrapVec3", "(FFF)Ljava/lang/Object;", wrapVec3Params, jDirection)) 
		{
			return false;
		}


		jobject jLight;

		jvalue params[12];
		params[0].l = env->NewStringUTF(cLight->mName.C_Str());;
		params[1].i = cLight->mType;
		params[2].l = jPosition;
		params[3].l = jDirection;
		params[4].f = cLight->mAttenuationConstant;
		params[5].f = cLight->mAttenuationLinear;
		params[6].f = cLight->mAttenuationQuadratic;
		params[7].l = jDiffuse;
		params[8].l = jSpecular;
		params[9].l = jAmbient;
		params[10].f = cLight->mAngleInnerCone;
		params[11].f = cLight->mAngleOuterCone;
		
		if (!createInstance(env, "jassimp/AiLight", "(Ljava/lang/String;ILjava/lang/Object;Ljava/lang/Object;FFFLjava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;FF)V",
			params, jLight))
		{
			return false;
		}

		/* add light to m_lights java.util.List */
		jobject jLights = NULL;

		if (!getField(env, jScene, "m_lights", "Ljava/util/List;", jLights))
		{
			return false;
		}

		jvalue addParams[1];
		addParams[0].l = jLight;
		if (!call(env, jLights, "java/util/Collection", "add", "(Ljava/lang/Object;)Z", addParams))
		{
			return false;
		}
	}

	lprintf("converting lights finished ...\n"); 

	return true;
}


static bool loadCameras(JNIEnv *env, const aiScene* cScene, jobject& jScene) 
{
	lprintf("converting %d cameras ...\n", cScene->mNumCameras);

	for (unsigned int c = 0; c < cScene->mNumCameras; c++)
	{
		const aiCamera *cCamera = cScene->mCameras[c];

		lprintf("converting camera %s ...\n", cCamera->mName.C_Str());

		/* wrap color nodes */
		jvalue wrapPositionParams[3];
		wrapPositionParams[0].f = cCamera->mPosition.x;
		wrapPositionParams[1].f = cCamera->mPosition.y;
		wrapPositionParams[2].f = cCamera->mPosition.z;
		jobject jPosition;
		if (!callStaticObject(env, "Ljassimp/Jassimp;", "wrapVec3", "(FFF)Ljava/lang/Object;", wrapPositionParams, jPosition)) 
		{
			return false;
		}

		wrapPositionParams[0].f = cCamera->mUp.x;
		wrapPositionParams[1].f = cCamera->mUp.y;
		wrapPositionParams[2].f = cCamera->mUp.z;
		jobject jUp;
		if (!callStaticObject(env, "Ljassimp/Jassimp;", "wrapVec3", "(FFF)Ljava/lang/Object;", wrapPositionParams, jUp)) 
		{
			return false;
		}

		wrapPositionParams[0].f = cCamera->mLookAt.x;
		wrapPositionParams[1].f = cCamera->mLookAt.y;
		wrapPositionParams[2].f = cCamera->mLookAt.z;
		jobject jLookAt;
		if (!callStaticObject(env, "Ljassimp/Jassimp;", "wrapVec3", "(FFF)Ljava/lang/Object;", wrapPositionParams, jLookAt)) 
		{
			return false;
		}


		jobject jCamera;

		jvalue params[8];
		params[0].l = env->NewStringUTF(cCamera->mName.C_Str());
		params[1].l = jPosition;
		params[2].l = jUp;
		params[3].l = jLookAt;
		params[4].f = cCamera->mHorizontalFOV;
		params[5].f = cCamera->mClipPlaneNear;
		params[6].f = cCamera->mClipPlaneFar;
		params[7].f = cCamera->mAspect;
		
		if (!createInstance(env, "jassimp/AiCamera", "(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;FFFF)V",
			params, jCamera))
		{
			return false;
		}

		/* add camera to m_cameras java.util.List */
		jobject jCameras = NULL;

		if (!getField(env, jScene, "m_cameras", "Ljava/util/List;", jCameras))
		{
			return false;
		}

		jvalue addParams[1];
		addParams[0].l = jCamera;
		if (!call(env, jCameras, "java/util/Collection", "add", "(Ljava/lang/Object;)Z", addParams))
		{
			return false;
		}
	}

	lprintf("converting cameras finished\n");

	return true;
}


JNIEXPORT jstring JNICALL Java_jassimp_Jassimp_getErrorString
  (JNIEnv *env, jclass jClazz)
{
	const char *err = aiGetErrorString();

	if (NULL == err)
	{
		return env->NewStringUTF("");
	}

	return env->NewStringUTF(err);
}


JNIEXPORT jobject JNICALL Java_jassimp_Jassimp_aiImportFile
  (JNIEnv *env, jclass jClazz, jstring jFilename, jlong postProcess)
{
	jobject jScene = NULL; 

	/* convert params */
	const char* cFilename = env->GetStringUTFChars(jFilename, NULL);


	lprintf("opening file: %s\n", cFilename);

	/* do import */
	const aiScene *cScene = aiImportFile(cFilename, (unsigned int) postProcess);

	if (!cScene)
	{
		lprintf("import file returned null\n");
		goto error;
	}

	if (!createInstance(env, "jassimp/AiScene", jScene)) 
	{
		goto error;
	}

	if (!loadMeshes(env, cScene, jScene))
	{
		goto error;
	}

	if (!loadMaterials(env, cScene, jScene))
	{
		goto error;
	}

	if (!loadAnimations(env, cScene, jScene))
	{
		goto error;
	}

	if (!loadLights(env, cScene, jScene))
	{
		goto error;
	}

	if (!loadCameras(env, cScene, jScene))
	{
		goto error;
	}

	if (!loadSceneGraph(env, cScene, jScene))
	{
		goto error;
	}

	/* jump over error handling section */
	goto end;

error:
	jclass exception = env->FindClass("java/io/IOException");

	if (NULL == exception)
	{
		/* thats really a problem because we cannot throw in this case */
		env->FatalError("could not throw java.io.IOException");
	}

	env->ThrowNew(exception, aiGetErrorString());

	lprintf("problem detected\n");

end:
	/* 
	 * NOTE: this releases all memory used in the native domain.
	 * Ensure all data has been passed to java before!
	 */
	aiReleaseImport(cScene);


	/* free params */
	env->ReleaseStringUTFChars(jFilename, cFilename);

	lprintf("return from native\n");

	return jScene;
}