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

import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme3.math.Transform;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.animations.Ipo;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.constraints.definitions.ConstraintDefinition;
import com.jme3.scene.plugins.blender.constraints.definitions.ConstraintDefinitionFactory;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;

/**
 * The implementation of a constraint.
 * 
 * @author Marcin Roguski (Kaelthas)
 */
public abstract class Constraint {
    private static final Logger          LOGGER = Logger.getLogger(Constraint.class.getName());

    /** The name of this constraint. */
    protected final String               name;
    /** Indicates if the constraint is already baked or not. */
    protected boolean                    baked;

    protected Space                      ownerSpace;
    protected final ConstraintDefinition constraintDefinition;
    protected Long                       ownerOMA;

    protected Long                       targetOMA;
    protected Space                      targetSpace;
    protected String                     subtargetName;

    /** The ipo object defining influence. */
    protected final Ipo                  ipo;
    /** The blender context. */
    protected final BlenderContext       blenderContext;
    protected final ConstraintHelper     constraintHelper;

    /**
     * This constructor creates the constraint instance.
     * 
     * @param constraintStructure
     *            the constraint's structure (bConstraint clss in blender 2.49).
     * @param ownerOMA
     *            the old memory address of the constraint owner
     * @param ownerType
     *            the type of the constraint owner
     * @param influenceIpo
     *            the ipo curve of the influence factor
     * @param blenderContext
     *            the blender context
     * @throws BlenderFileException
     *             this exception is thrown when the blender file is somehow
     *             corrupted
     */
    public Constraint(Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException {
        this.blenderContext = blenderContext;
        name = constraintStructure.getFieldValue("name").toString();
        Pointer pData = (Pointer) constraintStructure.getFieldValue("data");
        if (pData.isNotNull()) {
            Structure data = pData.fetchData().get(0);
            constraintDefinition = ConstraintDefinitionFactory.createConstraintDefinition(data, name, ownerOMA, blenderContext);
            Pointer pTar = (Pointer) data.getFieldValue("tar");
            if (pTar != null && pTar.isNotNull()) {
                targetOMA = pTar.getOldMemoryAddress();
                targetSpace = Space.valueOf(((Number) constraintStructure.getFieldValue("tarspace")).byteValue());
                Object subtargetValue = data.getFieldValue("subtarget");
                if (subtargetValue != null) {// not all constraint data have the
                                             // subtarget field
                    subtargetName = subtargetValue.toString();
                }
            }
        } else {
            // Null constraint has no data, so create it here
            constraintDefinition = ConstraintDefinitionFactory.createConstraintDefinition(null, name, null, blenderContext);
        }
        ownerSpace = Space.valueOf(((Number) constraintStructure.getFieldValue("ownspace")).byteValue());
        ipo = influenceIpo;
        this.ownerOMA = ownerOMA;
        constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
        LOGGER.log(Level.INFO, "Created constraint: {0} with definition: {1}", new Object[] { name, constraintDefinition });
    }

    /**
     * @return <b>true</b> if the constraint is implemented and <b>false</b>
     *         otherwise
     */
    public boolean isImplemented() {
        return constraintDefinition == null ? true : constraintDefinition.isImplemented();
    }

    /**
     * @return the name of the constraint type, similar to the constraint name
     *         used in Blender
     */
    public String getConstraintTypeName() {
        return constraintDefinition.getConstraintTypeName();
    }

    /**
     * @return the OMAs of the features whose transform had been altered beside the constraint owner
     */
    public Set<Long> getAlteredOmas() {
        return constraintDefinition.getAlteredOmas();
    }

    /**
     * Performs validation before baking. Checks factors that can prevent
     * constraint from baking that could not be checked during constraint
     * loading.
     */
    public abstract boolean validate();

    /**
     * @return the OMA of the target or 0 if no target is specified for the constraint
     */
    public abstract Long getTargetOMA();

    /**
     * Applies the constraint to owner (and in some cases can alter other bones of the skeleton).
     * @param frame
     *            the frame of the animation
     */
    public void apply(int frame) {
        if (LOGGER.isLoggable(Level.FINEST)) {
            LOGGER.log(Level.FINEST, "Applying constraint: {0} for frame {1}", new Object[] { name, frame });
        }
        Transform targetTransform = targetOMA != null ? constraintHelper.getTransform(targetOMA, subtargetName, targetSpace) : null;
        constraintDefinition.bake(ownerSpace, targetSpace, targetTransform, (float) ipo.calculateValue(frame));
    }

    /**
     * @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 constraintDefinition == null ? false : constraintDefinition.isTrackToBeChanged();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + (name == null ? 0 : name.hashCode());
        result = prime * result + (ownerOMA == null ? 0 : ownerOMA.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (this.getClass() != obj.getClass()) {
            return false;
        }
        Constraint other = (Constraint) obj;
        if (name == null) {
            if (other.name != null) {
                return false;
            }
        } else if (!name.equals(other.name)) {
            return false;
        }
        if (ownerOMA == null) {
            if (other.ownerOMA != null) {
                return false;
            }
        } else if (!ownerOMA.equals(other.ownerOMA)) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "Constraint(name = " + name + ", def = " + constraintDefinition + ")";
    }
}