package worms.model;

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

import static java.lang.Math.*;

import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 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.
 * 			|isValidMinRadius(getRadius())
 * @invar	The mass of the worm must bef 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
 *
 * Arthur Bols 1e bachelor Informatica
 * Leen Dereu 1e bachelor Informatica
 *
 * URL: https://github.com/KUL-ogp/ogp1718-project-bols-dereu
 */
public class Worm extends GameObject {

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

    /**
     *initialize the new worm with given location, orientation, name and radius
     *
     * @param world
     *          The world this worm belongs to
     * @param location
     * 		  the location for the new worm
     * @param name
     * 		  the name for the new worm
     * @param radius
     * 		  the radius for the new worm
     * @param team
     *          The team of 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
     */
	@Raw
    public Worm(World world, double[] location, double direction, double radius, String name, Team team) {

        this(world, location, direction, name, radius, 0.25, team);

    }

    /**
     *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
     * @post	actionpoints en hitpoints
     * @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)
     */
	@Raw
    public Worm(World world, double[] location, double orientation, String name, double radius, double minRadius, Team team)
            throws IllegalArgumentException {
        super(world, location, radius);
        setTeam(team);

        setOrientation(orientation);

        if (!isValidMinRadius(minRadius))
            throw new IllegalArgumentException("Invalid min radius");
        this.minRadius = minRadius;

        if (this.radius < this.minRadius) throw new IllegalArgumentException("Invalid radius");

        setActionPoints(getMaxActionPoints());

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

        long startHitPoints = ThreadLocalRandom.current().nextLong(1001, 2000);
        setHitPoints(startHitPoints);
    }

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


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

    /**
     * Return the orientation of the worm
     * 		the orientation of a worm expresses the direction in which
     * 		the worm is looking
     */
    @Basic @Raw
    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
     */
    @Raw
    private void setOrientation(double orientation) {
        assert isValidOrientation(orientation);
        this.orientation = orientation;
    }

    /**
     *check whether the given orientation is a valid orientation for the worm
     *
     * @param newOrientation
     * 		  the orientation to check
     * @return True if and only if the orientation is bigger then and smaller then 2pi
     * 		   |result == ( newOrientation >= 0 && newOrientation < 2 * PI )
     */
    public static boolean isValidOrientation(double newOrientation) {
        return newOrientation >= 0 && newOrientation < 2 * PI;
    }

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


    // region Shape mass/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)
     */
    @Raw @Override
    public void setRadius(double radius) throws IllegalArgumentException {
        if (!canHaveAsRadius(radius))
            throw new IllegalArgumentException("Invalid radius");

        this.radius = radius;
        final double rho = 1062;
        setMass(radius, rho);
        setMaxActionPoints(getMass());
    }

    /**
     * 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
     */
    @Basic @Raw
    public double getMinRadius() {
        return this.minRadius;
    }

    /**
     * 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 >= getMinRadius() && !Double.isNaN(radius))
     */
    @Raw
    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 @Raw
    public double getMass() {
        return this.mass;
    }

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



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

    /**
     * Return the current action points of the worm
     * 		the action points identifies the energy of the worm
     */
    @Basic @Raw
    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 @Raw
    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;
     */
    @Raw
    private 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)
     */
    @Raw
    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 subtracted
     * @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 @Immutable @Raw
    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) 
     */
    @Raw
    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
     *
     * @post the x-coordinate of the new location of the worm should be the location of
     * 		 the old x-coordinate plus the number of steps multiplied with the distance
     * 		 that the x-coordinate should move (distance is equal to the radius multiplied
     * 		 with the cosinus of the orientation)
     * 		 |distanceX = this.radius * cos(getOrientation())
     * 		 |new.xCoordinate = getLocation().getX() + numberSteps * distanceX
     * @post the y-coordinate of the new location of the worm should be the location of
     * 		 the old y-coordinate plus the number of steps multiplied with the distance
     * 		 that the y-coordinate should move (distance is equal to the radius multiplied
     * 		 with the sinus of the orientation)
     * 		 |distanceY = this.radius * sin(getOrientation())
     * 		 |new.yCoordinate = getLocation().getY() + 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)))
     * 		 |cost = (long) ceil(abs(cos(getOrientation())) + abs(4 * sin(getOrientation()))) * numberSteps
     * 		 |subtractActionPoints(cost)
     * @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() throws IllegalArgumentException {

        double newDirection = getFurthestLocationDirection();
        Coordinate newLocation = getFurthestLocationInDirection(newDirection, this.getRadius());
        double distance = getDistance(this.getLocation(), newLocation);

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

        if (cost > getActionPoints())
            throw new IllegalArgumentException();

    	setLocation(newLocation);
    	subtractActionPoints(cost);
    }

    /**
     *
     * @return
     */
    public Coordinate getFurthestLocationInDirection(double direction, double maxDistance) {
    	Coordinate currentLocation = getLocation();
    	double radius = getRadius();
    	World world = getWorld();
    	double step = Math.sqrt(Math.pow(world.getLengthX() * cos(direction), 2) + Math.pow(world.getLengthY() * sin(direction), 2));
    	if (step > radius) step = radius;
    	Coordinate nextLoc = currentLocation;

    	while(getDistance(currentLocation, nextLoc) < maxDistance) {

    	    nextLoc = Coordinate.create(round((nextLoc.getX() + 0.1 * cos(direction)) * 100.0) / 100.0,
                    round((nextLoc.getY() + step * sin(direction)) * 100.0) / 100.0);
    	    if (!world.isPassable(nextLoc.toArray(), radius)) {
                double max = radius * 0.1 + 0.001;
                while (!world.isAdjacent(nextLoc.toArray(), radius)) {
                    max -= 0.01;
                    nextLoc = Coordinate.create(round((nextLoc.getX() - 0.01 * cos(direction)) * 100.0) / 100.0,
                            round((nextLoc.getY() - 0.01 * sin(direction)) * 100.0) / 100.0);
                    if (max < 0) {
                        return currentLocation;
                    }
                }
                break;
            }
        }
//        while(getDistance(currentLocation, nextLoc) < maxDistance) {
//    	    nextLoc = Coordinate.create(round((nextLoc.getX() + 0.1 * cos(direction)) * 100.0) / 100.0,
//                    round((nextLoc.getY() + 0.1 * sin(direction)) * 100.0) / 100.0);
//    	    if (!world.isPassable(nextLoc.toArray(), radius)) {
//    	        break;
//            }
//        }
//        double max = radius * 0.1 + 0.001;
//        while(!world.isAdjacent(nextLoc.toArray(), radius)) {
//            max -= 0.01;
//            if (max < 0) {
//                nextLoc = currentLocation;
//                break;
//            }
//            nextLoc = Coordinate.create(round((nextLoc.getX() - 0.01 * cos(direction)) * 100.0) / 100.0,
//                    round((nextLoc.getY() - 0.01 * sin(direction)) * 100.0) / 100.0);
//        }
        //if (getDistance(currentLocation, prevLoc) > getDistance(currentLocation, nextLoc)) return prevLoc;

        return nextLoc;
    }

    public double getFurthestLocationDirection() {
        Coordinate location = getLocation();
        double direction = getOrientation();
        double minDirection = direction - 0.7875;
        double maxDirection = direction + 0.7875;
        double maxLocDirection = maxDirection;
        Coordinate maxLoc = location;
        for (; minDirection <= maxDirection; minDirection += 0.0175) {
            Coordinate tempLoc = getFurthestLocationInDirection(minDirection, this.getRadius());
            if (!getWorld().isAdjacent(tempLoc, getRadius())) tempLoc = location;
            if (getDistance(location, tempLoc) / Math.abs(direction - minDirection) > getDistance(location, maxLoc) / Math.abs(direction - maxLocDirection)) {
                maxLoc = tempLoc;
                maxLocDirection = minDirection;
            }
        }

        if (maxLoc.getX() == location.getX() && maxLoc.getY() == location.getY()) return direction;
        return maxLocDirection;
    }

    public double getDistance(Coordinate start, Coordinate end) {
        return Math.sqrt(Math.pow(Math.abs(start.getX() - end.getX()), 2) +
                Math.pow(Math.abs(start.getY() - end.getY()), 2));
    }

    public void collision(Worm basicWorm, Worm... worm) {
    	Worm largestWorm;
    	Worm smallestWorm;
    	for (Worm w1: worm) {
    			if (w1.getMass() > basicWorm.getMass()) {
    				largestWorm = w1;
    				smallestWorm = basicWorm;
    			}
    			else {
    				largestWorm = basicWorm;
    				smallestWorm = w1;
    			}
    			long total = 1 + (new Random().nextLong() * (10 - 1));
    	    	long loseSmallest = (long) (total / (largestWorm.getOrientation() / (smallestWorm.getOrientation() + largestWorm.getOrientation())));
    	    	long loseLargest = total - loseSmallest;
    	    	smallestWorm.incrementHitPoints(loseSmallest);
    	    	largestWorm.incrementHitPoints(loseLargest);
    	}
    }

    //===================================================================================
    // 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);
    	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) {
        double currentAngle = getOrientation();
        return 0 <= angle + currentAngle && angle + currentAngle < (2 * PI) &&
                !Double.isNaN(angle) &&
                getActionPoints() - (long) Math.abs(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
     * 		 |setLocation(Coordinate.create(getLocation().getX() + jumpDistance(this.jumpVelocity()), getLocation().getY()))
     * @post the current action actionPoints should be 0 after a jump
     * 		 |setActionPoints(0)
     * @throws  IllegalStateException
     * 			if the current action actionPoints is equal to 0 or the orientation is more then
     * 			pi the worm can not jump
     * 			|!canJump()
     */
    public void jump() throws IllegalStateException {

        if (!canJump())
            throw new IllegalStateException();
        double distance = jumpDistance(this.jumpVelocity());
        Coordinate newLocation = Coordinate.create( getLocation().getX() + distance, getLocation().getY());

        if (getWorld().isAdjacent(newLocation, getRadius()) && canHaveAsJumpDistance(distance)) {
            if (!isValidLocation(newLocation)) terminate();
            setLocation(newLocation);
            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))
     * @throws IllegalStateException
     * 		   Orientation is bigger then pi or action points is equal to 0
     * 		   |getOrientation() >= PI || getActionPoints() == 0
     */
    public double jumpTime() {

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

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

    /**
     *
     * @param deltaTime
     * 		  the total time the worm should jump
     * @return Coordinate 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
     * 		   |Coordinate.create(getLocation().getX() + this.jumpVelocity() * cos(getOrientation()) * deltaTime,
               |getLocation().getY() + 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 Coordinate jumpStep(double deltaTime) {
        if (Double.isNaN(deltaTime) || deltaTime > this.jumpTime() || deltaTime < 0)
            throw new IllegalArgumentException();

        return Coordinate.create(getLocation().getX() + this.jumpVelocity() * cos(getOrientation()) * deltaTime,
                getLocation().getY() + 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 == getActionPoints() > 0 && getOrientation() < 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
     * 		   |(pow(v, 2) * sin(2 * getOrientation())) / G
     */
    private double jumpDistance(double v) {
        return (pow(v, 2) * sin(2 * getOrientation())) / G;
    }

    /**
     *
     * @param distance
     * @return
     */
    public boolean canHaveAsJumpDistance(double distance) {

        return Math.abs(distance) >= getRadius();
    }

    /**
     * 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 * getActionPoints() + getMass() * G
     * 	       |velocity = (force / getMass()) * FORCE_TIME
     */
    private double jumpVelocity() {

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

    /**
     *
     * @param newLocation
     * @param oldLocation
     * @param worm1
     * @param worm2
     */
    public void fight(Coordinate newLocation, Coordinate oldLocation, Worm worm1, Worm... worm2) {
    	if (! worm1.isTerminated() && newLocation != oldLocation) {
    		for (Worm wormB: worm2) {
        		Worm attackedWorm;
            	Worm attacker;
            	Random rand = new Random();
            	int coin = rand.nextInt(2);
            	if (coin == 1) {
            		attackedWorm = worm1;
            		attacker = wormB;
            	}
            	else {
            		attackedWorm = wormB;
            		attacker = worm1;
            	}
            	double N = 10 * ceil((attacker.getRadius())/(attackedWorm.getRadius()));
            	long loseAttackedWorm = (long) rand.nextInt((((int) N) - 1) + 1) + 1;
            	attackedWorm.decreaseHitPoints(loseAttackedWorm);
        	}
    	}
    }


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

    // region hitPoints
    //===================================================================================

    /**
     * Return the current hit points of the worm
     * 		the hit points identifies the strength of the worm
     */
    @Basic @Raw
    public long getHitPoints() {
        return this.hitPoints;
    }

    /**
     * set the current hit points of the worm to the given points
     *
     * @param hitPoints
     * 		  the new points for the worm
     * @post  if the given points are equal or lower then zero, the worm should be dead.
     * 		  Otherwise the current hit points are set to the given points.
     * 		  |if (hitpoints <= 0)
     * 		  |		terminate();
     *   	  |else
     *   	  |		new.getHitPoints() == hitpoints
     */
    @Raw
    private void setHitPoints(long hitPoints) {
        if (hitPoints <= 0)
            terminate();
        this.hitPoints = hitPoints;
    }

    /**
     * this variable contains the current hitpoints of the worm
     */
    private long hitPoints;

    /**
     * increment the hit points with the given value
     *
     * @param value
     * 		  the value that should be substracted
     * @post  the current hit points should be substracted with the given value.
     * 		  |new.getHitPoints() == old.getHitPoints() - value
     */
    public void decreaseHitPoints(long value) {
    	setHitPoints(getHitPoints() - value);
    }

    public void incrementHitPoints(long value) {
    	setHitPoints(getHitPoints() + value);
    }

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

    // region falling
    //===================================================================================

	public void fall() {
        double height = getWorld().getHeight() - getRadius();
        Coordinate oldLocation = getLocation();
        if (canFall()) {
            for (double y = oldLocation.getY(); y < height; y -= 0.01) {
                Coordinate newLoc = Coordinate.create(oldLocation.getX(), Math.floor(y * 100.0) / 100.0);

                if (y - radius < 0) {
                    terminate();
                    break;
                }

                if (getWorld().isPassable(newLoc, getRadius())) {
                    setLocation(newLoc);
                } else {
                    break;
                }
            }
        }

        long cost = 3 * (long) Math.floor(oldLocation.getY() - getLocation().getY());
        decreaseHitPoints(cost);
	}

	public boolean canFall() {
		double[] center = {getLocation().getX(), getLocation().getY()};
		return ! getWorld().isAdjacent(center, getRadius());
	}
	
	public void collisionFall(Worm fallingWorm, Worm... worm) {
    	for (Worm stationaryWorm: worm) {
    		long value = stationaryWorm.getHitPoints()/2;
    		fallingWorm.incrementHitPoints(value);
    		stationaryWorm.decreaseHitPoints(value);
    	}
    }
    
	//===================================================================================
    // endregion


    // region team
    //===================================================================================
    public Team getTeam() {
        return this.team;
    }

    public void setTeam(Team team) {
        this.team = team;
    }

    private Team team;

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


    public void eat() {
        throw new IllegalStateException();
    }


}
