package j3deditor.bin.hierarchy.util;

import j3deditor.bin.hierarchy.GeometryManipulator;

import j3deditor.bin.hierarchy.Vertex;

import j3deditor.bin.util.ProgressChangeListener;

import j3deditor.bin.util.Utilities;

import j3deditor.bin.util.CancellableTask;

import java.util.ArrayList;

import javax.swing.SwingWorker;

import javax.vecmath.Vector3f;

/**
 * Utility for (re)calculating vertex normals.
 * 
 * @author Risto Seene
 */
public class NormalCalculator extends SwingWorker implements CancellableTask{

	/** <code>GeometryManipulator</code> which data will be manipulated */
	private GeometryManipulator gm;

	/** selected faces */
	private Vertex[][] faces;
	
	/** specifies if the faces will be smoothed or flattened */
	private boolean smooth;

	/** specifies if the edges of selection will be included to smoothing process */
	private boolean edges;

	/** <code>ProgressChangeListener</code> */
	private ProgressChangeListener listener;

	/** if set to false the computation is cancelled */
	private boolean running = true;


	/**
	 * Creates an instance of <code>NormalCalculator</code>.
	 * 
	 * @param gm
	 * 	<code>GeometryManipulator</code> which data will
	 *        be manipulated by this <code>NormalCalculator</code>
	 */
	public NormalCalculator(GeometryManipulator gm){
		if(gm != null){
			if(gm.areFacesSelected() && gm.getGeometry().getVertexGroupMode() > 2){
				this.faces = gm.getSelectedFaces();
				this.gm = gm;
			}
		}
	}

	/**
	 * Computes normals.
	 * 
	 * @see javax.swing.SwingWorker#doInBackground()
	 */
	@Override
	protected Object doInBackground() throws Exception{
		if(smooth){
			setSmooth();
		}
		else {
			setFlat();
		}

		return null;
	}


	/**
	 * Sets faces flat.
	 */
	public void recalculateFlat(){
		if(this.faces != null){
			this.smooth = false;
			this.execute();
		}
	}

	/**
	 * Sets faces smooth.
	 * 
	 * @param edges
	 * 	specifies if the edges of selection is included
	 */
	public void recalculateSmooth(boolean edges){
		if(this.faces != null){
			this.smooth = true;
			this.edges = edges;
			this.execute();
		}
	}

	/**
	 * Sets the <code>ProgressChangeListener</code>.
	 * 
	 * @param listener
	 * 	new <code>ProgressChangeListener</code>
	 */
	public void setProgressChangeListener(ProgressChangeListener listener){
		this.listener = listener;
	}


	/**
	 * Starts the flattening process.
	 */
	private void setFlat(){
		try{
			for(int i = 0; i < faces.length; i++){
				Vertex[] face = faces[i];
				Vector3f v = this.getFaceNormal(face);
				for(int j = 0; j < face.length; j++){
					face[j].setNormal(v);
				}
			}
		} catch(Throwable t){
			Utilities.out.throwWarning(t);
		}
	}

	/**
	 * Starts the smoothing process.
	 */
	private void setSmooth(){
		try{
			this.setFlat();

			int length = faces.length;
			for(int i = 0; i < length; i++){
				if(this.running){
					for(int j = 0; j < faces[i].length; j++){
						this.smoothVertexNormals(faces[i][j]);
					}
					this.progressChanged(i, length);
				}
			}

			this.resetVertexFlags();
		} catch(Throwable t){
			Utilities.out.throwWarning(t);
		}
	}


	/**
	 * Returns the normal of the specified face.
	 * @param face
	 * 	vertices representing a face
	 * @return the normal of the specified face
	 */
	private Vector3f getFaceNormal(Vertex[] face){
		if(face.length == 3){
			return this.calculateFaceNormal(face);
		}
		else if(face.length == 4){
			Vertex[] vs = new Vertex[3];
			for(int i = 0; i < 3; i++){
				vs[i] = face[i];
			}
			return this.calculateFaceNormal(vs);
		}
		return new Vector3f();
	}

	/**
	 * Calculates face normal using the vertex normals of three vertices.
	 * @param vs
	 * 	three vertices which normall will be used to
	 *        calculate face normal
	 * @return face normal
	 */
	private Vector3f calculateFaceNormal(Vertex[] vs){
		Vector3f v1 = new Vector3f(vs[1].getX() - vs[0].getX(), vs[1].getY() - vs[0].getY(), vs[1].getZ() - vs[0].getZ());
		Vector3f v2 = new Vector3f(vs[2].getX() - vs[0].getX(), vs[2].getY() - vs[0].getY(), vs[2].getZ() - vs[0].getZ());

		float nx = (v1.getY() * v2.getZ()) - (v1.getZ() * v2.getY());
		float ny = -((v2.getZ() * v1.getX()) - (v2.getX() * v1.getZ()));
		float nz = (v1.getX() * v2.getY()) - (v1.getY() * v2.getX());

		Vector3f normal = new Vector3f(nx, ny, nz);
		normal.normalize();

		return normal;
	}


	/**
	 * Finds the mean normal of this vertex and all other vertices that
	 * overlap with this one; applies the mean normal to all
	 * those vertices.
	 * 
	 * @param v
	 * 	a vertex
	 */
	private void smoothVertexNormals(Vertex v){
		if(!v.inUse){
			ArrayList<Vertex> vs = new ArrayList<Vertex>();
			vs.add(v);

			if(this.edges){
				int vertCount = this.gm.getVertexCount();
				for(int i = 0; i < vertCount; i++){
					Vertex vert = this.gm.getVertex(i);
					if(!vert.inUse){
						if(v.equals(vert) && v.getIndex() != vert.getIndex()){
							vs.add(vert);
						}
					}
				}
			}
			else {
				int faceSize = this.gm.getGeometry().getVertexGroupMode();
				for(int i=0; i<this.faces.length; i++){
					for(int j=0; j<faceSize; j++){
					          Vertex vert = this.faces[i][j];
					          if(!vert.inUse){
					                    if(v.equals(vert) && v.getIndex() != vert.getIndex()){
					                              vs.add(vert);
					                    }
					          }
					}
				}
			}

			Vertex[] overlapping = vs.toArray(new Vertex[vs.size()]);
			Vector3f normal = this.calculateMeanNormal(overlapping);

			for(int i = 0; i < overlapping.length; i++){
				Vertex vert = overlapping[i];
				if(vert.getState(Vertex.COORDINATE_VERTEX_STATE) > 0){
					vert.setNormal(normal);
					vert.inUse = true;
				}
			}
		}
	}

	/**
	 * Returns the mean normal of the given vertices.
	 * 
	 * @param vertices
	 * 	vertices which normals will be used to calculate
	 *        the mean normal
	 * @return the mean normal of the given vertices
	 */
	private Vector3f calculateMeanNormal(Vertex[] vertices){
		Vector3f normal = new Vector3f();
		for(int i = 0; i < vertices.length; i++){
			normal.add(vertices[i].getNormal());
		}
		normal.normalize();
		return normal;
	}


	/**
	 * Sets the <code>inUse</code> fields of all vertises back to false.
	 */
	private void resetVertexFlags(){
		int length = this.gm.getVertexCount();
		for(int i = 0; i < length; i++){
			this.gm.getVertex(i).inUse = false;
		}
	}


	/**
	 * Updates the <code>ProgressChangeListener</code>.
	 * 
	 * @param current
	 * 	current value
	 * @param length
	 * 	max value
	 */
	private void progressChanged(int current, int length){
		if(this.listener != null){
			listener.percentageChanged((float)(100 * ((double)current / (double)length)));
		}
	}

	/**
	 * Cancels the computation if faces are
	 * being smoothed.
	 * 
	 * @param sender
	 * 	doesn't do anything
	 */
	public void cancel(Object sender){
		this.running = false;
	}
	
}
