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.GLSim;
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 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;
  
  //The rendering engine
  protected GLSim glSim = new GLSim();

  /**
   * 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.7f, 0.7f, 0.7f, 1);
    gl.glShadeModel(GL.GL_SMOOTH);

    gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_REPLACE);
    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, 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());
        Matrix4f mat = new Matrix4f();
        mat.setIdentity();
        root.render(gl, mat);
      }

      drawGrid(gl);

      axes.renderConstantSize(d, camera);
      if (mainFrame.currentManip != null)
        mainFrame.currentManip.renderConstantSize(d, camera);
    }
  }

  /**
   * 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);
    }
    else {
      gl.glDisable(GL.GL_LIGHTING);
    }

    Light.turnOnLights(gl);
  }

  /**
   * 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.setViewportInfo(pickId, camera);
        picked = true;
      }
    }
    if (picked) {
      refresh();
      return;
    }

    // Then objects
    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) {
        transformationTreeView.setSelectionPath(new TreePath(Shape.get(pickId).getPath()));
        break;
      }
    }
    refresh();
  }

  /**
   * Recursively renders the GL tree
   * @param gl
   * @param node
   */
  protected void renderTree(GLSim gl, Treeable node) {

    if (node instanceof Transformation) {

      Transformation t = (Transformation) node;
      glSim.pushMatrix();
      t.applyTranformation(gl);
      for (int ctr = 0; ctr < node.getChildCount(); ctr++) {
        renderTree(gl, (Treeable) node.getChildAt(ctr));
      }
      glSim.popMatrix();

    }
    else if (node instanceof Shape) {
      ((Shape) node).render(gl.gl);
    }
  }

  /**
   * 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) {

    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);
      // }
    }

    protected void focusOnNode(TreePath path) {

      // DefaultMutableTreeNode last =
      // (DefaultMutableTreeNode)path.getLastPathComponent();
      // if (!(last.getUserObject() instanceof Shape)) return;
      //
      // Object[] nodes = path.getPath();
      // Matrix4f mat = new Matrix4f();
      // mat.setIdentity();
      // for (int ctr = 0; ctr < nodes.length - 1; ctr++) {
      // DefaultMutableTreeNode node = (DefaultMutableTreeNode)nodes[ctr];
      // if (!(node.getUserObject() instanceof Transformation)) return;
      // Transformation t = (Transformation)node.getUserObject();
      //
      // mat.mul(t.getMatrix());
      // }
      //
      // Point3f center = new Point3f(0, 0, 0);
      // mat.transform(center);
      //
      // center.sub(camera.getTarget());
      //
      // camera.getTarget().add(center);
      // camera.getEye().add(center);
      // camera.updateFrame();
    }

    /**
     * @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 {
        // TODO
        // since we're here, we're trying to pick or manipulate
        // gl-pick down on the curr position
        // if we hit a manip handle, land in that mouse mode
        // if we hit a shape, select it and stay in MOUSE_PICK
        // then update currentManipulator.
        // (most of this might be done in pickDisplay for eventqueue reasons)
        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) {

      mouseDelta.set(e.getX(), e.getY());
      windowToViewport(mouseDelta);

      mouseDelta.sub(lastMousePoint);

      switch (mouseMode) {
        case MOUSE_PICK:
          // don't support lasso-selection...
          break;
        case MOUSE_MANIP:
          mainFrame.currentManip.dragged(mouseDelta, e);
          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();
    }

  }

}
