package visualiser.Controllers;

import com.interactivemesh.jfx.importer.stl.StlMeshImporter;
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.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Translate;
import javafx.util.Callback;
import network.Messages.Enums.RaceStatusEnum;
import shared.dataInput.RaceDataSource;
import shared.model.Leg;
import shared.model.Mark;
import visualiser.app.App;
import visualiser.enums.TutorialState;
import visualiser.gameController.ControllerClient;
import visualiser.gameController.Keys.ControlKey;
import visualiser.gameController.Keys.KeyFactory;
import visualiser.layout.Subject3D;
import visualiser.layout.View3D;
import visualiser.model.Sparkline;
import visualiser.model.VisualiserBoat;
import visualiser.model.VisualiserRaceEvent;
import visualiser.model.VisualiserRaceState;
import visualiser.utils.GPSConverter;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Controller used to display a running race.
 */
public class RaceViewController extends Controller {
    private VisualiserRaceEvent visualiserRace;
    private VisualiserRaceState raceState;
    private ControllerClient controllerClient;
    private KeyFactory keyFactory = new KeyFactory();
    private boolean infoTableShow = true;  // shown or hidden
    private boolean isHost;
    private TutorialState currentState;
    private ArrayList<TutorialState> tutorialStates;
    private boolean isTutorial = false;
    private String keyToPress;
    private View3D view3D;
    private ObservableList<Subject3D> viewSubjects;

    // note: it says it's not used but it is! do not remove :)
    private @FXML ArrowController arrowController;
    private @FXML GridPane canvasBase;
    private @FXML SplitPane racePane;
    private @FXML StackPane arrowPane;
    private @FXML Label timer;
    private @FXML Label FPS;
    private @FXML Label timeZone;
    private @FXML CheckBox showFPS;
    private @FXML TableView<VisualiserBoat> boatInfoTable;
    private @FXML TableColumn<VisualiserBoat, String> boatPlacingColumn;
    private @FXML TableColumn<VisualiserBoat, String> boatTeamColumn;
    private @FXML TableColumn<VisualiserBoat, Leg> boatMarkColumn;
    private @FXML TableColumn<VisualiserBoat, Number> boatSpeedColumn;
    private @FXML LineChart<Number, Number> sparklineChart;
    private @FXML Label tutorialText;

    /**
     * Displays a specified race.
     * Intended to be called on loading the scene.
     * @param visualiserRace   Object modelling the race.
     * @param controllerClient Socket Client that manipulates the controller.
     * @param isHost           is user a host
     */
    public void startRace(VisualiserRaceEvent visualiserRace, ControllerClient controllerClient, Boolean isHost) {
        this.visualiserRace = visualiserRace;
        this.raceState = visualiserRace.getVisualiserRaceState();
        this.controllerClient = controllerClient;
        this.isHost = isHost;
        keyFactory.load();

        tutorialCheck();
        initKeypressHandler();
        initialiseRaceVisuals();
    }

    /**
     * Checks if the current game is a tutorial race and sets up initial
     * tutorial displays if it is.
     */
    private void tutorialCheck(){
        if (App.gameType == 4) {
            isTutorial = true;
            tutorialText.setVisible(true);
            tutorialStates = new ArrayList<>(Arrays.asList(TutorialState.values()));

            currentState = tutorialStates.get(0);
            tutorialStates.remove(0);
            searchMapForKey("Upwind");

            tutorialText.setText(
                    "Welcome to the tutorial! Exit at anytime with ESC. \nWe will first learn how to turn upwind. Press " +
                            keyToPress + " to turn upwind.");

        } else {
            isTutorial = false;
            tutorialText.setVisible(false);
        }
    }

    /**
     * Sets up the listener and actions that occur when a key is pressed.
     */
    private void initKeypressHandler() {
        racePane.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
            String codeString = event.getCode().toString();

            // tab key
            if (codeString.equals("TAB")){toggleTable();}

            // any key pressed
            ControlKey controlKey = keyFactory.getKey(codeString);
            if(controlKey != null) {
                try {
                    controlKey.onAction();  // Change key state if applicable

                    //Check if current race is a tutorial
                    if (isTutorial){
                        //Check if current tutorial state has the same boat protocol code as key press
                        if (controlKey.getProtocolCode().equals(currentState.getAction())){
                            //Update tutorial
                            checkTutorialState();
                        }
                    }

                    controllerClient.sendKey(controlKey);
                    event.consume();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    Logger.getGlobal().log(Level.WARNING, "RaceViewController was interrupted on thread: " + Thread.currentThread() + "while sending: " + controlKey, e);
                    Logger.getGlobal().log(Level.WARNING, "RaceController was interrupted on thread: " + Thread.currentThread() + "while sending: " + controlKey, e);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            // escape key
            if(event.getCode() == KeyCode.ESCAPE) {
                try {
                    if (isHost) {
                        Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
                        alert.setTitle("Exit Race");
                        alert.setContentText("Do you wish to quit the race? You are the host");
                        Optional<ButtonType> result = alert.showAndWait();
                        if (result.get() == ButtonType.OK) {
                            App.game.endEvent();
                            loadTitleScreen();
                        }
                    } else {
                        Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
                        alert.setTitle("Exit Race");
                        alert.setContentText("Do you wish to quit the race?");
                        Optional<ButtonType> result = alert.showAndWait();
                        if (result.get() == ButtonType.OK) {
                            loadTitleScreen();
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * Initialises the various UI components to listen to the {@link #visualiserRace}.
     */
    private void initialiseRaceVisuals() {
        // initialise displays
        initialiseFps();
        initialiseInfoTable();
        initialiseView3D();
        initialiseRaceClock();
        raceTimer();    // start the timer
        new Sparkline(this.raceState, this.sparklineChart);
        timeZone.setText(this.raceState.getRaceClock().getTimeZone());
        arrowController.setWindProperty(this.raceState.windProperty());
    }

    private void initialiseView3D() {
        viewSubjects = FXCollections.observableArrayList();

        // Import boat mesh
        URL asset = RaceViewController.class.getClassLoader().getResource("assets/V1.2 " +
                "Complete Boat.stl");
        StlMeshImporter importer = new StlMeshImporter();
        importer.read(asset);

        // Configure camera angles and control
        view3D = new View3D();
        view3D.setDistance(1050);
        view3D.setYaw(0);
        view3D.setPitch(60);
        view3D.enableTracking();
        canvasBase.add(view3D, 0, 0);

        // Set up projection from GPS to view
        RaceDataSource raceData = raceState.getRaceDataSource();
        final GPSConverter gpsConverter = new GPSConverter(raceData, 450, 450);

        view3D.setItems(viewSubjects);
        // Position and add each mark to view
        for (Mark mark : raceState.getMarks()) {
            Subject3D subject = new Subject3D(new Sphere(2));
            subject.setX(gpsConverter.convertGPS(mark.getPosition()).getX());
            subject.setZ(gpsConverter.convertGPS(mark.getPosition()).getY());

            viewSubjects.add(subject);
        }
        // Position and add each boat to view
        for (VisualiserBoat boat : raceState.getBoats()) {
            MeshView mesh = new MeshView(importer.getImport());
            Subject3D subject = new Subject3D(mesh);
            viewSubjects.add(subject);

            // Track this boat's movement with the new subject
            AnimationTimer trackBoat = new AnimationTimer() {
                @Override public void handle(long now) {
                    subject.setHeading(boat.getBearing().degrees());
                    subject.setX(gpsConverter.convertGPS(boat.getPosition()).getX());
                    subject.setZ(gpsConverter.convertGPS(boat.getPosition()).getY());
                }
            };
            trackBoat.start();
        }
        // Fix initial bird's-eye position
        view3D.updatePivot(new Translate(250, 0, 210));

        // Bind zooming to scrolling
        view3D.setOnScroll(e -> {
            view3D.updateDistance(e.getDeltaY());
        });

        // Bind zooming to keypress (Z/X default)
        racePane.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
            ControlKey key = keyFactory.getKey(e.getCode().toString());
            if (key != null) {
                switch (key.toString()) {
                    case "Zoom In":
                        //Check if race is a tutorial
                        if (isTutorial) {
                            //Check if the current tutorial state is zoom-in
                            if (currentState.equals(TutorialState.ZOOMIN)) {
                                try {
                                    //Update tutorial
                                    checkTutorialState();
                                } catch (Exception e1) {
                                    e1.printStackTrace();
                                }
                            }
                        }
                        view3D.updateDistance(-10);
                        break;
                    case "Zoom Out":
                        //Check if race is a tutorial
                        if(isTutorial) {
                            //Check if current tutorial state is zoom-out
                            if (currentState.equals(TutorialState.ZOOMOUT)) {
                                try {
                                    //Update tutorial
                                    checkTutorialState();
                                } catch (Exception e1) {
                                    e1.printStackTrace();
                                }
                            }
                        }
                        view3D.updateDistance(10);
                        break;
                }
            }
        });
    }

    /**
     * Initialises the frame rate functionality. This allows for toggling the
     * frame rate, and connect the fps label to the race's fps property.
     */
    private void initialiseFps() {
        // fps toggle listener
        showFPS.selectedProperty().addListener((ov, old_val, new_val) -> {
            if (showFPS.isSelected()) {
                FPS.setVisible(true);
            } else {
                FPS.setVisible(false);
            }
        });

        // fps label display
        this.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.
     */
    private void initialiseInfoTable() {
        // list of boats to display data for
        ObservableList<VisualiserBoat> boats = FXCollections
                .observableArrayList(this.visualiserRace.getVisualiserRaceState().getBoats());
        SortedList<VisualiserBoat> sortedBoats = new SortedList<>(boats);
        sortedBoats.comparatorProperty().bind(boatInfoTable.comparatorProperty());

        // update list when boat information changes
        this.visualiserRace.getVisualiserRaceState().getBoats().addListener(
                (ListChangeListener.Change<? extends VisualiserBoat> c) -> Platform.runLater(() -> {
            boats.setAll(this.visualiserRace.getVisualiserRaceState().getBoats());
        }));

        // set table data
        boatInfoTable.setItems(sortedBoats);
        boatTeamColumn.setCellValueFactory(
                cellData -> cellData.getValue().nameProperty());
        boatSpeedColumn.setCellValueFactory(
                cellData -> cellData.getValue().currentSpeedProperty());
        boatMarkColumn.setCellValueFactory(
                cellData -> cellData.getValue().legProperty());
        boatPlacingColumn.setCellValueFactory(
                cellData -> cellData.getValue().placingProperty());

        //Kind of ugly, but allows for formatting an observed speed.
        boatSpeedColumn.setCellFactory(
                new Callback<TableColumn<VisualiserBoat, Number>, TableCell<VisualiserBoat, Number>>() {
                    @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()));
                                }
                            }
                        };
                    }
                });

        //Kind of ugly, but allows for turning an observed Leg into a string.
        boatMarkColumn.setCellFactory(
                new Callback<TableColumn<VisualiserBoat, Leg>, TableCell<VisualiserBoat, Leg>>() {
                    @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());
                                }
                            }
                        };
                    }
                });
    }

    /**
     * Initialises the race clock to listen to the specified race.
     */
    private void initialiseRaceClock() {
        raceState.getRaceClock().durationProperty().addListener((observable,
                oldValue, newValue) -> {
            Platform.runLater(() -> {
                timer.setText(newValue);
            });
        });
    }

    /**
     * Transition from the race view to the finish view.
     */
    private void finishRace() throws IOException {
        RaceFinishController fc =
                (RaceFinishController)loadScene("raceFinish.fxml");
        fc.loadFinish(raceState.getBoats());
    }

    /**
     * Timer which monitors the race.
     */
    private void raceTimer() {
        new AnimationTimer() {
            @Override
            public void handle(long arg0) {
                //If the race has finished, go to finish view.
                if (raceState.getRaceStatusEnum() == RaceStatusEnum.FINISHED) {
                    stop(); // stop the timer
                    try {
                        finishRace();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else {
                    boatInfoTable.sort();
                }

                //Return to main screen if we lose connection.
                if (!visualiserRace.getServerConnection().isAlive()) {
                    try {
                        loadTitleScreen();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //TODO we should display an error to the user
                    //TODO also need to "reset" any state (race, connections, etc...).
                }
            }
        }.start();
    }

    /**
     * toggles if the info table is shown
     */
    private void toggleTable() {
        double tablePercent = 1 - (boatPlacingColumn.getPrefWidth() +
                boatTeamColumn.getPrefWidth() + boatMarkColumn.getPrefWidth()
                + boatSpeedColumn.getPrefWidth())/racePane.getWidth();

        if (infoTableShow) {
            racePane.setDividerPositions(tablePercent);
            arrowPane.setScaleX(0.5);
            arrowPane.setScaleY(0.5);
            arrowPane.setTranslateX(0 + (arrowPane.getScene().getWidth()/4)*tablePercent);
            arrowPane.setTranslateY(0 - arrowPane.getScene().getHeight()/4);
        } else {
            racePane.setDividerPositions(1);
            arrowPane.setScaleX(1);
            arrowPane.setScaleY(1);
            arrowPane.setTranslateX(0);
            arrowPane.setTranslateY(0);
        }
        boatInfoTable.refresh();
        infoTableShow = !infoTableShow;
    }

    /**
     * Get the next tutorial state
     */
    private void updateTutorialState(){
        //Next tutorial state is popped from list
        currentState = tutorialStates.get(0);
        tutorialStates.remove(0);
    }

    /**
     * Search key map for key given string of command
     * @param command the command of the key
     */
    private void searchMapForKey(String command){
        //For loop through keyFactory
        for (Map.Entry<String, ControlKey> entry: keyFactory.getKeyState().entrySet()){
            if(entry.getValue().toString().equals(command)){

                //Found next key required to press
                keyToPress = entry.getKey();
            }
        }
    }

    /**
     * Updates tutorial state and gui display for tutorial text
     * @throws Exception Exception thrown
     */
    private void checkTutorialState() throws Exception {
        //Switch statement to check what the current tutorial state is
        switch (currentState){
            case UPWIND:
                //Set next key to press as the downwind key
                searchMapForKey("Downwind");
                //Update tutorial text
                tutorialText.setText("Nice! To turn downwind press " + keyToPress + ".");
                updateTutorialState();
                break;
            case DOWNWIND:
                //Set next key to press as the tack/gybe key
                searchMapForKey("Tack/Gybe");
                //Update tutorial text
                tutorialText.setText("Nice! To tack or gybe press " + keyToPress + ".");
                updateTutorialState();
                break;
            case TACKGYBE:
                //Set next key to press as the VMG key
                searchMapForKey("VMG");
                //Update tutorial text
                tutorialText.setText("Nice! To use VMG press " + keyToPress + ". This will turn the boat.");
                updateTutorialState();
                break;
            case VMG:
                //Set next key to press as the sails-in key
                searchMapForKey("Toggle Sails");
                //Update tutorial text
                tutorialText.setText("Nice! To sails in press " + keyToPress + ". This will stop the boat.");
                updateTutorialState();
                break;
            case SAILSIN:
                //Set next key to press as the sails-out key
                searchMapForKey("Toggle Sails");
                //Update tutorial text
                tutorialText.setText("Nice! To sails out press " + keyToPress + " again. The will start moving again.");
                updateTutorialState();
                break;
            case SAILSOUT:
                //Set next key to press as the zoom-in key
                searchMapForKey("Zoom In");
                //Update tutorial text
                tutorialText.setText("Nice! To zoom in press " + keyToPress + ".");
                updateTutorialState();
                break;
            case ZOOMIN:
                //Set next key to press as the zoom-out key
                searchMapForKey("Zoom Out");
                //Update tutorial text
                tutorialText.setText("Nice! You will also be able to zoom into boats and marks by clicking them. \nTo zoom out press " + keyToPress + ".");
                updateTutorialState();
                break;
            case ZOOMOUT:
                //Finished tutorial. Display pop-up for exiting the tutorial
                tutorialText.setText("Congratulations! You're done!");
                Alert alert = new Alert(Alert.AlertType.INFORMATION);
                alert.setTitle("Finished Tutorial");
                alert.setHeaderText("You have finished the tutorial.");
                alert.setContentText("Now you know the controls you are ready to race!");
                Optional<ButtonType> result = alert.showAndWait();
                if (result.get() == ButtonType.OK) {
                    App.game.endEvent();
                    loadTitleScreen();
                }
                break;
            default:
                //State not found. Exit tutorial to title menu
                App.game.endEvent();
                loadTitleScreen();
                break;
        }
    }

}