/* Copyright (C) 1999-2004 by Peter Eastman

   This program 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 program 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. */

package artofillusion.object;

import artofillusion.*;
import artofillusion.animation.*;
import artofillusion.math.*;
import artofillusion.texture.*;
import artofillusion.ui.*;
import buoy.widget.*;
import java.io.*;

/** The Cylinder class represents cylinders and cones.  It is specified by the height, the
    radii along the two axes of its base, and the ratio of the top and bottom radii. */

public class Cylinder extends Object3D
{
  static final int SEGMENTS = 16;

  static double sine[], cosine[];
  double rx, ry, ratio, height;
  BoundingBox bounds;
  RenderingMesh cachedMesh;
  WireframeMesh cachedWire;

  static {
    sine = new double [SEGMENTS];
    cosine = new double [SEGMENTS];
    for (int i = 0; i < SEGMENTS; i++)
      {
	sine[i] = Math.sin(i*2.0*Math.PI/SEGMENTS);
	cosine[i] = Math.cos(i*2.0*Math.PI/SEGMENTS);
      }
  }

  public Cylinder(double height, double xradius, double yradius, double ratio)
  {
    this.height = height;
    rx = xradius;
    ry = yradius;
    this.ratio = ratio;
    bounds = new BoundingBox(-rx, rx, -height/2.0, height/2.0, -ry, ry);
  }

  public Object3D duplicate()
  {
    Cylinder obj = new Cylinder(height, rx, ry, ratio);
    obj.copyTextureAndMaterial(this);
    return obj;
  }
  
  public void copyObject(Object3D obj)
  {
    Cylinder c = (Cylinder) obj;
    Vec3 size = c.getBounds().getSize();
    
    setSize(size.x, size.y, size.z);
    ratio = c.ratio;
    copyTextureAndMaterial(obj);
    cachedMesh = null;
    cachedWire = null;
  }
  
  public double getRatio()
  {
    return ratio;
  }
  
  public BoundingBox getBounds()
  {
    return bounds;
  }

  public void setSize(double xsize, double ysize, double zsize)
  {
    rx = xsize/2.0;
    ry = zsize/2.0;
    height = ysize;
    bounds = new BoundingBox(-rx, rx, -height/2.0, height/2.0, -ry, ry);
    cachedMesh = null;
    cachedWire = null;
  }

  public WireframeMesh getWireframeMesh()
  {
    Vec3 vert[];
    int i, from[], to[];
    double y1, y2;
    
    if (cachedWire != null)
      return cachedWire;
    y1 = -height/2.0;
    y2 = height/2.0;
    if (ratio > 0.0)
      {
	vert = new Vec3 [2*SEGMENTS+2];
	from = new int [5*SEGMENTS];
	to = new int [5*SEGMENTS];
	vert[2*SEGMENTS] = new Vec3(0.0, y1, 0.0);
	vert[2*SEGMENTS+1] = new Vec3(0.0, y2, 0.0);
	for (i = 0; i < SEGMENTS; i++)
	  {
	    vert[i] = new Vec3(rx*cosine[i], y1, ry*sine[i]);
	    vert[i+SEGMENTS] = new Vec3(ratio*rx*cosine[i], y2, ratio*ry*sine[i]);
	    from[i] = 2*SEGMENTS;
	    to[i] = i;
	    from[i+SEGMENTS] = i;
	    to[i+SEGMENTS] = (i+1)%SEGMENTS;
	    from[i+2*SEGMENTS] = i;
	    to[i+2*SEGMENTS] = (i+1)%SEGMENTS+SEGMENTS;
	    from[i+3*SEGMENTS] = i+SEGMENTS;
	    to[i+3*SEGMENTS] = (i+1)%SEGMENTS+SEGMENTS;
	    from[i+4*SEGMENTS] = 2*SEGMENTS+1;
	    to[i+4*SEGMENTS] = i+SEGMENTS;
	  }
      }
    else
      {
	vert = new Vec3 [SEGMENTS+2];
	from = new int [3*SEGMENTS];
	to = new int [3*SEGMENTS];
	vert[SEGMENTS] = new Vec3(0.0, y1, 0.0);
	vert[SEGMENTS+1] = new Vec3(0.0, y2, 0.0);
	for (i = 0; i < SEGMENTS; i++)
	  {
	    vert[i] = new Vec3(rx*cosine[i], y1, ry*sine[i]);
	    from[i] = SEGMENTS;
	    to[i] = i;
	    from[i+SEGMENTS] = i;
	    to[i+SEGMENTS] = (i+1)%SEGMENTS;
	    from[i+2*SEGMENTS] = i;
	    to[i+2*SEGMENTS] = SEGMENTS+1;
	  }
      }
    return (cachedWire = new WireframeMesh(vert, from, to));
  }

  public int canConvertToTriangleMesh()
  {
    return APPROXIMATELY;
  }
  
  public TriangleMesh convertToTriangleMesh(double tol)
  {
    Vec3 vertices[];
    Vec2 v[] = new Vec2 [4], vtemp[];
    TriangleMesh.Edge edges[];
    TriangleMesh mesh;
    int i, faces[][];
    double angle;
    double y1, y2;
    
    y1 = -height/2.0;
    y2 = height/2.0;

    // Subdivide the edge of the large end.

    v[0] = new Vec2(rx, 0.0);
    v[1] = new Vec2(0.0, -ry);
    v[2] = new Vec2(-rx, 0.0);
    v[3] = new Vec2(0.0, ry);
    while (!withinTolerance(v, tol))
      {
	vtemp = v;
	v = new Vec2 [v.length*2];
	for (i = 0; i < vtemp.length; i++)
	  {
	    v[i*2] = vtemp[i];
	    angle = 2.0*Math.PI*(i+0.5)/vtemp.length;
	    v[i*2+1] = new Vec2(rx*Math.cos(angle), -ry*Math.sin(angle));
	  }
      }
    
    // Find the list of faces.

    if (ratio == 0.0)
      {
	vertices = new Vec3 [v.length+2];
	faces = new int [v.length*2][];
	vertices[0] = new Vec3(0.0, y1, 0.0);
	vertices[v.length+1] = new Vec3(0.0, y2, 0.0);
	for (i = 0; i < v.length; i++)
	  {
	    vertices[i+1] = new Vec3(v[i].x, y1, v[i].y);
	    faces[i] = new int[] {i+2, i+1, 0};
	    faces[i+v.length] = new int[] {v.length+1, i+1, i+2};
	  }
	faces[v.length-1][0] -= v.length;
	faces[v.length*2-1][2] -= v.length;
      }
    else
      {
	vertices = new Vec3 [v.length*2+2];
	faces = new int [v.length*4][];
	vertices[0] = new Vec3(0.0, y1, 0.0);
	vertices[v.length*2+1] = new Vec3(0.0, y2, 0.0);
	for (i = 0; i < v.length; i++)
	  {
	    vertices[i+1] = new Vec3(v[i].x, y1, v[i].y);
	    vertices[i+v.length+1] = new Vec3(v[i].x*ratio, y2, v[i].y*ratio);
	    faces[i] = new int[] {i+2, i+1, 0};
	    faces[i+v.length] = new int[] {i+v.length+1, i+1, i+2};
	    faces[i+v.length*2] = new int[] {i+v.length+1, i+2, i+v.length+2};
	    faces[i+v.length*3] = new int[] {v.length*2+1, i+v.length+1, i+v.length+2};
	  }
	faces[v.length-1][0] -= v.length;
	faces[v.length*2-1][2] -= v.length;
	faces[v.length*3-1][1] -= v.length;
	faces[v.length*3-1][2] -= v.length;
	faces[v.length*4-1][2] -= v.length;
      }
    mesh = new TriangleMesh(vertices, faces);
    edges = mesh.getEdges();
    for (i = 0; i < edges.length; i++)
      if (edges[i].v1 != 0 && edges[i].v2 != 0 && edges[i].v1 <= v.length && edges[i].v2 <= v.length)
	edges[i].smoothness = 0.0f;
    if (ratio != 0.0)
      for (i = 0; i < edges.length; i++)
	if (edges[i].v1 != v.length*2+1 && edges[i].v2 != v.length*2+1 && edges[i].v1 > v.length && edges[i].v2 > v.length)
	  edges[i].smoothness = 0.0f;
    mesh.copyTextureAndMaterial(this);
    mesh.copyModelEvent(this);
    return mesh;
  }

  boolean withinTolerance(Vec2 v[], double tol)
  {
    Vec2 point, truePoint;
    double angle;

    point = new Vec2(0.0, 0.0);
    truePoint = new Vec2(0.0, 0.0);
    for (int i = 0; i < v.length/2; i++)
      {
        point.x = (v[i].x+v[i+1].x)/2.0;
        point.y = (v[i].y+v[i+1].y)/2.0;
	angle = 2.0*Math.PI*(i+0.5)/v.length;
	truePoint.x = rx*Math.cos(angle);
	truePoint.y = -ry*Math.sin(angle);
	if (truePoint.distance(point) > tol)
	  return false;
      }
    return true;
  }

  public RenderingMesh getRenderingMesh(double tol, boolean interactive, ObjectInfo info)
  {
    Vec3 vert[], norm[];
    Vec2 v[], vtemp[];
    double angle, y1, y2;
    RenderingTriangle tri[];

    if (interactive && cachedMesh != null)
      return cachedMesh;
    y1 = -height/2.0;
    y2 = height/2.0;
    
    // Subdivide the edge of the large end.
    
    v = new Vec2 [] {new Vec2(rx, 0.0), new Vec2(0.0, -ry), new Vec2(-rx, 0.0), new Vec2(0.0, ry)};
    while (!withinTolerance(v, tol))
      {
	vtemp = v;
	v = new Vec2 [v.length*2];
	for (int i = 0; i < vtemp.length; i++)
	  {
	    v[i*2] = vtemp[i];
	    angle = 2.0*Math.PI*(i+0.5)/vtemp.length;
	    v[i*2+1] = new Vec2(rx*Math.cos(angle), -ry*Math.sin(angle));
	  }
      }
    
    // Build the mesh.
    
    if (ratio == 0.0)
      {
	vert = new Vec3 [v.length+2];
	norm = new Vec3 [v.length+1];
	tri = new RenderingTriangle [v.length*2];
	vert[0] = new Vec3(0.0, y1, 0.0);
	vert[v.length+1] = new Vec3(0.0, y2, 0.0);
	norm[0] = new Vec3(0.0, y1, 0.0);
	for (int i = 0; i < v.length; i++)
	  {
	    vert[i+1] = new Vec3(v[i].x, y1, v[i].y);
	    norm[i+1] = new Vec3(v[i].x/(rx*rx), 1.0/(y2-y1), v[i].y/(ry*ry));
	  }
	for (int i = 0; i < v.length-1; i++)
	  {
	    tri[i] = texMapping.mapTriangle(i+2, i+1, 0, 0, 0, 0, vert);
	    tri[i+v.length] = texMapping.mapTriangle(v.length+1, i+1, i+2, i+1, i+1, i+2, vert);
	  }
	tri[v.length-1] = texMapping.mapTriangle(1, v.length, 0, 0, 0, 0, vert);
	tri[v.length*2-1] = texMapping.mapTriangle(v.length+1, v.length, 1, v.length, v.length, 1, vert);
      }
    else
      {
	vert = new Vec3 [v.length*2+2];
	norm = new Vec3 [v.length+2];
	tri = new RenderingTriangle [v.length*4];
	vert[0] = new Vec3(0.0, y1, 0.0);
	vert[v.length*2+1] = new Vec3(0.0, y2, 0.0);
	norm[0] = new Vec3(0.0, y1, 0.0);
	norm[v.length+1] = new Vec3(0.0, y2, 0.0);
	for (int i = 0; i < v.length; i++)
	  {
	    vert[i+1] = new Vec3(v[i].x, y1, v[i].y);
	    vert[i+v.length+1] = new Vec3(v[i].x*ratio, y2, v[i].y*ratio);
	    norm[i+1] = new Vec3(v[i].x/(rx*rx), (1.0-ratio)/(y2-y1), v[i].y/(ry*ry));
	  }
	for (int i = 0; i < v.length-1; i++)
	  {
	    tri[i] = texMapping.mapTriangle(i+2, i+1, 0, 0, 0, 0, vert);
	    tri[i+v.length] = texMapping.mapTriangle(i+v.length+1, i+1, i+2, i+1, i+1, i+2, vert);
	    tri[i+v.length*2] = texMapping.mapTriangle(i+v.length+1, i+2, i+v.length+2, i+1, i+2, i+2, vert);
	    tri[i+v.length*3] = texMapping.mapTriangle(v.length*2+1, i+v.length+1, i+v.length+2, v.length+1, v.length+1, v.length+1, vert);
	  }
	tri[v.length-1] = texMapping.mapTriangle(1, v.length, 0, 0, 0, 0, vert);
	tri[v.length*2-1] = texMapping.mapTriangle(v.length*2, v.length, 1, v.length, v.length, 1, vert);
	tri[v.length*3-1] = texMapping.mapTriangle(v.length*2, 1, v.length+1, v.length, 1, 1, vert);
	tri[v.length*4-1] = texMapping.mapTriangle(v.length*2+1, v.length*2, v.length+1, v.length+1, v.length+1, v.length+1, vert);
      }
    for (int i = 0; i < norm.length; i++)
      norm[i].normalize();
    RenderingMesh mesh = new RenderingMesh(vert, norm, tri, texMapping, matMapping);
    mesh.setParameters(paramValue);
    if (interactive)
      cachedMesh = mesh;
    return mesh;
  }

  public void setTexture(Texture tex, TextureMapping mapping)
  {
    super.setTexture(tex, mapping);
    cachedMesh = null;
    cachedWire = null;
  }

  public boolean isEditable()
  {
    return true;
  }
  
  public void edit(EditingWindow parent, ObjectInfo info, Runnable cb)
  {
    ValueField xField = new ValueField(rx, ValueField.POSITIVE, 5);
    ValueField yField = new ValueField(ry, ValueField.POSITIVE, 5);
    ValueField heightField = new ValueField(height, ValueField.POSITIVE, 5);
    ValueSlider ratioSlider = new ValueSlider(0.0, 1.0, 100, ratio);
    ComponentsDialog dlg = new ComponentsDialog(parent.getFrame(), 
	Translate.text("editCylinderTitle"), new Widget [] {xField, yField, ratioSlider, heightField}, 
	new String [] {Translate.text("bottomRadiusX"), Translate.text("bottomRadiusZ"), Translate.text("radiusRatio"), Translate.text("Height")});
    if (!dlg.clickedOk())
      return;
    ratio = ratioSlider.getValue();
    setSize(2.0*xField.getValue(), heightField.getValue(), 2.0*yField.getValue());
    cb.run();
  }

  /* The following two methods are used for reading and writing files.  The first is a
     constructor which reads the necessary data from an input stream.  The other writes
     the object's representation to an output stream. */

  public Cylinder(DataInputStream in, Scene theScene) throws IOException, InvalidObjectException
  {
    super(in, theScene);

    short version = in.readShort();
    if (version != 0)
      throw new InvalidObjectException("");
    rx = in.readDouble();
    ry = in.readDouble();
    height = in.readDouble();
    ratio = in.readDouble();
    bounds = new BoundingBox(-rx, rx, -height/2.0, height/2.0, -ry, ry);
  }

  public void writeToFile(DataOutputStream out, Scene theScene) throws IOException
  {
    super.writeToFile(out, theScene);

    out.writeShort(0);
    out.writeDouble(rx);
    out.writeDouble(ry);
    out.writeDouble(height);
    out.writeDouble(ratio);
  }
  
  /* Return a Keyframe which describes the current pose of this object. */
  
  public Keyframe getPoseKeyframe()
  {
    return new CylinderKeyframe(rx, ry, height, ratio);
  }
  
  /* Modify this object based on a pose keyframe. */
  
  public void applyPoseKeyframe(Keyframe k)
  {
    CylinderKeyframe key = (CylinderKeyframe) k;
    
    ratio = key.ratio;
    setSize(2.0*key.rx, key.height, 2.0*key.ry);
  }
  
  /** This will be called whenever a new pose track is created for this object.  It allows
      the object to configure the track by setting its graphable values, subtracks, etc. */
  
  public void configurePoseTrack(PoseTrack track)
  {
    track.setGraphableValues(new String [] {"X Radius", "Z Radius", "Height", "Ratio"},
        new double [] {2.0*rx, 2.0*ry, height, ratio}, 
        new double [][] {{0.0, Double.MAX_VALUE}, {0.0, Double.MAX_VALUE}, 
        {0.0, Double.MAX_VALUE}, {0.0, 1.0}});
  }
  
  /* Return an array containing the names of the graphable values for the keyframes
     returned by getPoseKeyframe(). */
  
  public String [] getPoseValueNames()
  {
    return new String [] {"X Radius", "Z Radius", "Height", "Ratio"};
  }

  /* Get the default list of graphable values for a keyframe returned by getPoseKeyframe(). */
  
  public double [] getDefaultPoseValues()
  {
    return new double [] {2.0*rx, 2.0*ry, height, ratio};
  }
  
  /* Get the allowed range for graphable values for keyframes returned by getPoseKeyframe().
     This returns a 2D array, where elements [n][0] and [n][1] are the minimum and maximum 
     allowed values, respectively, for the nth graphable value. */
  
  public double[][] getPoseValueRange()
  {
    return new double [][] {{0.0, Double.MAX_VALUE}, {0.0, Double.MAX_VALUE}, 
      {0.0, Double.MAX_VALUE}, {0.0, 1.0}};
  }
  
  /* Allow the user to edit a keyframe returned by getPoseKeyframe(). */
  
  public void editKeyframe(EditingWindow parent, Keyframe k, ObjectInfo info)
  {
    CylinderKeyframe key = (CylinderKeyframe) k;
    ValueField xField = new ValueField(2.0*key.rx, ValueField.POSITIVE, 5);
    ValueField yField = new ValueField(2.0*key.ry, ValueField.POSITIVE, 5);
    ValueField heightField = new ValueField(key.height, ValueField.POSITIVE, 5);
    ValueSlider ratioSlider = new ValueSlider(0.0, 1.0, 100, key.ratio);
    ComponentsDialog dlg = new ComponentsDialog(parent.getFrame(), 
	Translate.text("editCylinderTitle"), new Widget [] {xField, yField, ratioSlider, heightField}, 
	new String [] {Translate.text("bottomRadiusX"), Translate.text("bottomRadiusZ"), Translate.text("radiusRatio"), Translate.text("Height")});
    if (!dlg.clickedOk())
      return;
    key.rx = 0.5*xField.getValue();
    key.ry = 0.5*yField.getValue();
    key.ratio = ratioSlider.getValue();
    key.height = heightField.getValue();
  }
  
  /* Inner class representing a pose for a cylinder. */
  
  public static class CylinderKeyframe implements Keyframe
  {
    public double rx, ry, ratio, height;
    
    public CylinderKeyframe(double rx, double ry, double height, double ratio)
    {
      this.rx = rx;
      this.ry = ry;
      this.height = height;
      this.ratio = ratio;
    }
    
    /* Create a duplicate of this keyframe. */
  
    public Keyframe duplicate()
    {
      return new CylinderKeyframe(rx, ry, height, ratio);
    }

    /* Create a duplicate of this keyframe for a (possibly different) object. */
  
    public Keyframe duplicate(Object owner)
    {
      return new CylinderKeyframe(rx, ry, height, ratio);
    }
  
    /* Get the list of graphable values for this keyframe. */
  
    public double [] getGraphValues()
    {
      return new double [] {rx, ry, height, ratio};
    }
  
    /* Set the list of graphable values for this keyframe. */
  
    public void setGraphValues(double values[])
    {
      rx = values[0];
      ry = values[1];
      height = values[2];
      ratio = values[3];
    }

    /* These methods return a new Keyframe which is a weighted average of this one and one,
       two, or three others. */
  
    public Keyframe blend(Keyframe o2, double weight1, double weight2)
    {
      CylinderKeyframe k2 = (CylinderKeyframe) o2;

      return new CylinderKeyframe(weight1*rx+weight2*k2.rx, weight1*ry+weight2*k2.ry, 
        weight1*height+weight2*k2.height, weight1*ratio+weight2*k2.ratio);
    }

    public Keyframe blend(Keyframe o2, Keyframe o3, double weight1, double weight2, double weight3)
    {
      CylinderKeyframe k2 = (CylinderKeyframe) o2, k3 = (CylinderKeyframe) o3;

      return new CylinderKeyframe(weight1*rx+weight2*k2.rx+weight3*k3.rx, 
        weight1*ry+weight2*k2.ry+weight3*k3.ry, 
        weight1*height+weight2*k2.height+weight3*k3.height, 
        weight1*ratio+weight2*k2.ratio+weight3*k3.ratio);
    }

    public Keyframe blend(Keyframe o2, Keyframe o3, Keyframe o4, double weight1, double weight2, double weight3, double weight4)
    {
      CylinderKeyframe k2 = (CylinderKeyframe) o2, k3 = (CylinderKeyframe) o3, k4 = (CylinderKeyframe) o4;

      return new CylinderKeyframe(weight1*rx+weight2*k2.rx+weight3*k3.rx+weight4*k4.rx, 
        weight1*ry+weight2*k2.ry+weight3*k3.ry+weight4*k4.ry, 
        weight1*height+weight2*k2.height+weight3*k3.height+weight4*k4.height, 
        weight1*ratio+weight2*k2.ratio+weight3*k3.ratio+weight4*k4.ratio);
    }

    /* Determine whether this keyframe is identical to another one. */
  
    public boolean equals(Keyframe k)
    {
      if (!(k instanceof CylinderKeyframe))
        return false;
      CylinderKeyframe key = (CylinderKeyframe) k;
      return (key.rx == rx && key.ry == ry && key.ratio == ratio && key.height == height);
    }
  
    /* Write out a representation of this keyframe to a stream. */
  
    public void writeToStream(DataOutputStream out) throws IOException
    {
      out.writeDouble(rx);
      out.writeDouble(ry);
      out.writeDouble(height);
      out.writeDouble(ratio);
    }

    /* Reconstructs the keyframe from its serialized representation. */

    public CylinderKeyframe(DataInputStream in, Object parent) throws IOException
    {
      this(in.readDouble(), in.readDouble(), in.readDouble(), in.readDouble());
    }
  }
}