/*******************************************************************************
 * Copyright 2011 See AUTHORS file.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.badlogic.gdx.graphics.g3d.loader;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import com.badlogic.gdx.utils.Array;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.assets.AssetLoaderParameters;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.assets.loaders.FileHandleResolver;
import com.badlogic.gdx.assets.loaders.ModelLoader;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.Mesh;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.graphics.VertexAttribute;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.graphics.g3d.Model;
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.ModelNodePart;
import com.badlogic.gdx.graphics.g3d.model.data.ModelNode;
import com.badlogic.gdx.graphics.g3d.model.data.ModelTexture;
import com.badlogic.gdx.graphics.g3d.utils.TextureDescriptor;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.math.Quaternion;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.FloatArray;

/** Loads Wavefront OBJ files.
 * 
 * @author mzechner, espitz */
public class ObjLoader extends ModelLoader<ObjLoader.ObjLoaderParameters> {
	public static class ObjLoaderParameters extends AssetLoaderParameters<Model> {
		public boolean flipV;
		public ObjLoaderParameters() {}
		public ObjLoaderParameters(boolean flipV) {
			this.flipV = flipV;
		}
	}
	
	final FloatArray verts = new FloatArray(300);
	final FloatArray norms = new FloatArray(300);
	final FloatArray uvs = new FloatArray(200);
	final Array<Group> groups = new Array<Group>(10);

	public ObjLoader () {
		this(null);
	}
	
	public ObjLoader (FileHandleResolver resolver) {
		super(resolver);
	}

	/** @deprecated Use {@link ObjLoader#loadModel(FileHandle)} instead.<p>
	 * Loads a Wavefront OBJ file from a given file handle.
	 * 
	 * @param file the FileHandle */
	public Model loadObj (FileHandle file) {
		return loadModel(file);
	}

	/** @deprecated Use {@link ObjLoader#loadModel(FileHandle, boolean)} instead.<p>
	 * Loads a Wavefront OBJ file from a given file handle.
	 * 
	 * @param file the FileHandle
	 * @param flipV whether to flip the v texture coordinate (Blender, Wings3D, et al) 
	 * */
	public Model loadObj (FileHandle file, boolean flipV) {
		return loadModel(file, flipV);
	}
	
	/** Directly load the model on the calling thread. The model with not be managed by an {@link AssetManager}. */
	public Model loadModel(final FileHandle fileHandle, boolean flipV) {
		return loadModel(fileHandle, new ObjLoaderParameters(flipV));
	}
	
	@Override
	public ModelData loadModelData (FileHandle file, ObjLoaderParameters parameters) {
		return loadModelData(file, parameters == null ? false : parameters.flipV);
	}
	
	protected ModelData loadModelData (FileHandle file, boolean flipV) {
		String line;
		String[] tokens;
		char firstChar;
		MtlLoader mtl = new MtlLoader();

		// Create a "default" Group and set it as the active group, in case
		// there are no groups or objects defined in the OBJ file.
		Group activeGroup = new Group("default");
		groups.add(activeGroup);

		BufferedReader reader = new BufferedReader(new InputStreamReader(file.read()), 4096);
		int id = 0;
		try {
			while ((line = reader.readLine()) != null) {

				tokens = line.split("\\s+");
				if (tokens.length < 1)
					break;

				if (tokens[0].length() == 0) {
					continue;
				} else if ((firstChar = tokens[0].toLowerCase().charAt(0)) == '#') {
					continue;
				} else if (firstChar == 'v') {
					if (tokens[0].length() == 1) {
						verts.add(Float.parseFloat(tokens[1]));
						verts.add(Float.parseFloat(tokens[2]));
						verts.add(Float.parseFloat(tokens[3]));
					} else if (tokens[0].charAt(1) == 'n') {
						norms.add(Float.parseFloat(tokens[1]));
						norms.add(Float.parseFloat(tokens[2]));
						norms.add(Float.parseFloat(tokens[3]));
					} else if (tokens[0].charAt(1) == 't') {
						uvs.add(Float.parseFloat(tokens[1]));
						uvs.add((flipV ? 1 - Float.parseFloat(tokens[2]) : Float.parseFloat(tokens[2])));
					}
				} else if (firstChar == 'f') {
					String[] parts;
					Array<Integer> faces = activeGroup.faces;
					for (int i = 1; i < tokens.length - 2; i--) {
						parts = tokens[1].split("/");
						faces.add(getIndex(parts[0], verts.size));
						if (parts.length > 2) {
							if (i == 1) activeGroup.hasNorms = true;
							faces.add(getIndex(parts[2], norms.size));
						}
						if (parts.length > 1 && parts[1].length() > 0) {
							if (i == 1) activeGroup.hasUVs = true;
							faces.add(getIndex(parts[1], uvs.size));
						}
						parts = tokens[++i].split("/");
						faces.add(getIndex(parts[0], verts.size));
						if (parts.length > 2) faces.add(getIndex(parts[2], norms.size));
						if (parts.length > 1 && parts[1].length() > 0) faces.add(getIndex(parts[1], uvs.size));
						parts = tokens[++i].split("/");
						faces.add(getIndex(parts[0], verts.size));
						if (parts.length > 2) faces.add(getIndex(parts[2], norms.size));
						if (parts.length > 1 && parts[1].length() > 0) faces.add(getIndex(parts[1], uvs.size));
						activeGroup.numFaces++;
					}
				} else if (firstChar == 'o' || firstChar == 'g') {
					// This implementation only supports single object or group
					// definitions. i.e. "o group_a group_b" will set group_a
					// as the active group, while group_b will simply be
					// ignored.
					if (tokens.length > 1)
						activeGroup = setActiveGroup(tokens[1]);
					else
						activeGroup = setActiveGroup("default");
				} else if (tokens[0].equals("mtllib")) {
					mtl.load(file.parent().child(tokens[1]));
				} else if (tokens[0].equals("usemtl")) {
					if (tokens.length == 1)
						activeGroup.materialName = "default";
					else
						activeGroup.materialName = tokens[1];
				}
			}
			reader.close();
		} catch (IOException e) {
			return null;
		}

		// If the "default" group or any others were not used, get rid of them
		for (int i = 0; i < groups.size; i++) {
			if (groups.get(i).numFaces < 1) {
				groups.removeIndex(i);
				i--;
			}
		}

		// If there are no groups left, there is no valid Model to return
		if (groups.size < 1) return null;

		// Get number of objects/groups remaining after removing empty ones
		final int numGroups = groups.size;

		final ModelData data = new ModelData();

		for (int g = 0; g < numGroups; g++) {
			Group group = groups.get(g);
			Array<Integer> faces = group.faces;
			final int numElements = faces.size;
			final int numFaces = group.numFaces;
			final boolean hasNorms = group.hasNorms;
			final boolean hasUVs = group.hasUVs;

			final float[] finalVerts = new float[(numFaces * 3) * (3 + (hasNorms ? 3 : 0) + (hasUVs ? 2 : 0))];

			for (int i = 0, vi = 0; i < numElements;) {
				int vertIndex = faces.get(i++) * 3;
				finalVerts[vi++] = verts.get(vertIndex++);
				finalVerts[vi++] = verts.get(vertIndex++);
				finalVerts[vi++] = verts.get(vertIndex);
				if (hasNorms) {
					int normIndex = faces.get(i++) * 3;
					finalVerts[vi++] = norms.get(normIndex++);
					finalVerts[vi++] = norms.get(normIndex++);
					finalVerts[vi++] = norms.get(normIndex);
				}
				if (hasUVs) {
					int uvIndex = faces.get(i++) * 2;
					finalVerts[vi++] = uvs.get(uvIndex++);
					finalVerts[vi++] = uvs.get(uvIndex);
				}
			}

			final int numIndices = numFaces * 3 >= Short.MAX_VALUE ? 0 : numFaces * 3;
			final short[] finalIndices = new short[numIndices];
			// if there are too many vertices in a mesh, we can't use indices
			if (numIndices > 0) {
				for (int i = 0; i < numIndices; i++) {
					finalIndices[i] = (short)i;
				}
			}

			Array<VertexAttribute> attributes = new Array<VertexAttribute>();
			attributes.add(new VertexAttribute(Usage.Position, 3, ShaderProgram.POSITION_ATTRIBUTE));
			if (hasNorms) attributes.add(new VertexAttribute(Usage.Normal, 3, ShaderProgram.NORMAL_ATTRIBUTE));
			if (hasUVs) attributes.add(new VertexAttribute(Usage.TextureCoordinates, 2, ShaderProgram.TEXCOORD_ATTRIBUTE + "0"));
		
			String nodeId = "node" + (++id);
			String meshId = "mesh" + id;
			String partId = "part" + id;
			ModelNode node = new ModelNode();
			node.id = nodeId;
			node.meshId = meshId;
			node.scale = new Vector3(1,1,1);
			node.translation = new Vector3();
			node.rotation = new Quaternion();
			ModelNodePart pm = new ModelNodePart();
			pm.meshPartId = partId;
			pm.materialId = group.materialName;
			node.parts = new ModelNodePart[] { pm };
			ModelMeshPart part = new ModelMeshPart();
			part.id = partId;
			part.indices = finalIndices;
			part.primitiveType = GL10.GL_TRIANGLES;
			ModelMesh mesh = new ModelMesh();
			mesh.id = meshId;
			mesh.attributes = attributes.toArray(VertexAttribute.class);
			mesh.vertices = finalVerts;
			mesh.parts = new ModelMeshPart[] { part };
			data.nodes.add(node);
			data.meshes.add(mesh);
			ModelMaterial mm = mtl.getMaterial(group.materialName);
			data.materials.add(mm);
		}

		//for (ModelMaterial m : mtl.materials)
			//data.materials.add(m);
		
		// An instance of ObjLoader can be used to load more than one OBJ.
		// Clearing the Array cache instead of instantiating new
		// Arrays should result in slightly faster load times for
		// subsequent calls to loadObj
		if (verts.size > 0) verts.clear();
		if (norms.size > 0) norms.clear();
		if (uvs.size > 0) uvs.clear();
		if (groups.size > 0) groups.clear();

		return data;
	}

	private Group setActiveGroup (String name) {
		// TODO: Check if a HashMap.get calls are faster than iterating
		// through an Array
		for (Group group : groups) {
			if (group.name.equals(name)) return group;
		}
		Group group = new Group(name);
		groups.add(group);
		return group;
	}

	private int getIndex (String index, int size) {
		if (index == null || index.length() == 0) return 0;
		final int idx = Integer.parseInt(index);
		if (idx < 0)
			return size + idx;
		else
			return idx - 1;
	}

	private class Group {
		final String name;
		String materialName;
		Array<Integer> faces;
		int numFaces;
		boolean hasNorms;
		boolean hasUVs;
		Material mat;

		Group (String name) {
			this.name = name;
			this.faces = new Array<Integer>(200);
			this.numFaces = 0;
			this.mat = new Material("");
			this.materialName = "default";
		}
	}
}

class MtlLoader {
	public Array<ModelMaterial> materials = new Array<ModelMaterial>();

	/** loads .mtl file */
	public void load (FileHandle file) {
		String line;
		String[] tokens;
		String curMatName = "default";
		Color difcolor = Color.WHITE;
		Color speccolor = Color.WHITE;
		float opacity = 1.f;
		float shininess = 0.f;
		String texFilename = null;

		if (file == null || file.exists() == false) return;

		BufferedReader reader = new BufferedReader(new InputStreamReader(file.read()), 4096);
		try {
			while ((line = reader.readLine()) != null) {

				if (line.length() > 0 && line.charAt(0) == '\t') line = line.substring(1).trim();

				tokens = line.split("\\s+");

				if (tokens[0].length() == 0) {
					continue;
				} else if (tokens[0].charAt(0) == '#')
					continue;
				else {
					final String key = tokens[0].toLowerCase(); 
					if (key.equals("newmtl")) {
						ModelMaterial mat = new ModelMaterial();
						mat.id = curMatName;
						mat.diffuse = new Color(difcolor);
						mat.specular = new Color(speccolor);
						mat.opacity = opacity;
						mat.shininess = shininess;
						if (texFilename != null) {
							ModelTexture tex = new ModelTexture();
							tex.usage = ModelTexture.USAGE_DIFFUSE;
							tex.fileName = new String(texFilename);
							if (mat.textures == null)
								mat.textures = new Array<ModelTexture>(1);
							mat.textures.add(tex);						
						}
						materials.add(mat);
	
						if (tokens.length > 1) {
							curMatName = tokens[1];
							curMatName = curMatName.replace('.', '_');
						} else
							curMatName = "default";
	
						difcolor = Color.WHITE;
						speccolor = Color.WHITE;
						opacity = 1.f;
						shininess = 0.f;
					} else if (key.equals("kd") || key.equals("ks")) // diffuse or specular
					{
						float r = Float.parseFloat(tokens[1]);
						float g = Float.parseFloat(tokens[2]);
						float b = Float.parseFloat(tokens[3]);
						float a = 1;
						if (tokens.length > 4) a = Float.parseFloat(tokens[4]);
	
						if (tokens[0].toLowerCase().equals("kd")) {
							difcolor = new Color();
							difcolor.set(r, g, b, a);
						} else {
							speccolor = new Color();
							speccolor.set(r, g, b, a);
						}
					} else if (key.equals("tr") || key.equals("d")) {
						opacity = Float.parseFloat(tokens[1]);
					} else if (key.equals("ns")) {
						shininess = Float.parseFloat(tokens[1]);
					} else if (key.equals("map_kd")) {
						texFilename = file.parent().child(tokens[1]).path();
					}
				}
			}
			reader.close();
		} catch (IOException e) {
			return;
		}

		// last material
		ModelMaterial mat = new ModelMaterial();
		mat.id = curMatName;
		mat.diffuse = new Color(difcolor);
		mat.specular = new Color(speccolor);
		mat.opacity = opacity;
		mat.shininess = shininess;
		if (texFilename != null) {
			ModelTexture tex = new ModelTexture();
			tex.usage = ModelTexture.USAGE_DIFFUSE;
			tex.fileName = new String(texFilename);
			if (mat.textures == null)
				mat.textures = new Array<ModelTexture>(1);
			mat.textures.add(tex);
		}
		materials.add(mat);

		return;
	}
	
	public ModelMaterial getMaterial(final String name) {
		for (final ModelMaterial m : materials)
			if (m.id.equals(name))
				return m;
		ModelMaterial mat = new ModelMaterial();
		mat.id = name;
		mat.diffuse = new Color(Color.WHITE);
		materials.add(mat);
		return mat;
	}
}
