/* 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.translators;

import artofillusion.*;
import artofillusion.math.*;
import artofillusion.object.*;
import artofillusion.texture.*;
import artofillusion.ui.*;
import buoy.event.*;
import buoy.widget.*;
import java.io.*;
import java.util.*;
import java.util.zip.GZIPOutputStream;

/** VRMLExporter contains the actual routines for exporting VRML files. */

public class VRMLExporter
{
  public static void exportFile(BFrame parent, Scene theScene)
  {
    // Display a dialog box with options on how to export the scene.
    
    ValueField errorField = new ValueField(0.05, ValueField.POSITIVE);
    final ValueField widthField = new ValueField(200.0, ValueField.INTEGER+ValueField.POSITIVE);
    final ValueField heightField = new ValueField(200.0, ValueField.INTEGER+ValueField.POSITIVE);
    final ValueSlider qualitySlider = new ValueSlider(0.0, 1.0, 100, 0.5);
    final BCheckBox texBox = new BCheckBox("Create Image Files for Textures", false);
    BCheckBox compressBox = new BCheckBox("Compress Output File", true);
    BCheckBox smoothBox = new BCheckBox("Subdivide Smooth Meshes", true);
    BComboBox exportChoice = new BComboBox(new String [] {
      Translate.text("exportWholeScene"),
      Translate.text("selectedObjectsOnly")
    });
    texBox.addEventLink(ValueChangedEvent.class, new Object() {
      void processEvent()
      {
        widthField.setEnabled(texBox.getState());
        heightField.setEnabled(texBox.getState());
        qualitySlider.setEnabled(texBox.getState());
      }
    });
    texBox.dispatchEvent(new ValueChangedEvent(texBox));
    ComponentsDialog dlg;
    if (theScene.getSelection().length > 0)
      dlg = new ComponentsDialog(parent, "Export to VRML File:", 
	  new Widget [] {exportChoice, errorField, compressBox, smoothBox, texBox, Translate.label("imageSizeForTextures"), widthField, heightField, qualitySlider}, 
	  new String [] {null, Translate.text("maxSurfaceError"), null, null, null, null, Translate.text("Width"), Translate.text("Height"), Translate.text("imageQuality")});
    else
      dlg = new ComponentsDialog(parent, "Export to VRML File:", 
	  new Widget [] {errorField, compressBox, smoothBox, texBox, Translate.label("imageSizeForTextures"), widthField, heightField, qualitySlider}, 
	  new String [] {Translate.text("maxSurfaceError"), null, null, null, null, Translate.text("Width"), Translate.text("Height"), Translate.text("imageQuality")});
    if (!dlg.clickedOk())
      return;

    // Ask the user to select the output file.

    BFileChooser fc = new BFileChooser(BFileChooser.SAVE_FILE, Translate.text("exportToVRML"));
    if (compressBox.getState())
      fc.setSelectedFile(new File("Untitled.wrz"));
    else
      fc.setSelectedFile(new File("Untitled.wrl"));
    if (ModellingApp.currentDirectory != null)
      fc.setDirectory(new File(ModellingApp.currentDirectory));
    if (!fc.showDialog(parent))
      return;
    File dir = fc.getDirectory();
    File f = fc.getSelectedFile();
    String name = f.getName();
    String baseName = (name.endsWith(".wrl") || name.endsWith(".wrz") ? name.substring(0, name.length()-4) : name);
    ModellingApp.currentDirectory = dir.getAbsolutePath();
    
    // Create the output files.

    try
    {
      TextureImageExporter textureExporter = null;
      if (texBox.getState())
      {
        textureExporter = new TextureImageExporter(dir, baseName, (int) (100*qualitySlider.getValue()),
            TextureImageExporter.DIFFUSE, (int) widthField.getValue(), (int) heightField.getValue());
        boolean wholeScene = (exportChoice.getSelectedIndex() == 0);
        for (int i = 0; i < theScene.getNumObjects(); i++)
        {
          ObjectInfo info = theScene.getObject(i);
          if (!wholeScene && !info.selected)
            continue;
          textureExporter.addObject(info);
        }
        textureExporter.saveImages();
      }
      OutputStream out = new BufferedOutputStream(new FileOutputStream(f));
      if (compressBox.getState())
        out = new GZIPOutputStream(out);
      writeScene(theScene, out, exportChoice.getSelectedIndex() == 0, errorField.getValue(), smoothBox.getState(), textureExporter);
      out.close();
    }
    catch (Exception ex)
      {
        new BStandardDialog("", new String [] {Translate.text("errorExportingScene"), ex.getMessage()}, BStandardDialog.ERROR).showMessageDialog(parent);
      }
  }

  /** Write out the scene in VRML format to the specified OutputStream.  The other parameters
      correspond to the options in the dialog box displayed by exportFile(). */

  private static void writeScene(Scene theScene, OutputStream os, boolean wholeScene, double tol, boolean smooth, TextureImageExporter textureExporter)
  {
    PrintWriter out = new PrintWriter(os);
    int i, selected[] = theScene.getSelection();
    RGBColor color;
    
    // Write the header information.

    write("#VRML V2.0 utf8", out, 0);
    write("#Produced by Art of Illusion, " + (new Date()).toString(), out, 0);

    // If we are exporting the whole scene, then write the environment information.

    if (wholeScene)
      {
	// Turn off the headlight.

	write("NavigationInfo {", out, 0);
	write("headlight FALSE", out, 1);
	write("}", out, 0);

	// Set the background color.

	color = theScene.getEnvironmentColor();
	write("Background {", out, 0);
	write("skyColor "+color.getRed()+" "+color.getGreen()+" "+color.getBlue(), out, 1);
	write("}", out, 0);

	// Set the ambient light.

	color = theScene.getAmbientColor();
	write("PointLight {", out, 0);
	write("color "+color.getRed()+" "+color.getGreen()+" "+color.getBlue(), out, 1);
	write("intensity 0", out, 1);
	write("ambientIntensity 1", out, 1);
	write("radius 1e15", out, 1);
	write("}", out, 0);

	// Add fog, if appropriate.

	if (theScene.getFogState())
	  {
	    color = theScene.getFogColor();
	    write("Fog {", out, 0);
	    write("color "+color.getRed()+" "+color.getGreen()+" "+color.getBlue(), out, 1);
	    write("fogType \"EXPONENTIAL\"", out, 1);
	    write("visibilityRange "+2.0*theScene.getFogDistance(), out, 1);
	    write("}", out, 0);
	  }
      }

    // Write the objects in the scene.

    if (wholeScene)
      for (i = 0; i < theScene.getNumObjects(); i++)
	writeObject(theScene.getObject(i), out, tol, smooth, 0, theScene, textureExporter);
    else
      for (i = 0; i < selected.length; i++)
	writeObject(theScene.getObject(selected[i]), out, tol, smooth, 0, theScene, textureExporter);
    out.flush();
  }

  /** Write a single line to the PrintWriter, indented by the specified number of spaces. */
  
  private static void write(String str, PrintWriter out, int indent)
  {
    for (int i = 0; i < indent; i++)
      out.print(" ");
    out.print(str);
    out.print("\r\n");
  }

  /** Write a single object to the PrintWriter.  indent is the number of spaces which should
      be placed at the beginning of each line. */

  private static void writeObject(ObjectInfo info, PrintWriter out, double tol, boolean smooth, int indent, Scene theScene, TextureImageExporter textureExporter)
  {
    CoordinateSystem coords = info.coords;
    Object3D obj = info.object;
    Vec3 orig = coords.getOrigin(), size = info.getBounds().getSize(), axis = new Vec3(0.0, 0.0, 0.0);
    double rot[] = new double [4], ratio = 0.0;
    double pos[] = new double [3], scale[] = new double [3];
    
    if (obj instanceof SceneCamera)
      {
	coords = coords.duplicate();
	coords.setOrientation(info.coords.getZDirection().times(-1.0), info.coords.getUpDirection().times(1.0));
      }
    rot[3] = coords.getAxisAngleRotation(axis);

    // Limit the values to six decimal places.
    
    pos[0] = Math.round(orig.x*1e6)/1e6;
    pos[1] = Math.round(orig.y*1e6)/1e6;
    pos[2] = Math.round(orig.z*1e6)/1e6;
    scale[0] = Math.round(size.x*1e6)/1e6;
    scale[1] = Math.round(size.y*1e6)/1e6;
    scale[2] = Math.round(size.z*1e6)/1e6;
    rot[0] = Math.round(axis.x*1e6)/1e6;
    rot[1] = Math.round(axis.y*1e6)/1e6;
    rot[2] = Math.round(axis.z*1e6)/1e6;
    rot[3] = Math.round(rot[3]*1e6)/1e6;

    if (obj instanceof DirectionalLight)
      {
	RGBColor color = ((Light) obj).getColor();
	Vec3 dir = coords.getZDirection();
	write("DirectionalLight {", out, indent);
	write("direction "+dir.x+" "+dir.y+" "+dir.z, out, indent+1);
	write("color "+color.getRed()+" "+color.getGreen()+" "+color.getBlue(), out, indent+1);
	write("intensity "+((Light) obj).getIntensity(), out, indent+1);
	write("}", out, indent);
	return;
      }
    if (obj instanceof SpotLight)
      {
	RGBColor color = ((Light) obj).getColor();
	float decay = ((Light) obj).getDecayRate();
	boolean ambient = ((Light) obj).isAmbient();
	Vec3 dir = coords.getZDirection();
	double inner = Math.acos(Math.pow(0.9, 1.0/((SpotLight) obj).getExponent()));
	double outer = Math.acos(Math.pow(0.1, 1.0/((SpotLight) obj).getExponent()));
	double cutoff = ((SpotLight) obj).getAngle()/2.0;
	if (cutoff < outer)
	  outer = cutoff;
	write("SpotLight {", out, indent);
	write("location "+pos[0]+" "+pos[1]+" "+pos[2], out, indent+1);
	write("direction "+dir.x+" "+dir.y+" "+dir.z, out, indent+1);
	write("cutOffAngle "+outer, out, indent+1);
	if (inner < outer)
	  write("beamWidth "+inner, out, indent+1);
	write("color "+color.getRed()+" "+color.getGreen()+" "+color.getBlue(), out, indent+1);
	if (ambient)
	  {
	    write("ambientIntensity "+((Light) obj).getIntensity(), out, indent+1);
	    write("intensity 0", out, indent+1);
	  }
	else
	  write("intensity "+((Light) obj).getIntensity(), out, indent+1);
	write("attenuation 1 "+decay+" "+(decay*decay), out, indent+1);
	write("}", out, indent);
	return;
      }
    if (obj instanceof PointLight)
      {
	RGBColor color = ((Light) obj).getColor();
	float decay = ((Light) obj).getDecayRate();
	boolean ambient = ((Light) obj).isAmbient();
	write("PointLight {", out, indent);
	write("location "+pos[0]+" "+pos[1]+" "+pos[2], out, indent+1);
	write("color "+color.getRed()+" "+color.getGreen()+" "+color.getBlue(), out, indent+1);
	if (ambient)
	  {
	    write("ambientIntensity "+((Light) obj).getIntensity(), out, indent+1);
	    write("intensity 0", out, indent+1);
	  }
	else
	  write("intensity "+((Light) obj).getIntensity(), out, indent+1);
	write("attenuation 1 "+decay+" "+(decay*decay), out, indent+1);
	write("}", out, indent);
	return;
      }
    if (obj instanceof SceneCamera)
      {
	write("Viewpoint {", out, indent);
	write("position "+pos[0]+" "+pos[1]+" "+pos[2], out, indent+1);
	write("orientation "+rot[0]+" "+rot[1]+" "+rot[2]+" "+rot[3], out, indent+1);
	write("fieldOfView "+((SceneCamera) obj).getFieldOfView()*Math.PI/180.0, out, indent+1);
	write("description "+"\""+info.name+"\"", out, indent+1);
	write("}", out, indent);
	return;
      }

    // This object will be represented by a Shape node.  First, create a Transform node.

    write("Transform {", out, indent);
    write("translation "+pos[0]+" "+pos[1]+" "+pos[2], out, indent+1);
    write("rotation "+rot[0]+" "+rot[1]+" "+rot[2]+" "+rot[3], out, indent+1);
    if (obj instanceof Cylinder)
      ratio = ((Cylinder) obj).getRatio();
    if (obj instanceof Sphere || (obj instanceof Cylinder && (ratio == 0.0 || ratio == 1.0)))
      write("scale "+scale[0]/2.0+" "+scale[1]/2.0+" "+scale[2]/2.0, out, indent+1);
    write("children [", out, indent+1);

    // Create an appropriate Shape node.

    TextureImageInfo ti = (textureExporter == null ? null : textureExporter.getTextureInfo(obj.getTexture()));
    boolean hasTexture = (ti != null && ti.diffuseFilename != null);
    if (obj instanceof Cube && !hasTexture)
      {
	write("Shape {", out, indent+2);
	writeTexture(info, out, indent+3, theScene, textureExporter);
	write("geometry Box {", out, indent+3);
	write("size "+scale[0]+" "+scale[1]+" "+scale[2], out, indent+4);
	write("}", out, indent+3);
	write("}", out, indent+2);
      }
    else if (obj instanceof Cylinder && ratio == 1.0 && !hasTexture)
      {
	write("Shape {", out, indent+2);
	writeTexture(info, out, indent+3, theScene, textureExporter);
	write("geometry Cylinder {}", out, indent+3);
	write("}", out, indent+2);
      }
    else if (obj instanceof Cylinder && ratio == 0.0 && !hasTexture)
      {
	write("Shape {", out, indent+2);
	writeTexture(info, out, indent+3, theScene, textureExporter);
	write("geometry Cone {}", out, indent+3);
	write("}", out, indent+2);
      }
    else if (obj instanceof Sphere && !hasTexture)
      {
	write("Shape {", out, indent+2);
	writeTexture(info, out, indent+3, theScene, textureExporter);
	write("geometry Sphere {", out, indent+3);
	write("}", out, indent+3);
	write("}", out, indent+2);
      }
    else if (obj instanceof Curve)
      {
	WireframeMesh mesh = obj.getWireframeMesh();
	if (mesh != null)
	  {
	    Vec3 vert[] = mesh.vert;
	    
	    write("Shape {", out, indent+2);
	    write("geometry IndexedLineSet {", out, indent+3);
	    write("coord Coordinate { point [", out, indent+4);
	    for (int i = 0; i < vert.length; i++)
	      {
		pos[0] = Math.round(vert[i].x*1e6)/1e6;
		pos[1] = Math.round(vert[i].y*1e6)/1e6;
		pos[2] = Math.round(vert[i].z*1e6)/1e6;
		write(pos[0]+" "+pos[1]+" "+pos[2]+",", out, indent+5);
	      }
	    write("] }", out, indent+4);
	    write("coordIndex [", out, indent+4);
	    for (int i = 0; i < vert.length-1; i++)
	      write(i+",", out, indent+5);
	    if (obj.isClosed())
	      write((vert.length-1)+", 0, -1", out, indent+5);
	    else
	      write((vert.length-1)+", -1", out, indent+5);
	    write("]", out, indent+4);
	    write("}", out, indent+3);
	    write("}", out, indent+2);
	  }
      }
    else if (obj instanceof TriangleMesh && !smooth)
      {
        writeMesh((TriangleMesh) obj, info, out, indent+2, theScene, textureExporter, false);
      }
    else if (obj instanceof ObjectCollection)
      {
        Enumeration enum = ((ObjectCollection) obj).getObjects(info, false, theScene);
        while (enum.hasMoreElements())
          writeObject((ObjectInfo) enum.nextElement(), out, tol, smooth, indent+2, theScene, textureExporter);
      }
    else
      {
	// All other objects are represented as IndexedFaceSets.

	TriangleMesh mesh = info.object.convertToTriangleMesh(tol);
	if (mesh != null)
          writeMesh(mesh, info, out, indent+2, theScene, textureExporter, true);
      }
    write("]", out, indent+1);
    write("}", out, indent);
  }
  
  /** Write out an IndexedFaceSet node describing a mesh. */
  
  private static void writeMesh(TriangleMesh mesh, ObjectInfo info, PrintWriter out, int indent, Scene theScene, TextureImageExporter textureExporter, boolean includeNormals)
  {
    MeshVertex vert[] = mesh.getVertices();
    TriangleMesh.Face face[] = mesh.getFaces();
    double pos[] = new double [3];

    write("Shape {", out, indent);
    writeTexture(info, out, indent+1, theScene, textureExporter);
    write("geometry IndexedFaceSet {", out, indent+1);
    if (info.object.isClosed())
      write("solid TRUE", out, indent+2);
    else
      write("solid FALSE", out, indent+2);
    write("coord Coordinate { point [", out, indent+2);
    for (int i = 0; i < vert.length; i++)
      {
        pos[0] = Math.round(vert[i].r.x*1e6)/1e6;
        pos[1] = Math.round(vert[i].r.y*1e6)/1e6;
        pos[2] = Math.round(vert[i].r.z*1e6)/1e6;
        write(pos[0]+" "+pos[1]+" "+pos[2]+",", out, indent+3);
      }
    write("] }", out, indent+2);
    write("coordIndex [", out, indent+2);
    for (int i = 0; i < face.length; i++)
      write(face[i].v1+", "+face[i].v2+", "+face[i].v3+", -1,", out, indent+3);
    write("]", out, indent+2);
    if (includeNormals)
      {
        Vec3 norm[] = mesh.getNormals();
        write("normal Normal { vector [", out, indent+2);
        for (int i = 0; i < norm.length; i++)
          {
            if (norm[i] == null)
              write("1 0 0,", out, indent+3);
            else
              {
                pos[0] = Math.round(norm[i].x*1e6)/1e6;
                pos[1] = Math.round(norm[i].y*1e6)/1e6;
                pos[2] = Math.round(norm[i].z*1e6)/1e6;
                write(pos[0]+" "+pos[1]+" "+pos[2]+",", out, indent+3);
              }
          }
        write("] }", out, indent+2);
        write("normalIndex [", out, indent+2);
        for (int i = 0; i < face.length; i++)
          write(face[i].v1+", "+face[i].v2+", "+face[i].v3+", -1,", out, indent+3);
        write("]", out, indent+2);
      }
    TextureImageInfo ti = (textureExporter == null ? null : textureExporter.getTextureInfo(mesh.getTexture()));
    if (ti != null && mesh.getTextureMapping() instanceof UVMapping && ((UVMapping) mesh.getTextureMapping()).isPerFaceVertex(mesh))
    {
      // A per-face-vertex texture mapping.
      
      Vec2 coords[][] = ((UVMapping) mesh.getTextureMapping()).findFaceTextureCoordinates(mesh);
      double uscale = (ti.maxu == ti.minu ? 1.0 : 1.0/(ti.maxu-ti.minu));
      double vscale = (ti.maxv == ti.minv ? 1.0 : 1.0/(ti.maxv-ti.minv));
      write("texCoord TextureCoordinate { point [", out, indent+2);
      for (int j = 0; j < face.length; j++)
        for (int k = 0; k < 3; k++)
        {
          pos[0] = (coords[k][j].x-ti.minu)*uscale;
          pos[1] = (coords[k][j].y-ti.minv)*vscale;
          pos[0] = Math.round(pos[0]*1e6)/1e6;
          pos[1] = Math.round(pos[1]*1e6)/1e6;
          write(pos[0]+" "+pos[1]+",", out, indent+3);
        }
      write("] }", out, indent+2);
      write("texCoordIndex [", out, indent+2);
      for (int i = 0; i < face.length; i++)
        write((i*3)+", "+(i*3+1)+", "+(i*3+2)+", -1,", out, indent+3);
      write("]", out, indent+2);
    }
    else if (ti != null && mesh.getTextureMapping() instanceof Mapping2D)
    {
      // A per-vertex texture mapping.
      
      Vec2 coords[] = ((Mapping2D) mesh.getTextureMapping()).findTextureCoordinates(mesh);
      double uscale = (ti.maxu == ti.minu ? 1.0 : 1.0/(ti.maxu-ti.minu));
      double vscale = (ti.maxv == ti.minv ? 1.0 : 1.0/(ti.maxv-ti.minv));
      write("texCoord TextureCoordinate { point [", out, indent+2);
      for (int i = 0; i < coords.length; i++)
        {
          pos[0] = (coords[i].x-ti.minu)*uscale;
          pos[1] = (coords[i].y-ti.minv)*vscale;
          pos[0] = Math.round(pos[0]*1e6)/1e6;
          pos[1] = Math.round(pos[1]*1e6)/1e6;
          write(pos[0]+" "+pos[1]+",", out, indent+3);
        }
      write("] }", out, indent+2);
      write("texCoordIndex [", out, indent+2);
      for (int i = 0; i < face.length; i++)
        write(face[i].v1+", "+face[i].v2+", "+face[i].v3+", -1,", out, indent+3);
      write("]", out, indent+2);
    }
    write("}", out, indent+1);
    write("}", out, indent);
  }

  /** Write out an Appearance node describing a Texture. */

  private static void writeTexture(ObjectInfo info, PrintWriter out, int indent, Scene theScene, TextureImageExporter textureExporter)
  {
    Texture tex = info.object.getTexture();
    TextureSpec spec;

    if (tex == null)
      return;
    TextureImageInfo ti = (textureExporter == null ? null : textureExporter.getTextureInfo(tex));
    boolean hasMap = (ti != null && ti.diffuseFilename != null);
    spec = new TextureSpec();
    tex.getAverageSpec(spec, theScene.getTime(), info.object.getAverageParameterValues());
    write("appearance Appearance {", out, indent);
    write("material Material {", out, indent+1);
    if (hasMap)
      write("diffuseColor 1 1 1", out, indent+2);
    else
      write("diffuseColor "+spec.diffuse.getRed()+" "+spec.diffuse.getGreen()+" "+spec.diffuse.getBlue(), out, indent+2);
    write("emissiveColor "+spec.emissive.getRed()+" "+spec.emissive.getGreen()+" "+spec.emissive.getBlue(), out, indent+2);
    write("specularColor "+spec.specular.getRed()+" "+spec.specular.getGreen()+" "+spec.specular.getBlue(), out, indent+2);
    write("shininess "+(1.0-spec.roughness), out, indent+2);
    write("transparency "+Math.max(spec.transparent.getRed(), Math.max(spec.transparent.getGreen(), spec.transparent.getBlue())), out, indent+2);
    write("ambientIntensity 1", out, indent+2);
    write("}", out, indent+1);
    if (hasMap)
      {
        write("texture ImageTexture {", out, indent+1);
        write("url \""+ti.diffuseFilename+"\"", out, indent+2);
        write("}", out, indent+1);
      }
    write("}", out, indent);
  }
}