package visualiser.Controllers;


import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.fxml.FXML;
import javafx.scene.chart.LineChart;
import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.util.Callback;
import network.Messages.Enums.RaceStatusEnum;
import shared.model.Leg;
import visualiser.gameController.ControllerClient;
import visualiser.gameController.Keys.ControlKey;
import visualiser.gameController.Keys.KeyFactory;
import visualiser.model.*;
import visualiser.network.ServerConnection;

import java.net.URL;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;



/**
 * Controller used to display a running race.
 */
public class RaceController extends Controller {


    /**
     * The race object which describes the currently occurring race.
     */
    private VisualiserRaceEvent visualiserRace;


    /**
     * Service for sending keystrokes to server
     */
    private ControllerClient controllerClient;




    /**
     * The canvas that draws the race.
     */
    private ResizableRaceCanvas raceCanvas;

    /**
     * The sparkline graph.
     */
    private Sparkline sparkline;

    /**
     * The arrow controller.
     */
    @FXML private ArrowController arrowController;



    @FXML private GridPane canvasBase;


    @FXML private SplitPane race;

    /**
     * This is the root node of the arrow control.
     */
    @FXML private Pane arrow;

    /**
     * This is the pane we place the actual arrow control inside of.
     */
    @FXML private StackPane arrowPane;
    @FXML private Label timer;
    @FXML private Label FPS;
    @FXML private Label timeZone;
    @FXML private CheckBox showFPS;
    @FXML private TableView<VisualiserBoat> boatInfoTable;
    @FXML private TableColumn<VisualiserBoat, String> boatPlacingColumn;
    @FXML private TableColumn<VisualiserBoat, String> boatTeamColumn;
    @FXML private TableColumn<VisualiserBoat, Leg> boatMarkColumn;
    @FXML private TableColumn<VisualiserBoat, Number> boatSpeedColumn;
    @FXML private LineChart<Number, Number> sparklineChart;
    @FXML private AnchorPane annotationPane;



    /**
     * Ctor.
     */
    public RaceController() {
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        KeyFactory keyFactory = KeyFactory.getFactory();

        // Initialise keyboard handler
        race.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
            String codeString = event.getCode().toString();
            ControlKey controlKey = keyFactory.getKey(codeString);
            if(controlKey != null) {
                try {
                    controllerClient.sendKey(controlKey);
                    controlKey.onAction();  // Change key state if applicable
                    event.consume();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    Logger.getGlobal().log(Level.WARNING, "RaceController was interrupted on thread: " + Thread.currentThread() + "while sending: " + controlKey, e);
                }
            }
        });
    }


    /**
     * Initialises the various UI components to listen to the {@link #visualiserRace}.
     */
    private void initialiseRace() {
        //Fps display.
        initialiseFps(this.visualiserRace);

        //Information table.
        initialiseInfoTable(this.visualiserRace);

        //Sparkline.
        initialiseSparkline(this.visualiserRace);

        //Arrow.
        initialiseArrow(this.visualiserRace);

        //Race canvas.
        initialiseRaceCanvas(this.visualiserRace);

        //Race timezone label.
        initialiseRaceTimezoneLabel(this.visualiserRace);

        //Race clock.
        initialiseRaceClock(this.visualiserRace);


        //Start the race animation timer.
        raceTimer();
    }



    /**
     * Initialises the frame rate functionality. This allows for toggling the frame rate, and connect the fps label to the race's fps property.
     * @param visualiserRace The race to connect the fps label to.
     */
    private void initialiseFps(VisualiserRaceEvent visualiserRace) {

        //On/off toggle.
        initialiseFpsToggle();

        //Label value.
        initialiseFpsLabel(visualiserRace);
    }

    /**
     * Initialises a listener for the fps toggle.
     */
    private void initialiseFpsToggle() {

        showFPS.selectedProperty().addListener((ov, old_val, new_val) -> {
            if (showFPS.isSelected()) {
                FPS.setVisible(true);

            } else {
                FPS.setVisible(false);

            }
        });

    }

    /**
     * Initialises the fps label to update when the race fps changes.
     * @param visualiserRace The race to monitor the frame rate of.
     */
    private void initialiseFpsLabel(VisualiserRaceEvent visualiserRace) {

        visualiserRace.getFrameRateProperty().addListener((observable, oldValue, newValue) -> {
            Platform.runLater(() ->  this.FPS.setText("FPS: " + newValue.toString()));
        });

    }



    /**
     * Initialises the information table view to listen to a given race.
     * @param race Race to listen to.
     */
    public void initialiseInfoTable(VisualiserRaceEvent race) {

        //Copy list of boats.
        ObservableList<VisualiserBoat> boats = FXCollections.observableArrayList(race.getVisualiserRaceState().getBoats());
        SortedList<VisualiserBoat> sortedBoats = new SortedList<>(boats);
        sortedBoats.comparatorProperty().bind(boatInfoTable.comparatorProperty());

        //Update copy when original changes.
        race.getVisualiserRaceState().getBoats().addListener((ListChangeListener.Change<? extends VisualiserBoat> c) -> Platform.runLater(() -> {
            boats.setAll(race.getVisualiserRaceState().getBoats());
        }));


        //Set up table.
        boatInfoTable.setItems(sortedBoats);


        //Set up each column.

        //Name.
        boatTeamColumn.setCellValueFactory(
                cellData -> cellData.getValue().nameProperty()  );

        //Speed.
        boatSpeedColumn.setCellValueFactory(
                cellData -> cellData.getValue().currentSpeedProperty()   );

        //Kind of ugly, but allows for formatting an observed speed.
        boatSpeedColumn.setCellFactory(
                //Callback object.
                new Callback<TableColumn<VisualiserBoat, Number>, TableCell<VisualiserBoat, Number>>() {

                    //Callback function.
                    @Override
                    public TableCell<VisualiserBoat, Number> call(TableColumn<VisualiserBoat, Number> param) {
                        //We return a table cell that populates itself with a Number, and formats it.
                        return new TableCell<VisualiserBoat, Number>(){

                            //Function to update the cell text.
                            @Override
                            protected void updateItem(Number item, boolean empty) {

                                if (item != null) {
                                    super.updateItem(item, empty);

                                    setText(String.format("%.2fkn", item.doubleValue()));
                                }
                            }
                        };

                    }

                }   );


        //Last mark.
        boatMarkColumn.setCellValueFactory(
                cellData -> cellData.getValue().legProperty()   );

        //Kind of ugly, but allows for turning an observed Leg into a string.
        boatMarkColumn.setCellFactory(
                //Callback object.
                new Callback<TableColumn<VisualiserBoat, Leg>, TableCell<VisualiserBoat, Leg>>() {

                    //Callback function.
                    @Override
                    public TableCell<VisualiserBoat, Leg> call(TableColumn<VisualiserBoat, Leg> param) {
                        //We return a table cell that populates itself with a Leg's name.
                        return new TableCell<VisualiserBoat, Leg>(){

                            //Function to update the cell text.
                            @Override
                            protected void updateItem(Leg item, boolean empty) {

                                if (item != null) {
                                    super.updateItem(item, empty);

                                    setText(item.getName());
                                }
                            }
                        };

                    }

                }   );

        //Current place within race.
        boatPlacingColumn.setCellValueFactory(
                cellData -> cellData.getValue().positionProperty()  );

    }


    /**
     * Initialises the {@link Sparkline}, and listens to a specified {@link VisualiserRaceEvent}.
     * @param race The race to listen to.
     */
    private void initialiseSparkline(VisualiserRaceEvent race) {
        //The race.getBoats() we are passing in is sorted by position in race inside the race class.
        this.sparkline = new Sparkline(this.visualiserRace.getVisualiserRaceState(), this.sparklineChart);
    }


    /**
     * Initialises the {@link ResizableRaceCanvas}, provides the race to read data from.
     * @param race Race to read data from.
     */
    private void initialiseRaceCanvas(VisualiserRaceEvent race) {

        //Create canvas.
        raceCanvas = new ResizableRaceCanvas(race);

        //Set properties.
        raceCanvas.setMouseTransparent(true);
        raceCanvas.widthProperty().bind(canvasBase.widthProperty());
        raceCanvas.heightProperty().bind(canvasBase.heightProperty());

        //Draw it and show it.
        raceCanvas.draw();
        raceCanvas.setVisible(true);

        //Add to scene.
        canvasBase.getChildren().add(0, raceCanvas);
    }


    /**
     * Intialises the race time zone label with the race's time zone.
     * @param race The race to get time zone from.
     */
    private void initialiseRaceTimezoneLabel(VisualiserRaceEvent race) {
        timeZone.setText(race.getVisualiserRaceState().getRaceClock().getTimeZone());
    }

    /**
     * Initialises the race clock to listen to the specified race.
     * @param race The race to listen to.
     */
    private void initialiseRaceClock(VisualiserRaceEvent race) {

        //RaceClock.duration isn't necessarily being changed in the javaFX thread, so we need to runlater the update.
        race.getVisualiserRaceState().getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> {
            Platform.runLater(() -> {
                timer.setText(newValue);
            });
        });
    }



    /**
     * Displays a specified race.
     * @param visualiserRace Object modelling the race.
     * @param controllerClient Socket Client that manipulates the controller.
     */
    public void startRace(VisualiserRaceEvent visualiserRace, ControllerClient controllerClient) {

        this.visualiserRace = visualiserRace;
        this.controllerClient = controllerClient;

        initialiseRace();

        //Display this controller.
        race.setVisible(true);


        // set up annotation displays
        new Annotations(annotationPane, raceCanvas);
    }

    /**
     * Transition from the race view to the finish view.
     * @param boats boats there are in the race.
     */
    public void finishRace(ObservableList<VisualiserBoat> boats) {
        race.setVisible(false);
        parent.enterFinish(boats);
    }


    /**
     * Initialises the arrow controller with data from the race to observe.
     * @param race The race to observe.
     */
    private void initialiseArrow(VisualiserRaceEvent race) {
        arrowController.setWindProperty(race.getVisualiserRaceState().windProperty());
    }


    /**
     * Timer which monitors the race.
     */
    private void raceTimer() {
        new AnimationTimer() {
            @Override
            public void handle(long arg0) {

                //Get the current race status.
                RaceStatusEnum raceStatus = visualiserRace.getVisualiserRaceState().getRaceStatusEnum();


                //If the race has finished, go to finish view.
                if (raceStatus == RaceStatusEnum.FINISHED) {
                    //Stop this timer.
                    stop();

                    //Hide this, and display the finish controller.
                    finishRace(visualiserRace.getVisualiserRaceState().getBoats());

                } else {
                    //Otherwise, render the canvas.
                    raceCanvas.drawRace();


                    //Sort the tableview. Doesn't automatically work for all columns.
                    boatInfoTable.sort();

                }

                //Return to main screen if we lose connection.
                if (!visualiserRace.getServerConnection().isAlive()) {
                    race.setVisible(false);
                    parent.enterTitle();
                    //TODO currently this doesn't work correctly - the title screen remains visible after clicking join game
                    //TODO we should display an error to the user
                    //TODO also need to "reset" any state (race, connections, etc...).
                }

            }
        }.start();
    }

}
