package org.xith3d.shapegeom;

import com.xith3d.scenegraph.GeometryArray;
import com.xith3d.scenegraph.TriangleArray;
import net.java.games.jogl.GLU;
import net.java.games.jogl.GLUtesselator;
import net.java.games.jogl.GLUtesselatorCallback;
import net.java.games.jogl.impl.GLUImpl;
import net.java.games.jogl.impl.GLUProcAddressTable;

import javax.vecmath.TexCoord2f;
import java.awt.*;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;

/**
 * <p>
 * The ShapeTiangulator will take a java.awt.Shape and create TriangleArray's from it.
 * You can get an extruded shape, or the three components (front and back faces and the sides). In
 * all cases the returned value is a TriangleArray.
 * </p>
 * <p>
 * ShapeTriangulator works by creating a flattened PathIterator from the Shape using the flatness
 * parameter in the sole constructor. The coordinates from the PathIterator are then triangulated
 * using the tesselate functions in glu. For the moment it uses the glu port in JOGL.
 * </p>
 *
 *
 *
 * Date: 8/04/2005
 * @author Alistair Dickie
 */
public class ShapeTriangulator implements GLUtesselatorCallback {

  private Shape shape;
  private ArrayList points;

  private GLUProcAddressTable gluProcAddressTable = new GLUProcAddressTable();

  private GLU glu = new GLUImpl(gluProcAddressTable);
  private GLUtesselator tess;
  private double flatness;
  private Rectangle2D bounds;

  /**
   * Creates a ShapeTriangulator. tesselate() is called from the constructor
   *
   *
   * @param shape The shape to triangulate
   * @param flatness The flatness to be passed in making the PathIterator (shape.getPathIterator(null, flatness))
   */
  public ShapeTriangulator(java.awt.Shape shape, double flatness) {

    this.shape = shape;
    bounds = shape.getBounds2D();
    this.flatness = flatness;
    this.tesselate();
  }

  /**
   * Tesselates the shape using glu - no need to call this. It is done from the constructor and
   * when you set shape or flatness.
   */
  public void tesselate() {
    points = new ArrayList();

    tess = glu.gluNewTess();


    PathIterator iter = shape.getPathIterator(null, flatness);
    float[] coords = new float[6];


    //setThis as the callback

    glu.gluTessCallback(tess, GLU.GLU_TESS_BEGIN, this);
    glu.gluTessCallback(tess, GLU.GLU_TESS_END, this);
    glu.gluTessCallback(tess, GLU.GLU_TESS_VERTEX, this);
    glu.gluTessCallback(tess, GLU.GLU_TESS_EDGE_FLAG, this);
    glu.gluTessCallback(tess, GLU.GLU_TESS_ERROR, this);

    //start it
    glu.gluTessBeginPolygon(tess, null);
    boolean startedContour = false;
    while (!iter.isDone()) {
      switch (iter.currentSegment(coords)) {

        case PathIterator.SEG_MOVETO:
          if (startedContour) {
            glu.gluTessEndContour(tess);
          }
          glu.gluTessBeginContour(tess);
          startedContour = true;
          double[] pointS = new double[]{coords[0], coords[1], 0.0};
          glu.gluTessVertex(tess, pointS, pointS);
          break;
        case PathIterator.SEG_LINETO:
          double[] point = new double[]{coords[0], coords[1], 0.0};
          glu.gluTessVertex(tess, point, point);
          break;
        case PathIterator.SEG_CLOSE:
          double[] pointC = new double[]{coords[0], coords[1], 0.0};
          glu.gluTessVertex(tess, pointC, pointC);

          break;
      }
      iter.next();

    }
    glu.gluTessEndContour(tess);
    glu.gluTessEndPolygon(tess);


  }

  public void setShape(Shape shape) {
    this.shape = shape;
    this.tesselate();
  }

  public void setFlatness(double flatness) {
    this.flatness = flatness;
    this.tesselate();
  }

  /**
   * Gets the geometry of the face of the Shape. The face is drawn in the xy plane with z = 0
   * @param vertexFormat OR's properties of the verticies (e.g. GeometryArray.COORDINATES|GeometryArray.NORMALS)
   * @return the face geometry
   */
  public TriangleArray getFaceGeometry(int vertexFormat) {



    TriangleArray geom = new TriangleArray(points.size(), vertexFormat);
    for (int i = 0; i < points.size(); i++) {
      double[] point = (double[]) points.get(i);

      int index;
      if(i%3 == 0){
        index = i + 2;
      }
      else if(i%3 == 2){
        index = i - 2;
      }
      else{
        index = i;
      }
      geom.setCoordinate(index, new float[]{(float) point[0], -(float) point[1], 0.0f});

      if ((vertexFormat & GeometryArray.COLOR_3) != 0) {
        geom.setColor(i, new float[]{(float) point[0], (float) point[1], (float) point[2]});
      }
      if ((vertexFormat & GeometryArray.TEXTURE_COORDINATE_2) != 0) {
        float xt = (float) ((point[0] - bounds.getX()) / bounds.getWidth());
        float yt = (float) ((-point[1] - bounds.getY()) / bounds.getHeight());

        TexCoord2f tex = new TexCoord2f(xt, yt);
        geom.setTextureCoordinate(0, index, tex);
      }
    }

    if ((vertexFormat & GeometryArray.NORMALS) != 0) {
      geom.calculateFaceNormals();
    }
    return geom;

  }

  /**
   * Gets the geometry of the back of the Shape. The back is drawn in the xy plane with z = depth
   * @param vertexFormat OR's properties of the verticies (e.g. GeometryArray.COORDINATES|GeometryArray.NORMALS)
   * @param depth depth in the z direction
   * @return the back geometry
   */
  public TriangleArray getBackGeometry(int vertexFormat, float depth) {

    Rectangle2D bounds = shape.getBounds2D();

    TriangleArray geom = new TriangleArray(points.size(), vertexFormat);
    for (int i = 0; i < points.size(); i++) {
      double[] point = (double[]) points.get(i);

      geom.setCoordinate(i, new float[]{(float) point[0], -(float) point[1], -depth});
      if ((vertexFormat & GeometryArray.COLOR_3) != 0) {
        geom.setColor(i, new float[]{(float) point[0], (float) point[1], (float) point[2]});
      }
      if ((vertexFormat & GeometryArray.TEXTURE_COORDINATE_2) != 0) {
        float xt = (float) ((point[0] - bounds.getX()) / bounds.getWidth());
        float yt = (float) ((-point[1] - bounds.getY()) / bounds.getHeight());

        TexCoord2f tex = new TexCoord2f(xt, yt);
        geom.setTextureCoordinate(0, i, tex);
      }
    }

    if ((vertexFormat & GeometryArray.NORMALS) != 0) {
      geom.calculateFaceNormals();
    }
    return geom;

  }

  /**
   * Gets the geometry of the sides of the extruded Shape
   * @param vertexFormat OR's properties of the verticies (e.g. GeometryArray.COORDINATES|GeometryArray.NORMALS)
   * @param depth depth in the z direction
   * @return the side geometry
   */

  public TriangleArray getSideGeometry(int vertexFormat, float depth){

    PathIterator iter = shape.getPathIterator(null, flatness);
    float[] coords = new float[6];




    //start it
    ArrayList contours = new ArrayList();
    ArrayList currentContour = null;

    while (!iter.isDone()) {
      switch (iter.currentSegment(coords)) {

        case PathIterator.SEG_MOVETO:

          currentContour = new ArrayList();
          contours.add(currentContour);
          float[] pointS = new float[]{coords[0], coords[1]};
          currentContour.add(pointS);

          break;
        case PathIterator.SEG_LINETO:
          float[] point = new float[]{coords[0], coords[1]};
          currentContour.add(point);

          break;
        case PathIterator.SEG_CLOSE:
          float[] pointC = new float[]{coords[0], coords[1]};
          currentContour.add(pointC);



          break;
      }
      iter.next();

    }

    //how many line segments
    int n = 0;
    for (int i = 0; i < contours.size(); i++) {
      ArrayList contour = (ArrayList) contours.get(i);
      n += contour.size()-1;
    }


    //two triangles for each line segment
    TriangleArray geom = new TriangleArray(n*6, vertexFormat);

    int index = 0;
    for (int c = 0; c < contours.size(); c++) {
      ArrayList contour = (ArrayList)contours.get(c);
      for (int i = 0; i < contour.size()-1; i++) {
        //get a line segment ane make its six triangles
        float[] pointa = (float[]) contour.get(i);
        float[] pointb = (float[]) contour.get(i+1);


        geom.setCoordinate(index, new float[]{pointa[0], -pointa[1], 0.0f});
        geom.setCoordinate(index+1, new float[]{ pointb[0], -pointb[1], 0.0f});
        geom.setCoordinate(index+2, new float[]{pointa[0], -pointa[1], -depth});
        geom.setCoordinate(index+3, new float[]{pointb[0], -pointb[1], 0.0f});
        geom.setCoordinate(index+4, new float[]{ pointb[0], -pointb[1], -depth});
        geom.setCoordinate(index+5, new float[]{ pointa[0], -pointa[1], -depth});


        if ((vertexFormat & GeometryArray.COLOR_3) != 0) {
          geom.setColor(index, new float[]{pointa[0], - pointa[1], 0.0f});
          geom.setColor(index+1, new float[]{pointb[0], - pointb[1], 0.0f});
          geom.setColor(index+2, new float[]{ pointa[0], - pointa[1], -depth});
          geom.setColor(index+3, new float[]{ pointb[0], - pointb[1], 0.0f});
          geom.setColor(index+4, new float[]{ pointb[0], - pointb[1], -depth});
          geom.setColor(index+5, new float[]{ pointa[0], - pointa[1], -depth});
        }
        if ((vertexFormat & GeometryArray.TEXTURE_COORDINATE_2) != 0) {
          //todo - what do I do here??
  //        float xt = ???;
  //        float yt = ???;
  //
  //        TexCoord2f tex = new TexCoord2f(xt, yt);
  //        geom.setTextureCoordinate(0, i, tex);
        }
        index += 6;
      }
    }

    if ((vertexFormat & GeometryArray.NORMALS) != 0) {
      geom.calculateFaceNormals();
    }
    return geom;


  }

  /**
   * Returns an extruded shape.
   * @param vertexFormat OR's properties of the verticies (e.g. GeometryArray.COORDINATES|GeometryArray.NORMALS)
   * @param depth depth in the z direction
   * @return the extruded geometry
   */
  public TriangleArray getExtrudedGeometry(int vertexFormat, float depth){
    TriangleArray face = this.getFaceGeometry(vertexFormat);
    TriangleArray back = this.getBackGeometry(vertexFormat, depth);
    TriangleArray sides = this.getSideGeometry(vertexFormat, depth);

    TriangleArray all = new TriangleArray(face.getVertexCount() + back.getVertexCount() + sides.getVertexCount(), vertexFormat);
    all.setCoordinates(0,face.getCoordRefFloat());
    all.setCoordinates(face.getVertexCount(), back.getCoordRefFloat());
    all.setCoordinates(face.getVertexCount()+back.getVertexCount(), sides.getCoordRefFloat());
    if ((vertexFormat & GeometryArray.COLOR_3) != 0) {
      all.setColor(0,face.getColorRefFloat());
      all.setColor(face.getVertexCount(), back.getColorRefFloat());
      all.setColor(face.getVertexCount()+back.getVertexCount(), sides.getColorRefFloat());

    }
    if ((vertexFormat & GeometryArray.TEXTURE_COORDINATE_2) != 0) {
      all.setTextureCoordinates(0, 0, face.getTexCoordRefFloat(0));
      all.setTextureCoordinates(0,face.getVertexCount(), back.getTexCoordRefFloat(0));
      all.setTextureCoordinates(0,face.getVertexCount()+back.getVertexCount(), sides.getTexCoordRefFloat(0));

    }
    if ((vertexFormat & GeometryArray.NORMALS) != 0) {
       all.setNormals(0,face.getNormalRefFloat());
      all.setNormals(face.getVertexCount(), back.getNormalRefFloat());
      all.setNormals(face.getVertexCount()+back.getVertexCount(), sides.getNormalRefFloat());

    }

   return all;


  }

  /**
   * An arraylist containing double[3]'s that represent all of the verticies in the front face
   * @return the verticies
   */
  public ArrayList getPoints() {
    return points;
  }

  /**
   * The bounds of the shape
   *
   * @return the bounds
   */
  public Rectangle2D getBounds() {
    return bounds;
  }

  /**
   * called by glu
   * @param type
   */

  public void begin(int type) {

  }

  /**
   * called by glu
   * @param type
   * @param polygonData
   */

  public void beginData(int type, Object polygonData) {

  }

  /**
   * called by glu
   * @param boundaryEdge
   */
  public void edgeFlag(boolean boundaryEdge) {

  }
  /**
   * called by glu
   * @param boundaryEdge
   * @param polygonData
   */
  public void edgeFlagData(boolean boundaryEdge, Object polygonData) {

  }

  /**
   * called by glu
   * @param vertexData
   */

  public void vertex(Object vertexData) {
    double[] point = (double[]) vertexData;
    points.add(point);


  }

  /**
   * called by glu
   * @param vertexData
   * @param polygonData
   */
  public void vertexData(Object vertexData, Object polygonData) {

  }

  /**
   * called by glu
   */

  public void end() {


  }
  /**
   * called by glu
   * @param polygonData
   */
  public void endData(Object polygonData) {

  }
  /**
   * called by glu
   * @param coords
   * @param data
   * @param weight
   * @param outData
   */
  public void combine(double[] coords, Object[] data, float[] weight, Object[] outData) {

  }
  /**
   * called by glu
   * @param coords
   * @param data
   * @param weight
   * @param outData
   * @param polygonData
   */
  public void combineData(double[] coords, Object[] data, float[] weight, Object[] outData, Object polygonData) {

  }

  /**
   * called by glu - Will throw an Error if there is something wrong with the Shape
   * @param errnum
   */
  public void error(int errnum) {
    throw new Error("Error " + errnum + " occured while tesselating a shape using the ShapeTriangulator");


  }

  /**
   * called by glu
   * @param errnum
   * @param polygonData
   */
  public void errorData(int errnum, Object polygonData) {

  }


}
