package seng302.Model;

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.RaceDataSource;

import java.util.Arrays;
import java.util.List;

/**
 * Parent class for races
 * Created by fwy13 on 3/03/17.
 */
public abstract class Race implements Runnable {
    protected final ObservableList<Boat> startingBoats;
    protected final List<Leg> legs;
    private RaceController controller;
    protected FinishController finishController;
    protected int boatsFinished = 0;
    private long totalTimeElapsed;

    private int lastFPS = 20;

    /**
     * Initializer 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 controller race controller
     * @param scaleFactor for race
     */
    private Race(List<Boat> boats, List<Leg> legs, RaceController controller, int scaleFactor) {

        this.startingBoats = FXCollections.observableArrayList(boats);
        this.legs = legs;
        this.legs.add(new Leg("Finish", this.legs.size()));
        this.controller = controller;
        if (startingBoats != null && startingBoats.size() > 0) {
            initialiseBoats();
        }
    }

    protected Race(RaceDataSource raceData, RaceController controller, int scaleFactor) {
        this(raceData.getBoats(), raceData.getLegs(), controller, scaleFactor);
    }

    /**
     * @deprecated
     * @param startingBoats boats starting the race
     * @param legs legs in race
     * @param controller controller for the race
     * @param scaleFactor factor to scale by
     */
    public Race(Boat[] startingBoats, List<Leg> legs, RaceController controller, int scaleFactor) {
        this(Arrays.asList(startingBoats), legs, controller, scaleFactor);
    }

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

    protected abstract void initialiseBoats();

    /**
     * Checks if the boat cannot finish the race
     * @return True if boat cannot finish the race
     */
    protected abstract boolean doNotFinish();

    /**
     * Checks the position of the boat, this updates the boats current position.
     *
     * @param boat        Boat that the position is to be updated for.
     * @param timeElapsed Time that has elapse since the start of the the race.
     * @see Boat
     */
    protected abstract void checkPosition(Boat boat, long timeElapsed);

    /**
     * Updates the boat's gps coordinates
     *
     * @param boat to be updated
     * @param millisecondsElapsed time since last update
     */
    protected abstract void updatePosition(Boat boat, int millisecondsElapsed);

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


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

        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;
                    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++;
                        }
                    }
                    //System.out.println(boatsFinished + ":" + startingBoats.size());
                } else {
                    controller.finishRace(startingBoats);
                    stop();
                }
                controller.updateMap(startingBoats);
                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.
     */
    protected 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;
    }
}
