const HEIGHT = 300;
const WIDTH = 250;
const CHARACTER_HEIGHT = 50;
const MARGIN_TOP = 100;
const MARGIN_BOTTOM = 75;

let client = null;
let sessionId = null;

const vm = new Vue({
	el: '#vue-app',
	data: {
		players: [],
		games: [],
		messages: [],
		username: null,
		usernameError: '',
		myGame: null,
		myScore: 0,
		myGuess: '',
		myGuesses: [],
		myProgress: [],
		myResults: [],
		opponentScore: 0,
		opponentResults: [],
		opponentUsername: null,
		lastWord: null
	},
	computed: {
		inGame: function() {
			return this.myGame !== null;
		},
		inStartedGame: function() {
			return this.myGame !== null && this.myGame.started === true;
		},
		characterWidth: function() {
			return this.myGame === null ? 50 : (WIDTH / this.myGame.wordLength);
		},
		wordLength: function() {
			return this.myGame === null ? 5 : this.myGame.wordLength;
		}
	},
	directives: {
		autoscroll: {
			bind: function(element, binding) {
				const observer = new MutationObserver(scrollToBottom);
				const config = { childList: true };
				observer.observe(element, config);

				function scrollToBottom() {
					element.scrollTop = element.scrollHeight;
				}
			}
		}
	},
	methods: {
		drawMyBoard: function(ctx) {
			const x = 25;
			const y = MARGIN_TOP;
			this.drawUsername(ctx, x, y, this.username);
			this.drawScore(ctx, x, y, this.myScore);
			this.drawInput(ctx, x, y, this.myGuess);
			const yStart = this.drawGuesses(ctx, x, y, this.myGuesses, this.myResults);
			this.drawHint(ctx, x, yStart, this.myProgress);
			this.drawGrid(ctx, x, y);
		},
		drawOpponentBoard: function(ctx) {
			const x = 325;
			const y = MARGIN_TOP;
			this.drawUsername(ctx, x, y, this.opponentUsername);
			this.drawScore(ctx, x, y, this.opponentScore);
			this.drawResults(ctx, x, y, this.opponentResults);
			this.drawGrid(ctx, x, y);
		},
		drawLastWord: function(canvas, ctx) {
			if (this.lastWord) {
				const x = canvas.width / 2;
				const y = canvas.height - MARGIN_BOTTOM / 2;
				ctx.fillStyle = 'black';
				ctx.fillText('Previous word: ' + this.lastWord.toUpperCase(), x, y);
			}
		},
		drawUsername: function(ctx, x, y, username) {
			const usernameX = x + WIDTH / 2;
			const usernameY = y - 60;
			ctx.fillStyle = 'black';
			ctx.fillText(username, usernameX, usernameY);
		},
		drawScore: function(ctx, x, y, score) {
			const scoreX = x + WIDTH / 2;
			const scoreY = y - 25;
			ctx.fillStyle = 'black';
			ctx.fillText(score, scoreX, scoreY);
		},
		drawGrid: function(ctx, xOrigin, yOrigin) {
			ctx.beginPath();
			for (let x = 0; x <= WIDTH; x += this.characterWidth) {
				ctx.moveTo(xOrigin + x, yOrigin);
				ctx.lineTo(xOrigin + x, yOrigin + HEIGHT);
			}
			for (let y = 0; y <= HEIGHT; y += CHARACTER_HEIGHT) {
				ctx.moveTo(xOrigin, yOrigin + y);
				ctx.lineTo(xOrigin + WIDTH, yOrigin + y);
			}
			ctx.strokeStyle = 'black';
			ctx.stroke();
		},
		drawInput: function(ctx, xOrigin, yOrigin, input) {
			ctx.fillStyle = 'green';
			let x = xOrigin + this.characterWidth * 0.5;
			let y = yOrigin + CHARACTER_HEIGHT * 0.5;
			for (let i = 0; i < input.length; i++) {
				ctx.fillText(input[i], x, y);
				x += this.characterWidth;
			}
		},
		drawGuesses: function(ctx, xOrigin, yOrigin, guesses, results) {
			let y = yOrigin + CHARACTER_HEIGHT * 1.5;
			const numGuesses = Math.min(4, guesses.length);
			for (let i = 0; i < numGuesses; i++) {
				let x = xOrigin + this.characterWidth * 0.5;
				const guess = guesses[guesses.length - numGuesses + i];
				const result = results[results.length - numGuesses + i];
				for (let j = 0; j < this.wordLength; j++) {
					if (result[j] === 1) {
						ctx.fillStyle = 'yellow';
						ctx.fillRect(x - this.characterWidth * 0.5, y - CHARACTER_HEIGHT * 0.5, this.characterWidth, CHARACTER_HEIGHT);
					} else if (result[j] === 2) {
						ctx.fillStyle = 'orange';
						ctx.fillRect(x - this.characterWidth * 0.5, y - CHARACTER_HEIGHT * 0.5, this.characterWidth, CHARACTER_HEIGHT);
					}
					ctx.fillStyle = 'green';
					ctx.fillText(guess[j], x, y);
					x += this.characterWidth;
				}
				y += CHARACTER_HEIGHT;
			}
			return y;
		},
		drawResults: function(ctx, xOrigin, yOrigin, results) {
			let y = yOrigin + CHARACTER_HEIGHT * 1.5;
			const numResults = Math.min(4, results.length);
			for (let i = 0; i < numResults; i++) {
				let x = xOrigin + this.characterWidth * 0.5;
				const result = results[results.length - numResults + i];
				for (let j = 0; j < this.wordLength; j++) {
					if (result[j] === 1) {
						ctx.fillStyle = 'yellow';
						ctx.fillRect(x - this.characterWidth * 0.5, y - CHARACTER_HEIGHT * 0.5, this.characterWidth, CHARACTER_HEIGHT);
					} else if (result[j] === 2) {
						ctx.fillStyle = 'orange';
						ctx.fillRect(x - this.characterWidth * 0.5, y - CHARACTER_HEIGHT * 0.5, this.characterWidth, CHARACTER_HEIGHT);
					} else if (result[j] === 9) {
						ctx.fillStyle = 'green';
						ctx.fillText('-', x, y);
					}
					x += this.characterWidth;
				}
				y += CHARACTER_HEIGHT;
			}
			return y;
		},
		drawHint: function(ctx, xOrigin, yOrigin, progress) {
			let x = xOrigin + this.characterWidth * 0.5;
			for (let i = 0; i < this.wordLength; i++) {
				ctx.fillText(progress[i], x, yOrigin);
				x += this.characterWidth;
			}
		},
		getGame: function(gameId) {
			for (let i = 0; i < this.games.length; i++) {
				if (this.games[i].id === gameId) {
					return this.games[i];
				}
			}
			return null;
		},
		hostGame5: function(e) {
			client.publish({destination: '/app/hostGame5'})
		},
		hostGame6: function(e) {
			client.publish({destination: '/app/hostGame6'})
		},
		joinGame: function(e) {
			// Discard 'game-' prefix
			const buttonId = e.target.id;
			const gameId = buttonId.substr(5);
			client.publish({destination: '/app/joinGame', body: gameId})
		},
		leaveGame: function(e) {
			client.publish({destination: '/app/leaveGame'})
		},
		removeGame: function(gameId) {
			let indexToRemove = null;
			for (let i = 0; i < this.games.length; i++) {
				if (this.games[i].id === gameId) {
					indexToRemove = i;
					break;
				}
			}
			this.games.splice(indexToRemove, 1);
		},
		removePlayer: function(name) {
			let indexToRemove = null;
			for (let i = 0; i < this.players.length; i++) {
				if (this.players[i].username === name) {
					indexToRemove = i;
					break;
				}
			}
			this.players.splice(indexToRemove, 1);
		},
		onCanvasKeydown: function(e) {
			if (e.key === 'Backspace') {
				e.preventDefault();
				this.myGuess = this.myGuess.substr(0, this.myGuess.length - 1);
				this.repaint();
			} else if (e.key === 'Enter') {
				e.preventDefault();
				if (this.myGuess.length === this.wordLength) {
					client.publish({destination: '/app/guess', body: this.myGuess})
					this.myGuess = '';
					this.repaint();
				}
			}
		},
		onCanvasKeypress: function(e) {
			const key = e.key.toUpperCase();
			if (this.myGuess.length < this.wordLength && isCharacter(key)) {
				this.myGuess += key;
				this.repaint();
			}
		},
		onChatKeypress: function(e) {
			const messageInput = e.target;
			if (e.key === 'Enter') {
				// Shift+Enter -> new line
				if (!e.shiftKey) {
					e.preventDefault();
					const text = messageInput.value.trim();
					if (text.length === 0) {
						return;
					}
					messageInput.value = '';
					client.publish({destination: '/app/chat', body: text})
					addChatMessage(this.username, text);
				}
			}
		},
		repaint: function() {
			const canvas = document.getElementById('canvas');
			const ctx = canvas.getContext('2d');
			ctx.font = '25px Monospace';
			ctx.textBaseline = 'middle';
			ctx.textAlign = 'center';
			ctx.clearRect(0, 0, canvas.width, canvas.height);
			this.drawMyBoard(ctx);
			this.drawOpponentBoard(ctx);
			this.drawLastWord(canvas, ctx);
		},
		reset: function(firstLetter, clearScore) {
			if (!firstLetter) {
				firstLetter = '';
			}
			this.myGuess = '';
			this.myGuesses = [];
			this.myProgress = [firstLetter];
			for (let i = 1; i < this.wordLength; i++) {
				this.myProgress[i] = '';
			}
			this.myResults = [];
			this.opponentResults = [];
			if (clearScore) {
				this.myScore = 0;
				this.opponentScore = 0;
			}
		}
	},
	mounted: function() {
		document.getElementById('nicknameInput').focus();
	}
});

function afterConnected(stompConnectedFrame) {
	console.log('Connected to STOMP endpoint')
	let sessionIdSubscription = null;
	const sessionIdTopic = '/user/topic/sessionId';
	const sessionIdHandler = function(message) {
		console.log('Session ID: ' + message.body);
		sessionId = message.body;
		sessionIdSubscription.unsubscribe();
	}
	sessionIdSubscription = client.subscribe(sessionIdTopic, sessionIdHandler);
}

function main() {

	let webSocketProtocol = 'ws';
	if (location.protocol.startsWith('https')) {
		webSocketProtocol = 'wss';
	}

	client = new StompJs.Client({
		brokerURL: `${webSocketProtocol}://${location.host}/stomp`,
		connectHeaders: {},
		//reconnectDelay: 5000, // TODO: implement reconnect functionality
		reconnectDelay: 0, // Disable automatic reconnects
		heartbeatIncoming: 25000, // send PING every 25 seconds
		heartbeatOutgoing: 25000, // receive PONG every 25 seconds
		//debug: function (str) { console.log(str); } // Print debug logs
	});

	client.onConnect = function(frame) {
		afterConnected(frame);
	}

	// Will be invoked in case of error encountered at broker.
	// Bad login/passcode typically will cause an error.
	// Complaint brokers will set `message` header with a brief message. Body may contain details.
	// Compliant brokers will terminate the connection after any error.
	client.onStompError = function(frame) {
		console.log('Broker reported error: ' + frame.headers['message']);
		console.log('Additional details: ' + frame.body);
		const errorMessage = frame.headers['message'];
		addChatAnnouncement(errorMessage);
		addChatAnnouncement('Please reload the page!');
	}

	client.onWebSocketClose = function(wsCloseEvent) {
		console.log('WebSocket close event: ' + JSON.stringify(wsCloseEvent));
		addChatAnnouncement('WebSocket closed!');
		addChatAnnouncement('Please reload the page!');
	}

	client.onWebSocketError = function(wsErrorEvent) {
		console.log('WebSocket error event: ' + JSON.stringify(wsErrorEvent));
		addChatAnnouncement('WebSocket error!');
		addChatAnnouncement('Please reload the page!');
	}

	client.activate();

	let usernameSubscription = null;
	const usernameInput = document.getElementById('nicknameInput');
	const usernameTopic = '/user/topic/sessionUsername';
	const usernameHandler = function(message) {
		const response = JSON.parse(message.body);
		if (response.success === true) {
			console.log('Username: ' + response.username);
			vm.username = response.username;
			start();
		} else {
			vm.usernameError = response.errorMessage;
		}
	};

	usernameInput.addEventListener('keydown', function(e) {
		if (e.key === 'Enter') {
			e.preventDefault();
			if (sessionId === null) {
				vm.usernameError = 'Not connected to server';
				return;
			}
			const usernameValue = usernameInput.value.trim();
			if (usernameValue.length === 0) {
				vm.usernameError = 'Name cannot be empty';
				return;
			}
			if (usernameSubscription === null) {
				usernameSubscription = client.subscribe(usernameTopic, usernameHandler);
				client.subscribe('/topic/playerJoined', onPlayerJoined);
			}
			client.publish({destination: '/app/setUsername', body: usernameValue})
		}
	});
	usernameInput.addEventListener('keyup', function(e) {
		if (e.key === 'Enter') {
			return;
		}
		const usernameValue = usernameInput.value.trim();
		if (usernameValue.length !== 0) {
			vm.usernameError = '';
		}
	});
}

function start() {

	// Request permission to show notifications
	try {
		Notification.requestPermission().then(function(result) {
			console.log('Notification permission: ' + result);
		});
	} catch (error) {
		// Safari doesn't return a promise for requestPermissions and it throws a TypeError.
		// It takes a callback as the first argument instead.
		if (error instanceof TypeError) {
			Notification.requestPermission(() => {
				console.log('Notification permission: ' + Notification.permission);
			});
		} else {
			throw error;
		}
	}

	// Load initial data
	doHttpGet('/data', function(data) {
		const players = data.players;
		const games = data.games;
		for (let i = 0; i < games.length; i++) {
			const game = games[i];
			vm.games.push({
				id: game.id,
				playerOne: game.playerOne.username,
				playerTwo: game.playerTwo ? game.playerTwo.username : null,
				wordLength: game.wordLength,
				started: game.playerTwo !== null
			});
		}
		for (let i = 0; i < players.length; i++) {
			const player = players[i];
			vm.players.push({
				username: player.username
			});
		}
	});

	// Subscribe to updates
	client.subscribe('/topic/chat', onChat);
	client.subscribe('/topic/gameClosed', onGameClosed);
	client.subscribe('/topic/gameHosted', onGameHosted);
	client.subscribe('/topic/gameJoined', onGameJoined);
	client.subscribe('/topic/gameLeft', onGameLeft);
	client.subscribe('/topic/playerLeft', onPlayerLeft);
	client.subscribe('/user/topic/opponentJoined', onOpponentJoined);
	client.subscribe('/user/topic/opponentReports', onOpponentReport);
	client.subscribe('/user/topic/playerReports', onPlayerReport);
}

function addChatAnnouncement(body) {
	vm.messages.push({
		body: body
	});
	showNotification('Announcement', body);
}

function addChatMessage(sender, body) {
	vm.messages.push({
		sender: sender,
		body: body
	});
	showNotification(sender, body);
}

function doHttpGet(url, callback) {
	const xhr = new XMLHttpRequest();
	xhr.onreadystatechange = function() {
		if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
			const response = JSON.parse(xhr.responseText);
			callback(response);
		}
	};
	xhr.open('GET', url, true);
	xhr.send();
}

function isCharacter(key) {
	return key >= "A" && key <= "Z";
}

function onChat(message) {
	const chatMessage = JSON.parse(message.body);
	const messageSender = chatMessage.username;
	const messageBody = chatMessage.message;
	if (messageSender === null) {
		addChatAnnouncement(messageBody);
	} else if (messageSender === vm.username) {
		// Ignore messages sent by yourself
	} else {
		console.log('Message from ' + messageSender + ': ' + messageBody);
		addChatMessage(messageSender, messageBody);
	}
}

function onGameClosed(message) {
	const game = JSON.parse(message.body);
	const gameId = game.id;
	const playerOne = game.playerOne.username;
	console.log(playerOne + ' closed Game ' + gameId);
	if (playerOne === vm.username) {
		vm.myGame = null;
	}
	vm.removeGame(gameId);
}

function onGameHosted(message) {
	const game = JSON.parse(message.body);
	const gameId = game.id;
	const wordLength = game.wordLength;
	const playerOne = game.playerOne.username;
	console.log(playerOne + ' hosted Game ' + gameId);
	const vueGame = {
		id: gameId,
		playerOne: playerOne,
		wordLength: wordLength,
		started: false
	};
	vm.games.push(vueGame);
	if (playerOne === vm.username) {
		vm.myGame = vueGame;
	}
}

function onGameJoined(message) {
	const game = JSON.parse(message.body);
	const gameId = game.id;
	const wordLength = game.wordLength;
	const playerOne = game.playerOne.username;
	const playerTwo = game.playerTwo.username;

	console.log(playerTwo + ' joined ' + playerOne + "'s game");

	let vueGame = null;
	for (let i = 0; i < vm.games.length; i++) {
		if (vm.games[i].id === gameId) {
			vm.games[i].playerTwo = playerTwo;
			vm.games[i].wordLength = wordLength;
			vm.games[i].started = true;
			vueGame = vm.games[i];
			break;
		}
	}

	if (playerTwo === vm.username) {
		vm.myGame = vueGame;
	}
}

function onGameLeft(message) {
	const report = JSON.parse(message.body);
	const game = report.game;
	const gameId = game.id;
	const playerOne = game.playerOne.username;
	const gameLeaver = report.gameLeaver.username;
	console.log(gameLeaver + ' left ' + playerOne + "'s game");
	const previousPlayers = [];
	for (let i = 0; i < vm.games.length; i++) {
		if (vm.games[i].id === gameId) {
			previousPlayers.push(vm.games[i].playerOne);
			previousPlayers.push(vm.games[i].playerTwo);
			vm.games[i].playerOne = playerOne;
			vm.games[i].playerTwo = game.playerTwo ? game.playerTwo.username : null;
			vm.games[i].started = false;
			break;
		}
	}
	if (gameLeaver === vm.username) {
		vm.myGame = null;
	}
	if (previousPlayers.indexOf(vm.username) !== -1) {
		vm.opponentUsername = null;
		vm.lastWord = null;
		vm.repaint();
	}
}

function onOpponentJoined(message) {
	const report = JSON.parse(message.body);
	const firstLetter = report[0];
	vm.opponentUsername = report[1];
	console.log('Opponent username: ' + vm.opponentUsername);
	vm.reset(firstLetter, true);
	vm.repaint();
}

function onOpponentReport(message) {
	console.log('Opponent report: ' + message.body);
	const report = JSON.parse(message.body);
	if (report.correct === true) {
		const guess = report.guess;
		const firstLetter = report.firstLetter;
		vm.opponentScore = vm.opponentScore + 100;
		vm.lastWord = guess;
		vm.reset(firstLetter, false);
		vm.repaint();
	} else {
		const result = report.result;
		vm.opponentResults.push(result);
		vm.repaint();
	}
}

function onPlayerReport(message) {
	console.log('My report: ' + message.body);
	const report = JSON.parse(message.body);
	if (report.correct === true) {
		const guess = report.guess;
		const firstLetter = report.firstLetter;
		vm.myScore = vm.myScore + 100;
		vm.lastWord = guess;
		vm.reset(firstLetter, false);
		vm.repaint();
	} else {
		const guess = report.guess;
		const result = report.result;
		if (result[0] === 9) {
			const invalidGuess = '-'.repeat(vm.wordLength);
			vm.myGuesses.push(invalidGuess);
		} else {
			for (let i = 0; i < vm.wordLength; i++) {
				if (result[i] === 2) {
					vm.myProgress[i] = guess[i];
				}
			}
			vm.myGuesses.push(guess);
		}
		vm.myResults.push(result);
		vm.repaint();
	}
}

function onPlayerJoined(message) {
	const report = JSON.parse(message.body);
	const username = report[0];
	const numUsers = report[1];
	if (username === vm.username) {
		addChatAnnouncement('Welcome to Lingo!');
		if (numUsers === 1) {
			addChatAnnouncement('You are the only player online');
		} else {
			addChatAnnouncement('There are ' + numUsers + ' players online');
		}
	} else {
		addChatAnnouncement(username + ' joined');
		vm.players.push({
			username: username
		});
	}
}

function onPlayerLeft(message) {
	const username = message.body;
	addChatAnnouncement(username + ' left');
	vm.removePlayer(username);
}

function canShowNotification() {
	if (document.hidden === 'undefined' || document.hidden === false) {
		return false;
	}
	return Notification.permission === 'granted';
}

function showNotification(messageSender, messageBody) {
	if (canShowNotification()) {
		const title = messageSender;
		const options = {
			body : messageBody,
			icon : '/chat-bubble.png'
		};
		const notification = new Notification(title, options);
		setTimeout(function() {
			notification.close();
		}, 3000);
	}
}

main();
