/*
 * Copyright (c) 2009-2010 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.scene.plugins.blender.textures;

import java.nio.FloatBuffer;
import java.util.List;
import java.util.logging.Logger;

import com.jme3.bounding.BoundingBox;
import com.jme3.bounding.BoundingSphere;
import com.jme3.bounding.BoundingVolume;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Format;
import com.jme3.scene.VertexBuffer.Usage;
import com.jme3.util.BufferUtils;

/**
 * This class is used for UV coordinates generation.
 * @author Marcin Roguski (Kaelthas)
 */
public class UVCoordinatesGenerator {
	private static final Logger	LOGGER						= Logger.getLogger(UVCoordinatesGenerator.class.getName());

	// texture UV coordinates types
	public static final int		TEXCO_ORCO					= 1;
	public static final int		TEXCO_REFL					= 2;
	public static final int		TEXCO_NORM					= 4;
	public static final int		TEXCO_GLOB					= 8;
	public static final int		TEXCO_UV					= 16;
	public static final int		TEXCO_OBJECT				= 32;
	public static final int		TEXCO_LAVECTOR				= 64;
	public static final int		TEXCO_VIEW					= 128;
	public static final int		TEXCO_STICKY				= 256;
	public static final int		TEXCO_OSA					= 512;
	public static final int		TEXCO_WINDOW				= 1024;
	public static final int		NEED_UV						= 2048;
	public static final int		TEXCO_TANGENT				= 4096;
	// still stored in vertex->accum, 1 D
	public static final int		TEXCO_PARTICLE_OR_STRAND	= 8192;													// strand is used
	public static final int		TEXCO_STRESS				= 16384;
	public static final int		TEXCO_SPEED					= 32768;

	// 2D texture mapping (projection)
	public static final int		PROJECTION_FLAT				= 0;
	public static final int		PROJECTION_CUBE				= 1;
	public static final int		PROJECTION_TUBE				= 2;
	public static final int		PROJECTION_SPHERE			= 3;

	/**
	 * This method generates UV coordinates for the given mesh.
	 * IMPORTANT! This method assumes that all geometries represent one node.
	 * Each containing mesh with separate material.
	 * So all meshes have the same reference to vertex table which stores all their vertices.
	 * @param texco
	 *        texture coordinates type
	 * @param projection
	 *        the projection type for 2D textures
	 * @param textureDimension
	 *        the dimension of the texture (only 2D and 3D)
	 * @param coordinatesSwappingIndexes
	 *        an array that tells how UV-coordinates need to be swapped
	 * @param geometries
	 *        a list of geometries the UV coordinates will be applied to
	 * @return created UV-coordinates buffer
	 */
	public static VertexBuffer generateUVCoordinates(int texco, int projection, int textureDimension, int[] coordinatesSwappingIndexes, List<Geometry> geometries) {
		if (textureDimension != 2 && textureDimension != 3) {
			throw new IllegalStateException("Unsupported texture dimension: " + textureDimension);
		}

		VertexBuffer result = new VertexBuffer(VertexBuffer.Type.TexCoord);
		Mesh mesh = geometries.get(0).getMesh();
		BoundingBox bb = UVCoordinatesGenerator.getBoundingBox(geometries);
		float[] inputData = null;// positions, normals, reflection vectors, etc.

		switch (texco) {
			case TEXCO_ORCO:
				inputData = BufferUtils.getFloatArray(mesh.getFloatBuffer(VertexBuffer.Type.Position));
				break;
			case TEXCO_UV:
				FloatBuffer uvCoordinatesBuffer = BufferUtils.createFloatBuffer(mesh.getVertexCount() * textureDimension);
				Vector2f[] data = new Vector2f[] { new Vector2f(0, 1), new Vector2f(0, 0), new Vector2f(1, 0) };
				for (int i = 0; i < mesh.getVertexCount(); ++i) {
					Vector2f uv = data[i % 3];
					uvCoordinatesBuffer.put(uv.x);
					uvCoordinatesBuffer.put(uv.y);
					if(textureDimension == 3) {
						uvCoordinatesBuffer.put(0);
					}
				}
				result.setupData(Usage.Static, textureDimension, Format.Float, uvCoordinatesBuffer);
				break;
			case TEXCO_NORM:
				inputData = BufferUtils.getFloatArray(mesh.getFloatBuffer(VertexBuffer.Type.Normal));
				break;
			case TEXCO_REFL:
			case TEXCO_GLOB:
			case TEXCO_TANGENT:
			case TEXCO_STRESS:
			case TEXCO_LAVECTOR:
			case TEXCO_OBJECT:
			case TEXCO_OSA:
			case TEXCO_PARTICLE_OR_STRAND:
			case TEXCO_SPEED:
			case TEXCO_STICKY:
			case TEXCO_VIEW:
			case TEXCO_WINDOW:
				LOGGER.warning("Texture coordinates type not currently supported: " + texco);
				break;
			default:
				throw new IllegalStateException("Unknown texture coordinates value: " + texco);
		}

		if (inputData != null) {// make calculations
			if (textureDimension == 2) {
				switch (projection) {
					case PROJECTION_FLAT:
						inputData = UVProjectionGenerator.flatProjection(mesh, bb);
						break;
					case PROJECTION_CUBE:
						inputData = UVProjectionGenerator.cubeProjection(mesh, bb);
						break;
					case PROJECTION_TUBE:
						BoundingTube bt = UVCoordinatesGenerator.getBoundingTube(geometries);
						inputData = UVProjectionGenerator.tubeProjection(mesh, bt);
						break;
					case PROJECTION_SPHERE:
						BoundingSphere bs = UVCoordinatesGenerator.getBoundingSphere(geometries);
						inputData = UVProjectionGenerator.sphereProjection(mesh, bs);
						break;
					default:
						throw new IllegalStateException("Unknown projection type: " + projection);
				}
			} else {
				Vector3f min = bb.getMin(null);
				float[] uvCoordsResults = new float[4];//used for coordinates swapping
				float[] ext = new float[] { bb.getXExtent() * 2, bb.getYExtent() * 2, bb.getZExtent() * 2 };

				// now transform the coordinates so that they are in the range of <0; 1>
				for (int i = 0; i < inputData.length; i += 3) {
					uvCoordsResults[1] = (inputData[i] - min.x) / ext[0];
					uvCoordsResults[2] = (inputData[i + 1] - min.y) / ext[1];
					uvCoordsResults[3] = (inputData[i + 2] - min.z) / ext[2];
					
					
					inputData[i] = uvCoordsResults[coordinatesSwappingIndexes[0]];
					inputData[i + 1] = uvCoordsResults[coordinatesSwappingIndexes[1]];
					inputData[i + 2] = uvCoordsResults[coordinatesSwappingIndexes[2]];
				}
			}
			result.setupData(Usage.Static, textureDimension, Format.Float, BufferUtils.createFloatBuffer(inputData));
		}

		// each mesh will have the same coordinates
		for (Geometry geometry : geometries) {
			mesh = geometry.getMesh();
			mesh.clearBuffer(VertexBuffer.Type.TexCoord);// in case there are coordinates already set
			mesh.setBuffer(result);
		}
		
		return result;
	}

	/**
	 * This method returns the bounding box of the given geometries.
	 * @param geometries
	 *        the list of geometries
	 * @return bounding box of the given geometries
	 */
	/* package */static BoundingBox getBoundingBox(List<Geometry> geometries) {
		BoundingBox result = null;
		for (Geometry geometry : geometries) {
			BoundingBox bb = UVCoordinatesGenerator.getBoundingBox(geometry.getMesh());
			if (result == null) {
				result = bb;
			} else {
				result.merge(bb);
			}
		}
		return result;
	}

	/**
	 * This method returns the bounding box of the given mesh.
	 * @param mesh
	 *        the mesh
	 * @return bounding box of the given mesh
	 */
	/* package */static BoundingBox getBoundingBox(Mesh mesh) {
		mesh.updateBound();
		BoundingVolume bv = mesh.getBound();
		if (bv instanceof BoundingBox) {
			return (BoundingBox) bv;
		} else if (bv instanceof BoundingSphere) {
			BoundingSphere bs = (BoundingSphere) bv;
			float r = bs.getRadius();
			return new BoundingBox(bs.getCenter(), r, r, r);
		} else {
			throw new IllegalStateException("Unknown bounding volume type: " + bv.getClass().getName());
		}
	}

	/**
	 * This method returns the bounding sphere of the given geometries.
	 * @param geometries
	 *        the list of geometries
	 * @return bounding sphere of the given geometries
	 */
	/* package */static BoundingSphere getBoundingSphere(List<Geometry> geometries) {
		BoundingSphere result = null;
		for (Geometry geometry : geometries) {
			BoundingSphere bs = UVCoordinatesGenerator.getBoundingSphere(geometry.getMesh());
			if (result == null) {
				result = bs;
			} else {
				result.merge(bs);
			}
		}
		return result;
	}

	/**
	 * This method returns the bounding sphere of the given mesh.
	 * @param mesh
	 *        the mesh
	 * @return bounding sphere of the given mesh
	 */
	/* package */static BoundingSphere getBoundingSphere(Mesh mesh) {
		mesh.updateBound();
		BoundingVolume bv = mesh.getBound();
		if (bv instanceof BoundingBox) {
			BoundingBox bb = (BoundingBox) bv;
			float r = Math.max(bb.getXExtent(), bb.getYExtent());
			r = Math.max(r, bb.getZExtent());
			return new BoundingSphere(r, bb.getCenter());
		} else if (bv instanceof BoundingSphere) {
			return (BoundingSphere) bv;
		} else {
			throw new IllegalStateException("Unknown bounding volume type: " + bv.getClass().getName());
		}
	}

	/**
	 * This method returns the bounding tube of the given mesh.
	 * @param mesh
	 *        the mesh
	 * @return bounding tube of the given mesh
	 */
	/* package */static BoundingTube getBoundingTube(Mesh mesh) {
		Vector3f center = new Vector3f();
		float maxx = -Float.MAX_VALUE, minx = Float.MAX_VALUE;
		float maxy = -Float.MAX_VALUE, miny = Float.MAX_VALUE;
		float maxz = -Float.MAX_VALUE, minz = Float.MAX_VALUE;

		FloatBuffer positions = mesh.getFloatBuffer(VertexBuffer.Type.Position);
		int limit = positions.limit();
		for (int i = 0; i < limit; i += 3) {
			float x = positions.get(i);
			float y = positions.get(i + 1);
			float z = positions.get(i + 2);
			center.addLocal(x, y, z);
			maxx = x > maxx ? x : maxx;
			minx = x < minx ? x : minx;
			maxy = y > maxy ? y : maxy;
			miny = y < miny ? y : miny;
			maxz = z > maxz ? z : maxz;
			minz = z < minz ? z : minz;
		}
		center.divideLocal(limit / 3);

		float radius = Math.max(maxx - minx, maxy - miny) * 0.5f;
		return new BoundingTube(radius, maxz - minz, center);
	}
	
	/**
	 * This method returns the bounding tube of the given geometries.
	 * @param geometries
	 *        the list of geometries
	 * @return bounding tube of the given geometries
	 */
	/* package */static BoundingTube getBoundingTube(List<Geometry> geometries) {
		BoundingTube result = null;
		for (Geometry geometry : geometries) {
			BoundingTube bt = UVCoordinatesGenerator.getBoundingTube(geometry.getMesh());
			if (result == null) {
				result = bt;
			} else {
				result.merge(bt);
			}
		}
		return result;
	}

	/**
	 * A very simple bounding tube. Id holds only the basic data bout the bounding tube
	 * and does not provide full functionality of a BoundingVolume.
	 * Should be replaced with a bounding tube that extends the BoundingVolume if it is ever created.
	 * @author Marcin Roguski (Kaelthas)
	 */
	/* package */static class BoundingTube {
		private float		radius;
		private float		height;
		private Vector3f	center;

		/**
		 * Constructor creates the tube with the given params.
		 * @param radius
		 *        the radius of the tube
		 * @param height
		 *        the height of the tube
		 * @param center
		 *        the center of the tube
		 */
		public BoundingTube(float radius, float height, Vector3f center) {
			this.radius = radius;
			this.height = height;
			this.center = center;
		}

		/**
		 * This method merges two bounding tubes.
		 * @param boundingTube
		 *        bounding tube to be merged woth the current one
		 * @return new instance of bounding tube representing the tubes' merge
		 */
		public BoundingTube merge(BoundingTube boundingTube) {
			// get tubes (tube1.radius >= tube2.radius)
			BoundingTube tube1, tube2;
			if (this.radius >= boundingTube.radius) {
				tube1 = this;
				tube2 = boundingTube;
			} else {
				tube1 = boundingTube;
				tube2 = this;
			}
			float r1 = tube1.radius;
			float r2 = tube2.radius;

			float minZ = Math.min(tube1.center.z - tube1.height * 0.5f, tube2.center.z - tube2.height * 0.5f);
			float maxZ = Math.max(tube1.center.z + tube1.height * 0.5f, tube2.center.z + tube2.height * 0.5f);
			float height = maxZ - minZ;
			Vector3f distance = tube2.center.subtract(tube1.center);
			Vector3f center = tube1.center.add(distance.mult(0.5f));
			distance.z = 0;// projecting this vector on XY plane
			float d = distance.length();
			// d <= r1 - r2: tube2 is inside tube1 or touches tube1 from the inside
			// d > r1 - r2: tube2 is outside or touches tube1 or crosses tube1
			float radius = d <= r1 - r2 ? tube1.radius : (d + r1 + r2) * 0.5f;
			return new BoundingTube(radius, height, center);
		}

		/**
		 * This method returns the radius of the tube.
		 * @return the radius of the tube
		 */
		public float getRadius() {
			return radius;
		}

		/**
		 * This method returns the height of the tube.
		 * @return the height of the tube
		 */
		public float getHeight() {
			return height;
		}

		/**
		 * This method returns the center of the tube.
		 * @return the center of the tube
		 */
		public Vector3f getCenter() {
			return center;
		}
	}
}
