import {Utils} from '../lib';
import {RoomGamePlayer, RoomGame} from "./room-game";
import type {RoomBattlePlayerOptions, RoomBattleOptions} from './room-battle';
import type {PrivacySetting, RoomSettings} from './rooms';

const BEST_OF_IN_BETWEEN_TIME = 40;

export class BestOfPlayer extends RoomGamePlayer<BestOfGame> {
	wins = 0;
	ready: boolean | null = null;
	options: Omit<RoomBattlePlayerOptions, 'user'> & {user: null};
	dcAutoloseTime: number | null = null;
	constructor(user: User | null, game: BestOfGame, num: number, options: RoomBattlePlayerOptions) {
		super(user, game, num);
		this.options = {...options, user: null};
	}
	avatar() {
		let avatar = Users.get(this.id)?.avatar;
		if (!avatar || typeof avatar === 'number') avatar = 'unknownf';
		const url = Chat.plugins.avatars?.Avatars.src(avatar) ||
			`https://${Config.routes.client}/sprites/trainers/${avatar}.png`;
		return url;
	}
	updateReadyButton() {
		const user = this.getUser();
		if (!user?.connected) return;

		this.dcAutoloseTime = null;
		const room = this.game.room;
		const battleRoom = this.game.games[this.game.games.length - 1]?.room as Room | undefined;
		const gameNum = this.game.games.length + 1;

		if (this.ready === false) {
			const notification = `|tempnotify|choice|Next game|It's time for game ${gameNum} in your best-of-${this.game.bestOf}!`;
			if (battleRoom && user.inRooms.has(battleRoom.roomid)) {
				battleRoom.send(notification);
			} else {
				this.sendRoom(notification);
			}
		} else {
			const notification = `|tempnotifyoff|choice`;
			battleRoom?.sendUser(user, notification);
			this.sendRoom(notification);
		}

		if (this.ready === null) {
			const button = `|c|~|/uhtml controls,`;
			this.sendRoom(button);
			battleRoom?.sendUser(user, button);
			return;
		}

		const cmd = `/msgroom ${room.roomid},/confirmready`;
		const button = `|c|~|/uhtml controls,<div class="infobox"><p style="margin:6px">Are you ready for game ${gameNum}, ${this.name}?</p><p style="margin:6px">` +
			(this.ready ?
				`<button class="button" disabled><i class="fa fa-check"></i> I'm ready!</button> &ndash; waiting for opponent...` :
				`<button class="button notifying" name="send" value="${cmd}">I'm ready!</button>`) +
			`</p></div>`;
		this.sendRoom(button);
		battleRoom?.sendUser(user, button);
	}
}

export class BestOfGame extends RoomGame<BestOfPlayer> {
	override readonly gameid = 'bestof' as ID;
	override allowRenames = false;
	override room!: GameRoom;
	bestOf: number;
	format: Format;
	winThreshold: number;
	options: Omit<RoomBattleOptions, 'players'> & {parent: Room, players: null};
	forcedSettings: {modchat?: string | null, privacy?: string | null} = {};
	ties = 0;
	games: {room: GameRoom, winner: BestOfPlayer | null | undefined, rated: number}[] = [];
	playerNum = 0;
	/** null = tie, undefined = not ended */
	winner: BestOfPlayer | null | undefined = undefined;
	/** when waiting between battles, this is the just-ended battle room, the one with the |tempnotify| */
	waitingBattle: GameRoom | null = null;
	nextBattleTimerEnd: number | null = null;
	nextBattleTimer: NodeJS.Timer | null = null;
	/** Does NOT control bestof's own timer, which is always-on. Controls timers in sub-battles. */
	needsTimer = false;
	score: number[] | null = null;
	constructor(room: GameRoom, options: RoomBattleOptions) {
		super(room);
		this.format = Dex.formats.get(options.format);
		this.bestOf = Number(Dex.formats.getRuleTable(this.format).valueRules.get('bestof'))!;
		this.winThreshold = Math.floor(this.bestOf / 2) + 1;
		this.title = this.format.name;
		if (!toID(this.title).includes('bestof')) {
			this.title += ` (Best-of-${this.bestOf})`;
		}
		this.room.bestOf = this;
		this.options = {
			...options,
			isBestOfSubBattle: true,
			parent: this.room,
			allowRenames: false,
			players: null,
		};
		for (const playerOpts of options.players) {
			this.addPlayer(playerOpts.user, playerOpts);
		}
		process.nextTick(() => this.nextGame());
	}
	override onConnect(user: User) {
		const player = this.playerTable[user.id];
		player?.sendRoom('|cantleave|');
		player?.updateReadyButton();
	}
	override makePlayer(user: User | null, options: RoomBattlePlayerOptions): BestOfPlayer {
		return new BestOfPlayer(user, this, ++this.playerNum, options);
	}
	override addPlayer(user: User, options: RoomBattlePlayerOptions) {
		const player = super.addPlayer(user, options);
		if (!player) throw new Error(`Failed to make player ${user} in ${this.roomid}`);
		this.room.auth.set(user.id, Users.PLAYER_SYMBOL);
		return player;
	}
	checkPrivacySettings(options: RoomBattleOptions & Partial<RoomSettings>) {
		let inviteOnly = false;
		const privacySetter = new Set<ID>([]);
		for (const p of options.players) {
			if (p.user) {
				if (p.inviteOnly) {
					inviteOnly = true;
					privacySetter.add(p.user.id);
				} else if (p.hidden) {
					privacySetter.add(p.user.id);
				}
				this.checkForcedUserSettings(p.user);
			}
		}

		if (privacySetter.size) {
			const room = this.room;
			if (this.forcedSettings.privacy) {
				room.setPrivate(false);
				room.settings.modjoin = null;
				room.add(`|raw|<div class="broadcast-blue"><strong>This best-of set is required to be public due to a player having a name starting with '${this.forcedSettings.privacy}'.</div>`);
			} else if (!options.tour || (room.tour?.allowModjoin)) {
				room.setPrivate('hidden');
				if (inviteOnly) room.settings.modjoin = '%';
				room.privacySetter = privacySetter;
				if (inviteOnly) {
					room.settings.modjoin = '%';
					room.add(`|raw|<div class="broadcast-red"><strong>This best-of set is invite-only!</strong><br />Users must be invited with <code>/invite</code> (or be staff) to join</div>`);
				}
			}
		}
	}
	checkForcedUserSettings(user: User) {
		this.forcedSettings = {
			modchat: this.forcedSettings.modchat || Rooms.RoomBattle.battleForcedSetting(user, 'modchat'),
			privacy: this.forcedSettings.privacy || Rooms.RoomBattle.battleForcedSetting(user, 'privacy'),
		};
		if (
			this.players.some(p => p.getUser()?.battleSettings.special) ||
			(this.options.rated && this.forcedSettings.modchat)
		) {
			this.room.settings.modchat = '\u2606';
		}
	}
	setPrivacyOfGames(privacy: PrivacySetting) {
		for (let i = 0; i < this.games.length; i++) {
			const room = this.games[i].room;
			const prevRoom = this.games[i - 1]?.room;
			const gameNum = i + 1;

			room.setPrivate(privacy);
			this.room.add(`|uhtmlchange|game${gameNum}|<a href="/${room.roomid}">${room.title}</a>`);
			room.add(`|uhtmlchange|bestof|<h2><strong>Game ${gameNum}</strong> of <a href="/${this.roomid}">a best-of-${this.bestOf}</a></h2>`).update();
			if (prevRoom) {
				prevRoom.add(`|uhtmlchange|next|Next: <a href="/${room.roomid}"><strong>Game ${gameNum} of ${this.bestOf}</strong></a>`).update();
			}
		}
		this.updateDisplay();
	}
	clearWaiting() {
		this.waitingBattle = null;
		for (const player of this.players) {
			player.ready = null;
			player.updateReadyButton();
		}
		if (this.nextBattleTimer) {
			clearInterval(this.nextBattleTimer);
			this.nextBattleTimerEnd = null;
		}
		this.nextBattleTimerEnd = null;
		this.nextBattleTimer = null;
	}
	getOptions(): RoomBattleOptions | null {
		const players = this.players.map(player => ({
			...player.options,
			user: player.getUser()!,
		}));
		if (players.some(p => !p.user)) {
			return null;
		}
		return {
			...this.options,
			players,
		};
	}
	nextGame() {
		const prevBattleRoom = this.waitingBattle;
		if (!prevBattleRoom && this.games.length) return; // should never happen
		this.clearWaiting();

		const options = this.getOptions();
		if (!options) {
			for (const p of this.players) {
				if (!p.getUser()) {
					// tbc this isn't just being offline, it's changing name or being offline for 15 minutes
					this.forfeitPlayer(p, ` lost by being unavailable at the start of a game.`);
					return;
				}
			}
			throw new Error(`Failed to get options for ${this.roomid}`);
		}
		const battleRoom = Rooms.createBattle(options);
		// shouldn't happen even in lockdown
		if (!battleRoom) throw new Error("Failed to create battle for " + this.title);
		this.games.push({
			room: battleRoom,
			winner: undefined,
			rated: battleRoom.rated,
		});
		// the absolute result is what counts for rating
		battleRoom.rated = 0;
		if (this.needsTimer) {
			battleRoom.battle?.timer.start();
		}
		const gameNum = this.games.length;
		const p1 = this.players[0];
		const p2 = this.players[1];
		battleRoom.add(
			Utils.html`|html|<table width="100%"><tr><td align="left">${p1.name}</td><td align="right">${p2.name}</tr>` +
			`<tr><td align="left">${this.renderWins(p1)}</td><td align="right">${this.renderWins(p2)}</tr></table>`
		);
		battleRoom.add(
			`|uhtml|bestof|<h2><strong>Game ${gameNum}</strong> of <a href="/${this.roomid}">a best-of-${this.bestOf}</a></h2>`
		).update();

		this.room.add(`|html|<h2>Game ${gameNum}</h2>`);
		this.room.add(Utils.html`|uhtml|game${gameNum}|<a href="/${battleRoom.roomid}">${battleRoom.title}</a>`);
		this.updateDisplay();

		prevBattleRoom?.add(
			`|uhtml|next|Next: <a href="/${battleRoom.roomid}"><strong>Game ${gameNum} of ${this.bestOf}</strong></a>`
		).update();
	}
	renderWins(player: BestOfPlayer) {
		const wins = this.games.filter(game => game.winner === player).length;
		const winBuf = `<i class="fa fa-circle"></i> `.repeat(wins);
		const restBuf = `<i class="fa fa-circle-o"></i> `.repeat(this.winThreshold - wins);
		return player.num === 1 ? winBuf + restBuf : restBuf + winBuf;
	}
	updateDisplay() {
		const p1name = this.players[0].name;
		const p2name = this.players[1].name;
		let buf = Utils.html`<br /><strong>${p1name} and ${p2name}'s Best-of-${this.bestOf} progress:</strong><br />`;
		buf += '<table>';
		for (const p of this.players) {
			buf += Utils.html`<tr><td>${p.name}: </td><td>`;
			for (let i = 0; i < this.bestOf; i++) {
				if (this.games[i]?.winner === p) {
					buf += `<i class="fa fa-circle"></i>`;
				} else {
					buf += `<i class="fa fa-circle-o"></i>`;
				}
				if (i !== this.bestOf - 1) {
					buf += ` `;
				}
			}
			buf += `</td></tr>`;
		}
		buf += `</table><br /><br />`;
		buf += `<table><tr>`;

		for (const i of [0, null, 1]) {
			if (i === null) {
				buf += `<td></td>`;
				continue;
			}
			buf += Utils.html`<td><center><strong>${this.players[i].name}</strong></center></td>`;
		}

		buf += `</tr><tr>`;

		for (const i of [0, null, 1]) {
			if (i === null) {
				buf += `<td></td>`;
				continue;
			}
			const p = this.players[i];
			const mirrorLeftPlayer = !i ? ' style="transform: scaleX(-1)"' : "";
			buf += `<td><center>`;
			buf += `<img class="trainersprite"${mirrorLeftPlayer} src="${p.avatar()}" />`;
			buf += `</center></td>`;
		}

		buf += `</tr><tr>`;

		for (const i of [0, null, 1]) {
			if (i === null) {
				buf += `<td> vs </td>`;
				continue;
			}
			const team = Teams.unpack(this.players[i].options.team || "");
			if (!team || !Dex.formats.getRuleTable(this.format).has('teampreview')) {
				buf += `<td>`;
				buf += `<psicon pokemon="unknown" /> `.repeat(3);
				buf += `<br />`;
				buf += `<psicon pokemon="unknown" /> `.repeat(3);
				buf += `</td>`;
				continue;
			}
			const mirrorLeftPlayer = !i ? ' style="transform: scaleX(-1)"' : "";
			buf += `<td>`;
			for (const [j, set] of team.entries()) {
				if (j % 3 === 0 && j > 1) buf += `<br />`;
				buf += `<psicon pokemon="${set.species}"${mirrorLeftPlayer} />`;
			}
			buf += `</td>`;
		}
		buf += `</tr></table>`;
		this.room.add(`|fieldhtml|<center>${buf}</center>`);

		buf = this.games.map(({room, winner}, index) => {
			let progress = `being played`;
			if (winner) progress = Utils.html`won by ${winner.name}`;
			if (winner === null) progress = `tied`;
			return Utils.html`<p>Game ${index + 1}: <a href="/${room.roomid}"><strong>${progress}</strong></a></p>`;
		}).join('');
		if (this.winner) {
			buf += Utils.html`<p>${this.winner.name} won!</p>`;
		} else if (this.winner === null) {
			buf += `<p>The battle was tied.</p>`;
		}
		this.room.add(`|controlshtml|<center>${buf}</center>`);
		this.room.update();
	}

	override startTimer() {
		this.needsTimer = true;
		for (const {room} of this.games) {
			room.battle?.timer.start();
		}
	}

	onBattleWin(room: Room, winnerid: ID) {
		if (this.ended) return; // can happen if the bo3 is destroyed fsr

		const winner = winnerid ? this.playerTable[winnerid] : null;
		this.games[this.games.length - 1].winner = winner;
		if (winner) {
			winner.wins++;
			const loserPlayer = room.battle!.players.filter(p => p.num !== winner.num)[0];
			if (loserPlayer && loserPlayer.dcSecondsLeft <= 0) { // disconnection means opp wins the set
				return this.forfeit(loserPlayer.name, ` lost the series due to inactivity.`);
			}
			this.room.add(Utils.html`|html|${winner.name} won game ${this.games.length}!`).update();
			if (winner.wins >= this.winThreshold) {
				return this.end(winner.id);
			}
		} else {
			this.ties++;
			this.winThreshold = Math.floor((this.bestOf - this.ties) / 2) + 1;
			this.room.add(`|html|Game ${this.games.length} was a tie.`).update();
		}
		if (this.games.length >= this.bestOf) {
			return this.end(''); // overall tie
		}

		// no one has won, skip onwards
		this.promptNextGame(room);
	}
	promptNextGame(room: Room) {
		if (!room.battle || this.winner) return; // ???
		this.updateDisplay();
		this.waitingBattle = room;
		const now = Date.now();
		this.nextBattleTimerEnd = now + BEST_OF_IN_BETWEEN_TIME * 1000;
		for (const player of this.players) {
			player.ready = false;
			const dcAutoloseTime = now + room.battle.players[player.num - 1].dcSecondsLeft * 1000;
			if (dcAutoloseTime < this.nextBattleTimerEnd) {
				player.dcAutoloseTime = dcAutoloseTime;
			}
			player.updateReadyButton();
		}
		this.nextBattleTimer = setInterval(() => this.pokeNextBattleTimer(), 10000);
	}
	pokeNextBattleTimer() {
		if (!this.nextBattleTimerEnd || !this.nextBattleTimer) return; // ??
		if (Date.now() >= this.nextBattleTimerEnd) {
			return this.nextGame();
		}
		for (const p of this.players) {
			if (!p.ready) {
				const now = Date.now() - 100; // fudge to make rounding work better
				if (p.dcAutoloseTime && now > p.dcAutoloseTime) {
					return this.forfeit(p.name, ` lost the series due to inactivity.`);
				}
				const message = (p.dcAutoloseTime ?
					`|inactive|${p.name} has ${Chat.toDurationString(p.dcAutoloseTime - now)} to reconnect!` :
					`|inactive|${p.name} has ${Chat.toDurationString(this.nextBattleTimerEnd - now)} to confirm battle start!`
				);
				this.waitingBattle?.add(message);
				this.room.add(message);
			}
		}
		this.waitingBattle?.update();
		this.room.update();
	}
	confirmReady(user: User) {
		const player = this.playerTable[user.id];
		if (!player) {
			throw new Chat.ErrorMessage("You aren't a player in this best-of set.");
		}
		if (!this.waitingBattle) {
			throw new Chat.ErrorMessage("The battle is not currently waiting for ready confirmation.");
		}

		player.ready = true;
		player.updateReadyButton();
		const readyMsg = `||${player.name} is ready for game ${this.games.length + 1}.`;
		this.waitingBattle.add(readyMsg).update();
		this.room.add(readyMsg).update();
		if (this.players.every(p => p.ready)) {
			this.nextGame();
		}
	}
	override setEnded() {
		this.clearWaiting();
		super.setEnded();
	}
	end(winnerid: ID) {
		if (this.ended) return;
		this.setEnded();
		this.room.add(`|allowleave|`).update();
		const winner = winnerid ? this.playerTable[winnerid] : null;
		this.winner = winner;
		if (winner) {
			this.room.add(`|win|${winner.name}`);
		} else {
			this.room.add(`|tie`);
		}
		this.updateDisplay();
		this.room.update();
		const p1 = this.players[0];
		const p2 = this.players[1];
		this.score = this.players.map(p => p.wins);
		this.room.parent?.game?.onBattleWin?.(this.room, winnerid);
		// run elo stuff here
		let p1score = 0.5;
		if (winner === p1) {
			p1score = 1;
		} else if (winner === p2) {
			p1score = 0;
		}

		const {rated, room} = this.games[this.games.length - 1];
		if (rated) {
			void room.battle?.updateLadder(p1score, winnerid);
		}
	}
	override forfeit(user: User | string, message = '') {
		const userid = (typeof user !== 'string') ? user.id : toID(user);

		const loser = this.playerTable[userid];
		if (loser) this.forfeitPlayer(loser, message);
	}
	forfeitPlayer(loser: BestOfPlayer, message = '') {
		if (this.ended || this.winner) return false;

		this.winner = this.players.filter(p => p !== loser)[0];
		this.room.add(`||${loser.name}${message || ' forfeited.'}`);
		this.end(this.winner.id);

		const lastBattle = this.games[this.games.length - 1].room.battle;
		if (lastBattle && !lastBattle.ended) lastBattle.forfeit(loser.id, message);
		return true;
	}
	override destroy() {
		this.setEnded();
		for (const {room} of this.games) room.expire();
		this.games = [];
		for (const p of this.players) p.destroy();
		this.players = [];
		this.playerTable = {};
		this.winner = null;
	}
}
