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

import artofillusion.*;
import artofillusion.animation.distortion.*;
import artofillusion.math.*;
import artofillusion.object.*;
import artofillusion.ui.*;
import buoy.event.*;
import buoy.widget.*;
import java.io.*;
import java.util.Vector;

/** This is a Track which modifies the shape of an object using inverse kinematics. */

public class IKTrack extends Track
{
  private ObjectInfo info;
  private Vector constraints;
  private boolean useGestures;
  private WeightTrack theWeight;
  
  public IKTrack(ObjectInfo info)
  {
    super("Inverse Kinematics");
    this.info = info;
    theWeight = new WeightTrack(this);
    constraints = new Vector();
    useGestures = true;
  }
  
  /** Get whether to reshape the mesh based on its gestures. */
  
  public boolean getUseGestures()
  {
    return useGestures;
  }
  
  /** Set whether to reshape the mesh based on its gestures. */
  
  public void setUseGestures(boolean use)
  {
    useGestures = use;
  }

  /** This method presents a window in which the user can edit the track. */
  
  public void edit(LayoutWindow win)
  {
    if (info.getSkeleton() == null)
      new BStandardDialog("", Translate.text("ikNotApplyWarning"), BStandardDialog.INFORMATION).showMessageDialog(win);
    else
      new Editor(win);
  }
  
  /** Modify the position of the object. */
  
  public void apply(double time)
  {
    double weight = theWeight.getWeight(time);
    Skeleton skeleton = info.getSkeleton();
    if (skeleton == null)
      return;
    Joint joint[] = skeleton.getJoints();
    if (joint.length == 0)
      return;
    boolean locked[] = new boolean [joint.length];
    Vec3 target[] = new Vec3 [joint.length];
    Mat4 toLocal = info.coords.toLocal();
    
    for (int i = 0; i < constraints.size(); i++)
    {
      Constraint c = (Constraint) constraints.elementAt(i);
      int index = skeleton.findJointIndex(c.jointID);
      if (index == -1)
        continue;
      if (c.target == null)
        locked[index] = true;
      else
      {
        CoordinateSystem coords = c.target.getCoords();
        if (coords == null)
          locked[index] = true;
        else
          target[index] = toLocal.times(coords.getOrigin());
      }
    }
    Actor actor = Actor.getActor(info.object);
      actor = (Actor) info.object;
    info.addDistortion(new IKDistortion(locked, target, weight, actor));
  }
  
  /** Create a duplicate of this track. */
  
  public Track duplicate(Object obj)
  {
    IKTrack t = new IKTrack((ObjectInfo) obj);
    
    t.name = name;
    t.enabled = enabled;
    t.quantized = quantized;
    t.constraints = new Vector();
    for (int i = 0; i < constraints.size(); i++)
    {
      Constraint c = (Constraint) constraints.elementAt(i);
      t.constraints.addElement(c.duplicate());
    }
    t.theWeight = (WeightTrack) theWeight.duplicate(t);
    return t;
  }
  
  /** Make this track identical to another one. */
  
  public void copy(Track tr)
  {
    IKTrack t = (IKTrack) tr;
    
    name = t.name;
    enabled = t.enabled;
    quantized = t.quantized;
    constraints = new Vector();
    for (int i = 0; i < t.constraints.size(); i++)
    {
      Constraint c = (Constraint) t.constraints.elementAt(i);
      constraints.addElement(c.duplicate());
    }
    theWeight = (WeightTrack) t.theWeight.duplicate(t);
  }
  
  /** Get a list of all keyframe times for this track. */
  
  public double [] getKeyTimes()
  {
    return new double [0];
  }

  /** Move a keyframe to a new time, and return its new position in the list. */
  
  public int moveKeyframe(int which, double time)
  {
    return -1;
  }
  
  /** Delete the specified keyframe. */
  
  public void deleteKeyframe(int which)
  {
  }
  
  /** This track is null if it has no targets. */
  
  public boolean isNullTrack()
  {
    for (int i = 0; i < constraints.size(); i++)
      if (((Constraint) constraints.elementAt(i)).target != null)
        return false;
    return true;
  }
  
  /** This has a single child track. */
  
  public Track [] getSubtracks()
  {
    return new Track [] {theWeight};
  }

  /** Determine whether this track can be added as a child of an object. */
  
  public boolean canAcceptAsParent(Object obj)
  {
    return (obj instanceof ObjectInfo && ((ObjectInfo) obj).object instanceof Mesh);
  }
  
  /** Get the parent object of this track. */
  
  public Object getParent()
  {
    return info;
  }
  
  /** Set the parent object of this track. */
  
  public void setParent(Object obj)
  {
    info = (ObjectInfo) obj;
  }
  
  /** Get an array of any objects which this track depends on (and which therefore must
      be updated before this track is applied). */ 
  
  public ObjectInfo [] getDependencies()
  {
    Vector v = new Vector();
    for (int i = 0; i < constraints.size(); i++)
    {
      Constraint c = (Constraint) constraints.elementAt(i);
      if (c.target != null)
        v.addElement(c.target.getObject());
    }
    ObjectInfo ref[] = new ObjectInfo [v.size()];
    v.copyInto(ref);
    return ref;
  }
  
  /** Delete all references to the specified object from this track.  This is used when an
      object is deleted from the scene. */
  
  public void deleteDependencies(ObjectInfo obj)
  {
    for (int i = constraints.size()-1; i >= 0; i--)
    {
      Constraint c = (Constraint) constraints.elementAt(i);
      if (c.target != null && c.target.getObject() == obj)
        constraints.removeElementAt(i);
    }
  }

  /** Write a serialized representation of this track to a stream. */
  
  public void writeToStream(DataOutputStream out, Scene scene) throws IOException
  {
    out.writeShort(1); // Version number
    out.writeUTF(name);
    out.writeBoolean(enabled);
    out.writeBoolean(useGestures);
    out.writeInt(constraints.size());
    for (int i = 0; i < constraints.size(); i++)
    {
      Constraint c = (Constraint) constraints.elementAt(i);
      out.writeInt(c.jointID);
      out.writeBoolean(c.target != null);
      if (c.target != null)
        c.target.writeToStream(out);
    }
    theWeight.writeToStream(out, scene);
  }
  
  /** Initialize this tracked based on its serialized representation as written by writeToStream(). */
  
  public void initFromStream(DataInputStream in, Scene scene) throws IOException, InvalidObjectException
  {
    short version = in.readShort();
    
    if (version < 0 || version > 1)
      throw new InvalidObjectException("");
    name = in.readUTF();
    enabled = in.readBoolean();
    if (version > 0)
      useGestures = in.readBoolean();
    int numConstraints = in.readInt();
    constraints = new Vector();
    for (int i = 0; i < numConstraints; i++)
    {
      Constraint c = new Constraint(in.readInt(), in.readBoolean() ? new ObjectRef(in, scene) : null);
      constraints.addElement(c);
    }
    theWeight.initFromStream(in, scene);
  }
  
  /** This inner class represents a single constraint. */
  
  private class Constraint
  {
    public int jointID;
    public ObjectRef target;
    
    public Constraint(int id, ObjectRef t)
    {
      jointID = id;
      target = t;
    }
    
    public Constraint duplicate()
    {
      if (target == null)
        return new Constraint(jointID, null);
      return new Constraint(jointID, target.duplicate());
    }
  }

  /** Inner class for editing constraint tracks. */
  
  private class Editor extends BDialog
  {
    LayoutWindow window;
    BList constraintList;
    BTextField nameField;
    BCheckBox gesturesBox;
    Vector tempConstraints;
    int tempJointID[];
    ObjectRef tempTarget[];
    BButton editButton, deleteButton;
    
    public Editor(LayoutWindow win)
    {
      super(win, Translate.text("ikTrackTitle"), true);
      window = win;
      tempConstraints = new Vector();
      for (int i = 0; i < constraints.size(); i++)
        tempConstraints.addElement(((Constraint) constraints.elementAt(i)).duplicate());
      
      // Layout the dialog.
      
      FormContainer content = new FormContainer(new double [] {0.0, 1.0}, new double [] {0.0, 1.0, 0.0, 0.0});
      setContent(content);
      content.setDefaultLayout(new LayoutInfo(LayoutInfo.CENTER, LayoutInfo.BOTH, null, null));
      content.add(Translate.label("trackName"), 0, 0);
      content.add(Translate.label("constraints"), 0, 1);
      content.add(nameField = new BTextField(IKTrack.this.getName(), 30), 1, 0);
      content.add(UIUtilities.createScrollingList(constraintList = new BList()), 1, 1);
      constraintList.setPreferredVisibleRows(5);
      constraintList.addEventLink(SelectionChangedEvent.class, this, "selectionChanged");
      constraintList.addEventLink(MouseClickedEvent.class, this, "mouseClicked");
      buildConstraintList();
      content.add(gesturesBox = new BCheckBox(Translate.text("useGesturesToShapeMesh"), useGestures), 0, 2, 2, 1);
      gesturesBox.setEnabled(Actor.getActor(info.object) != null);
      RowContainer buttons = new RowContainer();
      content.add(buttons, 0, 3, 2, 1, new LayoutInfo());
      buttons.add(Translate.button("add", "...", this, "doAdd"));
      buttons.add(editButton = Translate.button("edit", "...", this, "doEdit"));
      buttons.add(deleteButton = Translate.button("delete", this, "doDelete"));
      buttons.add(Translate.button("ok", this, "doOk"));
      buttons.add(Translate.button("cancel", this, "dispose"));
      pack();
      UIUtilities.centerWindow(this);
      editButton.setEnabled(false);
      deleteButton.setEnabled(false);
      setVisible(true);
    }
    
    /** Fill in the list of constraints. */
    
    private void buildConstraintList()
    {
      Skeleton skeleton = info.getSkeleton();
      constraintList.removeAll();
      if (tempConstraints.size() == 0)
      {
        constraintList.add("(No Constraints)");
        constraintList.setEnabled(false);
        return;
      }
      constraintList.setEnabled(true);
      for (int i = 0; i < tempConstraints.size(); i++)
      {
        Constraint c = (Constraint) tempConstraints.elementAt(i);
        Joint j = skeleton.getJoint(c.jointID);
        if (c.target == null)
          constraintList.add(Translate.text("jointIsLocked", j.name));
        else
          constraintList.add(Translate.text("jointFollowsTarget", j.name, c.target.toString()));
      }
    }
    
    private void doAdd()
    {
      Constraint c = new Constraint(-1, null);
      if (editConstraint(c))
      {
        tempConstraints.addElement(c);
        buildConstraintList();
      }
    }
    
    private void doEdit()
    {
      if (constraintList.getSelectedIndex() > -1)
      {
        Constraint c = (Constraint) tempConstraints.elementAt(constraintList.getSelectedIndex());
        if (editConstraint(c))
          buildConstraintList();
      }
    }
    
    private void doDelete()
    {
      if (constraintList.getSelectedIndex() > -1)
        tempConstraints.removeElementAt(constraintList.getSelectedIndex());
      buildConstraintList();
    }
    
    private void doOk()
    {
      window.setUndoRecord(new UndoRecord(window, false, UndoRecord.COPY_OBJECT_INFO, new Object [] {info, info.duplicate()}));
      IKTrack.this.setName(nameField.getText());
      IKTrack.this.setUseGestures(gesturesBox.getState());
      constraints = tempConstraints;
      window.getScore().repaintAll();
      dispose();
    }

    private void mouseClicked(MouseClickedEvent ev)
    {
      if (ev.getClickCount() == 2)
        doEdit();
    }
    
    /** Update the components when a selection changes. */
    
    private void selectionChanged()
    {
      editButton.setEnabled(constraintList.getSelectedIndex() > -1);
      deleteButton.setEnabled(constraintList.getSelectedIndex() > -1);
    }
    
    /** Display a window for editing a single constraint. */
    
    private boolean editConstraint(Constraint c)
    {
      Skeleton skeleton = info.getSkeleton();
      Joint joint[] = skeleton.getJoints();
      BList jointList = new BList();
      for (int i = 0; i < joint.length; i++)
        jointList.add(joint[i].name);
      if (skeleton.findJointIndex(c.jointID) > -1)
        jointList.setSelected(skeleton.findJointIndex(c.jointID), true);
      jointList.setPreferredVisibleRows(8);
      RadioButtonGroup group = new RadioButtonGroup();
      BRadioButton lockedBox = new BRadioButton(Translate.text("locked"), c.target == null, group);
      final BRadioButton targetBox = new BRadioButton(Translate.text("followsTarget"), c.target != null, group);
      RowContainer targetRow = new RowContainer();
      targetRow.add(lockedBox);
      targetRow.add(targetBox);
      ObjectRef tempRef = (c.target == null ? new ObjectRef() : c.target);
      final ObjectRefSelector selector = new ObjectRefSelector(tempRef, window, Translate.text("targetForJoint"), info);
      Object listener = new Object() {
        private void processEvent(ValueChangedEvent ev)
        {
          selector.setEnabled(ev.getWidget() == targetBox);
        }
      };
      lockedBox.addEventLink(ValueChangedEvent.class, listener);
      targetBox.addEventLink(ValueChangedEvent.class, listener);
      selector.setEnabled(c.target != null);
      ComponentsDialog dlg = new ComponentsDialog(window, Translate.text("editConstraint"), new Widget []
          {UIUtilities.createScrollingList(jointList), targetRow, selector}, new String [] {Translate.text("applyConstraintTo"), Translate.text("constraintType"),
          Translate.text("targetForJoint")});
      if (!dlg.clickedOk() || jointList.getSelectedIndex() == -1)
        return false;
      if (jointList.getSelectedIndex() > -1)
        c.jointID = joint[jointList.getSelectedIndex()].id;
      if (lockedBox.getState())
        c.target = null;
      else
        c.target = selector.getSelection();
      return true;
    }
  }
}
