Source: lib/BanchoUser.js

const EventEmitter = require("events").EventEmitter;
const Nodesu = require("nodesu");
const OutgoingBanchoMessage = require("./OutgoingBanchoMessage");
const BanchoWhoisReturn = require("./IrcCommands/whois/BanchoWhoisReturn");
const BanchoBotStatsCommand = require("./StatsCommand/BanchoBotStatsCommand");

/**
 * Bancho User
 * @description Optional properties are filled after fetching the API with fetchFromAPI().
 * 
 * @property {BanchoClient} banchojs Bancho.js client this user was instancied by
 * @property {string} ircUsername
 * @property {number} [id]
 * @property {string} [username]
 * @property {Date}   [joinDate]
 * @property {number} [count300]
 * @property {number} [count100]
 * @property {number} [count50]
 * @property {number} [playcount]
 * @property {number} [rankedScore]
 * @property {number} [totalScore]
 * @property {number} [ppRank]
 * @property {number} [level]
 * @property {number} [ppRaw]
 * @property {number} [accuracy]
 * @property {number} [countRankSS]
 * @property {number} [countRankS]
 * @property {number} [countRankA]
 * @property {number} [country]
 * @property {number} [totalSecondsPlayed]
 * @property {number} [ppCountryRank]
 * @property {Array.<nodesu.UserEvent>} [events]
 * @extends EventEmitter
 */
class BanchoUser extends EventEmitter {
	/**
	 * Creates an instance of BanchoUser.
	 * @param {BanchoClient} banchojs Bancho.js client this user was instancied by
	 * @param {string} ircUsername 
	 */
	constructor(banchojs, ircUsername) {
		super();
		this.banchojs = banchojs;
		this.ircUsername = ircUsername;
		this.whoisUserCallback = null;
		this.whoisChannelsCallback = null;
		this.whoisEndCallback = null;
		this.whoisUserNotFoundCallback = null;
		this.whoisPromise = null;
		this.wherePromise = null;
		this.statsPromise = null;
		this.listenersInitialized = false;
	}

	on() {
		if(!this.listenersInitialized) {
			this.banchojs.on("PM", (msg) => {
				if(msg.user == this)
					/** 
					 * Emitted when a message is received from a BanchoUser
					 * @event BanchoUser#message
					 * @type {PrivateMessage}
					 */
					this.emit("message", msg);
			});
			this.listenersInitialized = true;
		}
		super.on.apply(this, arguments);
	}

	/**
	 * Fetch the user from the osu! API if possible. Populates all the "optional" properties of BanchoUser.
	 * 
	 * @async
	 * @throws {Error} osu! API/no API key error
	 * @returns Promise<nodesu.User>
	 */
	fetchFromAPI() {
		if(this.banchojs.osuApi) {
			return new Promise((resolve, reject) => {
				this.banchojs.osuApi.user.get(this.ircUsername, this.banchojs.gamemode, null, Nodesu.LookupType.string).then((user) => {
					this.updateFromAPI(user);
					resolve(user);
				}, reject);
			});
		}
		else
			throw new Error("osu! API key is missing!");
	}

	updateFromAPI(user) {
		this.id = user.id;
		this.username = user.username;
		this.joinDate = user.joinDate;
		this.count300 = user.count300;
		this.count100 = user.count100;
		this.count50 = user.count50;
		this.playcount = user.playcount;
		this.rankedScore = user.rankedScore;
		this.totalScore = user.totalScore;
		this.ppRank = user.ppRank;
		this.level = user.level;
		this.ppRaw = user.ppRaw;
		this.accuracy = user.accuracy;
		this.countRankSS = user.countRankSS;
		this.countRankS = user.countRankS;
		this.countRankA = user.countRankA;
		this.country = user.country;
		this.totalSecondsPlayed = user.totalSecondsPlayed;
		this.ppCountryRank = user.ppCountryRank;
		this.events = user.events;
	}

	/**
	 * Returns true if the user is the client
	 * 
	 * @returns {boolean}
	 */
	isClient() {
		return (this.banchojs.username.toLowerCase() == this.ircUsername.toLowerCase());
	}

	/**
	 * Sends a PM to this user.
	 * 
	 * @async
	 * @throws {Error} If we're offline
	 * @param {string} message
	 * @returns {Promise<null>} Resolves when message is sent (rate-limiting)
	 */
	sendMessage(message) {
		return (new OutgoingBanchoMessage(this.banchojs, this, message)).send();
	}

	/**
	 * Sends an ACTION message to this user.
	 * 
	 * @async
	 * @throws {Error} If we're offline
	 * @param {string} message
	 * @returns {Promise<null>} Resolves when message is sent (rate-limiting)
	 */
	sendAction(message) {
		return (new OutgoingBanchoMessage(this.banchojs, this, `\x01ACTION ${message}\x01`)).send();
	}

	/**
	 * Fires an IRC WHOIS request to the server about this user. Only works on online users.
	 * Throws if the user can't be found or is offline.
	 * 
	 * @async
	 * @return {Promise<BanchoWhoisReturn>}
	 */
	whois() {
		if(!this.whoisPromise)
			this.whoisPromise = new Promise((resolve, reject) => {
				const whoisReturn = new BanchoWhoisReturn(this.ircUsername);
				this.whoisUserCallback = (userId) => whoisReturn.userId = userId;
				this.whoisChannelsCallback = (channels) => whoisReturn.channels = channels;
				const afterWhois = () => {
					this.whoisUserCallback = null;
					this.whoisChannelsCallback = null;
					this.whoisEndCallback = null;
					this.whoisUserNotFoundCallback = null;
					this.whoisPromise = null;
				};
				this.whoisEndCallback = () => {
					this.id = whoisReturn.userId;
					this.username = whoisReturn.name;
					resolve(whoisReturn);
					afterWhois();
				};
				this.whoisUserNotFoundCallback = () => {
					reject(new Error("User is offline"));
					afterWhois();
				};

				this.banchojs.send("WHOIS "+this.ircUsername);
			});
		return this.whoisPromise;
	}

	/**
	 * Fires a !where command to BanchoBot about this user. Only works on online users.
	 * Throws if the user can't be found or is offline.
	 * 
	 * @async
	 * @return {Promise<string>}
	 */
	where() {
		if(!this.wherePromise)
			this.wherePromise = new Promise((resolve, reject) => {
				const answerRegex = /(.+) is in (.+)/;
				const BanchoBot = this.banchojs.getUser("BanchoBot");
				const afterAnswer = () => {
					BanchoBot.removeListener("message", listener);
					this.wherePromise = null;
				};
				const listener = (message) => {
					if(message.message == "The user is currently not online.") {
						reject(new Error("User is offline"));
						return afterAnswer();
					}
					const m = answerRegex.exec(message.message);
					if(m && m[1] == this.ircUsername) {
						resolve(m[2]);
						afterAnswer();
					}
				};
				BanchoBot.on("message", listener);
				BanchoBot.sendMessage("!where "+this.ircUsername);
			});
		return this.wherePromise;
	}

	/**
	 * Fires a !stats command to BanchoBot about this user. Works on online and offline users.
	 * Will update the username, id, rankedScore, ppRank and playcount properties of this user.
	 * Status, levels and accuracy are stored in the returned object, as they are inaccurate data (missing decimals).
	 * It is recommended to get levels and accuracy by polling the osu! API instead.
	 * Throws if the user can't be found.
	 * 
	 * @async
	 * @return {Promise<BanchoBotStatsReturn>}
	 */
	stats() {
		if(!this.statsPromise)
			this.statsPromise = new Promise((resolve, reject) => {
				const statsCommand = new BanchoBotStatsCommand(this);
				statsCommand.run()
					.then(resolve, reject)
					.then(() => this.statsPromise = null);
			});
		return this.statsPromise;
	}

	get id() {
		return this._id;
	}

	set id(newId) {
		if(isNaN(newId))
			throw new Error("id needs to be a number!");
		if(this._id != null)
			this.banchojs.usersById.delete(this._id);
		this._id = newId;
		this.banchojs.usersById.set(this._id, this);
	}
}

module.exports = BanchoUser;