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.DefaultMutableTreeNode;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.vecmath.Matrix4f;
import javax.vecmath.Point3f;
import javax.vecmath.Tuple2f;
import javax.vecmath.Vector2f;
import javax.vecmath.Vector3f;

import modeler.shape.Cube;
import modeler.shape.Cylinder;
import modeler.shape.GLSim;
import modeler.shape.Shape;
import modeler.shape.Sphere;
import net.java.games.jogl.GLU;
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;

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

  static final long serialVersionUID = 1L;

  private static final float[] cameraColor = new float[] { 0.447f, 0.302f, 0.027f };
  private static final float[] upColor = new float[] { 0.651f, 0.439f, 0.043f };

  private static final String WIREFRAME_CHECKBOX_TEXT = "Wireframe";
  private static final String LIGHTING_CHECKBOX_TEXT = "Lighting";

  protected GLCanvas canvas;
  protected Camera camera;
  protected GLAxes axes;
  protected EventHandler handler;
  protected JTree transformationTreeView;
  protected MainFrame mainFrame;
  protected boolean wireframe = false;
  protected boolean lighting = true;
  protected GLSim glSim = new GLSim();
  protected Cube cube = new Cube();
  protected Sphere sphere = new Sphere();
  protected Cylinder cylinder = new Cylinder();

  public GLView(MainFrame newMainFrame, JTree newTransformationTreeView, Camera newCamera) {

    handler = new EventHandler();
    camera = newCamera;
    axes = new GLAxes();
    mainFrame = newMainFrame;
    transformationTreeView = newTransformationTreeView;
    transformationTreeView.addMouseListener(handler);

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

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

  public Camera getCamera() {

    return camera;
  }

  public void setCamera(Camera newCamera) {

    camera = newCamera;
  }

  public void displayChanged(GLDrawable d, boolean arg1, boolean arg2) {

    refresh();
  }

  public void refresh() {

    canvas.repaint();
  }

  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_NORMALIZE);
    gl.glShadeModel(GL.GL_FLAT);

    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.glMateriali(GL.GL_FRONT, GL.GL_SHININESS, 40);
    gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, new float[] { 1, 1, 1 });

    gl.glClearColor(0.7f, 0.7f, 0.7f, 1);

    gl.glEnable(GL.GL_COLOR_MATERIAL);
    gl.glColorMaterial(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE);
    gl.glShadeModel(GL.GL_SMOOTH);

  }

  public void display(GLDrawable d) {

    GL gl = d.getGL();
    GLU glu = d.getGLU();

    displaySetup(gl);
    gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
    gl.glViewport(0, 0, canvas.getWidth(), canvas.getHeight());

    camera.setProjection(d);
    camera.setup(d);

    gl.glMatrixMode(GL.GL_MODELVIEW);

    glSim.readyToRender(d);

    synchronized (transformationTreeView) {
      TreeModel transformationTree = transformationTreeView.getModel();
      gl.glColor3f(1, 0, 0);
      cylinder.drawShape(glSim);
    }

    axes.glRender(gl, glu);
    drawGrid(gl);
  }

  protected void displaySetup(GL gl) {

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

  protected void drawCamera(GL gl, PerspectiveCamera c) {

    gl.glDisable(GL.GL_LIGHTING);

    gl.glCullFace(GL.GL_BACK);
    gl.glDisable(GL.GL_CULL_FACE);
    gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE);

    gl.glColor3fv(cameraColor);

    Point3f eye = c.getEye();
    Point3f target = c.getTarget();
    Vector3f up = c.getUp();
    Vector3f right = c.getRight();
    Vector3f negGaze = new Vector3f(eye);
    negGaze.sub(target);

    negGaze.normalize();

    gl.glBegin(GL.GL_QUAD_STRIP);
    // Draw Camera Body
    gl.glVertex3f(eye.x + up.x / 2 + right.x / 2, eye.y + up.y / 2 + right.y / 2, eye.z + up.z / 2 + right.z / 2);
    gl.glVertex3f(eye.x + up.x / 2 - right.x / 2, eye.y + up.y / 2 - right.y / 2, eye.z + up.z / 2 - right.z / 2);
    gl.glVertex3f(eye.x - up.x / 2 + right.x / 2, eye.y - up.y / 2 + right.y / 2, eye.z - up.z / 2 + right.z / 2);
    gl.glVertex3f(eye.x - up.x / 2 - right.x / 2, eye.y - up.y / 2 - right.y / 2, eye.z - up.z / 2 - right.z / 2);

    gl.glVertex3f(eye.x - up.x / 2 + right.x / 2 + negGaze.x, eye.y - up.y / 2 + right.y / 2 + negGaze.y, eye.z - up.z / 2 + right.z / 2 + negGaze.z);
    gl.glVertex3f(eye.x - up.x / 2 - right.x / 2 + negGaze.x, eye.y - up.y / 2 - right.y / 2 + negGaze.y, eye.z - up.z / 2 - right.z / 2 + negGaze.z);

    gl.glVertex3f(eye.x + up.x / 2 + right.x / 2 + negGaze.x, eye.y + up.y / 2 + right.y / 2 + negGaze.y, eye.z + up.z / 2 + right.z / 2 + negGaze.z);
    gl.glVertex3f(eye.x + up.x / 2 - right.x / 2 + negGaze.x, eye.y + up.y / 2 - right.y / 2 + negGaze.y, eye.z + up.z / 2 - right.z / 2 + negGaze.z);

    gl.glVertex3f(eye.x + up.x / 2 + right.x / 2, eye.y + up.y / 2 + right.y / 2, eye.z + up.z / 2 + right.z / 2);
    gl.glVertex3f(eye.x + up.x / 2 - right.x / 2, eye.y + up.y / 2 - right.y / 2, eye.z + up.z / 2 - right.z / 2);

    gl.glEnd();

    gl.glBegin(GL.GL_LINES);
    // Draw Line to lookat point
    gl.glVertex3f(eye.x, eye.y, eye.z);
    gl.glVertex3f(target.x, target.y, target.z);

    gl.glColor3fv(upColor);
    // Draw Line for up vector
    gl.glVertex3f(eye.x, eye.y, eye.z);
    gl.glVertex3f(eye.x + up.x, eye.y + up.y, eye.z + up.z);
    gl.glEnd();

    displaySetup(gl);
  }

  protected void drawLight(GL gl, Light l) {

    gl.glDisable(GL.GL_LIGHTING);
    gl.glBegin(GL.GL_LINES);
    gl.glColor3fv(l.diffuse);

    float x = l.position[0];
    float y = l.position[1];
    float z = l.position[2];

    gl.glVertex3f(x - 0.5f, y, z);
    gl.glVertex3f(x + 0.5f, y, z);
    gl.glVertex3f(x, y - 0.5f, z);
    gl.glVertex3f(x, y + 0.5f, z);
    gl.glVertex3f(x, y, z - 0.5f);
    gl.glVertex3f(x, y, z + 0.5f);

    gl.glVertex3f(x - 0.5f, y - 0.5f, z);
    gl.glVertex3f(x + 0.5f, y + 0.5f, z);
    gl.glVertex3f(x - 0.5f, y + 0.5f, z);
    gl.glVertex3f(x + 0.5f, y - 0.5f, z);

    gl.glVertex3f(x - 0.5f, y, z - 0.5f);
    gl.glVertex3f(x + 0.5f, y, z + 0.5f);
    gl.glVertex3f(x - 0.5f, y, z + 0.5f);
    gl.glVertex3f(x + 0.5f, y, z - 0.5f);

    gl.glVertex3f(x, y - 0.5f, z - 0.5f);
    gl.glVertex3f(x, y + 0.5f, z + 0.5f);
    gl.glVertex3f(x, y - 0.5f, z + 0.5f);
    gl.glVertex3f(x, y + 0.5f, z - 0.5f);

    gl.glEnd();

    displaySetup(gl);
  }

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

  public void reshape(GLDrawable d, int x, int y, int width, int height) {

    camera.setAspect(((float) width) / height);
  }

  public class EventHandler extends MouseAdapter implements MouseMotionListener, ActionListener {

    Vector3f motion = new Vector3f();

    protected Vector2f lastMousePoint = new Vector2f();

    protected Vector2f currentMousePoint = new Vector2f();

    protected Vector2f mouseDelta = new Vector2f();

    protected final int MOUSE_PICK = 0;

    protected final int MOUSE_ROTATE_X = 11;

    protected final int MOUSE_ROTATE_Y = 12;

    protected final int MOUSE_ROTATE_Z = 13;

    protected final int MOUSE_TRANSLATE_X = 21;

    protected final int MOUSE_TRANSLATE_Y = 22;

    protected final int MOUSE_TRANSLATE_Z = 23;

    protected final int MOUSE_SCALE_X = 31;

    protected final int MOUSE_SCALE_Y = 32;

    protected final int MOUSE_SCALE_Z = 33;

    protected final int MOUSE_ORBIT = 61;

    protected final int MOUSE_ZOOM = 62;

    protected final int MOUSE_TRACK = 63;

    protected final int MOUSE_SPIN = 64;

    protected int mouseMode = MOUSE_PICK;

    public void mouseClicked(MouseEvent e) {

      if (e.getClickCount() != 2)
        return;

      if (e.getSource() == canvas) {
        mainFrame.setView(GLView.this);
      }
      else if (e.getSource() instanceof JTree) {
        JTree tree = (JTree) e.getSource();
        focusOnNode(tree.getClosestPathForLocation(e.getX(), e.getY()));
        mainFrame.refresh();
      }
    }

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

    public void mousePressed(MouseEvent e) {

      lastMousePoint.set(e.getX(), e.getY());
      windowToViewport(lastMousePoint);
      updateState(e);
    }

    public void mouseReleased(MouseEvent e) {

      // what?
    }

    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.
      }
    }

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

    protected boolean flagSet(MouseEvent e, int flag) {

      return (e.getModifiersEx() & flag) == flag;
    }

    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_ROTATE_X:
          break;
        case MOUSE_ROTATE_Y:
          break;
        case MOUSE_ROTATE_Z:
          break;
        case MOUSE_TRANSLATE_X:
          break;
        case MOUSE_TRANSLATE_Y:
          break;
        case MOUSE_TRANSLATE_Z:
          break;
        case MOUSE_SCALE_X:
          break;
        case MOUSE_SCALE_Y:
          break;
        case MOUSE_SCALE_Z:
          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();
    }

    public void mouseMoved(MouseEvent e) {

      // TODO: make sure nothing needs to be here
    }

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

  }

}
