package modeler.shape;

import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Vector;

import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.vecmath.Matrix4f;

import modeler.EditSplineDialog;
import modeler.ItemAttributePanel;
import modeler.MainFrame;
import modeler.Texture;
import modeler.Treeable;
import net.java.games.jogl.GL;
import net.java.games.jogl.GLU;

/**
 * @author ags
 */
public abstract class Shape extends Treeable {

  // The current sub-division levels for this shape
  protected int slices = 10;
  protected int segments = 30;
  
  // Auto-incrementing id for picking purposes.
  private static int nextId = 1000;
  private int pickId = getNextPickId(this);

  // A map from pickIds to Shape objects
  private static Vector pickableShapes = new Vector();

  // The default material parameters
  protected final float[] diffuseColor = new float[] {1.0f, 0, 0, 1.0f};
  protected final float[] specularColor = new float[] {0.0f, 0, 0, 1.0f};
  protected float expSpecular = 1;
  protected String currentTexture = null;
  protected static transient HashMap textureMap = new HashMap();

  // Image used in tree
  protected transient Icon icon;
  
  //The mesh for this shape
  protected transient Mesh mesh;
  
  //The attribute panel for this shape
  protected transient AttributePanel attribs; 

  /**
   * Basic constructor uses default material and builds the mesh.
   *
   */
  public Shape() {

    String classname = this.getClass().getName();
    setName(classname.substring(classname.lastIndexOf('.') + 1, classname.length()));
    
  }

  /**
   * Just sets the pick id and then calls drawShape
   * 
   * @param glSim
   */
  public void render(GL gl) {

    //Rebuild the mesh if the refinement changed
    if(mesh == null)
      buildMesh();
    
    //Update mesh positions if this is a BezierRotation
    if(this instanceof BezierRotation)
      ((BezierRotation) this).updateMesh();
    
    //Set the material parameters
    gl.glMaterialfv(GL.GL_FRONT, GL.GL_DIFFUSE, diffuseColor);
    gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT, diffuseColor);
    gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, specularColor);
    gl.glMaterialf(GL.GL_FRONT, GL.GL_SHININESS, expSpecular);
    gl.glColor3fv(diffuseColor);
    
    /**
     * Setup the texture
     */
    if(currentTexture != null) {
      
      //Try and get the currenttexture from the map
      Texture currTex = (Texture) textureMap.get(currentTexture);
      if(currTex == null) {
        
        try {
          currTex = new Texture(new File(currentTexture));
          textureMap.put(currentTexture, currTex);
        }
        catch (Exception e) {
          System.err.println("Error loading texture file.");
          currentTexture = null;
        }
      }
      if(currentTexture != null)
        currTex.enable(gl);
    }
    
    //Set the pick id
    gl.glLoadName(pickId);
    mesh.render(gl);
    gl.glDisable(GL.GL_TEXTURE_2D);
    
  }

  /**
   * Builds a mesh for this shape using the current subdivision levels.
   */
  public abstract void buildMesh();
  
  /**
   * Returns the next available pick ID
   * @param s
   * @return
   */
  private static int getNextPickId(Shape s) {

    // the index of s in pickableShapes is (s.pickId - 1000)
    pickableShapes.add(s);
    return nextId++;
    
  }

  /**
   * Gets the shape with the given ID
   * @param id
   * @return
   */
  public static Shape get(int id) {

    int idx = id - 1000;
    if (idx < 0 || idx >= pickableShapes.size())
      return null;
    
    return (Shape) pickableShapes.get(idx);
  }

  /**
   * Returns the pick id for this object
   * @return
   */
  public int getPickId() {

    return pickId;
  }

  /**
   * @see modeler.Treeable#getIcon()
   */
  public Icon getIcon() {

    if(icon == null)
      icon = new ImageIcon("../icons/thingy.gif");
    return icon;
  }
  
  /**
   * @see modeler.Treeable#getAttributePanel(modeler.MainFrame)
   */
  public ItemAttributePanel getAttributePanel(MainFrame mainFrame) {

    if (attribs == null) {
      attribs = new AttributePanel(mainFrame, this);
    }
    return attribs;
    
  }

  /**
   * @see modeler.Treeable#render(net.java.games.jogl.GL, javax.vecmath.Matrix4f)
   */
  public void render(GL gl, GLU glu, Matrix4f currMat) {

    //Flatten the matrix into OpenGL form
    float[] flatMatrix = new float[16];
    for (int i = 0; i < 4; i++) {
      for (int j = 0; j < 4; j++) {
        flatMatrix[j * 4 + i] = currMat.getElement(i, j);
      }
    }
    
    gl.glPushMatrix();
    gl.glMultMatrixf(flatMatrix);
    render(gl);
    gl.glPopMatrix();
    
  }

  /**
   * @see javax.swing.tree.TreeNode#getAllowsChildren()
   */
  public boolean getAllowsChildren() {

    return false;

  }

  /**
   * @see modeler.Treeable#readData(java.io.ObjectInputStream)
   */
  protected void readData(ObjectInputStream in) throws IOException {

    slices = in.readInt();
    segments = in.readInt();
    pickId = in.readInt();
    
    //Update the pickId maps
    if(nextId <= pickId)
      nextId = pickId + 1;
    if(pickableShapes.size() < (pickId - 999))
      pickableShapes.setSize(pickId - 999);
    pickableShapes.set(pickId-1000, this);
    
    diffuseColor[0] = in.readFloat();
    diffuseColor[1] = in.readFloat();
    diffuseColor[2] = in.readFloat();
    specularColor[0] = in.readFloat();
    specularColor[1] = in.readFloat();
    specularColor[2] = in.readFloat();
    expSpecular = in.readFloat();
    try {
      currentTexture = (String) in.readObject();
    }
    catch (ClassNotFoundException e) {
      throw new Error("modeler.shape.Shape.readData(): Error reading current texture filename.");
    }
  }

  /**
   * @see modeler.Treeable#saveData(java.io.ObjectOutputStream)
   */
  protected void saveData(ObjectOutputStream out) throws IOException {

    out.writeInt(slices);
    out.writeInt(segments);
    out.writeInt(pickId);
    out.writeFloat(diffuseColor[0]);
    out.writeFloat(diffuseColor[1]);
    out.writeFloat(diffuseColor[2]);
    out.writeFloat(specularColor[0]);
    out.writeFloat(specularColor[1]);
    out.writeFloat(specularColor[2]);
    out.writeFloat(expSpecular);
    out.writeObject(currentTexture);
    
  }

  /**
   * @see modeler.Treeable#exportMainData(java.util.HashMap, java.io.PrintWriter)
   */
  protected void exportMainData(HashMap materialsMap, PrintWriter pw) {

    MaterialWrapper mat = new MaterialWrapper(this);
    
    //Check and see if our material is already in the map
    if(materialsMap.containsKey(mat))
      return;
    
    //Add ourselves to the materials map
    String name = "Material"+materialsMap.size();
    materialsMap.put(mat, name);
    
    //Write our material out
    if(currentTexture == null) {

      pw.println("  <material name=\""+name+"\" type=\"ray1.material.Phong\">");
      pw.println("    <diffuseColor>"+diffuseColor[0]+" "+diffuseColor[1]+" "+diffuseColor[2]+"</diffuseColor>");
      pw.println("    <specularColor>"+specularColor[0]+" "+specularColor[1]+" "+specularColor[2]+"</specularColor>");
      pw.println("    <exponent>"+expSpecular+"</exponent>");
      pw.println("  </material>");
      
    } else {
      
      pw.println("  <material name=\""+name+"\" type=\"ray1.material.TexturedPhong\">");
      pw.println("    <diffuseColor>"+diffuseColor[0]+" "+diffuseColor[1]+" "+diffuseColor[2]+"</diffuseColor>");
      pw.println("    <specularColor>"+specularColor[0]+" "+specularColor[1]+" "+specularColor[2]+"</specularColor>");
      pw.println("    <exponent>"+expSpecular+"</exponent>");
      pw.println("    <texture>"+currentTexture+"</texture>");
      pw.println("  </material>");
      
    }
  }
  
  /**
   * @see modeler.Treeable#exportMeshData(java.util.HashMap, java.io.PrintWriter, javax.vecmath.Matrix4f)
   */
  protected void exportMeshData(HashMap materialsMap, PrintWriter pw, Matrix4f mat) {

    String matName = (String) materialsMap.get(new MaterialWrapper(this));
    pw.println(matName);
    if(mesh == null)
      buildMesh();
    mesh.writeMesh(mat, pw);
    
  }
  
  /**
   * @see javax.swing.tree.TreeNode#isLeaf()
   */
  public boolean isLeaf() {

    return true;

  }

  /**
   * Quick casting version of cosine
   * @param d
   * @return
   */
  protected float cos(double d) {

    return (float) Math.cos(d);
  }
  
  /**
   * Quick casting version of sine
   * @param d
   * @return
   */
  protected float sin(double d) {

    return (float) Math.sin(d);
  }
  
  /**
   * Used to test the equality of the material part of this shape
   * @author arbree
   * Oct 23, 2005
   * Shape.java
   * Copyright 2005 Program of Computer Graphics, Cornell University
   */
  private class MaterialWrapper {
    
    /**
     * The shape this wraps
     */
    protected final Shape shape;
    
    /**
     * Wraps the given shape
     */
    public MaterialWrapper(Shape inShape) {
      
      shape = inShape;
      
    }

    /**
     * @see java.lang.Object#equals(java.lang.Object)
     */
    public boolean equals(Object obj) {

      if(!(obj instanceof MaterialWrapper))
        return false;
      
      Shape oShape = ((MaterialWrapper) obj).shape;
      if(oShape.currentTexture == null && shape.currentTexture != null)
        return false;
      if(oShape.currentTexture != null && shape.currentTexture == null)
        return false;
      if(oShape.currentTexture != null && shape.currentTexture != null && !oShape.currentTexture.equalsIgnoreCase(shape.currentTexture))
        return false;
      
      if(oShape.expSpecular != shape.expSpecular)
        return false;
      
      for(int i = 0; i < 3; i++)      
        if(oShape.diffuseColor[i] != shape.diffuseColor[i] || oShape.specularColor[i] != shape.specularColor[i])
          return false;
      
      return true;
      
    }

    /**
     * @see java.lang.Object#hashCode()
     */
    public int hashCode() {

      int outCode = 0;
      if(shape.currentTexture != null)
        outCode = shape.currentTexture.hashCode();
      float total = shape.expSpecular;
      for(int i = 0; i < 3; i++)
        total += shape.diffuseColor[i]+shape.specularColor[i];
      total *= 10000;
      outCode += (int) total;
      return outCode;
      
    }
  }
  
  /**
   * The attribute panel for this node
   * @author arbree
   * Oct 21, 2005
   * Transformation.java
   * Copyright 2005 Program of Computer Graphics, Cornell University
   */
  private class AttributePanel extends ItemAttributePanel implements ActionListener, DocumentListener {

    private static final long serialVersionUID = 3256437006337718072L;
    
    //Text fields for each of the components
    protected JFormattedTextField Dr;
    protected JFormattedTextField Dg;
    protected JFormattedTextField Db;
    protected JFormattedTextField Sr;
    protected JFormattedTextField Sg;
    protected JFormattedTextField Sb;
    protected JFormattedTextField exp;
    protected JFormattedTextField sliceFld;
    protected JFormattedTextField segmentFld;
    protected JButton setButton;
    protected JButton clearButton;
    protected JTextField textureFile;
    protected JButton editSpline;
    
    protected JFileChooser fileChooser;

    /**
     * Inherited constructor
     * @param mf
     */
    protected AttributePanel(MainFrame mf, Shape shape) {

      super(mf, shape);
      
    }
    
    /**
     * Builds the attribute panel
     * @param mf
     */
    public void initialize() {
      
      Dr = new JFormattedTextField(new DecimalFormat("#0.0#"));
      Dg = new JFormattedTextField(new DecimalFormat("#0.0#"));
      Db = new JFormattedTextField(new DecimalFormat("#0.0#"));
      Sr = new JFormattedTextField(new DecimalFormat("#0.0#"));
      Sg = new JFormattedTextField(new DecimalFormat("#0.0#"));
      Sb = new JFormattedTextField(new DecimalFormat("#0.0#"));
      exp = new JFormattedTextField(new DecimalFormat("#0.0#"));
      sliceFld = new JFormattedTextField(new DecimalFormat("#"));
      segmentFld = new JFormattedTextField(new DecimalFormat("#"));
      fileChooser = new JFileChooser();

      JPanel colorPanel = new JPanel();
      colorPanel.setLayout(new GridLayout(4,4));

      colorPanel.add(new JLabel(""));
      JLabel lbl = new JLabel("R");
      lbl.setHorizontalAlignment(SwingConstants.CENTER);
      colorPanel.add(lbl);
      lbl = new JLabel("G");
      lbl.setHorizontalAlignment(SwingConstants.CENTER);
      colorPanel.add(lbl);
      lbl = new JLabel("B");
      lbl.setHorizontalAlignment(SwingConstants.CENTER);
      colorPanel.add(lbl);

      lbl = new JLabel("Diff: ");
      lbl.setHorizontalAlignment(SwingConstants.RIGHT);
      colorPanel.add(lbl);
      colorPanel.add(Dr);
      colorPanel.add(Dg);
      colorPanel.add(Db);
      Dr.getDocument().addDocumentListener(this);
      Dg.getDocument().addDocumentListener(this);
      Db.getDocument().addDocumentListener(this);

      lbl = new JLabel("Spec: ");
      lbl.setHorizontalAlignment(SwingConstants.RIGHT);
      colorPanel.add(lbl);
      colorPanel.add(Sr);
      colorPanel.add(Sg);
      colorPanel.add(Sb);
      Sr.getDocument().addDocumentListener(this);
      Sg.getDocument().addDocumentListener(this);
      Sb.getDocument().addDocumentListener(this);
      
      lbl = new JLabel("Exp: ");
      lbl.setHorizontalAlignment(SwingConstants.RIGHT);
      colorPanel.add(lbl);
      colorPanel.add(exp);
      exp.getDocument().addDocumentListener(this);
      colorPanel.add(new JLabel());
      colorPanel.add(new JLabel());
      
      JPanel lowerPanel = new JPanel();
      GridLayout gld = new GridLayout(3,1);
      lowerPanel.setLayout(gld);
      lbl = new JLabel("Texture Name");
      lbl.setHorizontalAlignment(SwingConstants.CENTER);
      lowerPanel.add(lbl);
      textureFile = new JTextField();
      textureFile.setEditable(false);
      lowerPanel.add(textureFile);
      JPanel buttons = new JPanel();
      gld = new GridLayout(1,2);
      buttons.setLayout(gld);
      setButton = new JButton("Set");
      setButton.addActionListener(this);
      buttons.add(setButton);
      clearButton = new JButton("Clear");
      clearButton.addActionListener(this);
      buttons.add(clearButton);
      lowerPanel.add(buttons);
      
      JPanel midPanel = new JPanel();
      midPanel.setLayout(new GridLayout(3,1));
      lbl = new JLabel("Mesh Refinement");
      lbl.setHorizontalAlignment(SwingConstants.CENTER);
      midPanel.add(lbl);
      midPanel.add(addLabelLeft(sliceFld, "Slices:"));
      sliceFld.getDocument().addDocumentListener(this);
      midPanel.add(addLabelLeft(segmentFld, "Segments:"));
      segmentFld.getDocument().addDocumentListener(this);
      
      BoxLayout fld = new BoxLayout(this, BoxLayout.Y_AXIS);
      this.setLayout(fld);
      this.add(colorPanel);
      this.add(lowerPanel);
      this.add(midPanel);
      
      if(host instanceof BezierRotation) {
        
        JPanel tempPanel = new JPanel();
        editSpline = new JButton("Edit Spline");
        tempPanel.add(editSpline);
        editSpline.addActionListener(this);
        this.add(tempPanel);
        
      }
    }

    /**
     * Sets the display fields to the current values of the transformations
     */
    public void refresh() {

      Dr.setValue(new Float(diffuseColor[0]));
      Dg.setValue(new Float(diffuseColor[1]));
      Db.setValue(new Float(diffuseColor[2]));
      Sr.setValue(new Float(specularColor[0]));
      Sg.setValue(new Float(specularColor[1]));
      Sb.setValue(new Float(specularColor[2]));
      exp.setValue(new Float(expSpecular));
      segmentFld.setValue(new Integer(segments));
      sliceFld.setValue(new Integer(slices));
      
    }

    /**
     * Updates the stored data
     */
    private void update() {

      try {
        diffuseColor[0] = Float.parseFloat(Dr.getText());
        diffuseColor[1] = Float.parseFloat(Dg.getText());
        diffuseColor[2] = Float.parseFloat(Db.getText());
        specularColor[0] = Float.parseFloat(Sr.getText());
        specularColor[1] = Float.parseFloat(Sg.getText());
        specularColor[2] = Float.parseFloat(Sb.getText());
        expSpecular = Float.parseFloat(exp.getText());
        int newSlice = Integer.parseInt(sliceFld.getText());
        int newSegments = Integer.parseInt(segmentFld.getText());
        newSlice = (newSlice < 4 ? 4 : newSlice);
        newSegments = (newSegments < 4 ? 4 : newSegments);
        
        //Update the refinement if necessary and tell the shape to rebuild mesh
        if(segments != newSegments || slices != newSlice) {
          
          segments = newSegments;
          slices = newSlice;
          mesh = null;
          
        }
        mf.refresh();
      }
      catch (NumberFormatException e) {
        // Do nothing just skip those updates for invalid data
      }

    }

    /**
     * @see javax.swing.event.DocumentListener#changedUpdate(javax.swing.event.DocumentEvent)
     */
    public void changedUpdate(DocumentEvent e) {

      update();
    }

    /**
     * @see javax.swing.event.DocumentListener#insertUpdate(javax.swing.event.DocumentEvent)
     */
    public void insertUpdate(DocumentEvent e) {

      update();
    }

    /**
     * @see javax.swing.event.DocumentListener#removeUpdate(javax.swing.event.DocumentEvent)
     */
    public void removeUpdate(DocumentEvent e) {

      update();
    }

    /**
     * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
     */
    public void actionPerformed(ActionEvent e) {

      if(e.getSource() == setButton) {
        
        //Pick a file
        int choice = fileChooser.showOpenDialog(this);
        if (choice != JFileChooser.APPROVE_OPTION) {
          return;
        }
        currentTexture = fileChooser.getSelectedFile().getAbsolutePath();
        String shortName = fileChooser.getSelectedFile().getName();
        textureFile.setText(shortName);
        
      } else if(e.getSource() == clearButton){
        
        textureFile.setText("");
        currentTexture = null;
        
      } else if(e.getSource() == editSpline) {
        
        (new EditSplineDialog(this.mf, mf.getTree(), (BezierRotation) host)).show();
        
      }
      mf.refresh();
      
    }
  }




}