package com.jme3.scene.plugins.blender.animations;

import java.util.ArrayList;
import java.util.List;

import com.jme3.animation.Bone;
import com.jme3.animation.Skeleton;
import com.jme3.math.Matrix4f;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;

/**
 * This class holds the basic data that describes a bone.
 * 
 * @author Marcin Roguski (Kaelthas)
 */
public class BoneContext {
    // the flags of the bone
    public static final int      SELECTED                            = 0x000001;
    public static final int      CONNECTED_TO_PARENT                 = 0x000010;
    public static final int      DEFORM                              = 0x001000;
    public static final int      NO_LOCAL_LOCATION                   = 0x400000;
    public static final int      NO_INHERIT_SCALE                    = 0x008000;
    public static final int      NO_INHERIT_ROTATION                 = 0x000200;
    
    /**
     * The bones' matrices have, unlike objects', the coordinate system identical to JME's (Y axis is UP, X to the right and Z toward us).
     * So in order to have them loaded properly we need to transform their armature matrix (which blender sees as rotated) to make sure we get identical results.
     */
    public static final Matrix4f BONE_ARMATURE_TRANSFORMATION_MATRIX = new Matrix4f(1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1);

    private static final int     IKFLAG_LOCK_X                       = 0x01;
    private static final int     IKFLAG_LOCK_Y                       = 0x02;
    private static final int     IKFLAG_LOCK_Z                       = 0x04;
    private static final int     IKFLAG_LIMIT_X                      = 0x08;
    private static final int     IKFLAG_LIMIT_Y                      = 0x10;
    private static final int     IKFLAG_LIMIT_Z                      = 0x20;

    private BlenderContext       blenderContext;
    /** The OMA of the bone's armature object. */
    private Long                 armatureObjectOMA;
    /** The OMA of the model that owns the bone's skeleton. */
    private Long                 skeletonOwnerOma;
    /** The structure of the bone. */
    private Structure            boneStructure;
    /** Bone's name. */
    private String               boneName;
    /** The bone's flag. */
    private int                  flag;
    /** The bone's matrix in world space. */
    private Matrix4f             globalBoneMatrix;
    /** The bone's matrix in the model space. */
    private Matrix4f             boneMatrixInModelSpace;
    /** The parent context. */
    private BoneContext          parent;
    /** The children of this context. */
    private List<BoneContext>    children                            = new ArrayList<BoneContext>();
    /** Created bone (available after calling 'buildBone' method). */
    private Bone                 bone;
    /** The length of the bone. */
    private float                length;
    /** The bone's deform envelope. */
    private BoneEnvelope         boneEnvelope;

    // The below data is used only for IK constraint computations.

    /** The bone's stretch value. */
    private float                ikStretch;
    /** Bone's rotation minimum values. */
    private Vector3f             limitMin;
    /** Bone's rotation maximum values. */
    private Vector3f             limitMax;
    /** The bone's stiffness values (how much it rotates during IK computations. */
    private Vector3f             stiffness;
    /** Values that indicate if any axis' rotation should be limited by some angle. */
    private boolean[]            limits;
    /** Values that indicate if any axis' rotation should be disabled during IK computations. */
    private boolean[]            locks;

    /**
     * Constructor. Creates the basic set of bone's data.
     * 
     * @param armatureObjectOMA
     *            the OMA of the bone's armature object
     * @param boneStructure
     *            the bone's structure
     * @param blenderContext
     *            the blender context
     * @throws BlenderFileException
     *             an exception is thrown when problem with blender data reading
     *             occurs
     */
    public BoneContext(Long armatureObjectOMA, Structure boneStructure, BlenderContext blenderContext) throws BlenderFileException {
        this(boneStructure, armatureObjectOMA, null, blenderContext);
    }

    /**
     * Constructor. Creates the basic set of bone's data.
     * 
     * @param boneStructure
     *            the bone's structure
     * @param armatureObjectOMA
     *            the OMA of the bone's armature object
     * @param parent
     *            bone's parent (null if the bone is the root bone)
     * @param blenderContext
     *            the blender context
     * @throws BlenderFileException
     *             an exception is thrown when problem with blender data reading
     *             occurs
     */
    @SuppressWarnings("unchecked")
    private BoneContext(Structure boneStructure, Long armatureObjectOMA, BoneContext parent, BlenderContext blenderContext) throws BlenderFileException {
        this.parent = parent;
        this.blenderContext = blenderContext;
        this.boneStructure = boneStructure;
        this.armatureObjectOMA = armatureObjectOMA;
        boneName = boneStructure.getFieldValue("name").toString();
        flag = ((Number) boneStructure.getFieldValue("flag")).intValue();
        length = ((Number) boneStructure.getFieldValue("length")).floatValue();
        ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);

        // first get the bone matrix in its armature space
        globalBoneMatrix = objectHelper.getMatrix(boneStructure, "arm_mat", blenderContext.getBlenderKey().isFixUpAxis());
        if (blenderContext.getBlenderKey().isFixUpAxis()) {
            // then make sure it is rotated in a proper way to fit the jme bone transformation conventions
            globalBoneMatrix.multLocal(BONE_ARMATURE_TRANSFORMATION_MATRIX);
        }

        Structure armatureStructure = blenderContext.getFileBlock(armatureObjectOMA).getStructure(blenderContext);
        Spatial armature = (Spatial) objectHelper.toObject(armatureStructure, blenderContext);
        ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
        Matrix4f armatureWorldMatrix = constraintHelper.toMatrix(armature.getWorldTransform(), new Matrix4f());

        // and now compute the final bone matrix in world space
        globalBoneMatrix = armatureWorldMatrix.mult(globalBoneMatrix);

        // load the bone deformation envelope if necessary
        if ((flag & DEFORM) == 0) {// if the flag is NOT set then the DEFORM is in use
            boneEnvelope = new BoneEnvelope(boneStructure, armatureWorldMatrix, blenderContext.getBlenderKey().isFixUpAxis());
        }

        // load bone's pose channel data
        Pointer pPose = (Pointer) armatureStructure.getFieldValue("pose");
        if (pPose != null && pPose.isNotNull()) {
            List<Structure> poseChannels = ((Structure) pPose.fetchData().get(0).getFieldValue("chanbase")).evaluateListBase();
            for (Structure poseChannel : poseChannels) {
                Long boneOMA = ((Pointer) poseChannel.getFieldValue("bone")).getOldMemoryAddress();
                if (boneOMA.equals(this.boneStructure.getOldMemoryAddress())) {
                    ikStretch = ((Number) poseChannel.getFieldValue("ikstretch")).floatValue();
                    DynamicArray<Number> limitMin = (DynamicArray<Number>) poseChannel.getFieldValue("limitmin");
                    this.limitMin = new Vector3f(limitMin.get(0).floatValue(), limitMin.get(1).floatValue(), limitMin.get(2).floatValue());

                    DynamicArray<Number> limitMax = (DynamicArray<Number>) poseChannel.getFieldValue("limitmax");
                    this.limitMax = new Vector3f(limitMax.get(0).floatValue(), limitMax.get(1).floatValue(), limitMax.get(2).floatValue());

                    DynamicArray<Number> stiffness = (DynamicArray<Number>) poseChannel.getFieldValue("stiffness");
                    this.stiffness = new Vector3f(stiffness.get(0).floatValue(), stiffness.get(1).floatValue(), stiffness.get(2).floatValue());

                    int ikFlag = ((Number) poseChannel.getFieldValue("ikflag")).intValue();
                    locks = new boolean[] { (ikFlag & IKFLAG_LOCK_X) != 0, (ikFlag & IKFLAG_LOCK_Y) != 0, (ikFlag & IKFLAG_LOCK_Z) != 0 };
                    // limits are enabled when locks are disabled, so we ween to take that into account here
                    limits = new boolean[] { (ikFlag & IKFLAG_LIMIT_X & ~IKFLAG_LOCK_X) != 0, (ikFlag & IKFLAG_LIMIT_Y & ~IKFLAG_LOCK_Y) != 0, (ikFlag & IKFLAG_LIMIT_Z & ~IKFLAG_LOCK_Z) != 0 };
                    break;// we have found what we need, no need to search further
                }
            }
        }

        // create the children
        List<Structure> childbase = ((Structure) boneStructure.getFieldValue("childbase")).evaluateListBase();
        for (Structure child : childbase) {
            children.add(new BoneContext(child, armatureObjectOMA, this, blenderContext));
        }

        blenderContext.setBoneContext(boneStructure.getOldMemoryAddress(), this);
    }

    /**
     * This method builds the bone. It recursively builds the bone's children.
     * 
     * @param bones
     *            a list of bones where the newly created bone will be added
     * @param skeletonOwnerOma
     *            the spatial of the object that will own the skeleton
     * @param blenderContext
     *            the blender context
     * @return newly created bone
     */
    public Bone buildBone(List<Bone> bones, Long skeletonOwnerOma, BlenderContext blenderContext) {
        this.skeletonOwnerOma = skeletonOwnerOma;
        Long boneOMA = boneStructure.getOldMemoryAddress();
        bone = new Bone(boneName);
        bones.add(bone);
        blenderContext.addLoadedFeatures(boneOMA, LoadedDataType.STRUCTURE, boneStructure);
        blenderContext.addLoadedFeatures(boneOMA, LoadedDataType.FEATURE, bone);
        ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);

        Structure skeletonOwnerObjectStructure = (Structure) blenderContext.getLoadedFeature(skeletonOwnerOma, LoadedDataType.STRUCTURE);
        // I could load 'imat' here, but apparently in some older blenders there were bugs or unfinished functionalities that stored ZERO matrix in imat field
        // loading 'obmat' and inverting it makes us avoid errors in such cases
        Matrix4f invertedObjectOwnerGlobalMatrix = objectHelper.getMatrix(skeletonOwnerObjectStructure, "obmat", blenderContext.getBlenderKey().isFixUpAxis()).invertLocal();
        if (objectHelper.isParent(skeletonOwnerOma, armatureObjectOMA)) {
            boneMatrixInModelSpace = globalBoneMatrix.mult(invertedObjectOwnerGlobalMatrix);
        } else {
            boneMatrixInModelSpace = invertedObjectOwnerGlobalMatrix.mult(globalBoneMatrix);
        }

        Matrix4f boneLocalMatrix = parent == null ? boneMatrixInModelSpace : parent.boneMatrixInModelSpace.invert().multLocal(boneMatrixInModelSpace);

        Vector3f poseLocation = parent == null || !this.is(CONNECTED_TO_PARENT) ? boneLocalMatrix.toTranslationVector() : new Vector3f(0, parent.length, 0);
        Quaternion rotation = boneLocalMatrix.toRotationQuat().normalizeLocal();
        Vector3f scale = boneLocalMatrix.toScaleVector();

        bone.setBindTransforms(poseLocation, rotation, scale);
        for (BoneContext child : children) {
            bone.addChild(child.buildBone(bones, skeletonOwnerOma, blenderContext));
        }

        return bone;
    }

    /**
     * @return built bone (available after calling 'buildBone' method)
     */
    public Bone getBone() {
        return bone;
    }

    /**
     * @return the old memory address of the bone
     */
    public Long getBoneOma() {
        return boneStructure.getOldMemoryAddress();
    }

    /**
     * The method returns the length of the bone.
     * If you want to use it for bone debugger take model space scale into account and do
     * something like this:
     * <b>boneContext.getLength() * boneContext.getBone().getModelSpaceScale().y</b>.
     * Otherwise the bones might not look as they should in the bone debugger.
     * @return the length of the bone
     */
    public float getLength() {
        return length;
    }

    /**
     * @return OMA of the bone's armature object
     */
    public Long getArmatureObjectOMA() {
        return armatureObjectOMA;
    }

    /**
     * @return the OMA of the model that owns the bone's skeleton
     */
    public Long getSkeletonOwnerOma() {
        return skeletonOwnerOma;
    }

    /**
     * @return the skeleton the bone of this context belongs to
     */
    public Skeleton getSkeleton() {
        return blenderContext.getSkeleton(armatureObjectOMA);
    }

    /**
     * @return the initial bone's matrix in model space
     */
    public Matrix4f getBoneMatrixInModelSpace() {
        return boneMatrixInModelSpace;
    }

    /**
     * @return the vertex assigning envelope of the bone
     */
    public BoneEnvelope getBoneEnvelope() {
        return boneEnvelope;
    }

    /**
     * @return bone's stretch factor
     */
    public float getIkStretch() {
        return ikStretch;
    }

    /**
     * @return indicates if the X rotation should be limited
     */
    public boolean isLimitX() {
        return limits != null ? limits[0] : false;
    }

    /**
     * @return indicates if the Y rotation should be limited
     */
    public boolean isLimitY() {
        return limits != null ? limits[1] : false;
    }

    /**
     * @return indicates if the Z rotation should be limited
     */
    public boolean isLimitZ() {
        return limits != null ? limits[2] : false;
    }

    /**
     * @return indicates if the X rotation should be disabled
     */
    public boolean isLockX() {
        return locks != null ? locks[0] : false;
    }

    /**
     * @return indicates if the Y rotation should be disabled
     */
    public boolean isLockY() {
        return locks != null ? locks[1] : false;
    }

    /**
     * @return indicates if the Z rotation should be disabled
     */
    public boolean isLockZ() {
        return locks != null ? locks[2] : false;
    }

    /**
     * @return the minimum values in rotation limitation (if limitation is enabled for specific axis).
     */
    public Vector3f getLimitMin() {
        return limitMin;
    }

    /**
     * @return the maximum values in rotation limitation (if limitation is enabled for specific axis).
     */
    public Vector3f getLimitMax() {
        return limitMax;
    }

    /**
     * @return the stiffness of the bone
     */
    public Vector3f getStiffness() {
        return stiffness;
    }

    /**
     * Tells if the bone is of specified property defined by its flag.
     * @param flagMask
     *            the mask of the flag (constants defined in this class)
     * @return <b>true</b> if the bone IS of specified proeprty and <b>false</b> otherwise
     */
    public boolean is(int flagMask) {
        return (flag & flagMask) != 0;
    }

    /**
     * @return the root bone context of this bone context
     */
    public BoneContext getRoot() {
        BoneContext result = this;
        while (result.parent != null) {
            result = result.parent;
        }
        return result;
    }

    /**
     * @return a number of bones from this bone to its root
     */
    public int getDistanceFromRoot() {
        int result = 0;
        BoneContext boneContext = this;
        while (boneContext.parent != null) {
            boneContext = boneContext.parent;
            ++result;
        }
        return result;
    }

    @Override
    public String toString() {
        return "BoneContext: " + boneName;
    }
}
