package org.jrenner.fps.headless;

import com.badlogic.gdx.assets.loaders.FileHandleResolver;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.VertexAttribute;
import com.badlogic.gdx.graphics.g3d.model.data.ModelAnimation;
import com.badlogic.gdx.graphics.g3d.model.data.ModelData;
import com.badlogic.gdx.graphics.g3d.model.data.ModelMaterial;
import com.badlogic.gdx.graphics.g3d.model.data.ModelMesh;
import com.badlogic.gdx.graphics.g3d.model.data.ModelMeshPart;
import com.badlogic.gdx.graphics.g3d.model.data.ModelNode;
import com.badlogic.gdx.graphics.g3d.model.data.ModelNodeAnimation;
import com.badlogic.gdx.graphics.g3d.model.data.ModelNodeKeyframe;
import com.badlogic.gdx.graphics.g3d.model.data.ModelNodePart;
import com.badlogic.gdx.graphics.g3d.model.data.ModelTexture;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Quaternion;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ArrayMap;
import com.badlogic.gdx.utils.BaseJsonReader;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.JsonValue;

public class HeadlessG3dModelLoader extends HeadlessModelLoader<HeadlessModelLoader.HeadlessModelParameters> {
	public static final short VERSION_HI = 0;
	public static final short VERSION_LO = 1;
	protected final BaseJsonReader reader;

	public HeadlessG3dModelLoader(final BaseJsonReader reader) {
		this(reader, null);
	}

	public HeadlessG3dModelLoader(BaseJsonReader reader, FileHandleResolver resolver) {
		super(resolver);
		this.reader = reader;
	}

	@Override
	public ModelData loadModelData (FileHandle fileHandle, HeadlessModelParameters parameters) {
		return parseModel(fileHandle);
	}

	public ModelData parseModel (FileHandle handle) {
		JsonValue json = reader.parse(handle);
		ModelData model = new ModelData();
		JsonValue version = json.require("version");
		model.version[0] = version.getShort(0);
		model.version[1] = version.getShort(1);
		if (model.version[0] != VERSION_HI || model.version[1] != VERSION_LO)
			throw new GdxRuntimeException("Model version not supported");

		model.id = json.getString("id", "");
		parseMeshes(model, json);
		//parseMaterials(model, json, handle.parent().path());
		parseNodes(model, json);
		parseAnimations(model, json);
		return model;
	}

	private void parseMeshes (ModelData model, JsonValue json) {
		JsonValue meshes = json.require("meshes");

		model.meshes.ensureCapacity(meshes.size);
		for (JsonValue mesh = meshes.child; mesh != null; mesh = mesh.next) {
			ModelMesh jsonMesh = new ModelMesh();

			String id = mesh.getString("id", "");
			jsonMesh.id = id;

			JsonValue attributes = mesh.require("attributes");
			jsonMesh.attributes = parseAttributes(attributes);
			jsonMesh.vertices = mesh.require("vertices").asFloatArray();

			JsonValue meshParts = mesh.require("parts");
			Array<ModelMeshPart> parts = new Array<ModelMeshPart>();
			for (JsonValue meshPart = meshParts.child; meshPart != null; meshPart = meshPart.next) {
				ModelMeshPart jsonPart = new ModelMeshPart();
				String partId = meshPart.getString("id", null);
				if (id == null) {
					throw new GdxRuntimeException("Not id given for mesh part");
				}
				for (ModelMeshPart other : parts) {
					if (other.id.equals(partId)) {
						throw new GdxRuntimeException("Mesh part with id '" + partId + "' already in defined");
					}
				}
				jsonPart.id = partId;

				String type = meshPart.getString("type", null);
				if (type == null) {
					throw new GdxRuntimeException("No primitive type given for mesh part '" + partId + "'");
				}
				jsonPart.primitiveType = parseType(type);

				jsonPart.indices = meshPart.require("indices").asShortArray();
				parts.add(jsonPart);
			}
			jsonMesh.parts = parts.toArray(ModelMeshPart.class);
			model.meshes.add(jsonMesh);
		}
	}

	private int parseType (String type) {
		if (type.equals("TRIANGLES")) {
			return GL20.GL_TRIANGLES;
		} else if (type.equals("LINES")) {
			return GL20.GL_LINES;
		} else if (type.equals("POINTS")) {
			return GL20.GL_POINTS;
		} else if (type.equals("TRIANGLE_STRIP")) {
			return GL20.GL_TRIANGLE_STRIP;
		} else if (type.equals("LINE_STRIP")) {
			return GL20.GL_LINE_STRIP;
		} else {
			throw new GdxRuntimeException("Unknown primitive type '" + type
					+ "', should be one of triangle, trianglestrip, line, linestrip, lineloop or point");
		}
	}

	private VertexAttribute[] parseAttributes (JsonValue attributes) {
		Array<VertexAttribute> vertexAttributes = new Array<VertexAttribute>();
		int unit = 0;
		int blendWeightCount = 0;
		for (JsonValue value = attributes.child; value != null; value = value.next) {
			String attribute = value.asString();
			String attr = (String)attribute;
			if (attr.equals("POSITION")) {
				vertexAttributes.add(VertexAttribute.Position());
			} else if (attr.equals("NORMAL")) {
				vertexAttributes.add(VertexAttribute.Normal());
			} else if (attr.equals("COLOR")) {
				vertexAttributes.add(VertexAttribute.ColorUnpacked());
			} else if (attr.equals("COLORPACKED")) {
				vertexAttributes.add(VertexAttribute.Color());
			} else if (attr.equals("TANGENT")) {
				vertexAttributes.add(VertexAttribute.Tangent());
			} else if (attr.equals("BINORMAL")) {
				vertexAttributes.add(VertexAttribute.Binormal());
			} else if (attr.startsWith("TEXCOORD")) {
				vertexAttributes.add(VertexAttribute.TexCoords(unit++));
			} else if (attr.startsWith("BLENDWEIGHT")) {
				vertexAttributes.add(VertexAttribute.BoneWeight(blendWeightCount++));
			} else {
				throw new GdxRuntimeException("Unknown vertex attribute '" + attr
						+ "', should be one of position, normal, uv, tangent or binormal");
			}
		}
		return vertexAttributes.toArray(VertexAttribute.class);
	}

	private Array<ModelNode> parseNodes (ModelData model, JsonValue json) {
		JsonValue nodes = json.get("nodes");
		if (nodes == null) {
			throw new GdxRuntimeException("At least one node is required.");
		}

		model.nodes.ensureCapacity(nodes.size);
		for (JsonValue node = nodes.child; node != null; node = node.next) {
			model.nodes.add(parseNodesRecursively(node));
		}
		return model.nodes;
	}

	private final Quaternion tempQ = new Quaternion();

	private ModelNode parseNodesRecursively (JsonValue json) {
		ModelNode jsonNode = new ModelNode();

		String id = json.getString("id", null);
		if (id == null) throw new GdxRuntimeException("Node id missing.");
		jsonNode.id = id;

		JsonValue translation = json.get("translation");
		if (translation != null && translation.size != 3) throw new GdxRuntimeException("Node translation incomplete");
		jsonNode.translation = translation == null ? null : new Vector3(translation.getFloat(0), translation.getFloat(1),
				translation.getFloat(2));

		JsonValue rotation = json.get("rotation");
		if (rotation != null && rotation.size != 4) throw new GdxRuntimeException("Node rotation incomplete");
		jsonNode.rotation = rotation == null ? null : new Quaternion(rotation.getFloat(0), rotation.getFloat(1),
				rotation.getFloat(2), rotation.getFloat(3));

		JsonValue scale = json.get("scale");
		if (scale != null && scale.size != 3) throw new GdxRuntimeException("Node scale incomplete");
		jsonNode.scale = scale == null ? null : new Vector3(scale.getFloat(0), scale.getFloat(1), scale.getFloat(2));

		String meshId = json.getString("mesh", null);
		if (meshId != null) jsonNode.meshId = meshId;

		JsonValue materials = json.get("parts");
		if (materials != null) {
			jsonNode.parts = new ModelNodePart[materials.size];
			int i = 0;
			for (JsonValue material = materials.child; material != null; material = material.next, i++) {
				ModelNodePart nodePart = new ModelNodePart();

				String meshPartId = material.getString("meshpartid", null);
				String materialId = material.getString("materialid", null);
				if (meshPartId == null || materialId == null) {
					throw new GdxRuntimeException("Node " + id + " part is missing meshPartId or materialId");
				}
				nodePart.materialId = materialId;
				nodePart.meshPartId = meshPartId;

				JsonValue bones = material.get("bones");
				if (bones != null) {
					nodePart.bones = new ArrayMap<String, Matrix4>(true, bones.size, String.class, Matrix4.class);
					int j = 0;
					for (JsonValue bone = bones.child; bone != null; bone = bone.next, j++) {
						String nodeId = bone.getString("node", null);
						if (nodeId == null) throw new GdxRuntimeException("Bone node ID missing");

						Matrix4 transform = new Matrix4();

						JsonValue val = bone.get("translation");
						if (val != null && val.size >= 3) transform.translate(val.getFloat(0), val.getFloat(1), val.getFloat(2));

						val = bone.get("rotation");
						if (val != null && val.size >= 4)
							transform.rotate(tempQ.set(val.getFloat(0), val.getFloat(1), val.getFloat(2), val.getFloat(3)));

						val = bone.get("scale");
						if (val != null && val.size >= 3) transform.scale(val.getFloat(0), val.getFloat(1), val.getFloat(2));

						nodePart.bones.put(nodeId, transform);
					}
				}

				jsonNode.parts[i] = nodePart;
			}
		}

		JsonValue children = json.get("children");
		if (children != null) {
			jsonNode.children = new ModelNode[children.size];

			int i = 0;
			for (JsonValue child = children.child; child != null; child = child.next, i++) {
				jsonNode.children[i] = parseNodesRecursively(child);
			}
		}

		return jsonNode;
	}

	private void parseAnimations (ModelData model, JsonValue json) {
		JsonValue animations = json.get("animations");
		if (animations == null) return;

		model.animations.ensureCapacity(animations.size);

		for (JsonValue anim = animations.child; anim != null; anim = anim.next) {
			JsonValue nodes = anim.get("bones");
			if (nodes == null) continue;
			ModelAnimation animation = new ModelAnimation();
			model.animations.add(animation);
			animation.nodeAnimations.ensureCapacity(nodes.size);
			animation.id = anim.getString("id");
			for (JsonValue node = nodes.child; node != null; node = node.next) {
				JsonValue keyframes = node.get("keyframes");

				ModelNodeAnimation nodeAnim = new ModelNodeAnimation();
				animation.nodeAnimations.add(nodeAnim);
				nodeAnim.nodeId = node.getString("boneId");
				nodeAnim.keyframes.ensureCapacity(keyframes.size);

				for (JsonValue keyframe = keyframes.child; keyframe != null; keyframe = keyframe.next) {
					ModelNodeKeyframe kf = new ModelNodeKeyframe();
					nodeAnim.keyframes.add(kf);
					kf.keytime = keyframe.getFloat("keytime") / 1000.f;
					JsonValue translation = keyframe.get("translation");
					if (translation != null && translation.size == 3)
						kf.translation = new Vector3(translation.getFloat(0), translation.getFloat(1), translation.getFloat(2));
					JsonValue rotation = keyframe.get("rotation");
					if (rotation != null && rotation.size == 4)
						kf.rotation = new Quaternion(rotation.getFloat(0), rotation.getFloat(1), rotation.getFloat(2),
								rotation.getFloat(3));
					JsonValue scale = keyframe.get("scale");
					if (scale != null && scale.size == 3)
						kf.scale = new Vector3(scale.getFloat(0), scale.getFloat(1), scale.getFloat(2));
				}
			}
		}
	}
}