package uk.mgrove.ac.soton.comp1206.scene;

import javafx.animation.FadeTransition;
import javafx.application.Platform;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Pos;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.util.Duration;
import javafx.util.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.component.ScoresList;
import uk.mgrove.ac.soton.comp1206.game.Game;
import uk.mgrove.ac.soton.comp1206.game.MultiplayerGame;
import uk.mgrove.ac.soton.comp1206.ui.GamePane;
import uk.mgrove.ac.soton.comp1206.ui.GameWindow;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Scene to show scores and record high scores
 */
public class ScoresScene extends BaseScene {

  /**
   * Logger
   */
  private static final Logger logger = LogManager.getLogger(ScoresScene.class);

  /**
   * The game that just finished
   */
  private final Game game;

  /**
   * List of scores stored locally
   */
  private final SimpleListProperty<Pair<String,Integer>> localScores = new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>()));

  /**
   * List of online scores
   */
  private final SimpleListProperty<Pair<String,Integer>> remoteScores = new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>()));

  /**
   * Root pane in scene inside the GamePane
   */
  private StackPane scoresPane;

  /**
   * Node to display high scores
   */
  private ScoresList scoresUiList;

  /**
   * Node to display online high scores
   */
  private ScoresList remoteScoresUiList;

  /**
   * Container for prompting for username when high score achieved
   */
  private VBox highScorePromptContainer;

  /**
   * Main container for pane
   */
  private VBox mainContainer;

  /**
   * Create a new scores scene
   * @param gameWindow the Game Window
   * @param game the game that just finished
   */
  public ScoresScene(GameWindow gameWindow, Game game) {
    super(gameWindow);
    logger.info("Creating Scores Scene");
    this.game = game;
  }

  /**
   * Build the layout of the scene
   */
  @Override
  public void build() {
    logger.info("Building " + this.getClass().getName());

    root = new GamePane(gameWindow.getWidth(),gameWindow.getHeight());

    scoresPane = new StackPane();
    scoresPane.setMaxWidth(gameWindow.getWidth());
    scoresPane.setMaxHeight(gameWindow.getHeight());
    scoresPane.getStyleClass().add("menu-background");
    root.getChildren().add(scoresPane);

    var mainPane = new BorderPane();
    scoresPane.getChildren().add(mainPane);

    String localScoresTitle;
    if (game.getClass().equals(MultiplayerGame.class)) {
      logger.info("Loading multiplayer scores from game");
      var multiplayerScores = FXCollections.observableArrayList(new ArrayList<Pair<String,Integer>>());
      for (var score : game.leaderboardScoresProperty().get()) {
        multiplayerScores.add(new Pair<>(score.getKey(),score.getValue().getKey()));
      }
      localScores.set(multiplayerScores);
      localScoresTitle = "Game Scores";
    } else {
      loadScores("scores.txt");
      localScoresTitle = "Local Scores";
    }
    loadOnlineScores();

    scoresUiList = new ScoresList(localScoresTitle);
    scoresUiList.bindScores(localScores);
    scoresUiList.getStyleClass().add("scorelist");

    remoteScoresUiList = new ScoresList("Online Scores");
    remoteScoresUiList.bindScores(remoteScores);
    remoteScoresUiList.getStyleClass().add("scorelist");

    var scoresContainer = new HBox();
    scoresContainer.setSpacing(40);
    scoresContainer.getChildren().addAll(scoresUiList, remoteScoresUiList);
    scoresContainer.setAlignment(Pos.CENTER);
    mainContainer = new VBox(scoresContainer);
    mainContainer.setSpacing(60);
    mainContainer.setAlignment(Pos.CENTER);
    mainPane.setCenter(mainContainer);
  }

  /**
   * Initialise this scene. Called after creation
   */
  @Override
  public void initialise() {
    logger.info("Initialising Scores");

    // exit instructions when escape key pressed
    scene.setOnKeyPressed((event) -> {
      if (event.getCode() == KeyCode.ESCAPE) {
        gameWindow.startMenu();
      }
    });
  }

  /**
   * Return to lobby on mouse click
   * @param event mouse click event
   */
  private void returnToLobby(MouseEvent event) {
    if (game.getClass().equals(MultiplayerGame.class)) {
      gameWindow.startMultiplayer();
    } else {
      gameWindow.startMenu();
    }
  }

  /**
   * Load high scores from file
   * @param filePath file path to load scores from
   */
  public void loadScores(String filePath) {
    logger.info("Loading scores from file: {}", filePath);
    localScores.set(getScores(filePath));
  }

  /**
   * Load high scores from server and reveal both local and remote scores
   */
  public void loadOnlineScores() {
    logger.info("Loading online scores");
    AtomicBoolean scoresLoaded = new AtomicBoolean(false);
    gameWindow.getCommunicator().addListener((message) -> {
      if (!scoresLoaded.get() && message.startsWith("HISCORES ")) {
        scoresLoaded.set(true);
        var strippedMessage = message.replaceFirst("HISCORES ", "");
        remoteScores.set(parseScores(new Scanner(strippedMessage)));
        var topLocalScore = localScores.getSize() > 0 ? localScores.get(localScores.getSize() - 1).getValue() : 0;
        var topRemoteScore = remoteScores.size() > 0 ? remoteScores.get(remoteScores.getSize() - 1).getValue() : 0;
        Platform.runLater(() -> {
          if (game.getScore() > topLocalScore || game.getScore() > topRemoteScore)
            showHighScorePrompt(game.getScore());
          else {
            scoresUiList.reveal();
            remoteScoresUiList.reveal();
            showPlayAgainButton();
          }
        });
      }
    });
    gameWindow.getCommunicator().send("HISCORES");
  }

  /**
   * Get scores from file
   * @param filePath file to load scores from
   * @return list of scores
   */
  public static SimpleListProperty<Pair<String,Integer>> getScores(String filePath) {
    logger.info("Retrieving scores from file: {}", filePath);

    SimpleListProperty<Pair<String, Integer>> scores;
    try {
      File scoresFile = new File(filePath);
      Scanner scanner = new Scanner(scoresFile);
      scores = parseScores(scanner);
    } catch (FileNotFoundException e1) {
      logger.error("Unable to load scores file, writing default scores instead: {}", filePath);

      scores = new SimpleListProperty<>(FXCollections.observableArrayList());
      scores.add(new Pair<>("Sam",3000));
      scores.add(new Pair<>("Jane",2000));
      scores.add(new Pair<>("Pete",1000));
      writeScores("scores.txt", scores);
    }
    logger.info("Retrieved scores: {}", scores);
    return scores;
  }

  /**
   * Parse high scores from a scanner to list property
   * @param scanner scanner to parse from
   * @return list property containing scores loaded
   */
  private static SimpleListProperty<Pair<String,Integer>> parseScores(Scanner scanner) {
    var scores = new SimpleListProperty<Pair<String,Integer>>(FXCollections.observableArrayList());

    while (scanner.hasNextLine()) {
      var line = scanner.nextLine();
      if (line.matches("^.+:[0-9]+$")) {
        int splitIndex = line.lastIndexOf(":");
        String[] info = {line.substring(0,splitIndex), line.substring(splitIndex+1)};
        scores.add(new Pair<>(info[0], Integer.valueOf(info[1])));
      }
    }
    scanner.close();

    logger.info("Parsed scores: {}", scores);
    return scores;
  }

  /**
   * Write list of scores to file
   * @param filePath file to write to
   * @param scores scores to write
   */
  public static void writeScores(String filePath, SimpleListProperty<Pair<String,Integer>> scores) {
    logger.info("Writing scores to file: {}", filePath);
    try {
      FileWriter scoresWriter = new FileWriter(filePath);

      for (var pair : scores.get()) {
        scoresWriter.write(pair.getKey() + ":" + pair.getValue() + "\n");
      }

      scoresWriter.close();
    } catch (IOException e) {
      logger.error("Unable to write scores file: {}", filePath);
    }
  }

  /**
   * Add new high score - prompt user for name and add to saved scores
   * @param score new high score to save
   */
  private void showHighScorePrompt(int score) {
    logger.info("Showing high score username input prompt");

    var usernameTitle = new Text("Enter username");
    usernameTitle.getStyleClass().add("title");
    usernameTitle.setTextAlignment(TextAlignment.CENTER);
    var userNamePrompt = new HBox();
    userNamePrompt.setAlignment(Pos.CENTER);
    userNamePrompt.setSpacing(4);
    var userNameInput = new TextField();
    userNameInput.setPromptText("Enter username...");
    var userNameSubmit = new Text("Submit");
    userNameSubmit.getStyleClass().add("channelItem");

    userNameInput.setOnAction((event) -> submitHighScore(score, userNameInput.getText()));
    userNameSubmit.setOnMouseClicked((event) -> submitHighScore(score, userNameInput.getText()));

    userNamePrompt.getChildren().addAll(userNameInput, userNameSubmit);
    highScorePromptContainer = new VBox(usernameTitle, userNamePrompt);
    highScorePromptContainer.setSpacing(20);
    scoresPane.getChildren().addAll(highScorePromptContainer);
    StackPane.setAlignment(highScorePromptContainer, Pos.CENTER);
  }

  /**
   * Submit a high score
   * @param score score to submit
   * @param username username of player
   */
  private void submitHighScore(int score, String username) {
    if (!username.isBlank()) {
      saveHighScore(username, score);
      writeOnlineScore(username, score);
      scoresPane.getChildren().remove(highScorePromptContainer);
      scoresUiList.reveal();
      remoteScoresUiList.reveal();
      showPlayAgainButton();
    }
  }

  private void showPlayAgainButton() {
    var playAgainButton = new Text("Continue");
    playAgainButton.getStyleClass().add("menuItem");
    playAgainButton.setOnMouseClicked(this::returnToLobby);
    playAgainButton.setOpacity(0);

    var ft = new FadeTransition(Duration.millis(1000), playAgainButton);
    ft.setToValue(1);

    mainContainer.getChildren().add(playAgainButton);
    ft.play();
  }

  /**
   * Save new high score
   * @param username username of user who set high score
   * @param score high score to save
   */
  private void saveHighScore(String username, int score) {
    logger.info("Saving high score: {} for user: {}", score, username);

    for (var i = 0; i <= localScores.getSize(); i++) {
      if (i == localScores.getSize() || localScores.get(i).getValue() < score) {
        localScores.add(i, new Pair<>(username, score));
        break;
      }
    }
    // if score isn't higher than last high score then this shouldn't have been triggered
    // so while it won't get stored, this isn't an issue
    writeScores("scores.txt", localScores);
  }

  private void writeOnlineScore(String username, int score) {
    logger.info("Saving online high score: {} for user: {}", score, username);

    gameWindow.getCommunicator().send("HISCORE " + username + ":" + (score));

    for (var i = 0; i <= remoteScores.getSize(); i++) {
      if (i == remoteScores.getSize() || remoteScores.get(i).getValue() < score) {
        remoteScores.add(i, new Pair<>(username, score));
        break;
      }
    }
  }

}
