package com.charego.lingo.client.multiplayer;

import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;

import javax.annotation.PostConstruct;

import com.charego.lingo.common.LobbyData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;
import org.springframework.messaging.simp.stomp.StompFrameHandler;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompSession.Subscription;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.VPos;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;
import javafx.scene.web.WebView;
import com.charego.lingo.api.Destinations;
import com.charego.lingo.client.util.FxmlController;
import com.charego.lingo.client.view.Board;
import com.charego.lingo.client.view.OpponentBoard;
import com.charego.lingo.client.view.PlayerBoard;
import com.charego.lingo.common.Game;
import com.charego.lingo.common.GameLeftMessage;
import com.charego.lingo.common.Report;

@Component
public class MultiplayerPresenter implements FxmlController {

	private static final Logger log = LoggerFactory.getLogger(MultiplayerPresenter.class);

	private static final double MARGIN_BOTTOM = 75;

	@FXML
	private Button backButton;

	@FXML
	private Canvas canvas;

	@FXML
	private StackPane contentPane;

	@FXML
	private WebView webView;

	@Autowired
	private TaskExecutor taskExecutor;

	@Autowired
	private RestTemplate restTemplate;

	@Autowired
	private StompTemplate stompTemplate;

	private EventHandler<ActionEvent> backButtonHandler;

	private GraphicsContext gc;

	private Game myGame;

	private String lastWord;

	private PlayerBoard playerBoard;

	private OpponentBoard opponentBoard;

	private final CountDownLatch subscriptionsLatch = new CountDownLatch(7);

	private String username;

	private String opponentUsername;

	private void clearBoards(boolean clearScore) {
		playerBoard.clearBoard();
		opponentBoard.clearBoard();
		if (clearScore) {
			playerBoard.clearScore();
			opponentBoard.clearScore();
		}
	}

	private void drawLastWord() {
		if (lastWord != null) {
			double x = canvas.getWidth() / 2;
			double y = canvas.getHeight() - MARGIN_BOTTOM / 2;
			gc.setFill(Color.BLACK);
			gc.fillText("Previous word: " + lastWord.toUpperCase(), x, y);
		}
	}

	@Override
	public void initialize() {
		// Needed for key event handling
		canvas.setFocusTraversable(true);

		gc = canvas.getGraphicsContext2D();
		gc.setFont(Font.font(24));
		gc.setTextAlign(TextAlignment.CENTER);
		gc.setTextBaseline(VPos.CENTER);

		Platform.runLater(() -> {
			String html = getClass().getResource("/cube-grid.html").toExternalForm();
			String css = getClass().getResource("/cube-grid.css").toExternalForm();
			webView.getEngine().load(html);
			webView.getEngine().setUserStyleSheetLocation(css);
			webView.setContextMenuEnabled(false);
			repaint();
		});

		backButton.setOnAction(backButtonHandler);

		taskExecutor.execute(() -> {
			while (subscriptionsLatch.getCount() != 0) {
				try {
					subscriptionsLatch.await();
				} catch (InterruptedException ok) {
					ok.printStackTrace();
				}
			}

			username = UUID.randomUUID().toString().substring(0, 8);
			stompTemplate.getSession().send("/app/setUsername", username);

			LobbyData lobbyData = restTemplate.getForObject("/data", LobbyData.class);
			boolean joinedGame = false;
			for (Game game : lobbyData.getGames()) {
				if (game.getPlayerTwo() == null) {
					log.debug("Joining game...");
					stompTemplate.getSession().send("/app/joinGame", game.getId());
					joinedGame = true;
					break;
				}
			}
			if (!joinedGame) {
				log.debug("Hosting game...");
				stompTemplate.getSession().send("/app/hostGame5", null);
			}
		});
	}

	@FXML
	private void keyPressed(KeyEvent e) {
		final KeyCode keyCode = e.getCode();
		if (keyCode == KeyCode.BACK_SPACE) {
			if (playerBoard.handleBackspace()) {
				repaint();
			}
		} else if (keyCode == KeyCode.ENTER) {
			final String guess = playerBoard.handleEnter();
			if (guess != null) {
				taskExecutor.execute(() -> stompTemplate.getSession().send("/app/guess", guess));
				repaint();
			}
		} else if (keyCode.isLetterKey()) {
			if (playerBoard.handleLetter(keyCode.getName())) {
				repaint();
			}
		}
	}

	private void newWord(String firstLetter) {
		playerBoard.setProgress(0, firstLetter.charAt(0));
	}

	@PostConstruct
	private void postConstruct() {
		Consumer<Subscription> defaultCallback = subscription -> {
			subscriptionsLatch.countDown();
			log.debug("Subscription received: {}", subscription.getSubscriptionHeaders());
		};
		taskExecutor.execute(() -> {
			stompTemplate.subscribe(Destinations.GAME_CLOSED, new GameClosedHandler(), defaultCallback);
			stompTemplate.subscribe(Destinations.GAME_HOSTED, new GameHostedHandler(), defaultCallback);
			stompTemplate.subscribe(Destinations.GAME_JOINED, new GameJoinedHandler(), defaultCallback);
			stompTemplate.subscribe(Destinations.GAME_LEFT, new GameLeftHandler(), defaultCallback);
			stompTemplate.subscribe("/user" + Destinations.OPPONENT_JOINED, new OpponentJoinedHandler(), defaultCallback);
			stompTemplate.subscribe("/user" + Destinations.OPPONENT_REPORTS, new OpponentReportHandler(), defaultCallback);
			stompTemplate.subscribe("/user" + Destinations.PLAYER_REPORTS, new PlayerReportHandler(), defaultCallback);
		});
	}

	private void repaint() {
		gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
		if (playerBoard != null) {
			playerBoard.drawBoard();
		}
		if (opponentBoard != null) {
			opponentBoard.drawBoard();
		}
		drawLastWord();
	}

	public void setOnBackButtonPressed(EventHandler<ActionEvent> handler) {
		backButtonHandler = handler;
	}

	private void showWaitingAnimation(boolean show) {
		if (show) {
			contentPane.getChildren().add(webView);
			backButton.toFront();
		} else {
			contentPane.getChildren().remove(webView);
		}
	}

	private class GameClosedHandler implements StompFrameHandler {

		@Override
		public Type getPayloadType(StompHeaders headers) {
			return Game.class;
		}

		@Override
		public void handleFrame(StompHeaders headers, Object payload) {
			handleMessage((Game) payload);
		}

		private void handleMessage(Game game) {
			log.debug("{} closed Game {}", game.getPlayerOne(), game.getId());
			if (game.getPlayerOne().getUsername().equals(username)) {
				myGame = null;
			}
		}
	}

	private class GameHostedHandler implements StompFrameHandler {

		@Override
		public Type getPayloadType(StompHeaders headers) {
			return Game.class;
		}

		@Override
		public void handleFrame(StompHeaders headers, Object payload) {
			handleMessage((Game) payload);
		}

		private void handleMessage(Game game) {
			log.debug("{} hosted Game {}", game.getPlayerOne(), game.getId());
			if (game.getPlayerOne().getUsername().equals(username)) {
				myGame = game;
				playerBoard = new PlayerBoard(canvas, 50, 50, game.getWordLength());
				opponentBoard = new OpponentBoard(canvas, 50 + Board.WIDTH + 50, 50, game.getWordLength());
			}
		}
	}

	private class GameJoinedHandler implements StompFrameHandler {

		@Override
		public Type getPayloadType(StompHeaders headers) {
			return Game.class;
		}

		@Override
		public void handleFrame(StompHeaders headers, Object payload) {
			handleMessage((Game) payload);
		}

		private void handleMessage(Game game) {
			log.debug("{} joined {}'s game", game.getPlayerTwo(), game.getPlayerOne());
			if (game.getPlayerTwo().getUsername().equals(username)) {
				myGame = game;
				playerBoard = new PlayerBoard(canvas, 50, 50, game.getWordLength());
				opponentBoard = new OpponentBoard(canvas, 50 + Board.WIDTH + 50, 50, game.getWordLength());
			}
		}
	}

	private class GameLeftHandler implements StompFrameHandler {

		@Override
		public Type getPayloadType(StompHeaders headers) {
			return GameLeftMessage.class;
		}

		@Override
		public void handleFrame(StompHeaders headers, Object payload) {
			handleMessage((GameLeftMessage) payload);
		}

		private void handleMessage(GameLeftMessage message) {
			final Game game = message.getGame();
			final String gameLeaver = message.getGameLeaver().getUsername();
			log.debug("{} left {}'s game", gameLeaver, game.getPlayerOne());
			if (gameLeaver.equals(username) || gameLeaver.equals(opponentUsername)) {
				Platform.runLater(() -> {
					if (gameLeaver.equals(username)) {
						myGame = null;
					}
					clearBoards(true);
					showWaitingAnimation(true);
					opponentUsername = null;
					lastWord = null;
					repaint();
				});
			}
		}
	}

	private class OpponentJoinedHandler implements StompFrameHandler {

		@Override
		public Type getPayloadType(StompHeaders headers) {
			return String[].class;
		}

		@Override
		public void handleFrame(StompHeaders headers, Object payload) {
			handleMessage((String[]) payload);
		}

		private void handleMessage(String[] message) {
			final String firstLetter = message[0];
			opponentUsername = message[1];
			Platform.runLater(() -> {
				clearBoards(true);
				newWord(firstLetter);
				showWaitingAnimation(false);
				repaint();
			});
		}
	}

	private class OpponentReportHandler implements StompFrameHandler {

		@Override
		public Type getPayloadType(StompHeaders headers) {
			return Report.class;
		}

		@Override
		public void handleFrame(StompHeaders headers, Object payload) {
			handleMessage((Report) payload);
		}

		private void handleMessage(Report report) {
			if (report.isCorrect()) {
				onCorrectGuess(report);
			} else {
				onIncorrectGuess(report);
			}
		}

		private void onCorrectGuess(Report report) {
			final String guess = report.getGuess();
			final String firstLetter = report.getFirstLetter();
			log.info("Opponent guessed correctly: " + guess);
			Platform.runLater(() -> {
				opponentBoard.addToScore(100);
				lastWord = guess;
				clearBoards(false);
				newWord(firstLetter);
				repaint();
			});
		}

		private void onIncorrectGuess(Report report) {
			final int[] result = report.getResult();
			log.info("Opponent result: " + Arrays.toString(result));
			Platform.runLater(() -> {
				opponentBoard.addResult(result);
				repaint();
			});
		}
	}

	private class PlayerReportHandler implements StompFrameHandler {

		@Override
		public Type getPayloadType(StompHeaders headers) {
			return Report.class;
		}

		@Override
		public void handleFrame(StompHeaders headers, Object payload) {
			handleMessage((Report) payload);
		}

		private void handleMessage(Report report) {
			if (report.isCorrect()) {
				onCorrectGuess(report);
			} else {
				onIncorrectGuess(report);
			}
		}

		private void onCorrectGuess(Report report) {
			final String guess = report.getGuess();
			final String firstLetter = report.getFirstLetter();
			log.info("I guessed correctly!");
			Platform.runLater(() -> {
				playerBoard.addToScore(100);
				lastWord = guess;
				clearBoards(false);
				newWord(firstLetter);
				repaint();
			});
		}

		private void onIncorrectGuess(Report report) {
			final String guess = report.getGuess();
			final int[] result = report.getResult();
			log.info("My result: " + Arrays.toString(result));
			Platform.runLater(() -> {
				if (myGame.isInvalid(result)) {
					playerBoard.addGuess("-".repeat(myGame.getWordLength()));
				} else {
					for (int i = 0; i < myGame.getWordLength(); i++) {
						if (result[i] == Game.CORRECT_CHARACTER_POSITION) {
							playerBoard.setProgress(i, guess.charAt(i));
						}
					}
					playerBoard.addGuess(guess);
				}
				playerBoard.addResult(result);
				repaint();
			});
		}
	}

}
