package shared.model;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import network.Messages.Enums.RaceStatusEnum;
import network.Messages.Enums.RaceTypeEnum;
import shared.dataInput.BoatDataSource;
import shared.dataInput.RaceDataSource;
import shared.dataInput.RegattaDataSource;
import visualiser.model.VisualiserRaceEvent;

import java.util.List;


/**
 * Represents a yacht race.
 * This is a base class inherited by {@link mock.model.MockRace} and {@link VisualiserRaceEvent}.
 * Has a course, state, wind, boundaries, etc.... Boats are added by inheriting classes (see {@link Boat}, {@link mock.model.MockBoat}, {@link visualiser.model.VisualiserBoat}.
 */
public abstract class Race {


    /**
     * The source of race related data.
     */
    protected RaceDataSource raceDataSource;

    /**
     * The source of boat related data.
     */
    protected BoatDataSource boatDataSource;

    /**
     * The source of regatta related data.
     */
    protected RegattaDataSource regattaDataSource;


    /**
     * A list of compound marks in the race.
     */
    protected List<CompoundMark> compoundMarks;

    /**
     * A list of legs in the race.
     */
    protected List<Leg> legs;

    /**
     * A list of coordinates describing the boundary of the course.
     */
    protected List<GPSCoordinate> boundary;



    /**
     * The clock which tracks the race's start time, current time, and elapsed duration.
     */
    protected RaceClock raceClock;


    /**
     * The race ID of the course.
     */
    protected int raceId;

    /**
     * The name of the regatta.
     */
    protected String regattaName;

    /**
     * The current status of the race.
     */
    protected RaceStatusEnum raceStatusEnum;

    /**
     * The type of race this is.
     */
    protected RaceTypeEnum raceType;


    /**
     * The race's wind.
     */
    protected Property<Wind> raceWind = new SimpleObjectProperty<>();


    /**
     * The number of frames per second.
     * We essentially track the number of frames generated per second, over a one second period. When {@link #lastFpsResetTime} reaches 1 second, {@link #currentFps} is reset.
     */
    private int currentFps = 0;

    /**
     * The number of frames per second we generated over the last 1 second period.
     */
    private IntegerProperty lastFps = new SimpleIntegerProperty(0);

    /**
     * The time, in milliseconds, since we last reset our {@link #currentFps} counter.
     */
    private long lastFpsResetTime;



    /**
     * Constructs a race object with a given BoatDataSource, RaceDataSource, and RegattaDataSource.
     * @param boatDataSource Data source for boat related data (yachts and marker boats).
     * @param raceDataSource Data source for race related data (participating boats, legs, etc...).
     * @param regattaDataSource Data source for race related data (course name, location, timezone, etc...).
     */
    public Race(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource) {

        //Keep a reference to data sources.
        this.raceDataSource = raceDataSource;
        this.boatDataSource = boatDataSource;
        this.regattaDataSource = regattaDataSource;



        //Marks.
        this.compoundMarks = raceDataSource.getCompoundMarks();

        //Boundaries.
        this.boundary = raceDataSource.getBoundary();


        //Legs.
        this.useLegsList(raceDataSource.getLegs());


        //Race ID.
        this.raceId = raceDataSource.getRaceId();

        //Regatta name.
        this.regattaName = regattaDataSource.getRegattaName();

        //Race clock.
        this.raceClock = new RaceClock(this.raceDataSource.getStartDateTime());

        //Race status.
        this.setRaceStatusEnum(RaceStatusEnum.NOT_ACTIVE);
        //Race type.
        this.raceType = raceDataSource.getRaceType();

        //Wind.
        this.setWind(Bearing.fromDegrees(0), 0);

    }



    /**
     * Initialise the boats in the race.
     * This sets their starting positions and current legs.
     */
    protected abstract void initialiseBoats();


    /**
     * Updates the race to use a new list of legs, and adds a dummy "Finish" leg at the end.
     * @param legs The new list of legs to use.
     */
    protected void useLegsList(List<Leg> legs) {
        //We add a "dummy" leg at the end of the race.
        this.legs = legs;
        this.legs.add(new Leg("Finish", this.legs.size()));
    }

    /**
     * Determines whether or not a specific leg is the last leg in the race.
     * @param leg The leg to check.
     * @return Returns true if it is the last, false otherwise.
     */
    protected boolean isLastLeg(Leg leg) {

        //Get the last leg.
        Leg lastLeg = this.legs.get(this.legs.size() - 1);

        //Check its ID.
        int lastLegID = lastLeg.getLegNumber();

        //Get the specified leg's ID.
        int legID = leg.getLegNumber();


        //Check if they are the same.
        return legID == lastLegID;
    }





    /**
     * Returns the current race status.
     * @return The current race status.
     */
    public RaceStatusEnum getRaceStatusEnum() {
        return raceStatusEnum;
    }

    /**
     * Sets the current race status.
     * @param raceStatusEnum The new status of the race.
     */
    public void setRaceStatusEnum(RaceStatusEnum raceStatusEnum) {
        this.raceStatusEnum = raceStatusEnum;
    }


    /**
     * Returns the type of race this is.
     * @return The type of race this is.
     */
    public RaceTypeEnum getRaceType() {
        return raceType;
    }

    /**
     * Returns the name of the regatta.
     * @return The name of the regatta.
     */
    public String getRegattaName() {
        return regattaName;
    }


    /**
     * Updates the race to have a specified wind bearing and speed.
     * @param windBearing New wind bearing.
     * @param windSpeedKnots New wind speed, in knots.
     */
    public void setWind(Bearing windBearing, double windSpeedKnots) {
        Wind wind = new Wind(windBearing, windSpeedKnots);
        setWind(wind);
    }

    /**
     * Updates the race to have a specified wind (bearing and speed).
     * @param wind New wind.
     */
    public void setWind(Wind wind) {
        this.raceWind.setValue(wind);
    }


    /**
     * Returns the wind bearing.
     * @return The wind bearing.
     */
    public Bearing getWindDirection() {
        return raceWind.getValue().getWindDirection();
    }

    /**
     * Returns the wind speed.
     * Measured in knots.
     * @return The wind speed.
     */
    public double getWindSpeed() {
        return raceWind.getValue().getWindSpeed();
    }

    /**
     * Returns the race's wind.
     * @return The race's wind.
     */
    public Property<Wind> windProperty() {
        return raceWind;
    }

    /**
     * Returns the RaceClock for this race.
     * This is used to track the start time, current time, and elapsed duration of the race.
     * @return The RaceClock for the race.
     */
    public RaceClock getRaceClock() {
        return raceClock;
    }


    /**
     * Returns the RaceDataSource used for the race.
     * @return The RaceDataSource used for the race.
     */
    public RaceDataSource getRaceDataSource() {
        return raceDataSource;
    }

    /**
     * Returns the number of legs in the race.
     * @return The number of legs in the race.
     */
    public int getLegCount() {
        //We minus one, as we have added an extra "dummy" leg.
        return legs.size() - 1;
    }


    /**
     * Returns the race boundary.
     * @return The race boundary.
     */
    public List<GPSCoordinate> getBoundary() {
        return boundary;
    }


    /**
     * Returns the marks of the race.
     * @return Marks of the race.
     */
    public List<CompoundMark> getCompoundMarks() {
        return compoundMarks;
    }

    /**
     * Returns the legs of the race.
     * @return Legs of the race.
     */
    public List<Leg> getLegs() {
        return legs;
    }

    /**
     * Returns the number of frames generated per second.
     * @return Frames per second.
     */
    public int getFps() {
        return lastFps.getValue();
    }

    /**
     * Returns the fps property.
     * @return The fps property.
     */
    public IntegerProperty fpsProperty() {
        return  lastFps;
    }



    /**
     * Increments the FPS counter, and adds timePeriod milliseconds to our FPS reset timer.
     * @param timePeriod Time, in milliseconds, to add to {@link #lastFpsResetTime}.
     */
    protected void incrementFps(long timePeriod) {
        //Increment.
        this.currentFps++;

        //Add period to timer.
        this.lastFpsResetTime += timePeriod;

        //If we have reached 1 second period, snapshot the framerate and reset.
        if (this.lastFpsResetTime > 1000) {
            this.lastFps.set(this.currentFps);

            this.currentFps = 0;
            this.lastFpsResetTime = 0;
        }
    }

    public int getRaceId() {
        return raceId;
    }
}
