
/* Copyright (C) 2002 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.script;

import artofillusion.*;
import artofillusion.animation.*;
import artofillusion.math.*;
import artofillusion.object.*;
import artofillusion.ui.*;
import bsh.*;
import java.io.*;
import java.util.*;

/** This class represents an Object3D whose properties are defined by a script. */

public class ScriptedObject extends ObjectCollection
{
  private String script;
  private ObjectScript parsedScript;
  private String paramName[];
  private double paramValue[];
  
  public ScriptedObject(String scriptText)
  {
    script = scriptText;
    paramName = new String [0];
    paramValue = new double [0];
  }

  /** Get the script which defines this object. */
  
  public String getScript()
  {
    return script;
  }

  /** Set the script which defines this object. */
  
  public void setScript(String scriptText)
  {
    script = scriptText;
    parsedScript = null;
    cachedObjects = null;
    cachedBounds = null;
  }
  
  /** Get the parsed form of the script. */
  
  public ObjectScript getObjectScript() throws EvalError
  {
    if (parsedScript == null)
      {
        try
          {
            parsedScript = ScriptRunner.parseObjectScript(script);
          }
        catch (Exception ex)
          {
            ScriptRunner.displayError(ex, 1);
            parsedScript = new ObjectScript() {
              public void execute(ScriptedObjectController script)
              {
              }
            };
          }
      }
    return parsedScript;
  }
  
  /** Get the number of parameters for this object. */
  
  public final int getNumParameters()
  {
    return paramName.length;
  }
  
  /** Get the name of the i'th parameter. */
  
  public final String getParameterName(int i)
  {
    return paramName[i];
  }
    
  /** Get the value of the i'th parameter. */
  
  public final double getParameterValue(int i)
  {
    return paramValue[i];
  }
  
  /** Set the name of the i'th parameter. */
  
  public void setParameterName(int i, String name)
  {
    paramName[i] = name;
  }
  
  /** Set the value of the i'th parameter. */
  
  public void setParameterValue(int i, double value)
  {
    paramValue[i] = value;
  }
  
  /** Set a new list of parameters. */
  
  public void setParameters(String names[], double values[])
  {
    paramName = names;
    paramValue = values;
  }
  
  /** Get an enumeration of ObjectInfos listing the objects which this object
      is composed of. */
// TODO (MB) Parameter type should be changed back from SceneRend... to Scene
  protected Enumeration enumerateObjects(ObjectInfo info, boolean interactive,
      SceneRenderInfoProvider sceneinfo)
  {
    return new ScriptedObjectEnumeration(info, interactive, sceneinfo);
  }

  /** Create a new object which is an exact duplicate of this one. */
  
  public Object3D duplicate()
  {
    ScriptedObject so = new ScriptedObject(script);
    so.paramName = new String [paramName.length];
    System.arraycopy(paramName, 0, so.paramName, 0, paramName.length);
    so.paramValue = new double [paramValue.length];
    System.arraycopy(paramValue, 0, so.paramValue, 0, paramValue.length);
    so.copyTextureAndMaterial(this);
    return so;
  }
  
  /** 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)
  {
    ScriptedObject so = (ScriptedObject) obj;
    script = so.script;
    paramName = new String [so.paramName.length];
    System.arraycopy(so.paramName, 0, paramName, 0, paramName.length);
    paramValue = new double [so.paramValue.length];
    System.arraycopy(so.paramValue, 0, paramValue, 0, paramValue.length);
    parsedScript = null;
    copyTextureAndMaterial(obj);
  }

  /** setSize() has no effect, since the geometry of the object is set by the script. */

  public void setSize(double xsize, double ysize, double zsize)
  {
  }

  /** Return a Keyframe which describes the current pose of this object. */
  
  public Keyframe getPoseKeyframe()
  {
    return new ScriptedObjectKeyframe(this, paramName, paramValue);
  }
  
  /** Modify this object based on a pose keyframe. */
  
  public void applyPoseKeyframe(Keyframe k)
  {
    if (paramName.length == 0)
      return;
    ScriptedObjectKeyframe key = (ScriptedObjectKeyframe) k;
    for (int i = 0; i < paramName.length; i++)
      {
        Double d = (Double) key.valueTable.get(paramName[i]);
        if (d == null)
          paramValue[i] = 0.0;
        else
          paramValue[i] = d.doubleValue();
      }
    cachedObjects = null;
    cachedBounds = null;
  }

  /** Return an array containing the names of the graphable values for the keyframes
      returned by getPoseKeyframe(). */
  
  public String [] getPoseValueNames()
  {
    return paramName;
  }

  /** Get the default list of graphable values for a keyframe returned by getPoseKeyframe(). */
  
  public double [] getDefaultPoseValues()
  {
    return paramValue;
  }
  
  /** 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)
  {
    String name[] = new String [paramValue.length];
    double value[] = new double [paramValue.length];
    double range[][] = new double [paramValue.length][];
    for (int i = 0; i < paramValue.length; i++)
    {
      name[i] = paramName[i];
      value[i] = paramValue[i];
      range[i] = new double [] {-Double.MAX_VALUE, Double.MAX_VALUE};
    }
    track.setGraphableValues(name, value, range);
  }
  
  /** Allow the user to edit a keyframe returned by getPoseKeyframe(). */
  
  public void editKeyframe(EditingWindow parent, Keyframe k, ObjectInfo info)
  {
    ScriptedObjectKeyframe key = (ScriptedObjectKeyframe) k;
    ValueField valField[] = new ValueField [paramName.length];
    
    for (int i = 0; i < paramName.length; i++)
      {
        Double d = (Double) key.valueTable.get(paramName[i]);
        if (d == null)
          valField[i] = new ValueField(0.0, ValueField.NONE, 5);
        else
          valField[i] = new ValueField(d.doubleValue(), ValueField.NONE, 5);
      }
    ComponentsDialog dlg = new ComponentsDialog(parent.getFrame(), Translate.text("editScriptedObjTitle"),
        valField, paramName);
    if (!dlg.clickedOk())
      return;
    for (int i = 0; i < paramName.length; i++)
      key.valueTable.put(paramName[i], new Double(valField[i].getValue()));
  }

  public boolean isEditable()
  {
    return true;
  }
  
  /** Allow the user to edit the script. */
  
  public void edit(EditingWindow parent, ObjectInfo info, Runnable cb)
  {
    new ScriptedObjectEditorWindow(parent, info);
  }

  /** This constructor reconstructs a ScriptedObject from an input stream. */

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

    short version = in.readShort();
    if (version != 0)
      throw new InvalidObjectException("");
    script = in.readUTF();
    short numParams = in.readShort();
    paramName = new String [numParams];
    paramValue = new double [numParams];
    for (int i = 0; i < numParams; i++)
      {
        paramName[i] = in.readUTF();
        paramValue[i] = in.readDouble();
      }
  }

  /** Write a serialized representation of this object to an output stream. */

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

    out.writeShort(0);
    out.writeUTF(script);
    out.writeShort(paramName.length);
    for (int i = 0; i < paramName.length; i++)
      {
        out.writeUTF(paramName[i]);
        out.writeDouble(paramValue[i]);
      }
  }
  
  /** Inner class representing a pose for a scripted object. */
  
  public static class ScriptedObjectKeyframe implements Keyframe
  {
    ScriptedObject script;
    public Hashtable valueTable;
    
    public ScriptedObjectKeyframe(ScriptedObject object, String names[], double values[])
    {
      script = object;
      valueTable = new Hashtable();
      for (int i = 0; i < names.length; i++)
        valueTable.put(names[i], new Double(values[i]));
    }
    
    /* Create a duplicate of this keyframe. */
  
    public Keyframe duplicate()
    {
      return duplicate(script);
    }

    /* Create a duplicate of this keyframe for a (possibly different) object. */
  
    public Keyframe duplicate(Object owner)
    {
      ScriptedObjectKeyframe key = new ScriptedObjectKeyframe((ScriptedObject) ((ObjectInfo) owner).object, new String [0], new double [0]);
      key.valueTable = (Hashtable) valueTable.clone();
      return key;
    }
  
    /* Get the list of graphable values for this keyframe. */
  
    public double [] getGraphValues()
    {
      String names[] = script.paramName;
      double values[] = new double [names.length];
      for (int i = 0; i < names.length; i++)
        {
          Double d = (Double) valueTable.get(names[i]);
          if (d == null)
            values[i] = script.paramValue[i];
          else
            values[i] = d.doubleValue();
        }
      return values;
    }
  
    /* Set the list of graphable values for this keyframe. */
  
    public void setGraphValues(double values[])
    {
      String names[] = script.paramName;
      for (int i = 0; i < names.length; i++)
        valueTable.put(names[i], new Double(values[i]));
    }

    /* 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)
    {
      return blend(new Keyframe [] {this, o2},
        new double [] {weight1, weight2});
    }

    public Keyframe blend(Keyframe o2, Keyframe o3, double weight1, double weight2, double weight3)
    {
      return blend(new Keyframe [] {this, o2, o3},
        new double [] {weight1, weight2, weight3});
    }

    public Keyframe blend(Keyframe o2, Keyframe o3, Keyframe o4, double weight1, double weight2, double weight3, double weight4)
    {
      return blend(new Keyframe [] {this, o2, o3, o4},
        new double [] {weight1, weight2, weight3, weight4});
    }
    
    /* Blend an arbitrary list of keyframes. */
    
    private Keyframe blend(Keyframe k[], double weight[])
    {
      String names[] = script.paramName;
      double values[] = new double [names.length];
      for (int i = 0; i < names.length; i++)
        {
          double d = 0.0;
          for (int j = 0; j < k.length; j++)
            {
              ScriptedObjectKeyframe key = (ScriptedObjectKeyframe) k[j];
              Double v = (Double) key.valueTable.get(names[i]);
              if (v != null)
                d += weight[j]*v.doubleValue();
            }
          values[i] = d;
        }
      return new ScriptedObjectKeyframe(script, names, values);
    }

    /* Determine whether this keyframe is identical to another one. */
  
    public boolean equals(Keyframe k)
    {
      if (!(k instanceof ScriptedObjectKeyframe))
        return false;
      ScriptedObjectKeyframe key = (ScriptedObjectKeyframe) k;
      String names[] = script.paramName;
      for (int i = 0; i < names.length; i++)
        {
          Double d1 = (Double) valueTable.get(names[i]);
          Double d2 = (Double) key.valueTable.get(names[i]);
          if (d1 == null && d2 == null)
            continue;
          if (d1 == null || d2 == null)
            return false;
          double diff = d1.doubleValue()-d2.doubleValue();
          if (diff > 1e-12 || diff < -1e12)
            return false;
        }
      return true;
    }
  
    /* Write out a representation of this keyframe to a stream. */
  
    public void writeToStream(DataOutputStream out) throws IOException
    {
      out.writeShort(0);
      double values[] = getGraphValues();
      for (int i = 0; i < values.length; i++)
        out.writeDouble(values[i]);
    }

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

    public ScriptedObjectKeyframe(DataInputStream in, Object parent) throws IOException
    {
      short version = in.readShort();
      if (version != 0)
        throw new InvalidObjectException("");
      script = (ScriptedObject) ((ObjectInfo) parent).object;
      double values[] = new double [script.paramName.length];
      for (int i = 0; i < values.length; i++)
        values[i] = in.readDouble();
      valueTable = new Hashtable();
      for (int i = 0; i < values.length; i++)
        valueTable.put(script.paramName[i], new Double(values[i]));
    }
  }
}
