/* Copyright (C) 2001-2003 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.material.*;
import artofillusion.math.*;
import artofillusion.texture.*;
import artofillusion.ui.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;

/** A CSGObject is an Object3D that represents the union, intersection, or difference of 
    two component objects. */

public class CSGObject extends Object3D
{
  ObjectInfo obj1, obj2;
  int operation;
  RenderingMesh cachedMesh;
  WireframeMesh cachedWire;
  BoundingBox bounds;

  public static final int UNION = 0;
  public static final int INTERSECTION = 1;
  public static final int DIFFERENCE12 = 2;
  public static final int DIFFERENCE21 = 3;
  
  /* Create a CSGObject based on two objects and an operation. */
  
  public CSGObject(ObjectInfo o1, ObjectInfo o2, int op)
  {
    obj1 = o1.duplicate();
    obj2 = o2.duplicate();
    obj1.object = obj1.object.duplicate();
    obj2.object = obj2.object.duplicate();
    operation = op;
    obj1.visible = obj2.visible = true;
  }
  
  /** Create a new object which is an exact duplicate of this one. */
  
  public Object3D duplicate()
  {
    CSGObject obj = new CSGObject(obj1, obj2, operation);
    obj.copyTextureAndMaterial(this);
    return obj;
  }
  
  /** Copy all the properties of another object, to make this one identical to it.  If the
      two objects are of different classes, this will throw a ClassCastException. */
  
  public void copyObject(Object3D obj)
  {
    CSGObject csg = (CSGObject) obj;

    obj1 = csg.obj1.duplicate();
    obj2 = csg.obj2.duplicate();
    obj1.object = obj1.object.duplicate();
    obj2.object = obj2.object.duplicate();
    operation = csg.operation;
    cachedMesh = csg.cachedMesh;
    cachedWire = csg.cachedWire;
    copyTextureAndMaterial(obj);
    bounds = null;
  }
  
  /** Get the first object. */
  
  public ObjectInfo getObject1()
  {
    return obj1;
  }
  
  /** Get the second object. */
  
  public ObjectInfo getObject2()
  {
    return obj2;
  }
  
  /** Get the boolean operation to be performed. */
  
  public int getOperation()
  {
    return operation;
  }
  
  /** Set the boolean operation to be performed. */
  
  public void setOperation(int op)
  {
    operation = op;
  }
  
  /** Set the component objects. */
  
  public void setComponentObjects(ObjectInfo o1, ObjectInfo o2)
  {
    obj1 = o1;
    obj2 = o2;
    bounds = null;
    cachedMesh = null;
    cachedWire = null;
  }

  /** Center the component objects, and return the vector by which they were displaced. */
  
  public Vec3 centerObjects()
  {
    BoundingBox b1 = obj1.getBounds().transformAndOutset(obj1.coords.fromLocal());
    BoundingBox b2 = obj2.getBounds().transformAndOutset(obj2.coords.fromLocal());
    BoundingBox b = b1.merge(b2);
    Vec3 center = b.getCenter();
    obj1.coords.setOrigin(obj1.coords.getOrigin().minus(center));
    obj2.coords.setOrigin(obj2.coords.getOrigin().minus(center));
    bounds = null;
    cachedMesh = null;
    cachedWire = null;
    return center;
  }

  /** Get a BoundingBox which just encloses the object. */

  public BoundingBox getBounds()
  {
    if (bounds == null)
      findBounds();
    return bounds;
  }

  /** Calculate the (approximate) bounding box for the object. */

  void findBounds()
  {
    Vec3 vert[];
    
    if (cachedMesh != null)
      vert = cachedMesh.vert;
    else if (cachedWire != null)
      vert = cachedWire.vert;
    else
      {
	getWireframeMesh();
	vert = cachedWire.vert;
      }
    bounds = BoundingBox.findBounds(vert);
  }

  /** Resize the object. */

  public void setSize(double xsize, double ysize, double zsize)
  {
    Vec3 size = bounds.getSize(), objSize;
    double xscale, yscale, zscale;
    
    if (size.x == 0.0)
      xscale = 1.0;
    else
      xscale = xsize / size.x;
    if (size.y == 0.0)
      yscale = 1.0;
    else
      yscale = ysize / size.y;
    if (size.z == 0.0)
      zscale = 1.0;
    else
      zscale = zsize / size.z;
    
    // Adjust the size and position of each component object.
    
    objSize = obj1.getBounds().getSize();
    obj1.object.setSize(objSize.x*xscale, objSize.y*yscale, objSize.z*zscale);
    objSize = obj2.getBounds().getSize();
    obj2.object.setSize(objSize.x*xscale, objSize.y*yscale, objSize.z*zscale);
    Mat4 m = Mat4.scale(xscale, yscale, zscale);
    obj1.coords.transformOrigin(m);
    obj2.coords.transformOrigin(m);
    cachedMesh = null;
    cachedWire = null;
    findBounds();
  }

  /** Tells whether the object can be converted to a TriangleMesh. */
  
  public int canConvertToTriangleMesh()
  {
    if (obj1.object.canConvertToTriangleMesh() == EXACTLY && obj2.object.canConvertToTriangleMesh() == EXACTLY)
      return EXACTLY;
    return APPROXIMATELY;
  }
  
  /** This object is closed if both of its component objects are closed. */
  
  public boolean isClosed()
  {
    return (obj1.object.isClosed() && obj2.object.isClosed());
  }
  
  /** Create a triangle mesh representing this object. */

  public TriangleMesh convertToTriangleMesh(double tol)
  {
    TriangleMesh mesh1, mesh2;
    
    mesh1 = obj1.object.convertToTriangleMesh(tol);
    mesh2 = obj2.object.convertToTriangleMesh(tol);
    CSGModeller modeller = new CSGModeller(mesh1, mesh2, obj1.coords, obj2.coords);
    TriangleMesh trimesh = modeller.getMesh(operation);
    trimesh.copyTextureAndMaterial(this);
    trimesh.copyModelEvent(this);
    return trimesh;
  }
  
  /** Allow the user to edit this object. */
  
  public boolean isEditable()
  {
    return true;
  }
  
  public void edit(EditingWindow parent, ObjectInfo info, Runnable cb)
  {
    new CSGEditorWindow(parent, info.name, this, cb);
  }
    
  /** When setting the texture or material, also set it for each of the component objects. */
     
  public void setTexture(Texture tex, TextureMapping mapping)
  {
    super.setTexture(tex, mapping);
    if (obj1 == null)
      return;
    obj1.object.setTexture(tex, mapping);
    obj2.object.setTexture(tex, mapping);
  }

  public void setMaterial(Material mat, MaterialMapping mapping)
  {
    super.setMaterial(mat, mapping);
    if (obj1 == null)
      return;
    obj1.object.setMaterial(mat, mapping);
    obj2.object.setMaterial(mat, mapping);
  }

  /** Get a RenderingMesh for this object. */

  public RenderingMesh getRenderingMesh(double tol, boolean interactive, ObjectInfo info)
  {
    if (interactive)
      {
        if (cachedMesh == null)
          cacheMeshes(tol, info);
        return cachedMesh;
      }
    return convertToTriangleMesh(tol).getRenderingMesh(tol, false, info);
  }
  
  /** Get a WireframeMesh for this object. */
  
  public WireframeMesh getWireframeMesh()
  {
    if (cachedWire == null)
      cacheMeshes(ModellingApp.getPreferences().getInteractiveSurfaceError(), null);
    return cachedWire;
  }

  /** Build the preview meshes and save them for later use. */
  
  private void cacheMeshes(double tol, ObjectInfo info)
  {
    TriangleMesh mesh = convertToTriangleMesh(tol);
    if (info == null)
      {
	cachedWire = mesh.getWireframeMesh();
	return;
      }
    cachedMesh = mesh.getRenderingMesh(tol, true, info);
    TriangleMesh.Edge edge[] = mesh.getEdges();
    int to[] = new int [edge.length], from[] = new int [edge.length];
    
    for (int i = 0; i < edge.length; i++)
      {
        to[i] = edge[i].v1;
        from[i] = edge[i].v2;
      }
    cachedWire = new WireframeMesh(cachedMesh.vert, from, to);
  }

  /** Save this object to an output stream. */
  
  public void writeToFile(DataOutputStream out, Scene theScene) throws IOException
  {
    super.writeToFile(out, theScene);
    out.writeShort(0);
    out.writeInt(operation);
    obj1.coords.writeToFile(out);
    out.writeUTF(obj1.name);
    out.writeUTF(obj1.object.getClass().getName());
    obj1.object.writeToFile(out, theScene);
    obj2.coords.writeToFile(out);
    out.writeUTF(obj2.name);
    out.writeUTF(obj2.object.getClass().getName());
    obj2.object.writeToFile(out, theScene);
  }
  
  public CSGObject(DataInputStream in, Scene theScene) throws IOException, InvalidObjectException
  {
    super(in, theScene);

    short version = in.readShort();
    if (version != 0)
      throw new InvalidObjectException("");
    operation = in.readInt();
    try
      {
	obj1 = new ObjectInfo(null, new CoordinateSystem(in), in.readUTF());
	Class cls = ModellingApp.getClass(in.readUTF());
	Constructor con = cls.getConstructor(new Class [] {DataInputStream.class, Scene.class});
	obj1.object = (Object3D) con.newInstance(new Object [] {in, theScene});
	obj2 = new ObjectInfo(null, new CoordinateSystem(in), in.readUTF());
	cls = ModellingApp.getClass(in.readUTF());
	con = cls.getConstructor(new Class [] {DataInputStream.class, Scene.class});
	obj2.object = (Object3D) con.newInstance(new Object [] {in, theScene});
      }
    catch (InvocationTargetException ex)
      {
	ex.getTargetException().printStackTrace();
	throw new IOException();
      }
    catch (Exception ex)
      {
	ex.printStackTrace();
	throw new IOException();
      }
    obj1.object.setTexture(getTexture(), getTextureMapping());
    obj2.object.setTexture(getTexture(), getTextureMapping());
    if (getMaterial() != null)
      {
	obj1.object.setMaterial(getMaterial(), getMaterialMapping());
	obj2.object.setMaterial(getMaterial(), getMaterialMapping());
      }
  }
  
  /** Return a Keyframe which describes the current pose of this object. */
  
  public Keyframe getPoseKeyframe()
  {
    return new CSGKeyframe(obj1.object.getPoseKeyframe(), obj2.object.getPoseKeyframe(),
      obj1.coords.duplicate(), obj2.coords.duplicate());
  }
  
  /** Modify this object based on a pose keyframe. */
  
  public void applyPoseKeyframe(Keyframe k)
  {
    CSGKeyframe key = (CSGKeyframe) k;
    
    obj1.object.applyPoseKeyframe(key.key1);
    obj2.object.applyPoseKeyframe(key.key2);
    obj1.coords.copyCoords(key.coords1);
    obj2.coords.copyCoords(key.coords2);
    cachedMesh = null;
    cachedWire = null;
  }
  
  /** Allow the user to edit a keyframe returned by getPoseKeyframe(). */
  
  public void editKeyframe(EditingWindow parent, Keyframe k, ObjectInfo info)
  {
/*    VectorKeyframe key = (VectorKeyframe) k;
    ValueField xField = new ValueField(key.x, ValueField.POSITIVE, 5);
    ValueField yField = new ValueField(key.y, ValueField.POSITIVE, 5);
    ValueField zField = new ValueField(key.z, ValueField.POSITIVE, 5);
    ComponentsDialog dlg = new ComponentsDialog((Frame) parent, "Select sphere diameters:",
      new Component [] {xField, yField, zField}, new String [] {"X", "Y", "Z"});
    
    if (!dlg.clickedOk())
      return;
    key.set(xField.getValue(), yField.getValue(), zField.getValue());*/
  }
  
  /** Inner class representing a pose for a CSGObject. */
  
  public static class CSGKeyframe implements Keyframe
  {
    public Keyframe key1, key2;
    public CoordinateSystem coords1, coords2;
    
    public CSGKeyframe(Keyframe key1, Keyframe key2, CoordinateSystem coords1, CoordinateSystem coords2)
    {
      this.key1 = key1;
      this.key2 = key2;
      this.coords1 = coords1;
      this.coords2 = coords2;
    }
    
    /** Create a duplicate of this keyframe. */
  
    public Keyframe duplicate()
    {
      return new CSGKeyframe(key1.duplicate(), key2.duplicate(), coords1.duplicate(), coords2.duplicate());
    }
    
    /** Create a duplicate of this keyframe for a (possibly different) object. */
  
    public Keyframe duplicate(Object owner)
    {
      return new CSGKeyframe(key1.duplicate(owner), key2.duplicate(owner), coords1.duplicate(), coords2.duplicate());
    }
  
    /** Get the list of graphable values for this keyframe. */
  
    public double [] getGraphValues()
    {
      return new double [0];
    }
  
    /** Set the list of graphable values for this keyframe. */
  
    public void setGraphValues(double values[])
    {
    }

    /* 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)
    {
      CSGKeyframe k2 = (CSGKeyframe) o2;
      RotationKeyframe rot1, rot2;
      VectorKeyframe orig1, orig2;
      CoordinateSystem c1, c2;
      
      // Find the new coordinate system for the first object.
      
      rot1 = new RotationKeyframe(coords1);
      rot2 = new RotationKeyframe(k2.coords1);
      rot1.setUseQuaternion(true);
      rot2.setUseQuaternion(true);
      orig1 = new VectorKeyframe(coords1.getOrigin());
      orig2 = new VectorKeyframe(k2.coords1.getOrigin());
      c1 = new CoordinateSystem((Vec3) orig1.blend(orig2, weight1, weight2), Vec3.vz(), Vec3.vy());
      ((RotationKeyframe) rot1.blend(rot2, weight1, weight2)).applyToCoordinates(c1, 1.0, null, null, false,
        true, true, true);
      
      // Find the new coordinate system for the second object.
      
      rot1 = new RotationKeyframe(coords2);
      rot2 = new RotationKeyframe(k2.coords2);
      rot1.setUseQuaternion(true);
      rot2.setUseQuaternion(true);
      orig1 = new VectorKeyframe(coords2.getOrigin());
      orig2 = new VectorKeyframe(k2.coords2.getOrigin());
      c2 = new CoordinateSystem((Vec3) orig1.blend(orig2, weight1, weight2), Vec3.vz(), Vec3.vy());
      ((RotationKeyframe) rot1.blend(rot2, weight1, weight2)).applyToCoordinates(c2, 1.0, null, null, false,
        true, true, true);
      
      // Construct the new keyframe.

      return new CSGKeyframe(key1.blend(k2.key1, weight1, weight2), 
        key2.blend(k2.key2, weight1, weight2), c1, c2);
    }

    public Keyframe blend(Keyframe o2, Keyframe o3, double weight1, double weight2, double weight3)
    {
      CSGKeyframe k2 = (CSGKeyframe) o2, k3 = (CSGKeyframe) o3;
      RotationKeyframe rot1, rot2, rot3;
      VectorKeyframe orig1, orig2, orig3;
      CoordinateSystem c1, c2;
      
      // Find the new coordinate system for the first object.
      
      rot1 = new RotationKeyframe(coords1);
      rot2 = new RotationKeyframe(k2.coords1);
      rot3 = new RotationKeyframe(k3.coords1);
      rot1.setUseQuaternion(true);
      rot2.setUseQuaternion(true);
      rot3.setUseQuaternion(true);
      orig1 = new VectorKeyframe(coords1.getOrigin());
      orig2 = new VectorKeyframe(k2.coords1.getOrigin());
      orig3 = new VectorKeyframe(k3.coords1.getOrigin());
      c1 = new CoordinateSystem((Vec3) orig1.blend(orig2, orig3, weight1, weight2, weight3), 
        Vec3.vz(), Vec3.vy());
      ((RotationKeyframe) rot1.blend(rot2, rot3, weight1, weight2, weight3)).applyToCoordinates(c1, 1.0, 
        null, null, false, true, true, true);
      
      // Find the new coordinate system for the second object.
      
      rot1 = new RotationKeyframe(coords2);
      rot2 = new RotationKeyframe(k2.coords2);
      rot3 = new RotationKeyframe(k3.coords2);
      rot1.setUseQuaternion(true);
      rot2.setUseQuaternion(true);
      rot3.setUseQuaternion(true);
      orig1 = new VectorKeyframe(coords2.getOrigin());
      orig2 = new VectorKeyframe(k2.coords2.getOrigin());
      orig3 = new VectorKeyframe(k3.coords2.getOrigin());
      c2 = new CoordinateSystem((Vec3) orig1.blend(orig2, orig3, weight1, weight2, weight3), 
        Vec3.vz(), Vec3.vy());
      ((RotationKeyframe) rot1.blend(rot2, rot3, weight1, weight2, weight3)).applyToCoordinates(c2, 1.0, 
        null, null, false, true, true, true);
      
      // Construct the new keyframe.

      return new CSGKeyframe(key1.blend(k2.key1, k3.key1, weight1, weight2, weight3), 
        key2.blend(k2.key2, k3.key2, weight1, weight2, weight3), c1, c2);
    }

    public Keyframe blend(Keyframe o2, Keyframe o3, Keyframe o4, double weight1, double weight2, double weight3, double weight4)
    {
      CSGKeyframe k2 = (CSGKeyframe) o2, k3 = (CSGKeyframe) o3, k4 = (CSGKeyframe) o4;
      RotationKeyframe rot1, rot2, rot3, rot4;
      VectorKeyframe orig1, orig2, orig3, orig4;
      CoordinateSystem c1, c2;
      
      // Find the new coordinate system for the first object.
      
      rot1 = new RotationKeyframe(coords1);
      rot2 = new RotationKeyframe(k2.coords1);
      rot3 = new RotationKeyframe(k3.coords1);
      rot4 = new RotationKeyframe(k4.coords1);
      rot1.setUseQuaternion(true);
      rot2.setUseQuaternion(true);
      rot3.setUseQuaternion(true);
      rot4.setUseQuaternion(true);
      orig1 = new VectorKeyframe(coords1.getOrigin());
      orig2 = new VectorKeyframe(k2.coords1.getOrigin());
      orig3 = new VectorKeyframe(k3.coords1.getOrigin());
      orig4 = new VectorKeyframe(k4.coords1.getOrigin());
      c1 = new CoordinateSystem((Vec3) orig1.blend(orig2, orig3, orig4, weight1, weight2, weight3, weight4), 
        Vec3.vz(), Vec3.vy());
      ((RotationKeyframe) rot1.blend(rot2, rot3, rot4, weight1, weight2, weight3, weight4)).applyToCoordinates(c1, 1.0, 
        null, null, false, true, true, true);
      
      // Find the new coordinate system for the second object.
      
      rot1 = new RotationKeyframe(coords2);
      rot2 = new RotationKeyframe(k2.coords2);
      rot3 = new RotationKeyframe(k3.coords2);
      rot4 = new RotationKeyframe(k4.coords2);
      rot1.setUseQuaternion(true);
      rot2.setUseQuaternion(true);
      rot3.setUseQuaternion(true);
      rot4.setUseQuaternion(true);
      orig1 = new VectorKeyframe(coords2.getOrigin());
      orig2 = new VectorKeyframe(k2.coords2.getOrigin());
      orig3 = new VectorKeyframe(k3.coords2.getOrigin());
      orig4 = new VectorKeyframe(k4.coords2.getOrigin());
      c2 = new CoordinateSystem((Vec3) orig1.blend(orig2, orig3, orig4, weight1, weight2, weight3, weight4), 
        Vec3.vz(), Vec3.vy());
      ((RotationKeyframe) rot1.blend(rot2, rot3, rot3, weight1, weight2, weight3, weight4)).applyToCoordinates(c2, 1.0, 
        null, null, false, true, true, true);
      
      // Construct the new keyframe.

      return new CSGKeyframe(key1.blend(k2.key1, k3.key1, k4.key1, weight1, weight2, 
        weight3, weight4), key2.blend(k2.key2, k3.key2, k4.key2, weight1, weight2, 
        weight3, weight4), c1, c2);
    }

    /** Determine whether this keyframe is identical to another one. */
  
    public boolean equals(Keyframe k)
    {
      if (!(k instanceof CSGKeyframe))
        return false;
      CSGKeyframe key = (CSGKeyframe) k;
      if (!key1.equals(key.key1))
        return false;
      if (!key2.equals(key.key2))
        return false;
      if (!coords1.getOrigin().equals(key.coords1.getOrigin()))
        return false;
      if (!coords1.getZDirection().equals(key.coords1.getZDirection()))
        return false;
      if (!coords1.getUpDirection().equals(key.coords1.getUpDirection()))
        return false;
      if (!coords2.getOrigin().equals(key.coords2.getOrigin()))
        return false;
      if (!coords2.getZDirection().equals(key.coords2.getZDirection()))
        return false;
      if (!coords2.getUpDirection().equals(key.coords2.getUpDirection()))
        return false;
      return true;
    }
  
    /** 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 CSGKeyframe(DataInputStream in, Object parent) throws IOException
    {
//      this(in.readDouble(), in.readDouble(), in.readDouble(), in.readDouble());
    }
  }
}