Source: Client/index.js

const net = require('net')
const {InvalidError} = require("../../Errors")
const EventEmitter = require('events')

const Channel = require("../Channel")
const User = require("../User")

const NickServ = require('../NickServ')

const Parser = require("../../Util/ParseMessage")


/**
 * New client
 * @class
 */
class Client extends EventEmitter{

	/**
	 * @param {Object} options - The options for the client
	 * @param {String} options.username - The username to use
	 * @param {String} options.hostname - The hostname to use
	 * @param {String} options.servername - The servername to use
	 * @param {String} options.realname - The realname for the client
	 * @param {String} options.pass - The pass to use
	 * @param {String} options.nick - The nick to use, defaults to username
	 * @param {Boolean} options.verbose - If verbose information should be outputted
	 * @param {Boolean} options.sasl - If the client should authenticate with SASL 
	 */
	constructor(options={}){
		super()

		if(!options.username){
			throw new InvalidError("An invalid username was supplied.")
		}

		this._clientData = {
			username: options.username,
			hostname: options.hostname||"0",
			servername: options.servername||"*",
			realname: options.realname||"CarpLib",
			pass: options.pass||new Date().valueOf().toString("36"),
			nick: options.nick||options.username
		}

		this._server = {}

		this._connection = null

		this._channels = new Map()

		this._verbose = options.verbose||false

		this._nickMod = 0

		this._users = new Map()

		this._ctcp = new Map()

		this._sasl = options.sasl||false

		this._supported = {
			channel: {
				idLength: [],
				length: 200,
				limit: [],
				modes: {a: '', b: '', c: '', d: ''}
			},
			kickLength: 0,
			maxList: [],
			maxTargets: [],
			modes: 3,
			nickLength: 9,
			topicLength: 0,
			usermodes: ''
		}

		this._prefixForMode = []
		this._modeForPrefix = []

		this._nickServ = null

		this._modes = ''
	}

	/**
	 * @type {Object}
	 */
	get info(){
		return this._clientData
	}

	/**
	 * @type {Map}
	 */
	get channels(){
		return this._channels
	}

	get users(){
		return this._users
	}

	/**
	 * Information about what the server supports
	 * @type {Object}
	 */
	get supported(){
		return this._supported
	}

	get NickServ(){
		return this._nickServ
	}

	/**
	 * Emits when the client is logged in (ready)
	 * @event Client#ready
	 * @type {Object}
	 */

	/*
	 * Emits when the client joins a channel
	 * @event Client#join
	 * @type {Object}
	 * @property {Object} data - The data emitted
	 * @property {String} data.channelName - The name of the channel joined
	 * @property {Channel} data.channel - The channel joined
	 */

	/**
	 * Emits when the client parts a channel
	 * @event Client#part
	 * @type {Object}
	 * @property {Object} data - The data emitted
	 * @property {String} data.channelName - The name of the channel joined
	 * @property {Channel} data.channel - The channel joined
	 */

	/**
	 * Emits when the client revieves a private message
	 * @event Client#message
	 * @type {Object}
	 * @property {User} user - The user that sent the message
	 * @property {String} message - The message that was sent
	 */

	 /**
	 * Emits when a the clients nick is changed
	 * @event Client#nick
	 * @type {Object}
	 * @property {String} oldNick - The old nickname of the user
	 * @property {String} newNick - The new nickname of the user
	 */

	/**
	 * Emits when a the client recieves an invite
	 * @event Client#invite
	 * @type {Object}
	 * @property {String} channelName - The name of the channel the client was invited to
	 * @property {String} from - The nick of the user that sent the invite
	 */

	/**
	 * Emits when the client gets their modes updated
	 * @event Client#+mode
	 * @type {Object}
	 * @property {Array.<String>} modes - The modes that were addded
	 */

	/**
	 * Emits when the client gets their modes updated
	 * @event Client#-mode
	 * @type {Object}
	 * @property {Array.<String>} modes - The modes that were removed
	 */

	/**
	 * Adds event listeners to the connection
	 * @function
	 * @private
	 */
	addEventListeners(){

		let buffer = new Buffer('')

		this.on('raw', message => {

			if(this._verbose){
				console.log(message)
			}

			let channelName = ""

			switch(message.command){
				case "RPL_WELCOME":
					let welcomeStringWords = message.args[1].split(/\s+/);
					this._clientData.hostMask = welcomeStringWords[welcomeStringWords.length - 1]
					this._clientData.nick = message.args[0]

					this.whois(this._clientData.nick).then(args => {
						this._clientData.nick = args.nick
						this._clientData.hostMask = `${args.user}@${args.host}`
						this._clientData.user = args.user

						this.emit("ready")	

					})
				break
				case "PING":
					this.sendCommand(`PONG ${message.args[0]}\n`)
				break
				case "ERR_NICKNAMEINUSE":
					this._nickMod++
					this.sendCommand(`NICK ${this._clientData.nick}${this._nickMod}\n`)
				break
				case "ERR_ERRONEUSNICKNAME":
					this.emit("error", message)
				break
				case "JOIN":
					channelName = message.args[0]

					if(message.user === this._clientData.user){
						
						if(!this._channels.get(channelName)){
							this._channels.set(channelName, new Channel(this._connection, channelName))
						}
						this.emit("join", {channelName: channelName, channel: this._channels.get(channelName)})
					}else{
						this._channels.get(channelName).handleRaw(message)
					}
				break
				case "PART":
					channelName = message.args[0]

					if(message.user === this._clientData.user){
						
						this.emit("part", {channelName: channelName, channel: this._channels.get(channelName)})
						this._channels.delete(channelName)
					}else{
						this._channels.get(channelName).handleRaw(message)
					}
				break
				case "PRIVMSG":
					if(message.args[1][0] === '\u0001' && message.args[1].lastIndexOf('\u0001') > 0){
						this._handleCTCP(message)
						this.emit("CTCP", message)
						break
					}
					if(message.args[0] === this._clientData.nick){

						let user = new User(message.nick, this._connection)
						user.set("prefix", message.prefix)
							.set("user", message.user)
							.set("host", message.host)

						this.emit("message", user, message.args[1])
					}else{
						this._channels.get(message.args[0]).handleRaw(message)
					}
				break
				case "NOTICE":
					if(message.user === "NickServ"){
						if(this._nickServ === null){
							this._nickServ = new NickServ(this._connection, this._clientData.nick)
						}

						if(this._nickServ !== null){
							this._nickServ.handleRaw(message)
						}
					}
				break
				case "TOPIC":
					channelName = message.args[0]
						
					if(this._channels.get(channelName)){
						this._channels.get(channelName).handleRaw(message)
					}
				break
				case "KICK":
					channelName = message.args[0]

					if(this._clientData.nick === message.args[1]){
						// Client was kicked
						// channelname, by, reason, channel
						this.emit("kick", channelName, message.nick, message.args[2], this._channels[channelName])
						this._channels.delete(channelName)
					}else{
						this._channels.get(channelName).handleRaw(message)
					}
				break
				case "RPL_NAMREPLY":
					channelName = message.args[2]
					this._channels.get(channelName).handleRaw(message)
				break
				case "RPL_ENDOFNAMES":
					
				break
				case "NICK":
					if(message.nick === this._clientData.nick){
						this._clientData.nick = message.args[0]
						if(this._nickServ !== null){
							this._nickServ.nick = message.args[0]
						}
						this.emit("nick", message.nick, message.args[0])
					}else{
						this._channels.forEach(channel => {
							if(channel.users.get(message.nick) !== undefined){
								channel.handleRaw(message)
							}
						})
					}
				break
				case "INVITE":
					this.emit("invite", message.args[1], message.nick)
				break
				case "MODE":
					if(message.nick === this._clientData.nick){

						let split = message.args[1].split("")

						let action = split[0]

						split.shift()

						if(action === "+"){
							this._modes += split.join("")
						}else if(action === "-"){
							split.map(mode => {
								this._modes = this._modes.replace(mode, "")
							})
						}

						this.emit(`${action}mode`, split)
					}else{
						channelName = message.args[0]

						let channel = this._channels.get(channelName)

						if(channel !== undefined){
							channel.handleRaw(message)
						}
					}
				break
				case "RPL_ISUPPORT":
					message.args.map(arg => {
						let match = arg.match(/([A-Z]+)=(.*)/)

						if(match){
							let param = match[1]
							let value = match[2]

							switch(param){
								case "CHANLIMIT":
									value.split(",").map(val => {
										val = val.split(":")
										this._supported.channel.limit[val[0]] = parseInt(val[1])
									})
								break
								case "CHANMODES":
									value = value.split(",")
									let type = "abcd".split("")

									for(let i=0;i<type.length;i++){
										this._supported.channel.modes[type[i]] += value[i]
									}
								break
								case "CHANTYPES":
									this._supported.channel.types = value
								break
								case "CHANNELLEN":
									this._supported.channel.length = parseInt(value)
								break
								case "IDCHAN":
									value.split(",").each(val => {
										val = val.split(":")
										this._supported.channel.idLength[value[0]] = val[1]
									})
								break
								case "KICKLEN":
									this._supported.kickLength = value
								break
								case "MAXLIST":
									value.split(',').map(val => {
										val = val.split(':')
										this._supported.maxList[val[0]] = parseInt(val[1])
									})
								break
								case "NICKLEN":
									this._supported.nickLength = parseInt(value)
								break
								case "PREFIX":
									match = value.match(/\((.*?)\)(.*)/)
									if(match){
										match[1] = match[1].split('')
										match[2] = match[2].split('')
										while(match[1].length){
											this._modeForPrefix[match[2][0]] = match[1][0]
											this._supported.channel.modes.b += match[1][0]
											this._prefixForMode[match[1].shift()] = match[2].shift()
										}
									}
								break
								case "STATUSMSG":

								break
								case "TARGMAX":
									value.split(",").map(val => {
										val = val.split(":")
										val[1] = (!val[1])?0:parseInt(val[1])

										this._supported.maxTargets[val[0]] = val[1]
									})
								break
								case "TOPICLEN":
									this._supported.topicLength = parseInt(value)
								break
							}
						}
					})
				break

				// SASL \\

				case "CAP":
					if(message.args[0] === "*" && message.args[1] === "ACK" && message.args[2] === "sasl "){
						this.sendCommand("AUTHENTICATE PLAIN\n")
					}
				break
				case "AUTHENTICATE":
					if(message.args[0] === '+'){
						let userString = new Buffer(`${this._clientData.nick}\0${this._clientData.username}\n${this._clientData.pass}`).toString("base64")

						this.sendCommand(`AUTHENTICATE ${userString}\n`)
					}
				break
				case "903":
					this.sendCommand(`CAP END\n`)
				break
				case "ERR_SASLFAIL":
					this.emit("error", "SASL Authentication failed")
				break


				// WHOIS \\

				case "RPL_AWAY":
					this._addWhois(message.args[1], 'away', message.args[2], true)
				break
				case "RPL_WHOISUSER":
					this._addWhois(message.args[1], 'user', message.args[2])
					this._addWhois(message.args[1], 'host', message.args[3])
					this._addWhois(message.args[1], 'realname', message.args[5])
				break
				case "RPL_WHOISIDLE":
					this._addWhois(message.args[1], 'idle', message.args[2])
				break
				case "RPL_WHOISCHANNELS":
					this._addWhois(message.args[1], 'channels', message.args[2].trim().split(/\s+/))
				break
				case "RPL_WHOISSERVER":
					this._addWhois(message.args[1], 'server', message.args[2])
					this._addWhois(message.args[1], 'serverinfo', message.args[3])
				break
				case "RPL_WHOISOPERATOR":
					this._addWhois(message.args[1], 'operator', message.args[2])
				break
				case "330":
					this._addWhois(message.args[1], 'account', message.args[2])
					this._addWhois(message.args[1], 'accountinfo', message.args[2])
				break
				case "RPL_ENDOFWHOIS":
					this.emit('whois', this._getWHOIS(message.args[1]))
				break
			}
		})

		// Gets data from net, parses it and emits it
		this._connection.on('data', (chunk) => {

			let message = ""
			if(typeof(chunk) === 'string'){
				buffer += chunk
			}else{
				buffer = Buffer.concat([buffer, chunk])
			}

			let lines = buffer.toString().split(/\r?\n|\r/)

			if(lines.pop()){
				// if buffer is not ended with \r\n, there's more chunks.
				return
			}else{
				buffer = new Buffer('')
			}

			let s = this

			lines.forEach(function iterator(line){
				if(line.length){
					message = Parser(line)
					
					s.emit('raw', message)
				}
			})
		})

		/**
		 * Emits when the client is disconnected
		 * @event Client#disconnected
		 * @type {Object}
		 * 
		 */
		this._connection.on('end', () => {
			this.emit('disconnected')
		})

		/**
		 * Emits when the connection gets an error
		 * @event Client#error
		 * @type {Object}
		 * @param {Error} e - The Error gotten
		 */
		this._connection.on('error', (e) => {
			this.emit('error', e)
		})
	}

	_getWHOIS(nick){
		this._users.get(nick).addWhois('nick', nick)
		let whoisData = this._users.get(nick).whois

		this._users.delete(nick)

		return whoisData
	}

	_addWhois(nick, key, value, onlyIfExists){
		if(onlyIfExists && !this._users.get(nick)){
			return
		}
		if(!this._users.get(nick)){
			this._users.set(nick, new User(nick))
		}

		this._users.get(nick).addWhois(key, value)
	}

	/**
	 * Sends a raw command to the server
	 * @private
	 * @function
	 * @param {String} command - The command to send
	 * @author Mackan
	 */
	sendCommand(command){
		this._connection.write(command)
	}

	/**
	 * Connects to a server
	 * @function
	 * @param {String} address - The address of the server to connect to
	 * @param {Number} port - The port to connect to
	 * @author Mackan
	 */
	connect(address=null, port=6667){
		if(address === null || address === undefined){
			throw new InvalidError("An invalid server address was supplied.")
		}else{
			if(isNaN(port)){
				throw new RangeError("Port must be numeric.")
			}else{
				this._connection = net.createConnection(port, address, () => {

					this._connection.setEncoding('utf8')

					console.log(`Connected to ${address}/${port}`)
					this.addEventListeners()

					if(this._sasl){
						this.sendCommand(`CAP REQ :sasl\n`)
					}else{
						this.sendCommand(`PASS ${this._clientData.pass}\n`)
					}
					this.sendCommand(`NICK ${this._clientData.nick}\n`)

					this.sendCommand(`USER ${this._clientData.username} 0 * ${this._clientData.realname}\n`)

					this.emit("connected")
				})
			}
		}
	}

	/** 
	 * Disconnects from the server
	 * @function
	 * @author Mackan
	 */
	disconnect(){
		if(this._connection !== null){
			this._connection.end()
		}
	}

	/**
	 * Sends a quit command to the server
	 * @function
	 * @param {?String} message - The message to quit with
	 * @author Mackan
	 */
	quit(message){

		let command = "QUIT"

		if(message !== undefined && message !== null){
			command += ` :${message}`
		}

		this.sendCommand(command)
		this._connection.end()
	}

	/**
	 * Joins channels
	 * @function
	 * @param {...String} channel - The channel to join
	 * @author Mackan
	 */
	join(...channels){
		this.sendCommand(`JOIN ${channels.join(",")}\n`)
	}

	/**
	 * Gets the whois of a user
	 * @function
	 * @param {String} nick - The user to check
	 * @returns {Promise.<Object>}
	 * @author Mackan
	 */
	whois(nick){
		return new Promise((resolve, reject) => {
			this.once('whois', info => {
				if(info.nick.toLowerCase() === nick.toLowerCase()){
					resolve(info)
				}
			})

			this.sendCommand(`WHOIS ${nick}\n`)
		})
	}

	/*
	 * Emits when the client revieves a ctcp message
	 * @event Client#ctcp
	 * @type {Object}
	 * @property {String} from - The nick of the user that sent the message
	 * @property {String} to - Who the message is for
	 * @property {String} type - The CTCP command
	 * @property {?Array.<String>} - Arguments
	 */
	_handleCTCP(message){
		let from = message.nick
		let to = message.args[0]
		let user = new User(from, this._connection)

		let text = message.args[1]
		text = text.slice(1)
		text = text.slice(0, text.indexOf('\u0001'))
		let parts = text.split(' ')

		let type = parts[0].toUpperCase()
		parts.shift()

		if(this._ctcp.get(type)){
			if(typeof(this._ctcp.get(type)) === 'function'){
				let value = this._ctcp.get(type).call(this, parts)

				if(typeof(value) !== undefined && typeof(value) !== undefined){
					user.sendCTCP(type, value)
				}
			}else{
				user.sendCTCP(type, this._ctcp.get(type))
			}
		}

		this.emit('ctcp', from, to, type, parts)
	}

	/**
	 * Adds a CTCP response to the client
	 * @function
	 * @param {String} name - The ctcp command
	 * @param {String} value - What to respond with
	 */
	addCTCP(name, value){
		this._ctcp.set(name.toUpperCase(), value)
	}

	/**
	 * Updates the modes of the client
	 * @function
	 * @param {String} mode - What modes to update
	 */
	mode(mode){
		this.sendCommand(`MODE ${this._clientData.nick} ${mode}\n`)
	}

	/**
	 * Sends a notice to a user
	 * @function
	 * @param {String} nick - The nick of the user to send the message to
	 * @param {String} message - The message to send
	 */
	notice(nick, message){
		this.sendCommand(`NOTICE ${nick} ${message}\n`)
	}
}

module.exports = Client