package seng302.Mock;

import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import seng302.Controllers.FinishController;
import seng302.Controllers.RaceController;
import seng302.GPSCoordinate;
import seng302.Model.Boat;
import seng302.Model.Leg;
import seng302.Model.Marker;
import seng302.Networking.Messages.BoatLocation;
import seng302.Networking.Messages.BoatStatus;
import seng302.Networking.Messages.Enums.BoatStatusEnum;
import seng302.VisualiserInput;

import java.util.List;

/**
 * Created by jjg64 on 21/04/17.
 */
public class StreamedRace implements Runnable {
    private final VisualiserInput visualiserInput;
    private final ObservableList<Boat> startingBoats;
    private final ObservableList<Marker> boatMarkers;
    private final List<Leg> legs;
    private RaceController controller;
    protected FinishController finishController;
    private int boatsFinished = 0;
    private long totalTimeElapsed;

    private int lastFPS = 20;

    public StreamedRace(VisualiserInput visualiserInput, RaceController controller) {
        StreamedCourse course = visualiserInput.getCourse();

        this.startingBoats = FXCollections.observableArrayList(course.getBoats());
        this.boatMarkers = FXCollections.observableArrayList(course.getMarkers());
        this.legs = course.getLegs();
        this.legs.add(new Leg("Finish", this.legs.size()));
        this.controller = controller;
        if (startingBoats != null && startingBoats.size() > 0) {
            initialiseBoats();
        }
        this.visualiserInput = visualiserInput;
    }

    private void initialiseBoats() {
        Leg officialStart = legs.get(0);
        String name = officialStart.getName();
        Marker endCompoundMark = officialStart.getEndMarker();

        for (Boat boat : startingBoats) {
            if (boat != null) {
                Leg startLeg = new Leg(name, 0);
                startLeg.setEndMarker(endCompoundMark);
                boat.setCurrentLeg(startLeg, controller.getRaceClock());
                boat.setTimeSinceLastMark(controller.getRaceClock().getTime());
            }
        }
    }

    /**
     * Checks if the boat cannot finish the race
     * @return True if boat cannot finish the race
     */
    protected boolean doNotFinish() {
        // DNF is no longer random and is now determined by a dnf packet
        return false;
    }

    /**
     * Checks the position of the boat.
     *
     * @param boat        Boat that the position is to be updated for.
     * @param timeElapsed Time that has elapse since the start of the the race.
     */
    private void checkPosition(Boat boat, long timeElapsed) {
        boolean legChanged = false;
        StreamedCourse raceData = visualiserInput.getCourse();
        BoatStatus boatStatusMessage = visualiserInput.getBoatStatusMap().get(boat.getSourceID());
        if (boatStatusMessage != null) {
            BoatStatusEnum boatStatusEnum = BoatStatusEnum.fromByte(boatStatusMessage.getBoatStatus());

            int legNumber = boatStatusMessage.getLegNumber();


            if (legNumber >= 1 && legNumber < legs.size()) {
                if (boat.getCurrentLeg() != legs.get(legNumber)){
                    boat.setCurrentLeg(legs.get(legNumber), controller.getRaceClock());
                    legChanged = true;
                }
            }

            if (boatStatusEnum == BoatStatusEnum.RACING) {
                boat.addTrackPoint(boat.getCurrentPosition());
            } else if (boatStatusEnum == BoatStatusEnum.DNF) {
                boat.setDnf(true);
            } else if (boatStatusEnum == BoatStatusEnum.FINISHED || legNumber == raceData.getLegs().size()) {
                boatsFinished++;
                boat.setTimeFinished(timeElapsed);
                boat.setFinished(true);
            }
        }
        if (legChanged) {
            //Update the boat display table in the GUI to reflect the leg change
            updatePositions();
            controller.updateSparkline(startingBoats);
        }
    }

    /**
     * Updates the boat's gps coordinates
     *
     * @param boat to be updated
     * @param millisecondsElapsed time since last update
     */
    private void updatePosition(Boat boat, int millisecondsElapsed) {
        int sourceID = boat.getSourceID();
        BoatLocation boatLocation = visualiserInput.getBoatLocationMessage(sourceID);
        if(boatLocation != null) {
            double lat = boatLocation.getLatitudeDouble();
            double lon = boatLocation.getLongitudeDouble();
            boat.setCurrentPosition(new GPSCoordinate(lat, lon));
            boat.setHeading(boatLocation.getHeadingDegrees());
            double MMPS_TO_KN = 0.001944;
            boat.setVelocity(boatLocation.getBoatSOG() * MMPS_TO_KN);
        }
    }

    /**
     * Updates the boat's gps coordinates
     *
     * @param mark to be updated
     */
    private void updateMarker(Marker mark) {
        int sourceID = mark.getSourceId1();
        BoatLocation boatLocation1 = visualiserInput.getBoatLocationMessage(sourceID);
        if(boatLocation1 != null) {
            double lat = boatLocation1.getLatitudeDouble();
            double lon = boatLocation1.getLongitudeDouble();
            mark.setCurrentPosition1(new GPSCoordinate(lat, lon));
        }
        int sourceID2 = mark.getSourceId2();
        BoatLocation boatLocation2 = visualiserInput.getBoatLocationMessage(sourceID2);
        if(boatLocation2 != null) {
            double lat = boatLocation2.getLatitudeDouble();
            double lon = boatLocation2.getLongitudeDouble();
            mark.setCurrentPosition2(new GPSCoordinate(lat, lon));
        }
    }

    public void setController(RaceController controller) {
        this.controller = controller;
    }


    /**
     * Runnable for the thread.
     */
    public void run() {
        setControllerListeners();
        Platform.runLater(() -> controller.createSparkLine(startingBoats));
        initialiseBoats();
        startRaceStream();
    }


    /**
     * Update the calculated fps to the fps label
     *
     * @param fps The new calculated fps value
     */
    private void updateFPS(int fps) {
        Platform.runLater(() -> controller.setFrames("FPS: " + fps));
    }

    /**
     * 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 startRaceStream() {

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

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

        new AnimationTimer() {

            final long timeRaceStarted = System.currentTimeMillis(); //start time of loop
            int fps = 0; //init fps value
            long timeCurrent = System.currentTimeMillis(); //current time

            @Override
            public void handle(long arg0) {
                if (boatsFinished < startingBoats.size()) {
                    boatsFinished = 0;
                    //controller.updateSparkline(startingBoats);
                    totalTimeElapsed = System.currentTimeMillis() - timeRaceStarted;

                    for (Boat boat : startingBoats) {
                        if (boat != null && !boat.isFinished()) {
                            updatePosition(boat, Math.round(1000 / lastFPS) > 20 ? 15 : Math.round(1000 / lastFPS));
                            checkPosition(boat, totalTimeElapsed);
                        }
                        if (boat.isFinished()){
                            boatsFinished++;
                        }
                    }
                    for (Marker mark: boatMarkers){
                        if (mark != null){
                            updateMarker(mark);
                        }
                    }
                    //System.out.println(boatsFinished + ":" + startingBoats.size());

                } else {
                    controller.finishRace(startingBoats);
                    stop();
                }
                controller.updateMap(startingBoats, boatMarkers);
                fps++;
                if ((System.currentTimeMillis() - timeCurrent) > 1000) {
                    updateFPS(fps);
                    lastFPS = fps;
                    fps = 0;
                    timeCurrent = System.currentTimeMillis();
                }
            }
        }.start();
    }

    /**
     * Update position of boats in race, no position if on starting leg or DNF.
     */
    private void updatePositions() {
        FXCollections.sort(startingBoats, (a, b) -> b.getCurrentLeg().getLegNumber() - a.getCurrentLeg().getLegNumber());
        for(Boat boat: startingBoats) {
            if(boat != null) {
                boat.setPosition(Integer.toString(startingBoats.indexOf(boat) + 1));
                if (boat.isDnf() || !boat.isStarted() || boat.getCurrentLeg().getLegNumber() < 0)
                    boat.setPosition("-");
            }
        }
    }

    /**
     * Update call for the controller.
     */
    private void setControllerListeners() {
        if (controller != null) controller.setInfoTable(this);
    }

    /**
     * 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;
    }

}
