/**
 * Make a donation http://sourceforge.net/donate/index.php?group_id=98797
 *
 * Microcrowd.com
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * Contact Josh DeFord jdeford@microcrowd.com
 */
package com.microcrowd.loader.java3d.max3ds.data;

import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import javax.media.j3d.Alpha;
import javax.media.j3d.Behavior;
import javax.media.j3d.Group;
import javax.media.j3d.Node;
import javax.media.j3d.RotPosPathInterpolator;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.TransformInterpolator;
import javax.vecmath.Matrix4f;
import javax.vecmath.Point3f;
import javax.vecmath.Quat4f;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;

/**
 * @author Josh DeFord 
 */
public class KeyFramer
{
    private HashMap lastGroupMap = new HashMap();
    private HashMap fatherMap = new HashMap();

    private Quat4f   rotation;
    private Point3f  position;
    private Point3f  pivotCenter;
    private Vector3f pivot;
    private Vector3f scale;
    private HashMap  namedObjectCoordinateSystems = new HashMap();

    private List positionKeys;
    private List orientationKeys;
    private List scaleKeys;

    private Integer id;
    private Group father;
    private Group dummyObject;


    /**
     * Retrieves the named object for the current key framer
     * inserts the rotation, position and pivot transformations for frame 0
     * and assigns the coordinate system to it.
     *
     * The inverse of the local coordinate system converts from 3ds 
     * semi-absolute coordinates (what is in the file) to local coordinates.
     *
     * Then these local coordinates are converted with matrix 
     * that will instantiate them to absolute coordinates:
     * Xabs = sx a1 (Xl-Px) + sy a2 (Yl-Py) + sz a3 (Zl-Pz) + Tx
     * Yabs = sx b1 (Xl-Px) + sy b2 (Yl-Py) + sz b3 (Zl-Pz) + Ty
     * Zabs = sx c1 (Xl-Px) + sy c2 (Yl-Py) + sz c3 (Zl-Pz) + Tz
     * Where:
     * (Xabs,Yabs,Zabs) = absolute coordinate
     * (Px,Py,Pz) = mesh pivot (constant)
     * (X1,Y1,Z1) = local coordinates
     *
     */
    public Behavior createBehavior(String meshName, Group transformGroup, Object testObject)
    {
        Group objectGroup = getObjectByName(meshName, transformGroup, testObject);
        //System.out.println("mesh " + meshName + " scale " + scale);
        if(objectGroup == null)
            return null;

        insertFather(objectGroup, meshName);

        TransformInterpolator behavior = null;
        Transform3D coordinateSystem  = (Transform3D)namedObjectCoordinateSystems.get(meshName);

        //Gonna put these children back later.
        Enumeration children = removeChildren(objectGroup);

        Transform3D coordinateTransform = coordinateSystem == null ? new Transform3D() : new Transform3D(coordinateSystem);

        Transform3D targetTransform = new Transform3D();
        TransformGroup targetGroup = new TransformGroup(targetTransform);

        TransformGroup localCoordinates = hasKeys() ? buildLocalCoordinates(coordinateSystem) : new TransformGroup();
        TransformGroup lastGroup = (TransformGroup)addGroups(objectGroup, new Group[]
                {
                    localCoordinates,
                    targetGroup,
                    buildPivotGroup(coordinateTransform, pivot),
                    buildKeysGroup(),
                });

        addChildren(children, lastGroup);
        lastGroupMap.put(objectGroup, lastGroup);


        behavior = buildInterpolator(targetGroup, coordinateSystem);
        if(behavior != null)
        {
            behavior.setEnable(false);
            targetGroup.addChild(behavior);

            behavior.computeTransform(0f, targetTransform);
            targetGroup.setTransform(targetTransform);
        }
        return behavior;
    }

    private Enumeration removeChildren(Group group)
    {
        Enumeration children = group.getAllChildren(); 
        group.removeAllChildren();
        return children;
    }

    private void addChildren(Enumeration children, Group group)
    {
        if(group == null)
            return;
        while(children.hasMoreElements())
        {
            Node node = (Node)(children.nextElement());
            group.addChild(node);
        }
    }

    /**
     * Looks up the current object.
     * objectGroup is returned if it is the right one to return
     * otherwise a new dummy object may be returned.
     * If it isn't there it gets the dummy object
     * from the frames description chunk.
     */
    private Group getObjectByName(String objectName, Group objectGroup, Object testObject)
    {

        //This means its a dummy object.  It needs to be created.
        if(objectGroup == null && testObject == null)
        {
            namedObjectCoordinateSystems.put(objectName, new Transform3D());
            objectGroup = dummyObject;
        }

        return objectGroup;
    }

    /**
     * Locates the father for the object named objectName and inserts 
     * its transform cluster between the parent and all
     * the parent's children. This only occurs in a hierarchical
     * model. like bones and stuff. The fatherGroup, if found,
     * is removed from whatever parent belonged to on before insertion.
     */
    private void insertFather(Group parentGroup, String objectName)
    {
        if(father == null)
            return;
        Group topGroup = new TransformGroup(); 
        topGroup.addChild(father);
        Group bottomGroup = (Group)lastGroupMap.get(father);

        if(topGroup == null)
            return;

        Group fatherParent = (Group)topGroup.getParent();
        if(fatherParent != null)
            fatherParent.removeChild(topGroup);
        
        Enumeration originalChildren = removeChildren(parentGroup);
        parentGroup.addChild(topGroup);
        addChildren(originalChildren, bottomGroup);
    }

    /**
     * Builds a transform group from the zeroth key of the 
     * position and rotation tracks.
     * @return transform group with position and rotation information
     */
    private TransformGroup buildKeysGroup()
    {
        Transform3D positionTransform   = new Transform3D();
        positionTransform.set(new Vector3f(position));

        Transform3D rotationTransform   = new Transform3D();
        rotationTransform.set(rotation);

        Transform3D scaleTransform = new Transform3D();
        scaleTransform.setScale(new Vector3d(scale));
        TransformGroup scaleGroup = new TransformGroup(scaleTransform);

        Transform3D keyTransform = new Transform3D(positionTransform);
        keyTransform.mul(scaleTransform);
        keyTransform.mul(rotationTransform);
        return new TransformGroup(keyTransform);
    }

    /**
     * Builds a pivot group that will allow the objects
     * to be positioned properly according to their rotations
     * and positions.
     * @param coordinateTransform the coordinate system defining the 
     * location and orientation of the local axis. This is not modified.
     * @param pivot the pivot defined in the 3ds file loaded by pivot chunk.
     * This is not changed.
     */
    private TransformGroup buildPivotGroup(Transform3D coordinateTransform, Vector3f pivot)
    {
            Transform3D pivotTransform = new Transform3D();
            pivotTransform.mulInverse(coordinateTransform);
            pivot = new Vector3f(pivot);
            pivot.negate();
            translatePivot(pivotTransform, pivot, pivotCenter);
            return new TransformGroup(pivotTransform);
    }

    /**
     * Builds a coordinate group that will allow the objects
     * to be positioned properly according to their rotations
     * and positions.
     * @param coordinateSystem the coordinate system defining the 
     * location and orientation of the local axis. This is modified 
     * so it will be useful during the construction
     * of the animations.
     */
    private TransformGroup buildLocalCoordinates(Transform3D coordinateSystem)
    {
        Matrix4f coordMatrix = new Matrix4f();
        Vector3f translation = new Vector3f();

        coordinateSystem.get(translation);
        coordinateSystem.invert();

        coordinateSystem.get(coordMatrix);
        coordMatrix.m03 = translation.x;
        coordMatrix.m13 = translation.y;
        coordMatrix.m23 = translation.z;
        coordinateSystem.set(coordMatrix);
        coordinateSystem.invert();
        TransformGroup systemGroup = new TransformGroup(coordinateSystem);
        coordinateSystem.invert();
        return systemGroup;
    }

   /**
     * Hierarchically adds the provided groups in order to parentGroup.
     * groups[0] is added to parentGroup, groups[1] is added to groups[0] etc.
     * @return the last group added (groups[groups.length - 1]).
     */
    private Group addGroups(Group parentGroup, Group[] groups)
    {
        Group nextGroup = parentGroup;
        for(int i=0; i < groups.length; i++)
        {
            nextGroup.addChild(groups[i]);
            nextGroup = groups[i];
        }
        return groups[groups.length - 1];
    }

    /**
     * Does a pre rotational translation of the pivot.
     * @param transform the matrix that will have a translation concatenated to it.
     * @param vector the vector which will be used to translate the matrix.
     * @param offset the offset used to offset the pivot. 
     */
    private void translatePivot(Transform3D transform, Vector3f vector, Point3f offset)
    {
        if(offset != null)
        {
            pivot.sub(offset);
        }
        Matrix4f matrix = new Matrix4f();
        transform.get(matrix);

        matrix.m03 += (matrix.m00*vector.x + matrix.m01*vector.y + matrix.m02*vector.z);
        matrix.m13 += (matrix.m10*vector.x + matrix.m11*vector.y + matrix.m12*vector.z);
        matrix.m23 += (matrix.m20*vector.x + matrix.m21*vector.y + matrix.m22*vector.z);

        transform.set(matrix);
    }


    /**
     * Builds a rotation position interpolator for use on this mesh using position and rotation information
     * adds it to targetGroup.
     * This does not set the capability bits that need to be set for the animation  
     * to be used. The capability bits of the targetGroup must be set by the client application.
     * The alpha object on the Interpolator must also be enabled. 
     * The Interpolator must also have its scheduling bounds set.
     * @param pivotGroup transform group which will be operated on by the interpolator.
     * @param interpolatorAxis the axis that about which rotations will be centered.
     */
    //TODO... This needs to use both a rotation interpolator and a position interpolator
    //in case there are keys with no position information but position information and 
    //vice versa right now its using RotPosPathInterpolator
    private TransformInterpolator buildInterpolator(TransformGroup targetGroup, Transform3D axisOfTransform)
    {
        makeTwoListsTheSameSize(positionKeys, orientationKeys);
        int numKeys = positionKeys.size();

        Point3f currentPoint = position; 
        Quat4f  currentQuat  = rotation; 
        RotPosPathInterpolator rotator = null; 
        if(numKeys > 1) 
        {
            float[]    knots = new float[numKeys];
            Point3f[] points = new Point3f[numKeys];
            Quat4f[]  quats  = new Quat4f[numKeys];

            for(int i=0; i < numKeys; i++) 
            {
                //Knots need to be between 0(beginning) and 1(end)
                knots[i]= (i==0?0:((float)i/((float)(numKeys-1))));
                if(positionKeys.size() > i)
                {
                    Point3f newPoint = (Point3f)positionKeys.get(i);
                    if(newPoint != null)
                    {
                        currentPoint = newPoint;
                    }

                    Quat4f newQuat = (Quat4f)orientationKeys.get(i);
                    if(newQuat != null)
                    {
                        currentQuat = newQuat;
                    }
                }

                points[i] = currentPoint;
                quats[i] = currentQuat;
                quats[i].inverse();
            }

            //This gives a continuous loop at a rate of 30 fps
            Alpha alpha = new Alpha(-1, (long)(numKeys/.03));
            alpha.setStartTime(System.currentTimeMillis());
            alpha.setDuplicateOnCloneTree(true);

            rotator = new RotPosPathInterpolator(alpha, targetGroup, 
                    axisOfTransform, knots, 
                    quats, points);
        }
        return rotator;
    }

    public void makeTwoListsTheSameSize(List list1, List list2)
    {
        growList(list2.size() - 1, list1);
        growList(list1.size() - 1, list2);
    }

    /**
     * Make sure the list is at least able to
     * hold a value at index.
     * @param index an int specifying the initial size
     * @parame the list that may need to grow
     */
    public void growList(int index, List list)
    {
        int numNeeded = (index + 1) - list.size();
        while(numNeeded-- > 0)
        {
            list.add(null);
        }
    }

    /**
     * Sets the center of the bounding box that the pivot
     * should offset.
     */
    public void setPivotCenter(Point3f center)
    {
        this.pivotCenter = center;
    }

    /**
     * Called to set the coordinate system transform for an object named
     * objectName. 
     * This is the first t
     */
    public void setCoordinateSystem(String objectName, Transform3D coordinateSystem)
    {
        namedObjectCoordinateSystems.put(objectName, coordinateSystem);
    }

    /**
     * Sets the group that will be used to center rotations.
     * This is applied to the mesh after all other transforms
     * have been applied.
     * @param group the group that will act as the rotation transform.
     */
    public void setRotation(Quat4f rotation)
    {
        this.rotation = rotation;
    }

    /**
     * Sets the pivot that will be used to as a pivot for
     * these transfomations.
     * @param group the group that will act as the pivot. 
     */
    public void setPivot(Vector3f pivot)
    {
        this.pivot = pivot;
    }

    /**
     * Sets the scale for x y and z axis for objects. 
     * This is applied to the mesh before the rotation transform 
     * has been applied.
     * @param group the group that will act as the scale 
     */
    public void setScale(Vector3f scale)
    {
        this.scale = scale;
    }

    /**
     * Sets the scale information necessary for animation.s
     * @param scaleKeys a list of Vector3f, which may contain null elements,
     * containing a position for some keys.
     */
    public void setScaleKeys(List scaleKeys)
    {
        this.scaleKeys = scaleKeys;
    }

    /**
     * Sets the group that will be used to translate the mesh..
     * This is applied to the mesh just before the rotation transform 
     * has been applied.
     * @param group the group that will act as the position transform.
     */
    public void setPosition(Point3f position)
    {
        this.position = position;
    }

    /**
     * Sets the position information necessary for animation.s
     * @param positions a list of Point3f, which may contain null elements,
     * containing a position for some keys.
     */
    public void setPositionKeys(List positions)
    {
        positionKeys = positions;
    }

    /**
     * Sets the orientation information necessary for animation.s
     * @param positions a list of Quat4f, which may contain null elements,
     * containing a orientation for some keys.
     */
    public void setOrientationKeys(List orientations)
    {
        orientationKeys = orientations;
    }

    /**
     *
     */
    public void setDummyObject(Group object)
    {
        dummyObject = object;
    }

    /**
     * returns true if position keys and orientation
     * keys are longer than one element each.
     */
    public boolean hasKeys()
    {
        return (positionKeys.size() > 1 || orientationKeys.size() > 1);
    }

    /**
     */
    public void addFather(int fatherID, TransformGroup newFather)
    {
        if(fatherID < 0)
        {
            father = null;
        }
        else
        {
            father = (TransformGroup)(fatherMap.get(new Integer(fatherID)));
            //Remove the father's father because the father will
            //be inserted somewhere later.
            Group grandFather = (Group)father.getParent();
            if(grandFather != null)
            {
                grandFather.removeChild(father);
            }
        }
        fatherMap.put(id, newFather);
    }

    /**
     * Sets the id for these frames.
     * @param id the id for these frames.
     */
    public void setID(int id)
    {
        this.id = new Integer(id);
    }
}
