package mock.model;


import shared.model.Bearing;
import shared.model.Wind;

import java.util.Random;

/**
 * This class generates Wind objects for use in a MockRace.
 * Bounds on bearing and speed can be specified.
 * Wind can be completely random, or random incremental change.
 */
public class RandomWindGenerator implements WindGenerator {

    /**
     * The bearing the wind direction starts at.
     */
    private Bearing windBaselineBearing;

    /**
     * The lower bearing angle that the wind may have.
     */
    private Bearing windBearingLowerBound;

    /**
     * The upper bearing angle that the wind may have.
     */
    private Bearing windBearingUpperBound;



    /**
     * The speed the wind starts at, in knots.
     */
    private double windBaselineSpeed;

    /**
     * The lower speed that the wind may have, in knots.
     */
    private double windSpeedLowerBound;

    /**
     * The upper speed that the wind may have, in knots.
     */
    private double windSpeedUpperBound;


    /**
     * Creates a wind generator, with a baseline, lower bound, and upper bound, for the wind speed and direction.
     * @param windBaselineBearing Baseline wind direction.
     * @param windBearingLowerBound Lower bound for wind direction.
     * @param windBearingUpperBound Upper bound for wind direction.
     * @param windBaselineSpeed Baseline wind speed, in knots.
     * @param windSpeedLowerBound Lower bound for wind speed, in knots.
     * @param windSpeedUpperBound Upper bound for wind speed, in knots.
     */
    public RandomWindGenerator(Bearing windBaselineBearing, Bearing windBearingLowerBound, Bearing windBearingUpperBound, double windBaselineSpeed, double windSpeedLowerBound, double windSpeedUpperBound) {

        this.windBaselineBearing = windBaselineBearing;
        this.windBearingLowerBound = windBearingLowerBound;
        this.windBearingUpperBound = windBearingUpperBound;
        this.windBaselineSpeed = windBaselineSpeed;
        this.windSpeedLowerBound = windSpeedLowerBound;
        this.windSpeedUpperBound = windSpeedUpperBound;

    }


    @Override
    public Wind generateBaselineWind() {
        return new Wind(windBaselineBearing, windBaselineSpeed);
    }

    /**
     * Generates a random Wind object, that is within the provided bounds.
     * @return Generated wind object.
     */
    public Wind generateRandomWind() {

        double windSpeed = generateRandomWindSpeed();
        Bearing windBearing = generateRandomWindBearing();

        return new Wind(windBearing, windSpeed);

    }

    /**
     * Generates a random wind speed within the specified bounds. In knots.
     * @return Wind speed, in knots.
     */
    private double generateRandomWindSpeed() {

        double randomSpeedKnots = generateRandomValueInBounds(windSpeedLowerBound, windSpeedUpperBound);

        return randomSpeedKnots;
    }


    /**
     * Generates a random wind bearing within the specified bounds.
     * @return Wind bearing.
     */
    private Bearing generateRandomWindBearing() {

        double randomBearingDegrees = generateRandomValueInBounds(windBearingLowerBound.degrees(), windBearingUpperBound.degrees());

        return Bearing.fromDegrees(randomBearingDegrees);
    }


    /**
     * Generates a random value within a specified interval.
     * @param lowerBound The lower bound of the interval.
     * @param upperBound The upper bound of the interval.
     * @return A random value within the interval.
     */
    private static double generateRandomValueInBounds(double lowerBound, double upperBound) {

        float proportion = new Random().nextFloat();

        double delta = upperBound - lowerBound;

        double amount = delta * proportion;

        double finalAmount = amount + lowerBound;

        return finalAmount;

    }


    /**
     * Generates a new value within an interval, given a start value, chance to change, and change amount.
     * @param lowerBound Lower bound of interval.
     * @param upperBound Upper bound of interval.
     * @param currentValue The current value to change.
     * @param changeAmount The amount to change by.
     * @param chanceToChange The change to actually change the value.
     * @return The new value.
     */
    private static double generateNextValueInBounds(double lowerBound, double upperBound, double currentValue, double changeAmount, double chanceToChange) {

        float chance = new Random().nextFloat();


        if (chance <= chanceToChange) {
            currentValue += changeAmount;

        } else if (chance <= (2 * chanceToChange)) {
            currentValue -= changeAmount;

        }

        currentValue = clamp(lowerBound, upperBound, currentValue);

        return currentValue;

    }


    @Override
    public Wind generateNextWind(Wind currentWind) {

        double windSpeed = generateNextWindSpeed(currentWind.getWindSpeed());
        Bearing windBearing = generateNextWindBearing(currentWind.getWindDirection());

        return new Wind(windBearing, windSpeed);

    }


    /**
     * Generates the next wind speed to use.
     * @param windSpeed Current wind speed, in knots.
     * @return Next wind speed, in knots.
     */
    private double generateNextWindSpeed(double windSpeed) {

        double chanceToChange = 0.2;
        double changeAmount = 0.1;

        double nextWindSpeed = generateNextValueInBounds(
                windSpeedLowerBound,
                windSpeedUpperBound,
                windSpeed,
                changeAmount,
                chanceToChange);

        return nextWindSpeed;
    }


    /**
     * Generates the next wind speed to use.
     * @param windBearing Current wind bearing.
     * @return Next wind speed.
     */
    private Bearing generateNextWindBearing(Bearing windBearing) {

        double chanceToChange = 0.2;
        double changeAmount = 0.5;

        double nextWindBearingDegrees = generateNextValueInBounds(
                windBearingLowerBound.degrees(),
                windBearingUpperBound.degrees(),
                windBearing.degrees(),
                changeAmount,
                chanceToChange);

        return Bearing.fromDegrees(nextWindBearingDegrees);
    }





    /**
     * Clamps a value to be within an interval.
     * @param lower Lower bound of the interval.
     * @param upper Upper bound of the interval.
     * @param value Value to clamp.
     * @return The clamped value.
     */
    private static double clamp(double lower, double upper, double value) {

        if (value > upper) {
            value = upper;

        } else if (value < lower) {
            value = lower;

        }

        return value;
    }





}
