package j3deditor.bin.editor;


import j3deditor.bin.editor.util.GeometryEditorListener;

import j3deditor.bin.editor.util.VertexManipulator;

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

import j3deditor.bin.util.Utilities;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import java.awt.event.MouseMotionListener;

import javax.swing.JCheckBox;
import javax.swing.JPanel;


/**
 * Interface for manipulating a geometry.
 * Visualizes the vertices and edges of a polygonal mesh.
 *
 * @author Risto Seene
 */
public class Scene2D extends JPanel implements VirtualScene2D, VirtualCamera, MouseListener, MouseMotionListener, KeyListener{

	/** SceneBuffer that updates the image of this scene */
	private SceneBuffer buffer;

	private GeometryManipulator gm;

	private GeometryEditorListener l;

	private JCheckBox synchronize;
	
	private Image[] icons;

	private int viewPoint;

	private int texCoordSet = 0;

	private Image texImage = null;

	private float scale;

	private float offsetX;

	private float offsetY;

	private Color backColor;

	private Color normalColor;

	private Color selectedColor;

	private Color transColor;

	private boolean transform = false;

	private boolean edgeVisibility = true;

	private J3DePoint2D cursorPoint;

	private J3DePoint2D vertCenter;


	private boolean guideLine = false;

	private boolean borderSelect = false;

	private J3DePoint2D initBorder;

	private J3DePoint2D borderStart;

	private J3DePoint2D borderEnd;


	/**
	 * Creates an instance of <code>Scene2D</code> with the
	 * specified viewpoint.
	 * 
	 * @param viewPoint
	 * 	<code>Axes2D.TEX</code>, <code>Axes2D.POS_X</code>, <code>Axes2D.POS_Y</code>,
	 * 	<code>Axes2D.POS_Z</code>, <code>Axes2D.NEG_X</code>, <code>Axes2D.NEX_Y</code>
	 * 	or <code>Axes2D.NEG_Z</code>
	 */
	public Scene2D(int viewPoint){
		this.buffer = new SceneBuffer(this);
		this.viewPoint = viewPoint;
		if(viewPoint == Axes2D.TEX){
			scale = 200;
			offsetX = 0.5f;
			offsetY = -0.5f;
		}
		else {
			scale = 10;
			offsetX = 0;
			offsetY = 0;
		}

		backColor = new Color(230, 230, 230);
		normalColor = new Color(0, 0, 240, 50);
		selectedColor = new Color(255, 150, 0);
		transColor = new Color(0, 255, 0);

		setFocusable(true);
		addMouseListener(this);
		addMouseMotionListener(this);
		addKeyListener(this);

		initContent();
	}

	/**
	 * Initializes the content of this <code>Scene2D</code>.
	 */
	private void initContent(){
	          this.setLayout(null);

	          this.synchronize = new JCheckBox("", true);
	          this.synchronize.setOpaque(false);
	          String syncInfo = Utilities.translator.translate(this.getClass(), "syncInfo");
	          this.synchronize.setToolTipText(syncInfo);
	          this.synchronize.setBounds(0, 0, (int)(synchronize.getPreferredSize().getWidth()), (int)(synchronize.getPreferredSize().getHeight()));
	          this.add(this.synchronize);
		
		icons = new Image[3];
		icons[0] = Utilities.image.getImage("icons/translate.png");
	          icons[1] = Utilities.image.getImage("icons/rotate.png");
	          icons[2] = Utilities.image.getImage("icons/resize.png");
	}

	/**
	 * Sets the <code>GeometryManipulator</code> this scene
	 * gets its geometry data.
	 * 
	 * @param gm
	 * 	GeometryManipulator
	 */
	public void setGeometryManipulator(GeometryManipulator gm){
		this.gm = gm;
		this.updateView(this);
	}

	/**
	 * Sets the geometry editor listener.
	 * 
	 * @param l
	 * 	listener
	 */
	public void setGeometryEditorListener(GeometryEditorListener l){
		this.l = l;
	}

	/**
	 * Sets the background image of this scene.
	 * Shown when in texture coordinate mode.
	 * 
	 * @param image
	 * 	image
	 */
	protected void setImage(Image image){
		this.texImage = image;
		this.updateView(this);
	}

	/**
	 * Notifies listener taht scene has been changed.
	 */
	private void contentUpdated(){
		if(l != null){
			l.sceneChanged(this);
		}
	}

	/**
	 * Notifies listener that geometry data has been changed.
	 */
	private void geometryUpdated(){
	          if(l != null){
	                    l.dataChanged(this);
	          }
	}

	/**
	 * Translates the selected part of the polygonal mesh.
	 * 
	 * @param movement
	 * 	change of the cursor position
	 */
	private void translateVertices(J3DePoint2D movement){
		VertexManipulator.translateVertices(movement.getX() / this.scale, movement.getY() / (-this.scale), this);
	}

	/**
	 * Rotates the selected part of the polygonal mesh.
	 * 
	 * @param newPoint
	 * 	new cursor position
	 */
	private void rotateVertices(J3DePoint2D newPoint){
		J3DePoint2D oldPoint = new J3DePoint2D(cursorPoint.getX() - vertCenter.getX(), cursorPoint.getY() - vertCenter.getY());
		float rotation = (float)(oldPoint.getVectorAngle() - newPoint.getVectorAngle());
		VertexManipulator.rotateVertices(rotation, this);
	}

	/**
	 * Resizes the selected part of the polygonal mesh.
	 * 
	 * @param newPoint
	 * 	new cursor position
	 */
	private void scaleVertices(J3DePoint2D newPoint){
		J3DePoint2D oldPoint = new J3DePoint2D(cursorPoint.getX() - vertCenter.getX(), cursorPoint.getY() - vertCenter.getY());
		VertexManipulator.scaleVertices((float)(newPoint.getVectorLength() - oldPoint.getVectorLength()), this);
	}

	/**
	 * Adds a vertex to this scene.
	 * 
	 * @param location
	 * 	the location of the vertex in scene coordinates
	 */
	private void addVertex(J3DePoint2D location){
		if(this.viewPoint != Axes2D.TEX){
			if(!VertexManipulator.addVertex(location, this)){
				String info = Utilities.translator.translate(this.getClass(), "geometryFull");
				Utilities.out.throwInfo(info);
			}
		}
	}


	/**
	 * Draws the scene on this panel.
	 * 
	 * @param graphics
	 * 	graphics
	 */
	public void paintComponent(Graphics graphics){
		Graphics2D g = (Graphics2D)graphics;
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		super.paintComponent(g);

		g.drawImage(this.buffer.getImage(), 0, 0, this);
		g.setColor(Color.gray);

		if(borderSelect){
			J3DePoint2D end = J3DePoint2D.sub(borderEnd, borderStart);
			g.drawRect(borderStart.getX(), borderStart.getY(), end.getX(), end.getY());
		}
		else if(guideLine){
			g.drawLine(vertCenter.getX(), vertCenter.getY(), cursorPoint.getX(), cursorPoint.getY());
		}
		
		if(this.gm != null && gm.getTransformMode() != GeometryManipulator.NONE){
			int y = this.getHeight() - 16;
			if(gm.getTransformMode() == GeometryManipulator.TRANSLATE){
				g.drawImage(icons[0], 0, y, this);
				g.drawString(Utilities.translator.translate(this.getClass(), "translate"), 16, y+12);
			}
		          else if(gm.getTransformMode() == GeometryManipulator.ROTATE){
		                    g.drawImage(icons[1], 0, y, this);
		                    g.drawString(Utilities.translator.translate(this.getClass(), "rotate"), 16, y+12);
		          }
		          else if(gm.getTransformMode() == GeometryManipulator.SCALE){
		                    g.drawImage(icons[2], 0, y, this);
		                    g.drawString(Utilities.translator.translate(this.getClass(), "scale"), 16, y+12);
		          }
		}
	}

	/**
	 * Draws the content of this scene.
	 * 
	 * @param graphics
	 * 	graphics
	 */
	protected void paintContent(Graphics graphics){
		Graphics2D g = (Graphics2D)graphics;
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		super.paintComponent(g);

		this.drawBackground(g);

		if(gm != null){
			J3DePoint2D size = new J3DePoint2D(getWidth(), getHeight());
			int stateType = VertexManipulator.getStateType(this);
			int vertCount = gm.getVertexCount();

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

				if(v.getState(stateType) != Vertex.HIDDEN){
					J3DePoint2D p = VertexManipulator.getVertexPosition2D(v, this);
					boolean visible = VertexManipulator.isInBounds(p, new J3DePoint2D(), size);

					if(edgeVisibility){
						int index = VertexManipulator.getEdgeEndIndex(i, this);
						if(index < vertCount){
							Vertex v2 = gm.getVertex(index);
							if(v2.getState(stateType) != Vertex.HIDDEN){
								J3DePoint2D p2 = VertexManipulator.getVertexPosition2D(v2, this);
								if(visible || VertexManipulator.isInBounds(p2, new J3DePoint2D(), size)){
									if(v.getState(stateType) == Vertex.SELECTED && v2.getState(stateType) == Vertex.SELECTED){
										g.setColor(selectedColor);
									}
									else if(v.getState(stateType) == Vertex.TRANSFORMABLE && v2.getState(stateType) == Vertex.TRANSFORMABLE){
										g.setColor(transColor);
									}
									else {
										g.setColor(normalColor);
									}
									g.drawLine(p.getX(), p.getY(), p2.getX(), p2.getY());
								}
							}
						}
					}

					if(visible){
						if(v.getState(stateType) == Vertex.NORMAL){
							g.setColor(normalColor);
						}
						else if(v.getState(stateType) == Vertex.SELECTED){
							g.setColor(selectedColor);
						}
						else if(v.getState(stateType) == Vertex.TRANSFORMABLE){
							g.setColor(transColor);
						}
						g.fillOval(p.getX() - 1, p.getY() - 1, 3, 3);
					}
				}
			}
		}
	}

	/**
	 * Draws the background of the image shown on
	 * this panel.
	 * 
	 * @param g
	 * 	graphics
	 */
	private void drawBackground(Graphics2D g){
		g.setColor(backColor);
		g.fillRect(0, 0, getWidth(), getHeight());

		if(this.viewPoint == Axes2D.TEX && this.texImage != null){
			int size = (int)scale;
			int posX = (int)(getWidth() / 2 - scale * offsetX);
			int posY = (int)(getHeight() / 2 - size - scale * offsetY);
			g.drawImage(texImage, posX, posY, size, size, this);
		}
	}


	/**
	 * Various things will be done.
	 * 
	 * @param e
	 * 	mouse event
	 */
	public void mousePressed(MouseEvent e){
		if(gm != null){
			if(gm.getTransformMode() == GeometryManipulator.NONE){
				borderSelect = true;
				initBorder = new J3DePoint2D(e.getX(), e.getY());
				borderStart = new J3DePoint2D(initBorder);
				borderEnd = new J3DePoint2D(initBorder);
			}
			else {
				transform = true;
				if(gm.getTransformMode() != GeometryManipulator.TRANSLATE){
					vertCenter = VertexManipulator.getVertexPosition2D(this.gm.getCenter(), this);
					guideLine = true;
				}
			}
			cursorPoint = new J3DePoint2D(e.getX(), e.getY());
			repaint();
		}
	}

	/**
	 * Various things will be done.
	 * 
	 * @param e
	 * 	mouse event
	 */
	public void mouseReleased(MouseEvent e){
		if(gm != null){
			if(e.getButton() == MouseEvent.BUTTON1 && gm.getTransformMode() == GeometryManipulator.NONE){
				if(borderStart.equals(borderEnd)){
					VertexManipulator.selectVertex(new J3DePoint2D(e.getX(), e.getY()), this, e.isShiftDown());
				}
				else {
					VertexManipulator.selectVertices(borderStart, borderEnd, this);
				}
				borderSelect = false;
				this.contentUpdated();
			          this.geometryUpdated();
			}
			else if(e.getButton() == MouseEvent.BUTTON1 && transform){
				transform = false;
				vertCenter = null;
				guideLine = false;
				repaint();
			}
			else if(e.getButton() == MouseEvent.BUTTON3){
				this.addVertex(new J3DePoint2D(e.getX(), e.getY()));
				this.contentUpdated();
			          this.geometryUpdated();
			}
		}
	}

	/**
	 * Not in use.
	 * 
	 * @param e
	 *        doesn't do anything
	 */
	public void mouseClicked(MouseEvent e){
	}

	/**
	 * Not in use.
	 * 
	 * @param e
	 *        doesn't do anything
	 */
	public void mouseEntered(MouseEvent e){
		requestFocus();
	}

	/**
	 * Not in use.
	 * 
	 * @param e
	 *        doesn't do anything
	 */
	public void mouseExited(MouseEvent e){
	}

	/**
	 * Not in use.
	 * 
	 * @param e
	 * 	doesn't do anything
	 */
	public void mouseMoved(MouseEvent e){
	}

	/**
	 * Border select or vertex transformation is performed.
	 * 
	 * @param e
	 * 	mouse event
	 */
	public void mouseDragged(MouseEvent e){
		if(borderSelect){
			if(e.getX() < initBorder.getX()){
				borderStart.setX(e.getX());
			}
			else {
				borderEnd.setX(e.getX());
			}
			if(e.getY() < initBorder.getY()){
				borderStart.setY(e.getY());
			}
			else {
				borderEnd.setY(e.getY());
			}
			repaint();
		}
		else if(transform && gm != null){
			if(gm.getTransformMode() == GeometryManipulator.TRANSLATE){
				J3DePoint2D movement = new J3DePoint2D(e.getX() - this.cursorPoint.getX(), e.getY() - this.cursorPoint.getY());
				this.cursorPoint.setCoordinates(e.getX(), e.getY());
				this.translateVertices(movement);
			}
			else if(gm.getTransformMode() == GeometryManipulator.ROTATE){
				this.rotateVertices(new J3DePoint2D(e.getX() - vertCenter.getX(), e.getY() - vertCenter.getY()));
				this.cursorPoint.setCoordinates(e.getX(), e.getY());
			}
			else if(gm.getTransformMode() == GeometryManipulator.SCALE){
				this.scaleVertices(new J3DePoint2D(e.getX() - vertCenter.getX(), e.getY() - vertCenter.getY()));
				this.cursorPoint.setCoordinates(e.getX(), e.getY());
			}

			this.contentUpdated();
		}
	}


	/**
	 * If T, R or S key is pressed the corresponding
	 * transformation mode is activated.
	 * 
	 * @param e
	 * 	key event
	 */
	public void keyPressed(KeyEvent e){
		if(gm != null){
			if(e.getKeyCode() == KeyEvent.VK_T){
				gm.setTransformMode(GeometryManipulator.TRANSLATE);
				this.contentUpdated();
			}
			else if(e.getKeyCode() == KeyEvent.VK_R){
				gm.setTransformMode(GeometryManipulator.ROTATE);
				this.contentUpdated();
			}
			else if(e.getKeyCode() == KeyEvent.VK_S){
				gm.setTransformMode(GeometryManipulator.SCALE);
				this.contentUpdated();
			}
		}
	}

	/**
	 * Not in use.
	 * 
	 * @param e
	 *        doesn't do anything
	 */
	public void keyReleased(KeyEvent e){
	}

	/**
	 * Not in use.
	 * 
	 * @param e
	 * 	doesn't do anything
	 */
	public void keyTyped(KeyEvent e){
	}


	/**
	 * Returns the camera of this scene.
	 * 
	 * @return the camera of this scene
	 */
	public VirtualCamera getCamera(){
		return this;
	}

	/**
	 * Moves the virtual focus of the camera.
	 *
	 * @param move1
	 *        movement of the focus along the X-axis of the camera.
	 *        Difference, not absolute position
	 * @param move2
	 *        movement of the focus along the Y-axis of the camera.
	 *        Difference, not absolute position
	 */
	public void moveFocus(float move1, float move2){
		offsetX += 100 * move1 / scale;
		offsetY += 100 * move2 / scale;
		this.updateView(this);
	}

	/**
	 * Zooms the camera in/out.
	 *
	 * @param zoom
	 *	the amount to zoom
	 */
	public void zoomCamera(float zoom){
		scale -= (scale * zoom);
		this.updateView(this);
	}


	/**
	 * Not in use.
	 * 
	 * @param angle
	 * 	doesn't do anything
	 * @param angleZ
	 * 	doesn't do anything
	 */
	public void rotateCamera(float angle, float angleZ){
	}


	/**
	 * Flips this scene.
	 */
	public void flip(){
		if(viewPoint == Axes2D.POS_X){
			viewPoint = Axes2D.NEG_X;
		}
		else if(viewPoint == Axes2D.POS_Y){
			viewPoint = Axes2D.NEG_Y;
		}
		else if(viewPoint == Axes2D.POS_Z){
			viewPoint = Axes2D.NEG_Z;
		}
		else if(viewPoint == Axes2D.NEG_X){
			viewPoint = Axes2D.POS_X;
		}
		else if(viewPoint == Axes2D.NEG_Y){
			viewPoint = Axes2D.POS_Y;
		}
		else if(viewPoint == Axes2D.NEG_Z){
			viewPoint = Axes2D.POS_Z;
		}

		this.updateView(this);
	}

	/**
	 * Returns the viewpoint of the scene camera.
	 * <code>Axes2D.TEX</code>, <code>Axes2D.POS_X</code>, <code>Axes2D.POS_Y</code>,
	 * <code>Axes2D.POS_Z</code>, <code>Axes2D.NEG_X</code>, <code>Axes2D.NEX_Y</code>
	 * or <code>Axes2D.NEG_Z</code>
	 * 
	 * @return the viewpoint of the scene camera
	 */
	public int getViewPoint(){
		return viewPoint;
	}

	/**
	 * Returns the camera Z location.
	 * 
	 * @return the camera Z location
	 */
	public float getScale(){
		return scale;
	}

	/**
	 * Returns the location of camera on scene X, Y axes.
	 * 
	 * @return the location of camera on scene X, Y axes
	 */
	public float[] getOffset(){
		return new float[]{ this.offsetX, this.offsetY };
	}

	/**
	 * Returns the <code>GeometryManipulator</code> object that
	 * this scene uses for getting geometry information.
	 * 
	 * @return the <code>GeometryManipulator</code> object that
	 * 	this scene uses for getting geometry information
	 */
	public GeometryManipulator getGeometryManipulator(){
		return this.gm;
	}


	/**
	 * Enables/disables the visibility of edges.
	 * 
	 * @param visibility
	 * 	if true edges will be visible,
	 *        otherwise invisible
	 */
	public void setEdgeVisibility(boolean visibility){
		this.edgeVisibility = visibility;
		this.updateView(this);
	}

	/**
	 * Updates the image of this <code>Scene2D</code>
	 * if this scene is synchronizable or the caller of this
	 * method is this <code>Scene2D</code> object.
	 * 
	 * @param sender
	 * 	method caller
	 */
	public void updateView(Object sender){
		if(sender == this){
			this.buffer.updateImage();
		}
		else if(this.synchronize.isSelected()){
			this.buffer.updateImage();
		}
	}

	/**
	 * Returns the texture coordinate set this scene shows
	 * if it would be of type of <code>Axes2D.TEX</code>.
	 * 
	 * @return the texture coordinate set this scene shows
	 * 	if it would be of type of <code>Axes2D.TEX</code>
	 */
	public int getTexCoordSet(){
		return this.texCoordSet;
	}

	/**
	 * Sets the texture coordinate set this scene shows
	 * if it would be of type of <code>Axes2D.TEX</code>.
	 * 
	 * @param texCoordSet
	 * 	texture coordinate set
	 */
	public void setTexCoordSet(int texCoordSet){
		this.texCoordSet = texCoordSet;
		if(this.viewPoint == Axes2D.TEX){
			this.updateView(this);
		}
	}

}
