/**
 * This package provides classes to facilitate the handling of opengl textures, glsl shaders and 
 * off-screen rendering in Processing.
 * @author Andres Colubri
 * @version 0.9.4.1
 *
 * Copyright (c) 2008 Andres Colubri
 *
 * This source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This code is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 * 
 * A copy of the GNU General Public License is available on the World
 * Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also
 * obtain it by writing to the Free Software Foundation,
 * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

package codeanticode.glgraphics;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;

import javax.media.opengl.GL;
import processing.core.PApplet;
import processing.core.PConstants;
import processing.opengl.PGraphicsOpenGL;
import processing.xml.XMLElement;

// TODO for 0.9.3 and eventual use in GLGraphics renderer:
// Edge flag pointers (with glEdgeFlagPointer)
// Restart method (deletes current arrays and creates new ones with specified size)
// ST atlas to apply different textures to different vertices in the model. See this 
// thread: 
// http://www.opengl.org/discussion_boards/ubbthreads.php?ubb=showflat&Number=247526

public class GLModelEffect implements PConstants {
  /**
   * Default constructor.
   */
  public GLModelEffect() {
    this.parent = null;
  }

  public GLModelEffect(PApplet parent, String filename) {
    this.parent = parent;
    initEffect(filename);
  }

  /**
   * Creates an instance of GLTextureFilter, loading the filter from a URL.
   */
  public GLModelEffect(PApplet parent, URL url) {
    this.parent = parent;
    initEffect(url);
  }

  /**
   * Returns the description of the filter.
   * 
   * @return String
   */
  public String getDescription() {
    return description;
  }

  public void apply(GLModel model) {
    model.render(this);
  }

  public void apply(GLModel[] models) {
    for (int i = 0; i < models.length; i++)
      models[i].render(this);
  }

  public void start() {
    shader.start();

    for (int i = 0; i < numParams; i++)
      paramsArray[i].copyToShader();
  }

  public void stop() {
    shader.stop();
  }

  public void setTextures(GLTexture[] textures) {
    // This implementation:

    for (int i = 0; i < textures.length; i++) {
      if (-1 < texUnitUniform[i])
        gl.glUniform1iARB(texUnitUniform[i], i);
    }

    // is incomplete. It assumes everything is nicely ordered.
    // The way it should work is as follows: each texture has a name, so:
    // name = texture[i].getName() (right GLTexture objects don't have name
    // property)
    // then look for the texuniform and texunit corresponding to name:
    // if (texHashMap.containsKey(name))
    // v = (vec2)texHashMap.get(name);
    // gl.glUniform1iARB(v.x, v.y);
    // here v.x stores the uniform and v.y the unit.
  }

  public void setVertexAttribs(int[] attribVBO, String[] attribName,
      int[] attribSize) {
    // This implementation:

    for (int i = 0; i < numVertAttribs; i++) {
      gl.glBindBufferARB(GL.GL_ARRAY_BUFFER_ARB, attribVBO[i]);
      vertAttribArray[i].setAttribArrayPointer(attribSize[i], false, 0);
    }

    // is also incomplete, because it also assumes everything is nicely ordered.
    // The array with attribute names should be use to determine which attribute
    // should be associated to the VBO:
    // if (vertAttribHashMap.containsKey(attribName[i]))
    // attribute = vertAttribHashMap.get(attribName[i]);
    // gl.glBindBufferARB(GL.GL_ARRAY_BUFFER_ARB, attribVBO[i]);
    // attribute.setAttribArrayPointer(attribSize[i], false, 0);
  }

  public void enableVertexAttribs() {
    for (int i = 0; i < numVertAttribs; i++) {
      vertAttribArray[i].enableVertexAttribArray();
    }
  }

  public void disableVertexAttribs() {
    for (int i = 0; i < numVertAttribs; i++) {
      vertAttribArray[i].disableVertexAttribArray();
    }
  }

  /**
   * Sets the parameter value when the type is int.
   * 
   * @param String
   *          paramName
   * @param int value
   */
  public void setParameterValue(String paramName, int value) {
    if (paramsHashMap.containsKey(paramName)) {
      GLModelEffectParameter param = (GLModelEffectParameter) paramsHashMap
          .get(paramName);
      param.setValue(value);
    }
  }

  /**
   * Sets the parameter value when the type is float.
   * 
   * @param String
   *          paramName
   * @param float value
   */
  public void setParameterValue(String paramName, float value) {
    if (paramsHashMap.containsKey(paramName)) {
      GLModelEffectParameter param = (GLModelEffectParameter) paramsHashMap
          .get(paramName);
      param.setValue(value);
    }
  }

  /**
   * Sets the parameter value for any type. When the type is int or float, the
   * first element of the value array is considered.
   * 
   * @param String
   *          paramName
   * @param value
   *          float[]
   */
  public void setParameterValue(String paramName, float[] value) {
    if (paramsHashMap.containsKey(paramName)) {
      GLModelEffectParameter param = (GLModelEffectParameter) paramsHashMap
          .get(paramName);
      param.setValue(value);
    }
  }

  /**
   * Sets the ith value for the parameter (only valid for vec or mat types).
   * 
   * @param String
   *          paramName
   * @param int i
   * @param value
   *          float
   */
  public void setParameterValue(String paramName, int i, float value) {
    if (paramsHashMap.containsKey(paramName)) {
      GLModelEffectParameter param = (GLModelEffectParameter) paramsHashMap
          .get(paramName);
      param.setValue(i, value);
    }
  }

  /**
   * Sets the (ith, jth) value for the parameter (only valid for mat types).
   * 
   * @param String
   *          paramName
   * @param int i
   * @param int j
   * @param value
   *          float
   */
  public void setParameterValue(String paramName, int i, int j, float value) {
    if (paramsHashMap.containsKey(paramName)) {
      GLModelEffectParameter param = (GLModelEffectParameter) paramsHashMap
          .get(paramName);
      param.setValue(i, j, value);
    }
  }

  /**
   * Sets all the value for all the parameters, by means of a parameter list of
   * variable length. values is an array of float[].
   * 
   * @param float[] values
   */
  public void setParameterValues(float[]... values) {
    float[] value;
    for (int i = 0; i < values.length; i++) {
      value = values[i];
      paramsArray[i].setValue(value);
    }
  }

  /**
   * Get number of parameters.
   * 
   * @return int
   */
  public int getParameterCount() {
    return numParams;
  }

  /**
   * Returns the type of the i-th parameter.
   * 
   * @return int
   */
  public int getParameterType(int i) {
    return paramsArray[i].getType();
  }

  /**
   * Returns the name of the i-th parameter.
   * 
   * @return String
   */
  public String getParameterName(int i) {
    return paramsArray[i].getName();
  }

  /**
   * Returns the label of the i-th parameter.
   * 
   * @return String
   */
  public String getParameterLabel(int i) {
    return paramsArray[i].getLabel();
  }

  /**
   * Returns the i-th parameter.
   * 
   * @return GLModelEffectParameter
   */
  public GLModelEffectParameter getParameter(int i) {
    return paramsArray[i];
  }

  /**
   * Sets the parameter value when the type is int.
   * 
   * @param int n
   * @param int value
   */
  public void setParameterValue(int n, int value) {
    paramsArray[n].setValue(value);
  }

  /**
   * Sets the parameter value when the type is float.
   * 
   * @param int n
   * @param float value
   */
  public void setParameterValue(int n, float value) {
    paramsArray[n].setValue(value);
  }

  /**
   * Sets the parameter value for any type. When the type is int or float, the
   * first element of the value array is considered.
   * 
   * @param int n
   * @param value
   *          float[]
   */
  public void setParameterValue(int n, float[] value) {
    paramsArray[n].setValue(value);
  }

  /**
   * Sets the ith value for the parameter (only valid for vec or mat types).
   * 
   * @param int n
   * @param int i
   * @param value
   *          float
   */
  public void setParameterValue(int n, int i, float value) {
    paramsArray[n].setValue(i, value);
  }

  /**
   * Sets the (ith, jth) value for the parameter (only valid for mat types).
   * 
   * @param int n
   * @param int i
   * @param int j
   * @param value
   *          float
   */
  public void setParameterValue(int n, int i, int j, float value) {
    paramsArray[n].setValue(i, j, value);
  }

  /**
   * Returns the parameter with the provided name.
   * 
   * @return GLModelEffectParameter
   */
  public GLModelEffectParameter getParameter(String paramName) {
    if (paramsHashMap.containsKey(paramName)) {
      GLModelEffectParameter param = (GLModelEffectParameter) paramsHashMap
          .get(paramName);
      return param;
    }
    return null;
  }

  /**
   * @invisible
   */
  protected void initEffect(String filename) {
    initEffectCommon();

    filename = filename.replace('\\', '/');
    XMLElement xml = new XMLElement(parent, filename);

    loadXML(xml);

    initShader(filename, false);
  }

  /**
   * @invisible
   */
  protected void initEffect(URL url) {
    initEffectCommon();

    try {
      String xmlText = PApplet
          .join(PApplet.loadStrings(url.openStream()), "\n");
      XMLElement xml = new XMLElement(xmlText);
      loadXML(xml);
    } catch (IOException e) {
      System.err.println("Error loading effect: " + e.getMessage());
    }

    initShader(url.toString(), true);
  }

  /**
   * Common initialization code
   * 
   * @invisible
   */
  private void initEffectCommon() {
    pgl = (PGraphicsOpenGL) parent.g;
    gl = pgl.gl;

    glstate = new GLState(gl);

    numTextures = 0;
    texHashMap = new HashMap<String, Integer>();
    texUnitUniform = null;
    texNameArray = null;

    numParams = 0;
    paramsHashMap = new HashMap<String, GLModelEffectParameter>();
    paramsArray = null;

    numVertAttribs = 0;
    vertAttribHashMap = new HashMap<String, GLModelEffectVertexAttrib>();
    vertAttribArray = null;
  }

  protected void loadXML(XMLElement xml) {
    // Parsing xml configuration.

    int n = xml.getChildCount();
    String name;
    XMLElement child;
    vertexFN = geometryFN = fragmentFN = "";
    for (int i = 0; i < n; i++) {
      child = xml.getChild(i);
      name = child.getName();
      if (name.equals("description")) {
        description = child.getContent();
      } else if (name.equals("vertex")) {
        // vertexFN = fixShaderFilename(child.getContent(), rootPath);
        vertexFN = child.getContent();
      } else if (name.equals("geometry")) {
        // geometryFN = fixShaderFilename(child.getContent(), rootPath);
        geometryFN = child.getContent();
        inGeoPrim = child.getStringAttribute("input");
        outGeoPrim = child.getStringAttribute("output");
        maxNumOutVert = child.getIntAttribute("vertcount");
      } else if (name.equals("fragment")) {
        // fragmentFN = fixShaderFilename(child.getContent(), rootPath);
        fragmentFN = child.getContent();
      } else if (name.equals("textures")) {
        loadTextures(child);
      } else if (name.equals("vertexattribs")) {
        loadVertAttribs(child);
      } else if (name.equals("parameters")) {
        loadParams(child);
      } else {
        System.err.println("Unrecognized element in effect config file!");
      }
    }    
  }

  protected void loadTextures(XMLElement xml) {
    int n = xml.getChildCount();
    numTextures = n;
    texNameArray = new String[numTextures];
    texUnitUniform = new int[numTextures];

    XMLElement child;
    String name, texName, valueStr;
    int texUnit;
    for (int i = 0; i < n; i++) {
      child = xml.getChild(i);
      name = child.getName();
      if (name.equals("texture")) {
        texName = child.getStringAttribute("name");
        valueStr = child.getContent();
        texUnit = PApplet.parseInt(PApplet.split(valueStr, ' '))[0];
        texHashMap.put(texName, new Integer(texUnit));
        texNameArray[i] = texName;
      }
    }
  }

  protected void loadVertAttribs(XMLElement xml) {
    int n = xml.getChildCount();
    numVertAttribs = n;
    vertAttribArray = new GLModelEffectVertexAttrib[numVertAttribs];

    XMLElement child;
    String name;
    String attrName, attrTypeStr, attrValueStr, attrLabelStr;
    int attrType;
    GLModelEffectVertexAttrib attrib;
    for (int i = 0; i < n; i++) {
      child = xml.getChild(i);
      name = child.getName();
      if (name.equals("vertexattrib")) {
        float[] attrValue;
        attrName = child.getStringAttribute("name");
        attrTypeStr = child.getStringAttribute("type");
        attrLabelStr = child.getStringAttribute("label");
        attrValueStr = child.getContent();
        attrType = GLModelEffectVertexAttrib.getType(attrTypeStr);

        attrValue = PApplet.parseFloat(PApplet.split(attrValueStr, ' '));

        if ((-1 < attrType) && !paramsHashMap.containsKey(attrName)) {
          attrib = new GLModelEffectVertexAttrib(parent, attrName,
              attrLabelStr, attrType);
          attrib.setValue(attrValue);
          vertAttribHashMap.put(attrName, attrib);
          vertAttribArray[i] = attrib;
        }
      }
    }
  }

  protected void loadParams(XMLElement xml) {
    int n = xml.getChildCount();
    numParams = n;
    paramsArray = new GLModelEffectParameter[numParams];

    XMLElement child;
    String name;
    String parName, parTypeStr, parValueStr, parLabelStr;
    int parType;
    GLModelEffectParameter param;
    for (int i = 0; i < n; i++) {
      child = xml.getChild(i);
      name = child.getName();
      if (name.equals("parameter")) {
        float[] parValue;
        parName = child.getStringAttribute("name");
        parTypeStr = child.getStringAttribute("type");
        parLabelStr = child.getStringAttribute("label");
        parValueStr = child.getContent();
        parType = GLModelEffectParameter.getType(parTypeStr);

        parValue = PApplet.parseFloat(PApplet.split(parValueStr, ' '));

        if ((-1 < parType) && !paramsHashMap.containsKey(parName)) {
          param = new GLModelEffectParameter(parent, parName, parLabelStr,
              parType);
          param.setValue(parValue);
          paramsHashMap.put(parName, param);
          paramsArray[i] = param;
        }
      }
    }
  }

  /**
   * @invisible
   */
  String fixShaderFilename(String filename, String rootPath) {
    String fixedFN = filename.replace('\\', '/');
    if (!rootPath.equals("") && (fixedFN.indexOf(rootPath) != 0))
      fixedFN = rootPath + fixedFN;
    return fixedFN;
  }

  /**
   * Initialize the GLSLShader object.
   * 
   * @param xmlFilename
   *          the XML filename for this filter, used to generate the proper path
   *          for the shader's programs
   * @param useURL
   *          if true, URL objects will be created to load the shader programs
   *          instead of direct filenames
   * @invisible
   */
  protected void initShader(String xmlFilename, boolean useURL) {
    // Getting the root path of the xml file
    int idx;
    String rootPath = "";
    idx = xmlFilename.lastIndexOf('/');
    if (-1 < idx) {
      rootPath = xmlFilename.substring(0, idx + 1);
    }

    // Initializing shader.
    shader = new GLSLShader(parent);

    if (!vertexFN.equals("")) {
      vertexFN = fixShaderFilename(vertexFN, rootPath);
      if (useURL) {
        try {
          shader.loadVertexShader(new URL(vertexFN));
        } catch (MalformedURLException e) {
          System.err.println(e.getMessage());
        }
      } else
        shader.loadVertexShader(vertexFN);
    }

    if (!geometryFN.equals("")) {
      geometryFN = fixShaderFilename(geometryFN, rootPath);
      if (useURL) {
        try {
          shader.loadGeometryShader(new URL(geometryFN));
        } catch (MalformedURLException e) {
          System.err.println(e.getMessage());
        }
      } else
        shader.loadGeometryShader(geometryFN);
      shader.setupGeometryShader(inGeoPrim, outGeoPrim, maxNumOutVert);
    }

    if (!fragmentFN.equals("")) {
      fragmentFN = fixShaderFilename(fragmentFN, rootPath);
      if (useURL) {
        try {
          shader.loadFragmentShader(new URL(fragmentFN));
        } catch (MalformedURLException e) {
          System.err.println(e.getMessage());
        }
      } else
        shader.loadFragmentShader(fragmentFN);
    }

    shader.setup();

    for (int i = 0; i < numTextures; i++) {
      texUnitUniform[i] = shader.getUniformLocation(texNameArray[i]);
    }

    // Generating the parameter IDs.
    for (int i = 0; i < numParams; i++) {
      paramsArray[i].setShader(shader);
      paramsArray[i].init();
    }

    // Generating the vertex attribute IDs.
    for (int i = 0; i < numVertAttribs; i++) {
      vertAttribArray[i].setShader(shader);
      vertAttribArray[i].init();
    }
  }

  /**
   * @invisible
   */
  protected PApplet parent;

  /**
   * @invisible
   */
  protected String description;

  /**
   * @invisible
   */
  protected GL gl;

  /**
   * @invisible
   */
  protected PGraphicsOpenGL pgl;

  /**
   * @invisible
   */
  protected GLState glstate;

  /**
   * @invisible
   */
  protected GLSLShader shader;

  /**
   * @invisible
   */
  protected String vertexFN;

  /**
   * @invisible
   */
  protected String geometryFN;

  /**
   * @invisible
   */
  protected String fragmentFN;

  /**
   * @invisible
   */
  protected String inGeoPrim;

  /**
   * @invisible
   */
  protected String outGeoPrim;

  /**
   * @invisible
   */
  protected int maxNumOutVert;

  /**
   * @invisible
   */
  protected int numTextures;

  /**
   * @invisible
   */
  protected HashMap<String, Integer> texHashMap;

  /**
   * @invisible
   */
  protected int[] texUnitUniform;

  /**
   * @invisible
   */
  protected String[] texNameArray;

  /**
   * @invisible
   */
  protected int numParams;

  /**
   * @invisible
   */
  protected HashMap<String, GLModelEffectParameter> paramsHashMap;  
  
  /**
   * @invisible
   */
  protected GLModelEffectParameter[] paramsArray;

  /**
   * @invisible
   */
  protected int numVertAttribs;

  /**
   * @invisible
   */
  protected HashMap<String, GLModelEffectVertexAttrib> vertAttribHashMap;

  /**
   * @invisible
   */
  protected GLModelEffectVertexAttrib[] vertAttribArray;
}