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

import artofillusion.*;
import artofillusion.math.*;
import artofillusion.object.*;
import artofillusion.ui.*;
import artofillusion.object.Mesh.*;
import buoy.event.*;
import buoy.widget.*;
import java.awt.*;
import java.text.*;
import java.util.Vector;

/** SkeletonTool is an EditingTool used for manipulating the skeletons of objects. */

public class SkeletonTool extends EditingTool
{
  private static final int CLICK_TOL = 6;
  private static final int HANDLE_SIZE = 4;
  
  private static final int NO_HANDLES = 0;
  private static final int UNLOCKED_HANDLES = 1;
  private static final int ALL_HANDLES = 2;

  private static Image icon, selectedIcon;
  private static int whichHandles = UNLOCKED_HANDLES;
  private Point clickPoint;
  private Vec3 clickPos;
  /** View independent handle position (without scaling) */
  private static Vec3 HANDLEPOS[] = new Vec3 [] {new Vec3(0.0, 1.0, 0.0), new Vec3(1.0, 0.0, 0.0),
          new Vec3(0.0, 0.0, 1.0), new Vec3(-0.6, -0.6, 0.4)};   // !!!

  private boolean hideHandle[], invertDragDir;
  private int clickedHandle;
  private IKSolver ik;
  private Mesh oldMesh, mesh;
  private UndoRecord undo;
  private EventProcessor process;
  private boolean allowCreating;
  private String helpText;
    
  public SkeletonTool(EditingWindow fr, boolean allowCreating)
  {
    super(fr);
    this.allowCreating = allowCreating;
    icon = loadImage("skeleton.gif");
    selectedIcon = loadImage("selected/skeleton.gif");
    clickedHandle = -1;
    helpText = Translate.text(allowCreating ? "skeletonTool.helpText" : "skeletonTool.helpTextNoCreate");
  }

  public void activate()
  {
    super.activate();
    theWindow.setHelpText(helpText);
  }

  public int whichClicks()
  {
    return ALL_CLICKS;
  }

  public Image getIcon()
  {
    return icon;
  }

  public Image getSelectedIcon()
  {
    return selectedIcon;
  }
  
  public String getToolTipText()
  {
    return Translate.text("skeletonTool.tipText");
  }

  /** Calculate screen coordinates for visible join handles and return it.
   * Some elements of the returned array may be null if the handle isn't
   * visible.
   */
  public Point[] getScreenPosOfHandles(MeshViewer mv, Joint j)
  {
    Camera cam = mv.getCamera();
    CoordinateSystem objCoords = mv.getDisplayCoordinates();
    Mat4 objToScreen = cam.getWorldToScreen().times(objCoords.fromLocal());
    
    double scale = 70.0/cam.getScale();
    Vec3 jointPos = new Vec3(j.coords.getOrigin());

    objToScreen.transform(jointPos);
    int jointX = (int) jointPos.x;
    int jointY = (int) jointPos.y;
    
    Point[] result = new Point[HANDLEPOS.length];
    Vec3 handlePos;
       
    for (int i = 0; i < HANDLEPOS.length; i++)
    {
      if (hideHandle[i])
      {
        result[i] = null;
        continue;
      }
      
      handlePos = HANDLEPOS[i].times(scale);

      j.coords.fromLocal().transformDirection(handlePos);
      handlePos.add(j.coords.getOrigin());
      objToScreen.transform(handlePos);
      int x = (int) handlePos.x;
      int y = (int) handlePos.y;
      if (x == jointX && y == jointY)
      {
        result[i] = null;
        continue;
      }
      result[i] = new Point(x, y);
    }
    return result;
  }

  public void drawOverlay(Graphics g, ViewerCanvas view)
  {
    if (ik != null)
      return;
    MeshViewer mv = (MeshViewer) view;
    int selected = mv.getSelectedJoint();
    mesh = (oldMesh == null ? (Mesh) mv.getObject().object : oldMesh);
    Skeleton s = mesh.getSkeleton();
    Joint j = s.getJoint(selected);
    if (j == null)
      return;
    Camera cam = mv.getCamera();
    CoordinateSystem objCoords = mv.getDisplayCoordinates();
    Mat4 objToScreen = cam.getWorldToScreen().times(objCoords.fromLocal());
    
    if (clickedHandle == -1)
    {
      // Draw the handles for each degree of freedom.
      
      if (j.parent == null || whichHandles == NO_HANDLES)
        hideHandle = new boolean [] {true, true, true, true};
      else if (whichHandles == UNLOCKED_HANDLES)
        hideHandle = new boolean [] {j.angle1.fixed, j.angle2.fixed, j.length.fixed, j.twist.fixed};
      else
        hideHandle = new boolean [4];
//      double scale = 70.0/cam.getScale();
//      Vec3 handlePos[] = new Vec3 [HANDLEPOS.length];
//      for (int i = 0; i < HANDLEPOS.length; i++)
//      {
//        handlePos[i] = HANDLEPOS[i].times(scale);
//      }
      Vec3 jointPos = new Vec3(j.coords.getOrigin());
      objToScreen.transform(jointPos);
      int jointX = (int) jointPos.x;
      int jointY = (int) jointPos.y;
      g.setPaintMode();
      
      Point handleScrPos[] = getScreenPosOfHandles(mv, j);
      for (int i = 0; i < hideHandle.length; i++)
      {
        if (handleScrPos[i] == null)
          continue;
//        if (hideHandle[i])
//          continue;
//        j.coords.fromLocal().transformDirection(handlePos[i]);   // !!!
//        handlePos[i].add(j.coords.getOrigin());
//        objToScreen.transform(handlePos[i]);
//        int x = (int) handlePos[i].x;
//        int y = (int) handlePos[i].y;
//        if (x == jointX && y == jointY)
//        {
//          hideHandle[i] = true;
//          continue;
//        }
        
        int x = handleScrPos[i].x;
        int y = handleScrPos[i].y;

        g.setColor(Color.darkGray);
        g.drawLine(jointX, jointY, x, y);
        g.setColor(Color.red);
        g.fillRect(x-HANDLE_SIZE/2, y-HANDLE_SIZE/2, HANDLE_SIZE, HANDLE_SIZE);
      }
    }
    else if (clickedHandle < 2)
    {
      // Draw a circle to show the DOF being moved.
      
      double len = j.length.pos;
      Mat4 m1, m2;
      if (clickedHandle == 0)
      {
        m1 = Mat4.zrotation(j.twist.pos*Math.PI/180.0);
        m2 = j.parent.coords.fromLocal().times(Mat4.yrotation(j.angle2.pos*Math.PI/180.0));
      }
      else
      {
        m1 = Mat4.xrotation(j.angle1.pos*Math.PI/180.0).times(Mat4.zrotation(j.twist.pos*Math.PI/180.0));
        m2 = j.parent.coords.fromLocal();
      }
      m2 = objToScreen.times(m2);
      Point pos[] = new Point [64];
      for (int i = 0; i < pos.length; i++)
      {
        double angle = i*2.0*Math.PI/pos.length;
        Vec3 p = new Vec3(0.0, 0.0, len);
        p = m1.timesDirection(p);
        if (clickedHandle == 0)
          p = m2.times(Mat4.xrotation(angle).timesDirection(p));
        else
          p = m2.times(Mat4.yrotation(angle).timesDirection(p));
        pos[i] = new Point((int) p.x, (int) p.y);
      }
      g.setColor(Color.darkGray);
      for (int i = 1; i < pos.length; i++)
        g.drawLine(pos[i-1].x, pos[i-1].y, pos[i].x, pos[i].y);
      g.drawLine(pos[0].x, pos[0].y, pos[pos.length-1].x, pos[pos.length-1].y);
    }
  }

  public void mousePressed(WidgetMouseEvent e, ViewerCanvas view)
  {
    MeshViewer mv = (MeshViewer) view;
    mesh = (Mesh) mv.getObject().object;
    Skeleton s = mesh.getSkeleton();
    Camera cam = mv.getCamera();
    CoordinateSystem objCoords = mv.getDisplayCoordinates();
    
    clickPoint = e.getPoint();
    if (e.isControlDown() && allowCreating)
      {
	// Create a new joint.
	
	undo = new UndoRecord(theWindow, false, UndoRecord.COPY_OBJECT, new Object [] {mesh, mesh.duplicate()});
	Joint j, parent = s.getJoint(mv.getSelectedJoint());
	if (parent == null)
	  {
	    double distToJoint = cam.getDistToScreen();
	    clickPos = cam.convertScreenToWorld(clickPoint, distToJoint);
            objCoords.toLocal().transform(clickPos);
	    j = new Joint(new CoordinateSystem(clickPos, Vec3.vz(), Vec3.vy()), null, "Root "+s.getNextJointID());
	    j.angle1.fixed = j.angle2.fixed = true;
	    s.addJoint(j, -1);
	  }
	else
	  {
	    double distToJoint = cam.getWorldToView().timesZ(parent.coords.getOrigin());
	    clickPos = cam.convertScreenToWorld(clickPoint, distToJoint);
            objCoords.toLocal().transform(clickPos);
	    Vec3 zdir = clickPos.minus(parent.coords.getOrigin());
	    zdir.normalize();
	    Vec3 ydir = cam.getCameraCoordinates().getZDirection().cross(zdir);
	    ydir.normalize(); 
	    j = new Joint(new CoordinateSystem(clickPos, zdir, ydir), parent, "Bone "+s.getNextJointID());
	    s.addJoint(j, parent.id);
	  }
	mv.setSelectedJoint(j.id);
        boolean moving[] = new boolean [s.getNumJoints()];
        moving[s.findJointIndex(j.id)] = true;
	ik = new IKSolver(s, mv.getLockedJoints(), moving);
	view.updateImage();
        view.repaint();
	theWindow.updateMenus();
	oldMesh = (Mesh) mesh.duplicate();
	return;
      }
    
    // See whether a handle was clicked.
    
    if (hideHandle != null)
    {
      Joint selectedJoint = s.getJoint(mv.getSelectedJoint());
      Point handleScrPos[] = getScreenPosOfHandles(mv, selectedJoint);
      for (int i = 0; i < handleScrPos.length; i++)
      {
        // System.out.println("SkeletonTool.mousePressed "+handleScrPos[i]+" "+clickPoint);
        if (handleScrPos[i] == null)
          continue;
        
        if (Math.abs(handleScrPos[i].x-clickPoint.x) <= 0.5*CLICK_TOL && Math.abs(handleScrPos[i].y-clickPoint.y) <= 0.5*CLICK_TOL)
        {
          clickedHandle = i;
          oldMesh = (Mesh) mesh.duplicate();
//          Joint selectedJoint = s.getJoint(mv.getSelectedJoint());
          Mat4 objToScreen = cam.getWorldToScreen().times(objCoords.fromLocal());
          Vec2 jointPos = objToScreen.timesXY(selectedJoint.coords.getOrigin());
          invertDragDir = (handleScrPos[i].x < jointPos.x);
          view.updateImage();
          view.repaint();
          return;
        }
      }
    }
    
    // Determine which joint was clicked on.
    
    Joint joint[] = s.getJoints();
    int i;
    Mat4 objToScreen = cam.getWorldToScreen().times(objCoords.fromLocal());
    for (i = 0; i < joint.length; i++)
      {
	Vec2 pos = objToScreen.timesXY(joint[i].coords.getOrigin());
	if ((clickPoint.x > pos.x-CLICK_TOL) && 
	    (clickPoint.x < pos.x+CLICK_TOL) &&
	    (clickPoint.y > pos.y-CLICK_TOL) && 
	    (clickPoint.y < pos.y+CLICK_TOL))
	  break;
      }
    if (i == joint.length)
      {
	// No joint was clicked on, so deselect the selected joint.
	
	mv.setSelectedJoint(-1);
	view.updateImage();
        view.repaint();
	theWindow.updateMenus();
	return;
      }
    if (e.isShiftDown())
      {
	// Toggle whether this joint is locked.
	
        if (mv.isJointLocked(joint[i].id))
          mv.unlockJoint(joint[i].id);
        else
          mv.lockJoint(joint[i].id);
	view.updateImage();
        view.repaint();
	return;
      }
    
    // Make it the selected joint, and prepare to drag it.
    
    mv.setSelectedJoint(joint[i].id);
    clickPos = joint[i].coords.getOrigin();
    view.updateImage();
    view.repaint();
    theWindow.updateMenus();
    boolean moving[] = new boolean [s.getNumJoints()];
    moving[s.findJointIndex(joint[i].id)] = true;
    ik = new IKSolver(s, mv.getLockedJoints(), moving);
    oldMesh = (Mesh) mesh.duplicate();
  }
  
  public void mouseDragged(final WidgetMouseEvent e, ViewerCanvas view)
  {
    final MeshViewer mv = (MeshViewer) view;
    final CoordinateSystem objCoords = mv.getDisplayCoordinates();
    final Camera cam = mv.getCamera();
    final Mesh mesh = (Mesh) mv.getObject().object;
    final Skeleton s = mesh.getSkeleton();
    final Joint selectedJoint = s.getJoint(mv.getSelectedJoint());

    if (clickedHandle > -1)
    {
      // Adjust a single degree of freedom.
      
      if (undo == null)
        undo = new UndoRecord(getWindow(), false, UndoRecord.COPY_OBJECT, new Object [] {mesh, mesh.duplicate()});
      double dist = clickPoint.x-e.getPoint().x;
      if (invertDragDir)
        dist = -dist;
      Joint origJoint = oldMesh.getSkeleton().getJoint(mv.getSelectedJoint());
      Joint.DOF dof = null;
      Joint.DOF origDof = null;
      String name = null;
      switch (clickedHandle)
      {
        case 0:
          dof = selectedJoint.angle1;
          origDof = origJoint.angle1;
          dist *= 1.8/Math.PI;
          name = "X Bend";
          break;
        case 1:
          dof = selectedJoint.angle2;
          origDof = origJoint.angle2;
          dist *= -1.8/Math.PI;
          name = "Y Bend";
          break;
        case 2:
          dof = selectedJoint.length;
          origDof = origJoint.length;
          dist /= -cam.getScale();
          name = "Length";
          break;
        case 3:
          dof = selectedJoint.twist;
          origDof = origJoint.twist;
          dist *= 1.8/Math.PI;
          name = "Twist";
          break;
      }
      dof.set(origDof.pos+dist);
      selectedJoint.recalcCoords(true);
      if (!mv.getSkeletonDetached())
        Skeleton.adjustMesh(oldMesh, mesh);
      mv.objectChanged();
      mv.updateImage();
      mv.repaint();
      NumberFormat nf = NumberFormat.getNumberInstance();
      nf.setMaximumFractionDigits(3);
      theWindow.setHelpText(name+": "+nf.format(dof.pos));
      return;
    }

    if (ik == null)
      return;
    
    boolean converged = false;
    Vec3 target[] = new Vec3 [s.getNumJoints()];
    int jointIndex = s.findJointIndex(selectedJoint.id);
    ActionProcessor process = view.getActionProcessor();
    do
    {
      Vec3 jointPos = objCoords.fromLocal().times(selectedJoint.coords.getOrigin());
      double distToJoint = cam.getWorldToView().timesZ(jointPos);
      Vec3 goal = cam.convertScreenToWorld(e.getPoint(), distToJoint);
      objCoords.toLocal().transform(goal);

      if (undo == null)
        undo = new UndoRecord(SkeletonTool.this.getWindow(), false, UndoRecord.COPY_OBJECT, new Object [] {mesh, mesh.duplicate()});
      target[jointIndex] = goal;
      converged = ik.solve(target, 100);
      if (!mv.getSkeletonDetached())
        adjustMesh(mesh);
      mv.objectChanged();
      mv.updateImage();
      mv.repaint();
    } while (!converged && (process == null || (!process.hasEvent() && !process.hasBeenStopped())));
        mesh.getSkeleton().informChanged(this, ModelEvent.CHANGEDUR_OBJECTEDITORDRAG);
        if (!mv.getSkeletonDetached())
          mesh.informChanged(this, ModelEvent.CHANGEDUR_OBJECTEDITORDRAG);
  }

  public void mouseReleased(WidgetMouseEvent e, ViewerCanvas view)
  {
    if (undo == null && e.getClickCount() == 2 && !e.isShiftDown() && !e.isControlDown())
      ((MeshEditorWindow) theWindow).editJointCommand(); // They double-clicked without dragging.
    ik = null;
    clickedHandle = -1;
    if (undo != null)
    {
      theWindow.setUndoRecord(undo);
      mesh.getSkeleton().informChanged(this, ModelEvent.CHANGEDUR_OBJECTEDITOR);
//      if (!mv.getSkeletonDetached())    // TODO(MB) Test if necessary
//        mesh.informChanged(this, ModelEvent.CHANGEDUR_OBJECTEDITORDRAG);
    }
    mesh = oldMesh = null;
    undo = null;
    theWindow.setHelpText(helpText);
  }
  
  public void iconDoubleClicked()
  {
    BComboBox dofChoice = new BComboBox(new String [] {
      Translate.text("noDOF"),
      Translate.text("unlockedDOF"),
      Translate.text("allDOF"),
    });
    dofChoice.setSelectedIndex(whichHandles);
    ComponentsDialog dlg = new ComponentsDialog(theFrame, Translate.text("skeletonToolTitle"), 
		new Widget [] {dofChoice}, new String [] {null});
    if (!dlg.clickedOk())
      return;
    whichHandles = dofChoice.getSelectedIndex();
    theWindow.updateImage();
  }
  
  /** Adjust the mesh after the skeleton has changed. */
  
  protected void adjustMesh(Mesh newMesh)
  {
    Skeleton.adjustMesh(oldMesh, newMesh);
  }
}