public class Worm {


    // region properties
    //===================================================================================


    // Location
    private Tuple<Double, Double> location;
    private Tuple<Double, Double> oldLocation;

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

    /**
     * this variable contains the radius of the worm
     */
    private double radius;
    
    /**
     * this variable contains the minimum value of the radius
     */
    private final double minRadius;
    
    /**
     * this variable contains the mass of the worm
     */
    private double mass;

    /**
     * this variable contains the current action points of the worm
     */
    private int points;
    
    /**
     * this variable contains the maximum points a worm can have
     */
    private int maxPoints;

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

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


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

    /**
     *
     * @param location ...
     * @param orientation ...
     * @param name ...
     * @param radius ...
     */
    public Worm(Tuple<Double, Double> location, double orientation, String name, double radius) {

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

    /**
     *
     * @param location ...
     * @param orientation ...
     * @param name ...
     * @param radius ...
     * @param minRadius ...
     * @throws IllegalArgumentException ...
     */
    public Worm(Tuple<Double, Double> location, double orientation, String name, double radius, double minRadius)
            throws IllegalArgumentException {

        setLocation(location);

        this.orientation = orientation;

        if (minRadius < 0)
            throw new IllegalArgumentException("minRadius must be larger than 0"); // TODO add decent exception msg

        setRadius(radius);
        this.minRadius = minRadius;
        this.points = this.maxPoints;

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

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



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


    public Tuple<Double, Double> getLocation() {
        return this.location;
    }

    /**
     * sets the location of the worm to the given location
     * 
     * @param location
     * 		  
     * @throws IllegalArgumentException
     */
    private void setLocation(Tuple<Double, Double> location) throws IllegalArgumentException {

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

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


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

    /**
     * @param   orientation
     *          | ...
     *
     */
    public void setOrientation(double orientation) {

        this.orientation = orientation;
    }
    public double getOrientation() {
        return orientation;
    }

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




    // region Shape mass/radius
    //===================================================================================

    public double getRadius() {
        return this.radius;
    }

    public void setRadius(double radius) throws IllegalArgumentException {
        if (radius < this.minRadius)
            throw new IllegalArgumentException("Radius is smaller than " + this.minRadius);

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

    public double getMinRadius() {
        return this.minRadius;
    }

    public double getMass() {
        return this.mass;
    }

    private void setMass(double radius) {

        final int rho = 1062;
        double mass = rho * (4 / 3 * Math.PI * Math.pow(radius, 3));

        this.mass = mass;
        setMaxPoints(mass);
    }

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



    // region Points
    //===================================================================================

    public int getPoints() {
        return this.points;
    }

    public void setPoints(int points) {
        if (points > this.maxPoints)
            points = this.maxPoints;
        else if (points < 0)
            points = 0;

        this.points = points;
    }
    private void setMaxPoints(double maxPoints) {
        this.maxPoints = (int) Math.ceil(maxPoints);
        setPoints(this.points);
    }
    private void subtractPoints (int value) {

        setPoints(this.points - value);
    }
    private void subtractPoints (double angle) {

        setPoints(this.points - (int) Math.ceil(Math.toDegrees(angle) / 6));
    }

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



    // region name
    //===================================================================================

    public String getName() {
        return this.name;
    }

    public void setName(String name) {

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

        this.name = name;
    }

    /**
     * @param name ...
     * @return ...
     */
    private 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;
    }

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


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

    /**
     * @param numberSteps ...
     * 		  the number of steps the worm should take
     *
     * @post the new x-coordinate schould be the old one plus the number of steps multiplied
     * 		 with the distance of a step in the x-orientation
     * 		|new.CoordX = CoordX + NumberSteps * distanceX
     * @post the new y-coordinate schould be the old one plus the number of steps multiplied
     * 		 with the distance of a step in the y-orientation
     * 		|new.CoordY = CoordY + NumberSteps * distanceY
     *
     * @throws IllegalArgumentException
     * 		when the total of steps is lower then 0
     * 		|NumberSteps < 0
     */
    public void move(int numberSteps) {
    	if (numberSteps < 0)
    		throw new IllegalArgumentException(); // TODO add decent exception msg

        int cost = (int) Math.ceil(Math.abs(Math.cos(this.orientation)) + Math.abs(4 * Math.sin(this.orientation)));
        if (cost > this.points)
            throw new IllegalArgumentException(); // TODO add decent exception msg

    	double distanceX = this.radius * Math.cos(this.orientation);
    	double distanceY = this.radius * Math.sin(this.orientation);
    	this.location = Tuple.create(this.location.item1 + numberSteps * distanceX,
                this.location.item2 + numberSteps * distanceY);
    	subtractPoints(cost);

    }

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


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

    /**
     * @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.orientation = orientation + angle
     * @post the resulting angle (= the new orientation) must be between 0 and 2pi (including 0)
     * 		|0 <= new.orientation < 2pi
     */
    public void turn(double angle) {
    	assert 0 <= angle && angle < (2 * Math.PI);

    	setOrientation((this.orientation + angle) % (2 * Math.PI));
    	subtractPoints(angle);
    }

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


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

    private final double G = 5.0;
    private final double FORCE_TIME = 0.5;

    public void jump() {

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

        this.oldLocation = this.location;
        this.location = Tuple.create(location.item1 + jumpDistance(jumpVelocity()), location.item2);

        this.points = 0;
    }

    private boolean canJump() {
        return this.points > 0 && this.orientation < Math.PI;
    }

    public double jumpTime() {

        double v = jumpVelocity();
        return jumpDistance(v) / (v * Math.cos(this.orientation));
    }
    public double jumpStep() {

        return 0.0;
    }
    private double jumpDistance(double v) {
        return (Math.pow(v, 2) * Math.sin(2 * this.orientation)) / this.G;
    }

    private double jumpVelocity() {

        double force = 5 * this.points + this.mass * G;
        return (force / this.mass) * FORCE_TIME;
    }


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


}
