package com.jme3.scene.plugins.blender.constraints.definitions;

import java.util.Set;

import com.jme3.animation.Bone;
import com.jme3.math.Transform;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Structure;

/**
 * A base class for all constraint definitions.
 * 
 * @author Marcin Roguski (Kaelthas)
 */
public abstract class ConstraintDefinition {
    protected ConstraintHelper constraintHelper;
    /** Constraints flag. Used to load user's options applied to the constraint. */
    protected int              flag;
    /** The constraint's owner. Loaded during runtime. */
    private Object             owner;
    /** The blender context. */
    protected BlenderContext   blenderContext;
    /** The constraint's owner OMA. */
    protected Long             ownerOMA;
    /** Stores the OMA addresses of all features whose transform had been altered beside the constraint owner. */
    protected Set<Long>        alteredOmas;
    /** The variable that determines if the constraint will alter the track in any way. */
    protected boolean          trackToBeChanged = true;
    /** The name of the constraint. */
    protected String           constraintName;

    /**
     * Loads a constraint definition based on the constraint definition
     * structure.
     * 
     * @param constraintData
     *            the constraint definition structure
     * @param ownerOMA
     *            the constraint's owner OMA
     * @param blenderContext
     *            the blender context
     */
    public ConstraintDefinition(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
        if (constraintData != null) {// Null constraint has no data
            Number flag = (Number) constraintData.getFieldValue("flag");
            if (flag != null) {
                this.flag = flag.intValue();
            }
        }
        this.blenderContext = blenderContext;
        constraintHelper = (ConstraintHelper) (blenderContext == null ? null : blenderContext.getHelper(ConstraintHelper.class));
        this.ownerOMA = ownerOMA;
    }
    
    public void setConstraintName(String constraintName) {
        this.constraintName = constraintName;
    }

    /**
     * @return determines if the definition of the constraint will change the bone in any way; in most cases
     *         it is possible to tell that even before the constraint baking simulation is started, so we can discard such bones from constraint
     *         computing to improve the computation speed and lower the computations complexity
     */
    public boolean isTrackToBeChanged() {
        return trackToBeChanged;
    }

    /**
     * @return determines if this constraint definition requires a defined target or not
     */
    public abstract boolean isTargetRequired();

    /**
     * This method is here because we have no guarantee that the owner is loaded
     * when constraint is being created. So use it to get the owner when it is
     * needed for computations.
     * 
     * @return the owner of the constraint or null if none is set
     */
    protected Object getOwner() {
        if (ownerOMA != null && owner == null) {
            owner = blenderContext.getLoadedFeature(ownerOMA, LoadedDataType.FEATURE);
            if (owner == null) {
                throw new IllegalStateException("Cannot load constraint's owner for constraint type: " + this.getClass().getName());
            }
        }
        return owner;
    }

    /**
     * The method gets the owner's transformation. The owner can be either bone or spatial.
     * @param ownerSpace
     *            the space in which the computed transformation is given
     * @return the constraint owner's transformation
     */
    protected Transform getOwnerTransform(Space ownerSpace) {
        if (this.getOwner() instanceof Bone) {
            BoneContext boneContext = blenderContext.getBoneContext(ownerOMA);
            return constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), ownerSpace);
        }
        return constraintHelper.getTransform(ownerOMA, null, ownerSpace);
    }

    /**
     * The method applies the given transformation to the owner.
     * @param ownerTransform
     *            the transformation to apply to the owner
     * @param ownerSpace
     *            the space that defines which owner's transformation (ie. global, local, etc. will be set)
     */
    protected void applyOwnerTransform(Transform ownerTransform, Space ownerSpace) {
        if (this.getOwner() instanceof Bone) {
            BoneContext boneContext = blenderContext.getBoneContext(ownerOMA);
            constraintHelper.applyTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), ownerSpace, ownerTransform);
        } else {
            constraintHelper.applyTransform(ownerOMA, null, ownerSpace, ownerTransform);
        }
    }

    /**
     * @return <b>true</b> if the definition is implemented and <b>false</b>
     *         otherwise
     */
    public boolean isImplemented() {
        return true;
    }

    /**
     * @return a list of all OMAs of the features that the constraint had altered beside its owner
     */
    public Set<Long> getAlteredOmas() {
        return alteredOmas;
    }

    /**
     * @return the type name of the constraint
     */
    public abstract String getConstraintTypeName();

    /**
     * Bakes the constraint for the current feature (bone or spatial) position.
     * 
     * @param ownerSpace
     *            the space where owner transform will be evaluated in
     * @param targetSpace
     *            the space where target transform will be evaluated in
     * @param targetTransform
     *            the target transform used by some of the constraints
     * @param influence
     *            the influence of the constraint (from range <0; 1>)
     */
    public abstract void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence);

    @Override
    public String toString() {
        return this.getConstraintTypeName();
    }
}
