package worms.model;

import be.kuleuven.cs.som.annotate.*;
import worms.util.Tuple;
import worms.util.IllegalNameException;

import static java.lang.Math.*;

/**
 * a class with the specifications of the worm
 *
 * @invar	the location of the worm must be a valid location
 * 			|isValidLocation(getLocation())
 * @invar	the orientation of the worm must be a valid location
 * 			|isValidOrientation(getOrientation())
 * @invar	the radius of the worm mus be a valid radius
 * 			|isValidMinRadis(getRadius())
 * @invar	the mass of the worm must be a valid mass
 * 			|isValidMass(getMass())
 * @invar	the value of the action points of the worm must be a valid value for action points
 * 			|isValidValueActionPoints(getActionPoints())
 * @invar	the name of the worm must be a valid name
 * 			|isValidName(getName())
 *
 * @version	1.0
 * @author Arthur Bols and Leen Dereu
 */
public class Worm {

    // region constructor
    //===================================================================================

    /**
     *initialize the new worm with given location, orientation, name and radius
     *
     * @param location
     * 		  the location for the new worm
     * @param orientation
     * 		  the orientation for the new worm
     * @param name
     * 		  the name for the new worm
     * @param radius
     * 		  the radius for the new worm
     * @post  the new location of the worm is equal to the given location
     * 		  |new.getLocation() == location
     * @post  the new orientation of the worm is equal to the given orientation
     * 		  |new.getOrientation() == orientation
     * @post  the new name of the worm is equal to the given name
     * 		  |new.getName() == name
     * @post  the new radius of the worm is equal to the given radius
     * 		  |new.getRadius() == radius
     */
    public Worm(Tuple<Double, Double> location, double orientation, String name, double radius) {

        this(location, orientation, name, radius, 0.25);
    }

    /**
     *initialize the new worm with given location, orientation, name, radius and minimum radius
     *
     * @param location
     * 		  the location for the new worm
     * @param orientation
     * 		  the orientation for the new worm
     * @param name
     * 		  the name for the new worm
     * @param radius
     * 		  the radius for the new worm
     * @param minRadius
     *   	  the minimum radius the new worm can have
     * @post  the new location of the worm is equal to the given location
     * 		  |new.getLocation() == location
     * @post  the new orientation of the worm is equal to the given orientation
     * 		  |new.getOrientation() == orientation
     * @post  the new name of the worm is equal to the given name
     * 		  |new.getName() == name
     * @post  the new radius of the worm is equal to the given radius
     * 		  |new.getRadius() == radius
     * @post  the new minimum radius of the worm is equal to the given minimum radius
     * 		  |new.getMinRadius() == minRadius
     * @throws IllegalArgumentException
     * 		   the given location is not a valid location for a worm
     * 		   |!isValidLocation(location)
     * @throws IllegalArgumentException
     * 		   the given minimum radius is not a valid minimum radius for a worm
     * 		   |!isValidMinRadius(minRadius)
     * @throws IllegalArgumentException
     * 		   the given radius can not be a minimum radius for a worm
     * 		   |!canHaveAsMinRadius(radius)
     * @throws IllegalNameException
     * 		   the given name is not a valid name for a worm
     * 		   |isValidName(name)
     */
    public Worm(Tuple<Double, Double> location, double orientation, String name, double radius, double minRadius)
            throws IllegalArgumentException {

        if (!isValidLocation(location))
            throw new IllegalArgumentException("Illegal value for location");
        setLocation(location);

        setOrientation(orientation);

        if (!isValidMinRadius(minRadius))
            throw new IllegalArgumentException("Invalid min radius"); // TODO add decent exception msg
        this.minRadius = minRadius;

        if (!canHaveAsRadius(radius))
            throw new IllegalArgumentException("Invalid radius");

        setRadius(radius);

        setActionPoints(getMaxActionPoints());

        int validName = isValidName(name);
        if (validName != -1)
            throw new IllegalNameException(validName, name);
        this.name = name;
    }

    //===================================================================================
    // endregion



    // region location
    //===================================================================================

    /**
     * Return the location of the worm
     * 		the location of the worm expresses the place of the worm
     * 		in the play area 
     */
    @Basic
    public Tuple<Double, Double> getLocation() {
        return this.location;
    }

    /**
     * check whether the given location is a valid location for the worm
     *
     * @param location
     * 		  the location to check
     * @return True if and only if the location is not equal to null and the coordinates of
     * 		   the worm are numbers
     * 		   |result == (location != null) && (!Double.isNaN(location.item1)) && (!Double.isNaN(location.item2))
     */
    public static boolean isValidLocation(Tuple<Double, Double>location) {
        return location != null && location.item1 != null && location.item2 != null
                && !Double.isNaN(location.item1) && !Double.isNaN(location.item2);
    }

    /**
     * this variable contains the location of the worm (a tuple)
     */
    private Tuple<Double, Double> location;

    /**
     * set the location of the worm to the given location
     * 
     * @param location
     * 		  the new location for the worm
     * @post  the new location of the worm is equal to the given location
     * 		  |new.getLocation() == location 
     * @throws IllegalArgumentException
     * 		   the given location is not a valid location for a worm
     * 		   |! isValidLocation(location)
     */
    private void setLocation(Tuple<Double, Double> location) throws IllegalArgumentException {

        if (!isValidLocation(location))
            throw new IllegalArgumentException("Illegal value for location"); // TODO add decent exception msg
        this.location = location;
    }


    //===================================================================================
    // endregion


    // region Orientation
    //===================================================================================

    /**
     * Return the orientation of the worm
     * 		the orientation of a worm expresses the direction in which
     * 		the worm is looking
     */
    @Basic
    public double getOrientation() {
        return orientation;
    }

    /**
     * this variable contains the orientation of the worm
     */
    private double orientation;

    /**
     * set the orientation of the worm to the given orientation
     * 
     * @param   orientation
     *          the new orientation of the worm
     * @pre		the given orientation must be a valid orientation for any worm
     *			|isValidOrientation(orientation)
     * @post	the new orientation of the worm must be equal to the given orientation	
     *			|new.getOrientation() == orientation
     */
    private void setOrientation(double orientation) {
        // TODO nominal checking
    	// TODO enkel 0 - 2pi toegelaten (isValidOrientation => invar)
        this.orientation = orientation;
    }

    //===================================================================================
    // endregion




    // region Shape mass/radius
    //===================================================================================
    
    /**
     * Return the radius of the worm
     * 		the radius of the worm expresses half of the 
     * 		width of the worm
     */
    @Basic
    public double getRadius() {
        return this.radius;
    }

    /**
     * check whether the given radius is a valid minimum radius for the worm
     *
     * @param minRadius
     * 		  the radius to check
     * @return True if and only if the radius is a number and the radius is bigger then 0
     * 		   |result == ((!Double.isNaN(radius)) && (radius > 0))
     */
    public static boolean isValidMinRadius(double minRadius) {
        return !Double.isNaN(minRadius) && minRadius > 0;
    }

    /**
     * set the radius of the worm to the given radius
     * 
     * @param radius
     * 		  the new radius for the worm
     * @post  the new radius of the worm is equal to the given radius
     * 		  |new.getRadius() == radius
     * @throws IllegalArgumentException
     * 		   the given radius is not a valid radius for any worm
     * 		   |! canHaveAsMinRadius(radius)
     */
    public void setRadius(double radius) throws IllegalArgumentException {
        if (!canHaveAsRadius(radius))
            throw new IllegalArgumentException("Invalid radius");

        this.radius = radius;
        setMass(radius);
    }

    /**
     * Return the minimum radius the worm can have
     * 		the minimum radius of the worm expresses the minimum length
     * 		of half of the width of the worm
     */
    public double getMinRadius() {
        return this.minRadius;
    }


    /**
     * This variable contains the radius of the worm
     */
    private double radius;

    /**
     * This variable contains the minimum value of the radius
     */
    private final double minRadius;



    /**
     * Check whether the given radius is a valid radius for the worm
     * 
     * @param radius
     * 		  the radius to check
     * @return True if and only if the radius is bigger then the minimum radius
     * 	       (or equal) and the radius is a number
     * 		   |result == (radius >= this.minRadius && !Double.isNaN(radius))
     */
    private boolean canHaveAsRadius(double radius) {
        return !Double.isNaN(radius) && radius >= getMinRadius();
    }

    /**
     * Return the mass of the worm
     * 		the mass of the worm expresses the weight of the worm
     */
    @Basic
    public double getMass() {
        return this.mass;
    }

    /**
     * this variable contains the mass of the worm
     */
    private double mass;

    /**
     * set the mass of the worm to the given mass (dependent on the radius)
     * 
     * @param radius
     * 		  part of the formula to calculate the mass
     * @post  the new mass of the worm is equal to 
     * 		  rho * (4 / 3 * Math.PI * Math.pow(radius, 3))
     * 		  |new.getMass() == rho * (4 / 3 * Math.PI * Math.pow(radius, 3))
     */
    private void setMass(double radius) {

        final double rho = 1062.0;
        double mass = round(rho * (4.0 / 3.0 * PI * pow(radius, 3)));
        this.mass = mass;
        setMaxActionPoints(mass);
    }

    //===================================================================================
    // endregion



    // region ActionPoints
    //===================================================================================

    /**
     * Return the current action points of the worm
     * 		the action points identifies the energy of the worm
     */
    @Basic
    public long getActionPoints() {
        return this.actionPoints;
    }

    /**
     * Return the maximum of action points of the worm
     * 		the maximum action points identifies the maximum energy of the worm
     */
    @Basic
    public long getMaxActionPoints() {
        return this.maxActionPoints;
    }

    /**
     * set the current points of the worm to the given points
     *
     * @param actionPoints
     * 		  the new points for the worm
     * @post  if the given points are bigger then the maximum points, the current points
     * 		  are equal to the maximum points. If the given points are lower then 0
     * 		  the current points are equal to 0. If the given points is between the
     * 		  maximum points and 0, the current points is equal to the given points
     * 		  |if (actionPoints > getMaxActionPoints())
     *        |		actionPoints = getMaxActionPoints();
     *   	  |else if (actionPoints < 0)
     *        |		actionPoints = 0;
	 *		  |this.actionPoints = actionPoints;
     */
    public void setActionPoints(long actionPoints) {
        if (actionPoints > getMaxActionPoints())
            actionPoints = getMaxActionPoints();
        else if (actionPoints < 0)
            actionPoints = 0;

        this.actionPoints = actionPoints;
    }

    /**
     *substract the current points of the worm
     * 
     * @param delta
     * 		  the value which should be decreased
     * @post  the current points are set to the old current points minus the given value
     * 		  |setActionPoints(getActionPoints() - delta)
     */
    public void decreaseActionPoints (long delta) {

        setActionPoints(getActionPoints() - delta);
    }

    /**
     * this variable contains the current action points of the worm
     */
    private long actionPoints;

    /**
     * this variable contains the maximum action points a worm can have
     */
    private long maxActionPoints;

    /**
     * set the maximum of points to the given maximum of points
     *
     * @param maxActionPoints
     * 		  the new maximum of points for the worm
     * @post  the new maximum points is set to the given maximum points (as an integer)
     * 		  |this.maxPoints = round(maxActionPoints)
     * @post  when the maximum points change, the current points should change too
     * 		  |setActionPoints(getActionPoints)
     */
    private void setMaxActionPoints(double maxActionPoints) {
        this.maxActionPoints = round(maxActionPoints);
        setActionPoints(getActionPoints());
    }
    
    /**
     * substract the current points of the worm
     * 
     * @param value
     * 		  the value which should be substracted
     * @post  the current points are set to the old current points minus the given value
     * 		  |setActionPoints(getActionPoints() - value)
     */
    private void subtractActionPoints (long value) {

        setActionPoints(getActionPoints() - value);
    }
    
    /**
     * substract the current points of the worm
     * 
     * @param angle
     * 		  the angle needed to calculate the new current points
     * @post  the current points are set to the old current points minus
     * 		  the angle (in degrees) divided by 6
     * 		  |setActionPoints(getActionPoints() - (long) ceil(toDegrees(angle) / 6))
     */
    private void subtractActionPoints (double angle) {

        setActionPoints(getActionPoints() - (long) ceil(toDegrees(angle) / 6));
    }

    //===================================================================================
    // endregion



    // region name
    //===================================================================================
    
    /**
     * Return the name of the worm
     * 		the name of the worm expresses the identity of the worm
     */
    @Basic
    public String getName() {
        return this.name;
    }

    /**
     * check whether the given name is a valid name for all worms
     *
     * @param name
     * 		  the name to check
     * @return -1 if and only if the given name is longer then 2,
     * 		   the first letter is uppercase and the name only exists
     * 		   of letters, " ", " ' " and " "" "
     * 		   |for (i = 0; i < name.length(); i++)
     * 		   		|result == (name.length() > 2 && Character.isUpperCase(name.charAt(0)
     * 		   		|&& Character.isLetter(name.charAt(i)) &&
     * 				|allowedCharacters.indexOf(name.charAt(i)))
     */
    public static int isValidName (String name) {

        if (name.length() < 2 || !Character.isUpperCase(name.charAt(0))) {
            return 0;
        }
        String allowedCharacters = "'\" ";

        for (int i = 0; i < name.length(); i++) {
            if (!Character.isLetter(name.charAt(i))) {
                if (allowedCharacters.indexOf(name.charAt(i)) == -1) {
                    return i;
                }
            }
        }
        return -1;
    }

    /**
     * set the name of the worm tot the given name
     * 
     * @param name
     * 		  the new name for the worm
     * @post  the new name of the worm is equal to the given name
     * 		  |new.GetName() == name
     * @throws IllegalNameException
     * 		   the given name is not a valid name for any worm
     * 		   |! isValidName(name) 
     */
    public void setName(String name) throws IllegalNameException {

        int validName = isValidName(name);
        if (validName != -1)
            throw new IllegalNameException(validName, name);

        this.name = name;
    }

    /**
     * this variable contains the name of the worm
     */
    private String name;

    //===================================================================================
    // endregion



    // region move
    //===================================================================================

    /**
     * move the worm for the given number of steps
     * 
     * @param numberSteps
     * 		  the number of steps the worm should take
     *
     * @post the x-coordinate of the new location of the worm schould be the location of
     * 		 the old x-coordinate plus the number of steps multiplied with the distance
     * 		 that the x-coordinate schould move (distance is equal to the radius multiplied
     * 		 with the cosinus of the orientation)
     * 		 |distanceX = this.radius * Math.cos(this.orientation)
     * 		 |new.xCoordinate = this.location.item1 + numberSteps * distanceX
     * @post the y-coordinate of the new location of the worm schould be the location of
     * 		 the old y-coordinate plus the number of steps multiplied with the distance
     * 		 that the y-coordinate schould move (distance is equal to the radius multiplied
     * 		 with the sinus of the orientation)
     * 		 |distanceY = this.radius * Math.sin(this.orientation)
     * 		 |new.yCoordinate = this.location.item2 + numberSteps * distanceY
     * @post the current value of action actionPoints has changed. The current value of action actionPoints
     * 		 minus the cost of moving (abs(cos(theta)) + abs(4 sin(theta)))
     * 		 |value = (int) Math.ceil(Math.abs(Math.cos(this.orientation)) + Math.abs(4 * Math.sin(this.orientation)))
     * 		 |setActionPoints(this.actionPoints - value)
     * @throws IllegalArgumentException
     * 		   when the total of steps is lower then 0 or when the cost of action point is more
     * 		   then the current value of action point
     * 		   |NumberSteps < 0 || cost > this.actionPoints
     */
    public void move(int numberSteps) throws IllegalArgumentException {
    	if (numberSteps < 0)
    		throw new IllegalArgumentException(); // TODO add decent exception msg

        long cost = (long) ceil(abs(cos(getOrientation())) + abs(4 * sin(getOrientation())));

        if (cost > getActionPoints())
            throw new IllegalArgumentException(); // TODO add decent exception msg

    	double distanceX = this.radius * cos(getOrientation());
    	double distanceY = this.radius * sin(getOrientation());
    	setLocation(Tuple.create(getLocation().item1 + numberSteps * distanceX,
                getLocation().item2 + numberSteps * distanceY));
    	subtractActionPoints(cost);
    }

    //===================================================================================
    // endregion


    // region turn
    //===================================================================================

    /**
     * turns the worm with the given angle
     * 
     * @param angle
     * 		  the angle that must be added to the orientation
     * @pre  the angle to add must be between 0 and 2pi (including 0)
     * 		 |0 <= angle < 2pi
     * @post the new orientation is the old orientation plus the given angle
     * 		 |new.getOrientation() = this.getOrientation() + angle
     * @post the resulting angle (= the new orientation) must be between 0 and 2pi (including 0)
     * 		 |0 <= new.getOrientation() < 2pi
     * @post current actionPoints of action actionPoints should be reduced, for this there is another
     * 		 method with as parameter the given angle
     * 		 |substractActionPoints(angle)
     */
    public void turn(double angle) {
    	assert canTurn(angle);
    	setOrientation((getOrientation() + angle) % (2 * PI));
    	subtractActionPoints(abs(angle));
    }

    /**
     *return a boolean reflecting whether the worm can accept the given angle to turn
     *
     * @param angle
     *        the angle to be checked
     * @return true if and only if the given angle is between 0 (including) and 2pi, 
     * 		   the angle is a number and the current number of action points is high enough to turn 
     * 		   |result == ( (0 <= angle) && (angle < (2 * PI)) &&
               |		  (!Double.isNaN(angle)) &&
               |		  (getActionPoints() - (long) ceil(toDegrees(angle) / 6) >= 0) )
     */
    private boolean canTurn(double angle) {
        return 0 <= angle && angle < (2 * PI) &&
                !Double.isNaN(angle) &&
                getActionPoints() - (long) ceil(toDegrees(angle) / 6) >= 0;
    }

    //===================================================================================
    // endregion


    // region Jump
    //===================================================================================

    /** this constant contains the gravity */
    public static final double G = 5.0;

    /**
     * this constant contains the duration of the force
     */
    public static final double FORCE_TIME = 0.5;

    /**
     * let the worm jump
     * 
     * @post the worm jumps to his new place. The new place is the x-coordinate plus the jump distance
     * 		 with the jump velocity 
     * 		 |worms.util.Tuple.create(location.item1 + jumpDistance(this.jumpVelocity), location.item2)
     * @post the current action actionPoints schould be 0 after a jump
     * 		 |this.actionPoints = 0
     * @throws  IllegalStateException
     * 			if the current action actionPoints is equal to 0 or the orientation is more then
     * 			pi the worm can not jump
     * 			|this.actionPoints == 0 || this.orientation < Math.PI
     */
    public void jump() throws IllegalStateException {

        if (!canJump())
            throw new IllegalStateException();

        setLocation(Tuple.create(getLocation().item1 + jumpDistance(this.jumpVelocity()), getLocation().item2));
        setActionPoints(0);
    }

    /**
     * Return the time the worm will jump
     *
     * @return the time the worm will jump. The distance divided by the velocity multiplied
     * 		   with the cosinus of the orientation
     * 		   |jumpDistance(this.jumpVelocity) / (this.jumpVelocity * Math.cos(this.orientation))
     */
    public double jumpTime() {

        if (getOrientation() >= PI || getActionPoints() == 0)
            throw new IllegalStateException();

        return (jumpDistance(jumpVelocity()) / (jumpVelocity() * cos(getOrientation())));
    }

    /**
     *
     * @param deltaTime
     * 		  the total time the worm schould jump
     * @return Tuple<Double, Double> with the new location of the worm. The new x-coordinate is the old x-coordinate plus the jump velocity 
     * 		   multiplied with the cosinus of the orientation multiplied with delta time. The new y-coordinate is the old y-coordinate plus
     * 		   the jump velocity multiplied with the sinus of the orientation multiplied with delta time minus 0,5 times the gravity multiplied
     * 		   with the second power of delta time
     * 		   |Tuple.create(getLocation().item1 + this.jumpVelocity() * cos(getOrientation()) * deltaTime,
               |getLocation().item2 + this.jumpVelocity() * sin(getOrientation()) * deltaTime - 0.5 * G * pow(deltaTime, 2))
     * @throws IllegalArgumentException()
     * 		   if the deltaTime is not a number or bigger then jumpTime or smaller then 0
     * 		   |if (Double.isNaN(deltaTime) || deltaTime > this.jumpTime() || deltaTime < 0)
     */
    public Tuple<Double, Double> jumpStep(double deltaTime) {
        if (Double.isNaN(deltaTime) || deltaTime > this.jumpTime() || deltaTime < 0)
            throw new IllegalArgumentException();

        return Tuple.create(getLocation().item1 + this.jumpVelocity() * cos(getOrientation()) * deltaTime,
                getLocation().item2 + this.jumpVelocity() * sin(getOrientation()) * deltaTime - 0.5 * G * pow(deltaTime, 2));
    }

    /**
     * Return a boolean whether the worm can jump or not
     * 
     * @return True if and only if the action actionPoints is bigger then 0 and the orientation
     * 		   is lower then pi
     * 		   |result == this.actionPoints > 0 && this.orientation < Math.PI
     */
    private boolean canJump() {
        return getActionPoints() > 0 && getOrientation() < PI;
    }

    /**
     * Return the distance the worm will jump
     * 
     * @param v
     * 		  The velocity of the jump
     * @return The distance the worm will jump. The distance is equal to the velocity powered by 2 multiplied
     * 		   with the sinus of 2 times the orientation en this divided with the gravity
     * 		   |(Math.pow(v, 2) * Math.sin(2 * this.orientation)) / this.G
     */
    private double jumpDistance(double v) {
        return (pow(v, 2) * sin(2 * getOrientation())) / G;
    }

    /**
     * Return the velocity of the jump
     * 
     * @return the velocity of the jump. The force of the jump is equal to 5 multiplied with the
     * 		   current action actionPoints plus the mass multiplied with the gravity. Therefrom the velocity
     * 		   is equal to the force divided with the mass, multiplied with the time te force takes
     * 		   |force = 5 * this.actionPoints + this.mass * G
     * 	       |velocity = (force / this.mass) * FORCE_TIME
     */
    private double jumpVelocity() {

        double force = 5 * getActionPoints() + getMass() * G;
        return force / getMass() * FORCE_TIME;
    }


    //===================================================================================
    // endregion
}
