package modeler;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;

import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.vecmath.Matrix4f;
import javax.vecmath.Tuple2f;
import javax.vecmath.Vector2f;
import javax.vecmath.Vector3f;

import modeler.shape.Shape;
import net.java.games.jogl.GL;
import net.java.games.jogl.GLCanvas;
import net.java.games.jogl.GLCapabilities;
import net.java.games.jogl.GLDrawable;
import net.java.games.jogl.GLDrawableFactory;
import net.java.games.jogl.GLEventListener;
import net.java.games.jogl.GLU;

/**
 * This class is a swing widget that will display the OpenGL implementation of
 * what is going on in the modeler.
 * 
 * @author ags
 */
public class GLView extends JPanel implements GLEventListener {
	
	//GUI element names
	private static final String WIREFRAME_CHECKBOX_TEXT = "Wireframe";
	private static final String LIGHTING_CHECKBOX_TEXT = "Lighting";
	
	static final long serialVersionUID = 1L;
	
	//GUI parameters
	private final int[] viewport = new int[4];
	protected boolean drawInPickMode = false;
	protected boolean multiPickMode = false;
	protected boolean wireframe = false;
	protected boolean lighting = true;
	
	//GUI elements
	protected GLCanvas canvas;
	protected Camera camera;
	protected GLAxes axes;
	protected EventHandler handler;
	protected JTree transformationTreeView;
	protected MainFrame mainFrame;
	
	/**
	 * Create a OpenGL drawing panel
	 * @param newMainFrame
	 * @param newTransformationTreeView
	 * @param newCamera
	 */
	public GLView(MainFrame newMainFrame, JTree newTransformationTreeView, Camera newCamera) {
		
		handler = new EventHandler();
		camera = newCamera;
		axes = new GLAxes();
		mainFrame = newMainFrame;
		transformationTreeView = newTransformationTreeView;
		
		GLCapabilities capabilities = new GLCapabilities();
		capabilities.setDoubleBuffered(true);
		canvas = GLDrawableFactory.getFactory().createGLCanvas(capabilities);
		canvas.addGLEventListener(this);
		
		setLayout(new BorderLayout());
		add(canvas, BorderLayout.CENTER);
		add(createTopPanel(), BorderLayout.NORTH);
		
		canvas.addMouseListener(handler);
		canvas.addMouseMotionListener(handler);
		
		setMinimumSize(new Dimension(250, 250));
	}
	
	/**
	 * Initialize the wireframe and lighting boxes in the panel
	 * @return
	 */
	protected JComponent createTopPanel() {
		
		JPanel toReturn = new JPanel(new BorderLayout());
		toReturn.add(new JLabel(" " + camera), BorderLayout.WEST);
		
		JPanel temp = new JPanel(new GridLayout(1, 2));
		
		JCheckBox wireBox = new JCheckBox(WIREFRAME_CHECKBOX_TEXT, wireframe);
		wireBox.addActionListener(handler);
		temp.add(wireBox);
		JCheckBox lightBox = new JCheckBox(LIGHTING_CHECKBOX_TEXT, lighting);
		lightBox.addActionListener(handler);
		temp.add(lightBox);
		
		toReturn.add(temp, BorderLayout.EAST);
		toReturn.add(new JPanel(), BorderLayout.CENTER);
		return toReturn;
	}
	
	/**
	 * Getter for camera
	 * @return
	 */
	public Camera getCamera() {
		
		return camera;
		
	}
	
	/**
	 * Setter for camera
	 * @param newCamera
	 */
	public void setCamera(Camera newCamera) {
		
		camera = newCamera;
	}
	
	/**
	 * @see net.java.games.jogl.GLEventListener#displayChanged(net.java.games.jogl.GLDrawable, boolean, boolean)
	 */
	public void displayChanged(GLDrawable d, boolean arg1, boolean arg2) {
		
		refresh();
		
	}
	
	/**
	 * Repaints the canvas
	 *
	 */
	public void refresh() {
		
		canvas.repaint();
		
	}
	
	/**
	 * @see net.java.games.jogl.GLEventListener#init(net.java.games.jogl.GLDrawable)
	 */
	public void init(GLDrawable d) {
		
		GL gl = d.getGL();
		gl.glDepthFunc(GL.GL_LESS);
		gl.glEnable(GL.GL_DEPTH_TEST);
		gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
		gl.glEnable(GL.GL_BLEND);
		gl.glEnable(GL.GL_NORMALIZE);
		
		gl.glLightModeli(GL.GL_LIGHT_MODEL_COLOR_CONTROL, GL.GL_SEPARATE_SPECULAR_COLOR);
		gl.glLightModelfv(GL.GL_LIGHT_MODEL_AMBIENT, new float[] { 0.1f, 0.1f, 0.1f });
		gl.glLightModeli(GL.GL_LIGHT_MODEL_LOCAL_VIEWER, GL.GL_TRUE);
		gl.glLightModeli(GL.GL_LIGHT_MODEL_TWO_SIDE, GL.GL_TRUE);
		gl.glClearColor(0.2f, 0.2f, 0.2f, 1);
		gl.glShadeModel(GL.GL_SMOOTH);
		
		gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_MODULATE);
		gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP);
		gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP);
		gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);
		gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);
		
		gl.glSelectBuffer(mainFrame.pickBuffer.capacity(), mainFrame.pickBuffer);
	}
	
	/**
	 * Choosing pick or regular via flag is necessary
	 * because all drawable calls must be made from
	 * the AWT event thread (i.e., start at display())
	 * @see net.java.games.jogl.GLEventListener#display(net.java.games.jogl.GLDrawable)
	 */
	public void display(GLDrawable d) {
		
		GL gl = canvas.getGL();
		GLU glu = canvas.getGLU();

    if (drawInPickMode) {
			gl.glRenderMode(GL.GL_SELECT);
			
			gl.glViewport(0, 0, canvas.getWidth(), canvas.getHeight());
			gl.glGetIntegerv(GL.GL_VIEWPORT, viewport);
			
			// Never wireframe
			gl.glCullFace(GL.GL_BACK);
			gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL);
			
			// Camera and pick projections
			gl.glMatrixMode(GL.GL_PROJECTION);
			gl.glLoadIdentity();
			glu.gluPickMatrix(handler.lastMousePixel_X, (canvas.getHeight() - 1) - handler.lastMousePixel_Y, 6, 6, viewport);
			camera.doProjection(canvas);
			
			// Camera lookat
			gl.glMatrixMode(GL.GL_MODELVIEW);
			gl.glLoadIdentity();
			camera.doModelview(canvas);
			
			gl.glInitNames();
			gl.glPushName(1); // push a dummy name onto the stack so we can call
			// glLoadName from now on
			
			// Render only pickable items
			synchronized (transformationTreeView) {
				TreeModel transformationTree = transformationTreeView.getModel();
				Treeable root = (Treeable) (transformationTree.getRoot());
				Matrix4f mat = new Matrix4f();
				mat.setIdentity();
				root.render(gl, glu, mat);
			}
			
			if (mainFrame.currentManip != null)
				mainFrame.currentManip.renderConstantSize(d, camera);
			
			// Stop pick mode
			drawInPickMode = false;
			
			// Process hits
			processHits(gl.glRenderMode(GL.GL_RENDER));
			
		}
		else {
			displaySetup();
			gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
			gl.glViewport(0, 0, canvas.getWidth(), canvas.getHeight());
			
			gl.glMatrixMode(GL.GL_PROJECTION);
			gl.glLoadIdentity();
			camera.doProjection(canvas);
			
			gl.glMatrixMode(GL.GL_MODELVIEW);
			gl.glLoadIdentity();
			camera.doModelview(canvas);
			
			synchronized (transformationTreeView) {
				TreeModel transformationTree = transformationTreeView.getModel();
				Treeable root = (Treeable) (transformationTree.getRoot());
				
				drawLights(root, camera, gl, glu);
				
				Matrix4f mat = new Matrix4f();
				mat.setIdentity();
				root.render(gl, glu, mat);
			}
			
			drawGrid(gl);
			
			axes.renderConstantSize(d, camera);
			if (mainFrame.currentManip != null)
				mainFrame.currentManip.renderConstantSize(d, camera);
		}
	}
	
	/**
	 * Draws the lights
	 * @param root
	 * @param camera2
	 * @param gl
	 * @param glu
	 */
	private void drawLights(Treeable root, Camera camera, GL gl, GLU glu) {
		
		for (int i=0; i<root.getChildCount(); i++) {
			Treeable node = (Treeable)root.getChildAt(i);
			if (node instanceof Light) {
				Light light = (Light)node;
				light.draw(gl, glu, camera);
			}
		}
		
	}
	
  /**
   * Setup all the lights
   * @param gl
   */
	protected void lightSetup(GL gl) {
		TreeModel transformationTree = transformationTreeView.getModel();
		Treeable root = (Treeable) (transformationTree.getRoot());
		
		for (int i=0; i<root.getChildCount(); i++) {
			Treeable node = (Treeable)root.getChildAt(i);
			if (node instanceof Light) {
				Light light = (Light)node;
				gl.glLightfv(light.id, GL.GL_POSITION, light.position);
				gl.glLightfv(light.id, GL.GL_DIFFUSE, light.diffuse);
				gl.glLightfv(light.id, GL.GL_AMBIENT, light.ambient);
				gl.glLightfv(light.id, GL.GL_SPECULAR, light.specular);
			}
		}
	}
	
	/**
	 * Set the culling, wireframe, and lighting paramters based on current settings
	 */
	protected void displaySetup() {
		
		GL gl = canvas.getGL();
		if (wireframe) {
			gl.glCullFace(GL.GL_BACK);
			gl.glDisable(GL.GL_CULL_FACE);
			gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE);
		}
		else {
			gl.glCullFace(GL.GL_BACK);
			gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL);
		}
		
		if (lighting) {
			gl.glEnable(GL.GL_LIGHTING);
      Light.turnOnLights(gl);
      lightSetup(gl);
		}
		else {
			gl.glDisable(GL.GL_LIGHTING);
		}
	}
	
	/**
	 * Handle the picking events
	 * @param numHits
	 */
	public void processHits(int numHits) {
		
		boolean picked = false;
		// Give hitting manipulators first priority
		for (int i_hit = 0; i_hit < numHits; i_hit++) {
			int idx = i_hit * 4;
			int pickId = mainFrame.pickBuffer.get(idx + 3);
			
			if (pickId < 100) {
				handler.mouseMode = handler.MOUSE_MANIP;
				if (mainFrame.currentManip.axisMode != Manip.PICK_OTHER) // give
					// priority to
					// PICK_OTHER
					mainFrame.currentManip.setPickedInfo(pickId, camera, handler.lastMousePoint);
				picked = true;
			}
		}
		if (picked) {
			refresh();
			return;
		}
		
		// Then objects
		int closestZ = Integer.MAX_VALUE;
		int closestId = 0;
		for (int i_hit = 0; i_hit < numHits; i_hit++) {
			int idx = i_hit * 4;

			int pickId = mainFrame.pickBuffer.get(idx + 3);
			if (pickId < 1000) {
				break;
			}
			int pickZ = mainFrame.pickBuffer.get(idx+1);
			picked = true;
			if (pickZ < closestZ) {
				closestZ = pickZ;
				closestId = pickId;
			} else continue;
		}
		if (picked) {
			Treeable t = Shape.get(closestId);
			if (t.parent != null) t=t.parent; 
			if (multiPickMode) transformationTreeView.addSelectionPath(new TreePath(t.getPath()));
			else transformationTreeView.setSelectionPath(new TreePath(t.getPath()));
		} else {
			transformationTreeView.clearSelection();
			if (mainFrame.currentManip != null) mainFrame.currentManip.drawEnabled = false;
		}
		refresh();
	}
	
	/**
	 * Draws the object position grid
	 */
	protected void drawGrid(GL gl) {
		
		int bigness = 10;
		gl.glPushAttrib(GL.GL_LIGHTING);
		gl.glDisable(GL.GL_LIGHTING);
		gl.glColor3f(0.4f, 0.4f, 0.4f);
		gl.glBegin(GL.GL_LINES);
		if (camera instanceof PerspectiveCamera) {
			for (int i = -bigness; i <= bigness; i++) {
				gl.glVertex3f(i, 0, -bigness);
				gl.glVertex3f(i, 0, bigness);
				gl.glVertex3f(-bigness, 0, i);
				gl.glVertex3f(bigness, 0, i);
			}
		}
		else if (camera == mainFrame.xOrthoCam) {
			OrthoCamera oc = (OrthoCamera) camera;
			// w and h are width and height divided by 2
			float w = oc.height * oc.aspect;
			float h = oc.height;
			for (int i = (int) (oc.target.z - w); i <= (int) (oc.target.z + w); i++) {
				gl.glVertex3f(0, oc.target.y + h, i);
				gl.glVertex3f(0, oc.target.y - h, i);
			}
			for (int i = (int) (oc.target.y - h); i <= (int) (oc.target.y + h); i++) {
				gl.glVertex3f(0, i, oc.target.z + w);
				gl.glVertex3f(0, i, oc.target.z - w);
			}
		}
		else if (camera == mainFrame.yOrthoCam) {
			OrthoCamera oc = (OrthoCamera) camera;
			float w = oc.height * oc.aspect;
			float h = oc.height;
			for (int i = (int) (oc.target.x - w); i <= (int) (oc.target.x + w); i++) {
				gl.glVertex3f(i, 0, oc.target.z + h);
				gl.glVertex3f(i, 0, oc.target.z - h);
			}
			for (int i = (int) (oc.target.z - h); i <= (int) (oc.target.z + h); i++) {
				gl.glVertex3f(oc.target.x + w, 0, i);
				gl.glVertex3f(oc.target.x - w, 0, i);
			}
		}
		else if (camera == mainFrame.zOrthoCam) {
			OrthoCamera oc = (OrthoCamera) camera;
			float w = oc.height * oc.aspect;
			float h = oc.height;
			for (int i = (int) (oc.target.x - w); i <= (int) (oc.target.x + w); i++) {
				gl.glVertex3f(i, oc.target.y + h, 0);
				gl.glVertex3f(i, oc.target.y - h, 0);
			}
			for (int i = (int) (oc.target.y - h); i <= (int) (oc.target.y + h); i++) {
				gl.glVertex3f(oc.target.x + w, i, 0);
				gl.glVertex3f(oc.target.x - w, i, 0);
			}
		}
		gl.glEnd();
		gl.glPopAttrib();
	}
	
	/**
	 * @see net.java.games.jogl.GLEventListener#reshape(net.java.games.jogl.GLDrawable, int, int, int, int)
	 */
	public void reshape(GLDrawable d, int x, int y, int width, int height) {
		axes = new GLAxes();
		camera.setAspect(((float) width) / height);
	}
	
	/**
	 * Handles all mouse events in this window
	 * @author arbree
	 * Oct 21, 2005
	 * GLView.java
	 * Copyright 2005 Program of Computer Graphics, Cornell University
	 */
	private class EventHandler extends MouseAdapter implements MouseMotionListener, ActionListener {
		
		Vector3f motion = new Vector3f();
		protected int lastMousePixel_X, lastMousePixel_Y;
		protected Vector2f lastMousePoint = new Vector2f();
		protected Vector2f currentMousePoint = new Vector2f();
		protected Vector2f mouseDelta = new Vector2f();
		
		//Mode constants
		protected final int MOUSE_PICK = 0;
		protected final int MOUSE_MANIP = 1;
		protected final int MOUSE_ORBIT = 61;
		protected final int MOUSE_ZOOM = 62;
		protected final int MOUSE_TRACK = 63;
		protected final int MOUSE_SPIN = 64;
		
		public int mouseMode = MOUSE_PICK;
		
		public void mouseClicked(MouseEvent e) {
			
			if (e.getClickCount() == 2 && e.getSource() == canvas) {
				mainFrame.setView(GLView.this);
			}
		}
		
		/**
		 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
		 */
		public void mousePressed(MouseEvent e) {
			
			lastMousePixel_X = e.getX();
			lastMousePixel_Y = e.getY();
			lastMousePoint.set(lastMousePixel_X, lastMousePixel_Y);
			windowToViewport(lastMousePoint);
			updateState(e);
		}
		
		/**
		 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
		 */
		public void mouseReleased(MouseEvent e) {
			
			mouseMode = MOUSE_PICK;
			if (mainFrame.currentManip != null)
				mainFrame.currentManip.released();
		}
		
		/**
		 * Compute the current drawing state from the current mouse position 
		 * @param e
		 */
		protected void updateState(MouseEvent e) {
			
			if (flagSet(e, MouseEvent.BUTTON1_DOWN_MASK) && !flagSet(e, MouseEvent.BUTTON3_DOWN_MASK) && !flagSet(e, MouseEvent.CTRL_DOWN_MASK) && flagSet(e, MouseEvent.ALT_DOWN_MASK)) {
				mouseMode = MOUSE_ORBIT;
			}
			else if (!flagSet(e, MouseEvent.BUTTON1_DOWN_MASK) && flagSet(e, MouseEvent.BUTTON3_DOWN_MASK) && !flagSet(e, MouseEvent.CTRL_DOWN_MASK) && flagSet(e, MouseEvent.ALT_DOWN_MASK)) {
				mouseMode = MOUSE_ZOOM;
			}
			else if (flagSet(e, MouseEvent.BUTTON1_DOWN_MASK) && !flagSet(e, MouseEvent.BUTTON3_DOWN_MASK) && flagSet(e, MouseEvent.CTRL_DOWN_MASK) && flagSet(e, MouseEvent.ALT_DOWN_MASK)) {
				mouseMode = MOUSE_TRACK;
			}
			else if (!flagSet(e, MouseEvent.BUTTON1_DOWN_MASK) && flagSet(e, MouseEvent.BUTTON3_DOWN_MASK) && flagSet(e, MouseEvent.CTRL_DOWN_MASK) && flagSet(e, MouseEvent.ALT_DOWN_MASK)) {
				mouseMode = MOUSE_SPIN;
			}
			else {
				multiPickMode = flagSet(e, MouseEvent.SHIFT_DOWN_MASK);  
				drawInPickMode = true;
				refresh();
			}
		}
		
		/**
		 * Compute viewport mouse positions from window position
		 * @param p
		 */
		protected void windowToViewport(Tuple2f p) {
			
			int w = canvas.getWidth();
			int h = canvas.getHeight();
			p.set((2 * p.x - w) / w, (2 * (h - p.y - 1) - h) / h);
		}
		
		/**
		 * Return the state of a mouse flag
		 * @param e
		 * @param flag
		 * @return
		 */
		protected boolean flagSet(MouseEvent e, int flag) {
			
			return (e.getModifiersEx() & flag) == flag;
		}
		
		/**
		 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
		 */
		public void mouseDragged(MouseEvent e) {
			
			currentMousePoint.set(e.getX(), e.getY());
			windowToViewport(currentMousePoint);
			
			//mouseDelta.sub(currentMousePoint, lastMousePoint);
			mouseDelta.set(currentMousePoint);
			mouseDelta.sub(lastMousePoint);
			
			switch (mouseMode) {
			case MOUSE_PICK:
				// don't support lasso-selection...
				break;
			case MOUSE_MANIP:
				mainFrame.currentManip.dragged(currentMousePoint, mouseDelta );
				break;
			case MOUSE_TRACK:
				camera.convertMotion(mouseDelta, motion);
				mainFrame.trackAllCams(motion);
				break;
			case MOUSE_ORBIT:
				if (camera == mainFrame.pViewCam)
					((PerspectiveCamera) camera).orbit(mouseDelta);
				else {
					camera.convertMotion(mouseDelta, motion);
					mainFrame.trackAllCams(motion);
				}
				break;
			case MOUSE_ZOOM:
				if (camera != mainFrame.pViewCam) {
					mainFrame.zoomAllOrtho(mouseDelta.y);
				}
				else {
					camera.zoom(mouseDelta.y);
				}
				break;
			case MOUSE_SPIN:
				break;
			}
			
			lastMousePoint.set(e.getX(), e.getY());
			windowToViewport(lastMousePoint);
			mainFrame.refresh();
		}
		
		/**
		 * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
		 */
		public void mouseMoved(MouseEvent e) {
			
			// TODO: make sure nothing needs to be here
		}
		
		/**
		 * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
		 */
		public void actionPerformed(ActionEvent e) {
			
			String cmd = e.getActionCommand();
			
			if (cmd == null) {
				return;
			}
			else if (cmd.equals(WIREFRAME_CHECKBOX_TEXT)) {
				wireframe = !wireframe;
			}
			else if (cmd.equals(LIGHTING_CHECKBOX_TEXT)) {
				lighting = !lighting;
			}
			
			refresh();
		}
		
	}
	
}
