package seng302.Model;


import javafx.animation.AnimationTimer;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.geotools.referencing.GeodeticCalculator;


import seng302.Constants;
import seng302.DataInput.RaceDataSource;
import seng302.MockOutput;
import seng302.Networking.Messages.BoatStatus;
import seng302.Networking.Messages.Enums.BoatStatusEnum;
import seng302.Networking.Messages.RaceStatus;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;


/**
 * Parent class for races
 * Created by fwy13 on 3/03/17.
 */
public class Race implements Runnable {
    //protected Boat[] startingBoats;
    protected ObservableList<Boat> startingBoats;
    protected List<Leg> legs;
    protected int boatsFinished = 0;
    protected long totalTimeElapsed;
    protected int scaleFactor=25;
    protected int PRERACE_TIME = 180000; //time in milliseconds to pause during pre-race. At the moment, 3 minutes
    private long startTime;
    protected boolean countdownFinish = false;
    protected boolean runRace = true;
    private int lastFPS = 20;
    private int raceId;
    private int dnfChance = 0; //percentage chance a boat fails at each checkpoint
    private MockOutput mockOutput;
    private static int boatOffset = 0;
    private int finished = 0;


    /**
     * Initailiser for Race
     *
     * @param boats       Takes in an array of boats that are participating in the race.
     * @param legs        Number of marks in order that the boats pass in order to complete the race.
     * @param scaleFactor for race
     */
    public Race(List<Boat> boats, List<Leg> legs, int raceID, int scaleFactor, MockOutput mockOutput) {

        this.startingBoats = FXCollections.observableArrayList(boats);
        this.legs = legs;
        this.legs.add(new Leg("Finish", this.legs.size()));
        this.raceId = raceID;
        this.scaleFactor = scaleFactor;
        this.mockOutput = mockOutput;

        //TODO refactor
        this.startTime = System.currentTimeMillis() + (this.PRERACE_TIME / this.scaleFactor);

        if (startingBoats != null && startingBoats.size() > 0) {
            initialiseBoats();
        }

    }

    public Race(RaceDataSource raceData, int scaleFactor, MockOutput mockOutput) {
        this(raceData.getBoats(), raceData.getLegs(), raceData.getRaceId(), scaleFactor, mockOutput);
    }

    /**
     * Calculates the boats next GPS position based on its distance travelled and heading
     *
     * @param oldCoordinates    GPS coordinates of the boat's starting position
     * @param distanceTravelled distance in nautical miles
     * @param azimuth           boat's current direction. Value between -180 and 180
     * @return The boat's new coordinate
     */
    public static GPSCoordinate calculatePosition(GPSCoordinate oldCoordinates, double distanceTravelled, double azimuth) {

        //Find new coordinate using current heading and distance

        GeodeticCalculator geodeticCalculator = new GeodeticCalculator();
        //Load start point into calculator
        Point2D startPoint = new Point2D.Double(oldCoordinates.getLongitude(), oldCoordinates.getLatitude());
        geodeticCalculator.setStartingGeographicPoint(startPoint);
        //load direction and distance tranvelled into calculator
        geodeticCalculator.setDirection(azimuth, distanceTravelled * Constants.NMToMetersConversion);
        //get new point
        Point2D endPoint = geodeticCalculator.getDestinationGeographicPoint();

        return new GPSCoordinate(endPoint.getY(), endPoint.getX());
    }

    /**
     * Runnable for the thread.
     */
    public void run() {
        initialiseBoats();
        countdownTimer();
    }

    /**
     * Starts the heartbeat timer, which sends a heartbeat message every so often (i.e., 5 seconds).
     */
    /**
     * Countdown timer until race starts. Use PRERACE_TIME to set countdown duration.
     */
    protected void countdownTimer() {
        AnimationTimer timer = new AnimationTimer() {
            long currentTime = System.currentTimeMillis();
            //long startTime = currentTime + (PRERACE_TIME / scaleFactor);
            //long minutes;
            //long hours;
            long timeLeft;

            @Override
            public void handle(long arg0) {
                timeLeft = startTime - currentTime;
                ArrayList<BoatStatus> boatStatuses = new ArrayList<>();
                //For each boat, we update it's position, and generate a BoatLocationMessage.
                for (int i = 0; i < startingBoats.size(); i++) {
                    Boat boat = startingBoats.get((i + boatOffset) % startingBoats.size());
                    if (boat != null) {
                        mockOutput.parseBoatLocation(boat.getSourceID(), boat.getCurrentPosition().getLatitude(), boat.getCurrentPosition().getLongitude(), boat.getHeading(), 0);
                        boatStatuses.add(new BoatStatus(boat.getSourceID(),
                                boat.getCurrentLeg().getLegNumber() >= 0 ? BoatStatusEnum.RACING : BoatStatusEnum.DNF, boat.getCurrentLeg().getLegNumber()));
                    }
                }
                boatOffset = (boatOffset + 1) % (startingBoats.size());

                if (timeLeft <= 60000/scaleFactor && timeLeft > 0) {
                    RaceStatus raceStatus = new RaceStatus(System.currentTimeMillis(), raceId, 2, startTime, 0, 2300, 1, boatStatuses);
                    mockOutput.parseRaceStatus(raceStatus);
                }
                else if (timeLeft <= 0) {
                    countdownFinish =  true;
                    if (runRace) {
                        simulateRace();
                    }
                    stop();
                }
                else {
                    RaceStatus raceStatus = new RaceStatus(System.currentTimeMillis(), raceId, 1, startTime, 0, 2300,1, boatStatuses);
                    mockOutput.parseRaceStatus(raceStatus);
                }
                currentTime = System.currentTimeMillis();
            }
        };
        timer.start();
        //countdownFinish = true;
    }

    /**
     * Starts the Race Simulation, playing the race start to finish with the timescale.
     * This prints the boats participating, the order that the events occur in time order, and the respective information of the events.
     */
    private void simulateRace() {

        System.setProperty("javafx.animation.fullspeed", "true");

        for (Boat boat : startingBoats) {
            boat.setStarted(true);
        }

        new AnimationTimer() {
            //Start time of loop.
            long timeRaceStarted = System.currentTimeMillis();

            @Override
            public void handle(long arg0) {
                if (boatsFinished < startingBoats.size()) {
                    //Get the current time.
                    long currentTime = System.currentTimeMillis();
                    //Update the total elapsed time.
                    totalTimeElapsed = currentTime - timeRaceStarted;
                    ArrayList<BoatStatus> boatStatuses = new ArrayList<BoatStatus>();
                    finished = 0;
                    //For each boat, we update it's position, and generate a BoatLocationMessage.
                    for (int i = 0; i < startingBoats.size(); i++) {
                        Boat boat = startingBoats.get((i + boatOffset) % startingBoats.size());
                        if (boat != null) {
                            //Update position.
                            if (boat.getTimeFinished() < 0) {
                                updatePosition(boat, Math.round(1000 / lastFPS) > 20 ? 15 : Math.round(1000 / lastFPS));
                                checkPosition(boat, totalTimeElapsed);
                            }
                            if (boat.getTimeFinished() > 0) {
                                mockOutput.parseBoatLocation(boat.getSourceID(), boat.getCurrentPosition().getLatitude(), boat.getCurrentPosition().getLongitude(), boat.getHeading(), boat.getVelocity());
                                boatStatuses.add(new BoatStatus(boat.getSourceID(), BoatStatusEnum.FINISHED, boat.getCurrentLeg().getLegNumber()));
                                finished++;
                            } else {
                                mockOutput.parseBoatLocation(boat.getSourceID(), boat.getCurrentPosition().getLatitude(), boat.getCurrentPosition().getLongitude(), boat.getHeading(), boat.getVelocity());
                                boatStatuses.add(new BoatStatus(boat.getSourceID(),
                                        boat.getCurrentLeg().getLegNumber() >= 0 ? BoatStatusEnum.RACING : BoatStatusEnum.DNF, boat.getCurrentLeg().getLegNumber()));
                            }
                            if (startingBoats.size()==finished){
                                RaceStatus raceStatus = new RaceStatus(System.currentTimeMillis(), raceId, 4, startTime, 0, 2300, 2, boatStatuses);//TODO FIX the second currentTime is a placeholder! Also, replace magic values.
                                mockOutput.parseRaceStatus(raceStatus);
                            }
                        } else {
                            stop();
                        }
                    }
                    boatOffset = (boatOffset + 1) % (startingBoats.size());
                    RaceStatus raceStatus = new RaceStatus(System.currentTimeMillis(), raceId, 3, startTime, 0, 2300, 2, boatStatuses);//TODO FIX the second currentTime is a placeholder! Also, replace magic values.
                    mockOutput.parseRaceStatus(raceStatus);
                }
            }
        }.start();
    }
    public void initialiseBoats() {
        Leg officialStart = legs.get(0);
        String name = officialStart.getName();
        Marker endMarker = officialStart.getEndMarker();

        ArrayList<Marker> startMarkers = getSpreadStartingPositions();
        for (int i = 0; i < startingBoats.size(); i++) {
            Boat boat = startingBoats.get(i);
            if (boat != null) {
                boat.setScaledVelocity(boat.getVelocity() * scaleFactor);
                Leg startLeg = new Leg(name, 0);
                boat.setCurrentPosition(startMarkers.get(i).getAverageGPSCoordinate());
                startLeg.setStartMarker(startMarkers.get(i));
                startLeg.setEndMarker(endMarker);
                startLeg.calculateDistance();
                boat.setCurrentLeg(startLeg);
                boat.setHeading(boat.calculateHeading());
            }
        }
    }

    /**
     * Creates a list of starting positions for the different boats, so they do not appear cramped at the start line
     *
     * @return list of starting positions
     */
    public ArrayList<Marker> getSpreadStartingPositions() {

        int nBoats = startingBoats.size();
        Marker marker = legs.get(0).getStartMarker();

        GeodeticCalculator initialCalc = new GeodeticCalculator();
        initialCalc.setStartingGeographicPoint(marker.getMark1().getLongitude(), marker.getMark1().getLatitude());
        initialCalc.setDestinationGeographicPoint(marker.getMark2().getLongitude(), marker.getMark2().getLatitude());

        double azimuth = initialCalc.getAzimuth();
        double distanceBetweenMarkers = initialCalc.getOrthodromicDistance();
        double distanceBetweenBoats = distanceBetweenMarkers / (nBoats + 1);

        GeodeticCalculator positionCalc = new GeodeticCalculator();
        positionCalc.setStartingGeographicPoint(marker.getMark1().getLongitude(), marker.getMark1().getLatitude());
        ArrayList<Marker> positions = new ArrayList<>();

        for (int i = 0; i < nBoats; i++) {
            positionCalc.setDirection(azimuth, distanceBetweenBoats);
            Point2D position = positionCalc.getDestinationGeographicPoint();
            positions.add(new Marker(new GPSCoordinate(position.getY(), position.getX())));

            positionCalc = new GeodeticCalculator();
            positionCalc.setStartingGeographicPoint(position);
        }
        return positions;
    }

    /**
     * Sets the chance each boat has of failing at a gate or marker
     *
     * @param chance percentage chance a boat has of failing per checkpoint.
     */
    protected void setDnfChance(int chance) {
        if (chance >= 0 && chance <= 100) {
            dnfChance = chance;
        }
    }

    protected boolean doNotFinish() {
        Random rand = new Random();
        return rand.nextInt(100) < dnfChance;
    }

    /**
     * Calculates the distance a boat has travelled and updates its current position according to this value.
     *
     * @param boat                to be updated
     * @param millisecondsElapsed since last update
     */
    protected void updatePosition(Boat boat, int millisecondsElapsed) {

        //distanceTravelled = velocity (nm p hr) * time taken to update loop
        double distanceTravelled = (boat.getScaledVelocity() * millisecondsElapsed) / 3600000;

        double totalDistanceTravelled = distanceTravelled + boat.getDistanceTravelledInLeg();

        boolean finish = boat.getCurrentLeg().getName().equals("Finish");
        if (!finish) {
            boat.setHeading(boat.calculateHeading());
            //update boat's distance travelled
            boat.setDistanceTravelledInLeg(totalDistanceTravelled);
            //Calculate boat's new position by adding the distance travelled onto the start point of the leg
            boat.setCurrentPosition(calculatePosition(boat.getCurrentLeg().getStartMarker().getAverageGPSCoordinate(),
                    totalDistanceTravelled, boat.calculateAzimuth()));
        }
    }

    protected void checkPosition(Boat boat, long timeElapsed) {
        if (boat.getDistanceTravelledInLeg() > boat.getCurrentLeg().getDistance()) {
            //boat has passed onto new leg
            if (boat.getCurrentLeg().getName().equals("Finish")) {
                //boat has finished
                boatsFinished++;
                boat.setTimeFinished(timeElapsed);
                boat.setTimeFinished(timeElapsed);
            } else if (doNotFinish()) {
                boatsFinished++;
                boat.setTimeFinished(timeElapsed);
                boat.setCurrentLeg(new Leg("DNF", -1));
                boat.setVelocity(0);
                boat.setScaledVelocity(0);
            } else {
                //Calculate how much the boat overshot the marker by
                boat.setDistanceTravelledInLeg(boat.getDistanceTravelledInLeg() - boat.getCurrentLeg().getDistance());
                //Move boat on to next leg
                Leg nextLeg = legs.get(boat.getCurrentLeg().getLegNumber() + 1);

                boat.setCurrentLeg(nextLeg);
                //Add overshoot distance into the distance travelled for the next leg
                boat.setDistanceTravelledInLeg(boat.getDistanceTravelledInLeg());
            }
        }
    }

    /**
     * Returns the boats that have started the race.
     *
     * @return ObservableList of Boat class that participated in the race.
     * @see ObservableList
     * @see Boat
     */
    public ObservableList<Boat> getStartingBoats() {
        return startingBoats;
    }
}
