package j3deditor.bin.editor.util;

import j3deditor.bin.editor.Axes2D;

import j3deditor.bin.editor.Scene2D;

import j3deditor.bin.hierarchy.GeometryManipulator;
import j3deditor.bin.hierarchy.Vertex;
import j3deditor.bin.hierarchy.util.J3DePoint2D;

import java.util.ArrayList;


/**
 * Utility for converting coordinates between 3D scene
 * and <code>Scene2D</code>.
 * 
 * @author Risto Seene
 */
public class VertexManipulator{

	/**
	 * Returns the manipulation target of the specified
	 * scene: <code>Vertex.TEXTURE_VERTEX_STATE</code>
	 * or <code>Vertex.COORDINATE_VERTEX_STATE</code>.
	 * 
	 * @param scene
	 * 	<code>Scene2D</code> which manipulation target will be returned
	 * @return <code>Vertex.TEXTURE_VERTEX_STATE</code> or
	 * 	<code>Vertex.COORDINATE_VERTEX_STATE</code>
	 */
	public static int getStateType(Scene2D scene){
		if(scene.getViewPoint() == Axes2D.TEX){
			return Vertex.TEXTURE_VERTEX_STATE;
		}
		else {
			return Vertex.COORDINATE_VERTEX_STATE;
		}
	}

	/**
	 * Selects a vertex which is close enough to cursor.
	 * 
	 * @param p
	 * 	the position of cursor
	 * @param scene
	 * 	<code>Scene2D</code> the selection was made in
	 * @param isShiftDown
	 * 	is user holding shift key or not.<br/>
	 *        if true more vertices can be selected by each selection,<br/>
	 *        otherwise all selected vertices (if any) will be unselected and then
	 *        the new one will be selected (if possible)
	 */
	public static void selectVertex(J3DePoint2D p, Scene2D scene, boolean isShiftDown){
		GeometryManipulator gm = scene.getGeometryManipulator();
		ArrayList<Vertex> selected = new ArrayList<Vertex>();

		int stateType = getStateType(scene);

		for(int i = 0; i < gm.getVertexCount(); i++){
			Vertex v = gm.getVertex(i);

			if(v.getState(stateType) != Vertex.HIDDEN && isCloseEnough(p, getVertexPosition2D(v, scene))){
				if(selected.size() > 0){
					if(isCloser(p, v, selected.get(0), scene)){
						selected.clear();
						selected.add(v);
					}
					else if(gm.isStickyVertsEnabled() && v.equals(selected.get(0))){
						selected.add(v);
					}
				}
				else {
					selected.add(v);
				}
			}
		}

		if(isShiftDown){
			for(int i = 0; i < selected.size(); i++){
				Vertex v = selected.get(i);
				if(v.getState(stateType) == Vertex.SELECTED){
					v.setState(Vertex.NORMAL, stateType);
				}
				else {
					v.setState(Vertex.SELECTED, stateType);
				}
			}
		}
		else {
			gm.deselectAllVertices();
			for(int i = 0; i < selected.size(); i++){
				selected.get(i).setState(Vertex.SELECTED, stateType);
			}
		}
	}

	/**
	 * Selects all vertices which are located inside the specified rectangle.
	 * 
	 * @param p1
	 * 	top left corner of the rectangle
	 * @param p2
	 * 	bottom right corner of the rectangle
	 * @param scene
	 * 	<code>Scene2D</code> the selection was made in
	 */
	public static void selectVertices(J3DePoint2D p1, J3DePoint2D p2, Scene2D scene){
		GeometryManipulator gm = scene.getGeometryManipulator();
		int stateType = getStateType(scene);

		for(int i = 0; i < gm.getVertexCount(); i++){
			Vertex v = gm.getVertex(i);
			J3DePoint2D vertPos = getVertexPosition2D(v, scene);

			if(v.getState(stateType) != Vertex.HIDDEN && isInBounds(vertPos, p1, p2)){
				v.setState(Vertex.SELECTED, stateType);
			}
		}
	}

	/**
	 * Returns true if the space between the specified points is less than 10.
	 * 
	 * @param p1
	 * 	first point
	 * @param p2
	 * 	second point
	 * @return true if the space between the specified points is less than 10,
	 * 	otherwise false
	 */
	private static boolean isCloseEnough(J3DePoint2D p1, J3DePoint2D p2){
		return (Math.abs(p1.getX() - p2.getX()) < 10 && Math.abs(p1.getY() - p2.getY()) < 10);
	}

	/**
	 * Returns true if the first vertex is closer to the specified point
	 * in the specified <code>Scene2D</code>.
	 * 
	 * @param p
	 * 	the point
	 * @param v1
	 * 	first vertex
	 * @param v2
	 * 	second vertex
	 * @param scene
	 * 	<code>Scene2D</code> which coordinates are used for comparison
	 * @return true if the first vertex is closer to the specified point
	 * 	in the specified <code>Scene2D</code>
	 */
	private static boolean isCloser(J3DePoint2D p, Vertex v1, Vertex v2, Scene2D scene){
		boolean result = false;
		J3DePoint2D p1 = getVertexPosition2D(v1, scene);
		J3DePoint2D p2 = getVertexPosition2D(v2, scene);
		float dist1 = (float)Math.pow(Math.abs(p.getX() - p1.getX()), 2) + (float)Math.pow(Math.abs(p.getY() - p1.getY()), 2);
		float dist2 = (float)Math.pow(Math.abs(p.getX() - p2.getX()), 2) + (float)Math.pow(Math.abs(p.getY() - p2.getY()), 2);

		if(dist1 < dist2){
			result = true;
		}
		else if(dist1 == dist2){
			if(scene.getViewPoint() == Axes2D.POS_X){
				result = (v1.getX() > v2.getX());
			}
			else if(scene.getViewPoint() == Axes2D.NEG_X){
				result = (v1.getX() < v2.getX());
			}
			else if(scene.getViewPoint() == Axes2D.POS_Y){
				result = (v1.getY() > v2.getY());
			}
			else if(scene.getViewPoint() == Axes2D.NEG_Y){
				result = (v1.getY() < v2.getY());
			}
			else if(scene.getViewPoint() == Axes2D.POS_Z){
				result = (v1.getZ() > v2.getZ());
			}
			else if(scene.getViewPoint() == Axes2D.NEG_Z){
				result = (v1.getZ() < v2.getZ());
			}
		}

		return result;
	}

	/**
	 * Returns true if the specified point is located inside the
	 * specified rectangle.
	 * 
	 * @param pos
	 * 	the point
	 * @param p1
	 * 	top left corner of the rectangle
	 * @param p2
	 * 	bottom right corner of the rectangle
	 * @return true if the specified point is located inside the
	 * 	specified rectangle
	 */
	public static boolean isInBounds(J3DePoint2D pos, J3DePoint2D p1, J3DePoint2D p2){
		return (pos.getX() >= p1.getX() && pos.getX() <= p2.getX() && pos.getY() >= p1.getY() && pos.getY() <= p2.getY());
	}


	/**
	 * Returns the position of a vertex in the coordinate system
	 * of the specified <code>Scene2D</code>.
	 * 
	 * @param v
	 * 	vertex which coordinates will be returned
	 * @param scene
	 * 	<code>Scene2D</code> which coordinate system
	 *        the specified vertex will be transformed
	 * @return the position of a vertex in the coordinate system
	 * 	of the specified <code>Scene2D</code>
	 */
	public static J3DePoint2D getVertexPosition2D(Vertex v, Scene2D scene){
		int viewPoint = scene.getViewPoint();
		float scale = scene.getScale();
		float[] offset = scene.getOffset();
		J3DePoint2D sceneSize = new J3DePoint2D(scene.getWidth(), scene.getHeight());

		if(viewPoint == Axes2D.TEX){
			int texCoordSet = scene.getTexCoordSet();
			return new J3DePoint2D((int)(sceneSize.getX() / 2) + (int)((v.getTex(0, texCoordSet) - offset[0]) * scale), (int)(sceneSize.getY() / 2) - (int)((v.getTex(1, texCoordSet) + offset[1]) * scale));
		}
		else if(viewPoint == Axes2D.POS_X){
			return new J3DePoint2D((int)(sceneSize.getX() / 2) - (int)((v.getZ() + offset[0]) * scale), (int)(sceneSize.getY() / 2) - (int)((v.getY() + offset[1]) * scale));
		}
		else if(viewPoint == Axes2D.NEG_X){
			return new J3DePoint2D((int)(sceneSize.getX() / 2) + (int)((v.getZ() - offset[0]) * scale), (int)(sceneSize.getY() / 2) - (int)((v.getY() + offset[1]) * scale));
		}
		else if(viewPoint == Axes2D.POS_Y){
			return new J3DePoint2D((int)(sceneSize.getX() / 2) + (int)((v.getX() - offset[0]) * scale), (int)(sceneSize.getY() / 2) + (int)((v.getZ() - offset[1]) * scale));
		}
		else if(viewPoint == Axes2D.NEG_Y){
			return new J3DePoint2D((int)(sceneSize.getX() / 2) + (int)((v.getX() - offset[0]) * scale), (int)(sceneSize.getY() / 2) - (int)((v.getZ() + offset[1]) * scale));
		}
		else if(viewPoint == Axes2D.POS_Z){
			return new J3DePoint2D((int)(sceneSize.getX() / 2) + (int)((v.getX() - offset[0]) * scale), (int)(sceneSize.getY() / 2) - (int)((v.getY() + offset[1]) * scale));
		}
		else if(viewPoint == Axes2D.NEG_Z){
			return new J3DePoint2D((int)(sceneSize.getX() / 2) - (int)((v.getX() + offset[0]) * scale), (int)(sceneSize.getY() / 2) - (int)((v.getY() + offset[1]) * scale));
		}
		else {
			return new J3DePoint2D();
		}
	}

	/**
	 * Returns the index of the second vertex of the edge
	 * with the specified first vertex.
	 * 
	 * @param vertexIndex
	 * 	index of the first vertex of this edge
	 * @param scene
	 * 	<code>Scene2D</code> which has the reference to that
	 *        <code>GeometryManipulator</code> in which the vertices are
	 *        located in
	 * @return the index of the second vertex of the edge
	 * 	with the specified first vertex
	 */
	public static int getEdgeEndIndex(int vertexIndex, Scene2D scene){
		int vertGMode = scene.getGeometryManipulator().getGeometry().getVertexGroupMode();
		if(((vertexIndex + 1) % vertGMode) == 0){
			return vertexIndex - (vertGMode - 1);
		}
		return vertexIndex + 1;
	}

	/**
	 * Translates the selected vertices.
	 * 
	 * @param x
	 * 	change of position on <code>Scene2D</code> X axis
	 * @param y
	 * 	change of position on <code>Scene2D</code> Y axis
	 * @param scene
	 * 	<code>Scene2D</code> in which the vertices are translated
	 */
	public static void translateVertices(float x, float y, Scene2D scene){
		GeometryManipulator gm = scene.getGeometryManipulator();
		float[] coords = new float[]{ 0, 0, 0 };

		if(scene.getViewPoint() == Axes2D.TEX){
			gm.translateVertices(new float[]{ x, y });
			return;
		}
		else if(scene.getViewPoint() == Axes2D.POS_X){
			coords[2] -= x;
			coords[1] += y;
		}
		else if(scene.getViewPoint() == Axes2D.NEG_X){
			coords[2] += x;
			coords[1] += y;
		}
		else if(scene.getViewPoint() == Axes2D.POS_Y){
			coords[0] += x;
			coords[2] -= y;
		}
		else if(scene.getViewPoint() == Axes2D.NEG_Y){
			coords[0] += x;
			coords[2] += y;
		}
		else if(scene.getViewPoint() == Axes2D.POS_Z){
			coords[0] += x;
			coords[1] += y;
		}
		else if(scene.getViewPoint() == Axes2D.NEG_Z){
			coords[0] -= x;
			coords[1] += y;
		}

		gm.translateVertices(coords);
	}

	/**
	 * Resizes the selected part of a polygonal mesh.
	 * 
	 * @param scaleChange
	 * 	change of scale
	 * @param scene
	 * 	<code>Scene2D</code> in which the vertices are scaled
	 */
	public static void scaleVertices(float scaleChange, Scene2D scene){
		scaleChange = (scaleChange / (2 * scene.getScale()));
		scene.getGeometryManipulator().scaleVertices(scaleChange);
	}

	/**
	 * Rotates the selected part of a polygonal mesh.
	 * 
	 * @param rotation
	 * 	angle of rotation
	 * @param scene
	 * 	<code>Scene2D</code> in which the vertices are rotated
	 */
	public static void rotateVertices(float rotation, Scene2D scene){
		GeometryManipulator gm = scene.getGeometryManipulator();
		int viewPoint = scene.getViewPoint();
		
	          if(viewPoint == Axes2D.POS_X){
	                    gm.rotateVertices(-rotation, 1);
	          }
	          else if(viewPoint == Axes2D.NEG_X){
	                    gm.rotateVertices(rotation, 1);
	          }
	          else if(viewPoint == Axes2D.POS_Y){
	                    gm.rotateVertices(-rotation, 2);
	          }
	          else if(viewPoint == Axes2D.NEG_Y){
	                    gm.rotateVertices(rotation, 2);
	          }
	          else if(viewPoint == Axes2D.POS_Z){
	                    gm.rotateVertices(rotation, 3);
	          }
	          else if(viewPoint == Axes2D.NEG_Z){
	                    gm.rotateVertices(-rotation, 3);
	          }
		else {
			gm.rotateVertices(rotation, 0);
		}
	}

	/**
	 * Adds a new vertex to the specified polygonal mesh
	 * if possible.
	 * 
	 * @param location
	 * 	location of vertex in the coordinate system of the specified <code>Scene2D</code>
	 * @param scene
	 * 	<code>Scene2D</code> in which the vertex was added
	 * @return true if vertex has been successfully added,
	 * 	otherwise false
	 */
	public static boolean addVertex(J3DePoint2D location, Scene2D scene){
		int viewPoint = scene.getViewPoint();
		GeometryManipulator gm = scene.getGeometryManipulator();
		float scale = scene.getScale();
		float[] offset = scene.getOffset();
		J3DePoint2D sceneSize = new J3DePoint2D(scene.getWidth(), scene.getHeight());

		if(viewPoint == Axes2D.POS_X){
			float y = (((sceneSize.getY() / 2) - location.getY()) / scale) - offset[1];
			float z = (((sceneSize.getX() / 2) - location.getX()) / scale) - offset[0];
			return gm.addVertex(0, y, z);
		}
		else if(viewPoint == Axes2D.NEG_X){
			float y = (((sceneSize.getY() / 2) - location.getY()) / scale) - offset[1];
			float z = ((location.getX() - (sceneSize.getX() / 2)) / scale) + offset[0];
			return gm.addVertex(0, y, z);
		}
		else if(viewPoint == Axes2D.POS_Y){
			float x = ((location.getX() - (sceneSize.getX() / 2)) / scale) + offset[0];
			float z = ((location.getY() - (sceneSize.getY() / 2)) / scale) + offset[1];
			return gm.addVertex(x, 0, z);
		}
		else if(viewPoint == Axes2D.NEG_Y){
			float x = ((location.getX() - (sceneSize.getX() / 2)) / scale) + offset[0];
			float z = (((sceneSize.getY() / 2) - location.getY()) / scale) - offset[1];
			return gm.addVertex(x, 0, z);
		}
		else if(viewPoint == Axes2D.POS_Z){
			float x = ((location.getX() - (sceneSize.getX() / 2)) / scale) + offset[0];
			float y = (((sceneSize.getY() / 2) - location.getY()) / scale) - offset[1];
			return gm.addVertex(x, y, 0);
		}
		else if(viewPoint == Axes2D.NEG_Z){
			float x = (((sceneSize.getX() / 2) - location.getX()) / scale) - offset[0];
			float y = (((sceneSize.getY() / 2) - location.getY()) / scale) - offset[1];
			return gm.addVertex(x, y, 0);
		}
		return true;
	}

}
