/**
 * Copyright (c) 2014, jMonkeyEngine All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of 'jMonkeyEngine' nor the names of its contributors may be
 * used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.ai.agents.behaviors.npc.steering;

import com.jme3.ai.agents.Agent;
import com.jme3.ai.agents.behaviors.npc.steering.SteeringExceptions.NegativeValueException;

import com.jme3.math.Plane;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;

/**
 * With this class it will be possible to increase or decrease the steering
 * behavior force.
 * <br> <br>
 *
 * It can be changed the length of this vector itself, multiplying it by a
 * scalar or modifying the force on a especific axis (x, y, z) <br> <br>
 *
 * This class also allows you force the steering to stay within a plane.
 * <br><br>
 *
 * You need to call setupStrengthControl( ... ), otherwhise this class will work
 * the same as AstractSteeringBehavior.
 *
 * @see SteerStrengthType
 * @see AbstractSteeringBehavior
 *
 * @author Jesús Martín Berlanga
 * @author Tihomir Radosavljević
 * @version 2.1.1
 */
public abstract class AbstractStrengthSteeringBehavior extends AbstractSteeringBehavior {

    /**
     * Defines how the "strength" will be applied to a steer force.
     */
    private static enum SteerStrengthType {

        NO_STRENGTH,
        SCALAR,
        AXIS,
        PLANE
    }
    /**
     * Type of steer force that is applied to steering behavior.
     */
    private SteerStrengthType type = SteerStrengthType.NO_STRENGTH;
    /**
     * Used if the steer type is scalar.
     */
    private float scalar;
    /**
     * Used if the steer type is axis.
     */
    private float x, y, z;
    /**
     * Used if the steer type is plane.
     */
    private Plane plane;

    /**
     * @see
     * AbstractSteeringBehavior#AbstractSteeringBehavior(com.jme3.ai.agents.Agent)
     */
    public AbstractStrengthSteeringBehavior(Agent agent) {
        super(agent);
    }

    /**
     * @see
     * AbstractSteeringBehavior#AbstractSteeringBehavior(com.jme3.ai.agents.Agent,
     * com.jme3.scene.Spatial)
     */
    public AbstractStrengthSteeringBehavior(Agent agent, Spatial spatial) {
        super(agent, spatial);
    }

    /**
     * If you call this function you will be able to increase or decrease the
     * steering behavior force multiplying it by a scalar.
     *
     * @param scalar Scalar that will multiply the raw steer force.
     *
     * @throws NegativeValueException If scalar is lower than 0
     */
    public void setupStrengthControl(float scalar) {
        this.validateScalar(scalar);
        this.scalar = scalar;
        this.type = SteerStrengthType.SCALAR;
    }

    /**
     * If you call this function you will be able to modify the raw steering
     * force on the specific axis (x, y, z).
     *
     * @param x X axis multiplier
     * @param y Y axis multiplier
     * @param z Z axis multiplier
     *
     * @throws NegativeValueException If any axis multiplier is lower than 0
     */
    public void setupStrengthControl(float x, float y, float z) {
        this.validateScalar(x);
        this.validateScalar(y);
        this.validateScalar(z);
        this.x = x;
        this.y = y;
        this.z = z;
        this.type = SteerStrengthType.AXIS;
    }

    /**
     * If you call this function you will be able to modify the raw steering
     * force on the specific axis (x, y, z).
     *
     * @param x X axis multiplier
     * @param y Y axis multiplier
     * @param z Z axis multiplier
     *
     * @throws NegativeValueException If any axis multiplier is lower than 0
     */
    public void setupStrengthControl(Vector3f vector) {
        validateScalar(vector.getX());
        validateScalar(vector.getY());
        validateScalar(vector.getZ());
        x = vector.getX();
        y = vector.getY();
        z = vector.getZ();
        type = SteerStrengthType.AXIS;
    }

    /**
     * Forces the steer to stay inside a plane.
     *
     * @param Plane plane where the steer will be
     */
    public void setupStrengthControl(Plane plane) {
        this.scalar = 1.0f;
        this.plane = plane;
        this.type = SteerStrengthType.PLANE;
    }

    /**
     * @see AbstractStrengthSteeringBehavior#setupStrengthControl(float)
     * @see
     * AbstractStrengthSteeringBehavior#setupStrengthControl(com.jme3.math.Plane)
     */
    public void setupStrengthControl(Plane plane, float scalar) {
        this.validateScalar(scalar);
        this.scalar = scalar;
        this.plane = plane;
        this.type = SteerStrengthType.PLANE;
    }

    private void validateScalar(float scalar) {
        if (scalar < 0) {
            throw new NegativeValueException("The scalar multiplier must be positive.", scalar);
        }
    }

    /**
     * If this function is called, this class work as AbstractSteeringBehavior.
     *
     * @see AbstractSteeringBehavior
     */
    public void turnOffStrengthControl() {
        this.type = SteerStrengthType.NO_STRENGTH;
    }

    /**
     * Calculates the steering force with the specified strength. <br><br>
     *
     * If the strength was not setted up it the return calculateSteering(), the
     * unmodified force.
     *
     * @return The steering force with the specified strength.
     */
    @Override
    protected Vector3f calculateSteering() {

        Vector3f strengthSteeringForce = calculateRawSteering();

        switch (this.type) {
            case SCALAR:
                strengthSteeringForce = strengthSteeringForce.mult(this.scalar);
                break;

            case AXIS:
                strengthSteeringForce.setX(strengthSteeringForce.getX() * this.x);
                strengthSteeringForce.setY(strengthSteeringForce.getY() * this.y);
                strengthSteeringForce.setZ(strengthSteeringForce.getZ() * this.z);
                break;

            case PLANE:
                strengthSteeringForce = this.plane.getClosestPoint(strengthSteeringForce).mult(this.scalar);
                break;

        }
        
        //if there is no steering force, than the steering vector is zero
        if (strengthSteeringForce.equals(Vector3f.NAN)) {
            strengthSteeringForce = Vector3f.ZERO.clone();
        }
        
        return strengthSteeringForce;
    }

    /**
     * If a behavior class extend from CompoundSteeringBehaviour instead of
     * AbstractSteeringBehavior, it must implement this method instead of
     * calculateSteering().
     *
     * @see AbstractSteeringBehavior#calculateSteering()
     * @return
     */
    protected abstract Vector3f calculateRawSteering();
}