/**
 * 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;

import com.jme3.ai.agents.behaviors.Behavior;
import com.jme3.ai.agents.behaviors.BehaviorExceptions.NullBehaviorException;
import com.jme3.ai.agents.behaviors.npc.SimpleMainBehavior;
import com.jme3.ai.agents.behaviors.npc.steering.SteeringExceptions;
import com.jme3.ai.agents.util.GameEntity;
import com.jme3.scene.Spatial;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;

/**
 * Class that represents Agent.
 *
 * @author Jesús Martín Berlanga
 * @author Tihomir Radosavljević
 * @version 1.7.4
 */
public class Agent<T> extends GameEntity {

    /**
     * Class that enables you to add all variable you need for your agent.
     */
    private T model;
    /**
     * Unique name of Agent.
     */
    private String name;
    /**
     * Name of team. Primarily used for enabling friendly fire.
     */
    private Team team;
    /**
     * Main behaviour of Agent. Behavior that will be active while his alive.
     */
    private Behavior mainBehavior;

    public Agent() {
    }

    /**
     * @param name name of agent
     */
    public Agent(String name) {
        this.name = name;
    }

    /**
     * @param spatial spatial that will agent have durring game
     */
    public Agent(Spatial spatial) {
        this.spatial = spatial;
    }

    /**
     * @param name name of agent
     * @param spatial spatial that will agent have durring game
     */
    public Agent(String name, Spatial spatial) {
        this.name = name;
        this.spatial = spatial;
    }

    /**
     * @return main behavior of agent
     */
    public Behavior getMainBehavior() {
        return mainBehavior;
    }

    /**
     * Setting main behavior to agent. For more how should main behavior look
     * like:
     *
     * @see SimpleMainBehavior
     * @param mainBehavior
     */
    public void setMainBehavior(Behavior mainBehavior) {
        this.mainBehavior = mainBehavior;
        this.mainBehavior.setEnabled(false);
    }

    /**
     * @return unique name/id of agent
     */
    public String getName() {
        return name;
    }

    /**
     * Method for starting agent.
     *
     * @see Agent#enabled
     */
    public void start() {
        enabled = true;
        if (mainBehavior == null) {
            throw new NullBehaviorException("Agent " + name + " does not have set main behavior.");
        }
        mainBehavior.setEnabled(true);
    }

    /**
     * Method for stoping agent. Note: It will not remove spatial, it will just
     * stop agent from acting.
     *
     * @see Agent#enabled
     */
    public void stop() {
        enabled = false;
        mainBehavior.setEnabled(false);
    }

    /**
     * @return model of agent
     */
    public T getModel() {
        return model;
    }

    /**
     * @param model of agent
     */
    public void setModel(T model) {
        this.model = model;
    }

    @Override
    protected void controlUpdate(float tpf) {
        if (mainBehavior != null) {
            mainBehavior.update(tpf);
        }
    }

    /**
     * @return team in which agent belongs
     */
    public Team getTeam() {
        return team;
    }

    /**
     * @param team in which agent belongs
     */
    public void setTeam(Team team) {
        this.team = team;
    }

    /**
     * Check if this agent is in same team as another agent.
     *
     * @param agent
     * @return true if they are in same team, false otherwise
     */
    public boolean isSameTeam(Agent agent) {
        if (team == null || agent.getTeam() == null) {
            return false;
        }
        return team.equals(agent.getTeam());
    }


    /**
     * Check if this agent is considered in the same "neighborhood" in relation
     * with another agent. <br> <br>
     *
     * If the distance is lower than minDistance It is definitely considered in
     * the same neighborhood. <br> <br>
     *
     * If the distance is higher than maxDistance It is defenitely not
     * considered in the same neighborhood. <br> <br>
     *
     * If the distance is inside [minDistance. maxDistance] It is considered in
     * the same neighborhood if the forwardness is higher than "1 -
     * sinMaxAngle".
     *
     * @param GameEntity The other agent
     * @param minDistance Min. distance to be in the same "neighborhood"
     * @param maxDistance Max. distance to be in the same "neighborhood"
     * @param maxAngle Max angle in radians
     *
     * @throws SteeringExceptions.NegativeValueException If minDistance or
     * maxDistance is lower than 0
     *
     * @return If this agent is in the same "neighborhood" in relation with
     * another agent.
     */
    public boolean inBoidNeighborhood(GameEntity neighbour, float minDistance, float maxDistance, float maxAngle) {
        if (minDistance < 0) {
            throw new SteeringExceptions.NegativeValueException("The min distance can not be negative.", minDistance);
        } else if (maxDistance < 0) {
            throw new SteeringExceptions.NegativeValueException("The max distance can not be negative.", maxDistance);
        }
        boolean isInBoidNeighborhood;
        if (this == neighbour) {
            isInBoidNeighborhood = false;
        } else {
            float distanceSquared = distanceSquaredRelativeToGameEntity(neighbour);
            // definitely in neighborhood if inside minDistance sphere
            if (distanceSquared < (minDistance * minDistance)) {
                isInBoidNeighborhood = true;
            } // definitely not in neighborhood if outside maxDistance sphere
            else if (distanceSquared > maxDistance * maxDistance) {
                isInBoidNeighborhood = false;
            } // otherwise, test angular offset from forward axis.
            else {
                if (this.getAcceleration() != null) {
                    Vector3f unitOffset = this.offset(neighbour).divide(distanceSquared);
                    float forwardness = this.forwardness(unitOffset);
                    isInBoidNeighborhood = forwardness > FastMath.cos(maxAngle);
                } else {
                    isInBoidNeighborhood = false;
                }
            }
        }

        return isInBoidNeighborhood;
    }

    /**
     * Given two vehicles, based on their current positions and velocities,
     * determine the time until nearest approach.
     *
     * @param gameEntity Other gameEntity
     * @return The time until nearest approach
     */
    public float predictNearestApproachTime(GameEntity gameEntity) {
        Vector3f agentVelocity = velocity;
        Vector3f otherVelocity = gameEntity.getVelocity();

        if (agentVelocity == null) {
            agentVelocity = new Vector3f();
        }

        if (otherVelocity == null) {
            otherVelocity = new Vector3f();
        }

        /* "imagine we are at the origin with no velocity,
         compute the relative velocity of the other vehicle" */
        Vector3f relVel = otherVelocity.subtract(agentVelocity);
        float relSpeed = relVel.length();

        /* "Now consider the path of the other vehicle in this relative
         space, a line defined by the relative position and velocity.
         The distance from the origin (our vehicle) to that line is
         the nearest approach." */

        // "Take the unit tangent along the other vehicle's path"
        Vector3f relTangent = relVel.divide(relSpeed);

        /* "find distance from its path to origin (compute offset from
         other to us, find length of projection onto path)" */
        Vector3f offset = gameEntity.offset(this);
        float projection = relTangent.dot(offset);

        return projection / relSpeed;
    }

    /**
     * Given the time until nearest approach (predictNearestApproachTime)
     * determine position of each vehicle at that time, and the distance between
     * them.
     *
     * @param agent Other agent
     * @param time The time until nearest approach
     * @return The time until nearest approach
     *
     * @see Agent#predictNearestApproachTime(com.jme3.ai.agents.Agent)
     */
    public float computeNearestApproachPositions(Agent agent, float time) {
        Vector3f agentVelocity = velocity;
        Vector3f otherVelocity = agent.getVelocity();

        if (agentVelocity == null) {
            agentVelocity = new Vector3f();
        }

        if (otherVelocity == null) {
            otherVelocity = new Vector3f();
        }

        Vector3f myTravel = agentVelocity.mult(time);
        Vector3f otherTravel = otherVelocity.mult(time);

        return myTravel.distance(otherTravel);
    }

    /**
     * Given the time until nearest approach (predictNearestApproachTime)
     * determine position of each vehicle at that time, and the distance between
     * them. <br> <br>
     *
     * Anotates the positions at nearest approach in the given vectors.
     *
     * @param gameEntity Other gameEntity
     * @param time The time until nearest approach
     * @param ourPositionAtNearestApproach Pointer to a vector, This bector will
     * be changed to our position at nearest approach
     * @param hisPositionAtNearestApproach Pointer to a vector, This bector will
     * be changed to other position at nearest approach
     *
     * @return The time until nearest approach
     *
     * @see Agent#predictNearestApproachTime(com.jme3.ai.agents.Agent)
     */
    public float computeNearestApproachPositions(GameEntity gameEntity, float time, Vector3f ourPositionAtNearestApproach, Vector3f hisPositionAtNearestApproach) {
        Vector3f agentVelocity = this.getVelocity();
        Vector3f otherVelocity = gameEntity.getVelocity();

        if (agentVelocity == null) {
            agentVelocity = new Vector3f();
        }

        if (otherVelocity == null) {
            otherVelocity = new Vector3f();
        }

        Vector3f myTravel = agentVelocity.mult(time);
        Vector3f otherTravel = otherVelocity.mult(time);

        //annotation
        ourPositionAtNearestApproach.set(myTravel);
        hisPositionAtNearestApproach.set(otherTravel);

        return myTravel.distance(otherTravel);
    }

    @Override
    public String toString() {
        return "Agent{" + "name=" + name + ", id=" + id + '}';
    }
}