Source: Client.js

const Eris = require('eris')
const glob = require('glob')
const path = require('path')
const Logger = require('another-logger')
const reload = require('require-reload')(require)
const Command = require('./Command')
const Category = require('./Category')
const Component = require('./Component')
const builtinCommandRequirements = require('./requirements')

const DEFAULT_CATEGORY = 'Default'

// Logger
const logger = new Logger({
	label: 'Aghanim',
	timestamps: true,
	levels: {
		dev: { style: 'magenta' },
		commandrunerror: { text: 'command:run:error', style: 'red' },
		componentrunerror: { text: 'component:run:error', style: 'red' },
		commandadderror: { text: 'command:add:error', style: 'red' },
		componentadderror: { text: 'component:add:error', style: 'red' },
		categoryadderror: { text: 'category:run:error', style: 'red' },
	},
	// ignoredLevels: [this.devLogs ? '' : 'dev']
})
/**
 * Aghanim Client extends from Eris.Client
 * @extends Eris.Client
 */
class Client extends Eris.Client {
	/**
	* Create a client instance.
	* @class
	* @param {string} token - The token used to log into the bot.
	* @param {Object} options - Options to start the client with. This object is also passed to Eris.
	*     If there are a aghanim.config.js/json in project root, that will be loaded instead object passed to constructor.
	*     See Eris Client constructor options https://abal.moe/Eris/docs/Client
	* @param {string} [options.prefix = ''] - The prefix the bot will respond to in
	*     guilds for which there is no other confguration. (Currently everywhere)
	* @param {boolean} [options.allowMention = false] - Whether or not the bot can respond
	*     to messages starting with a mention of the bot.
	* @param {boolean} [options.ignoreBots = true] - Whether or not the bot ignoresBots. Default: true
	* @param {boolean} [options.ignoreSelf = true] - Whether or not the bot ignores self. Default: true
	* @param {string} [options.helpMessage = '**Help**'] - Title for default command help Message
	* @param {string} [options.helpMessageAfterCategories = '**Note**: Use \`${options.prefix}help <category>\` to see the commands']
	* Message after categories in default command help message are shown
	* @param {boolean} [options.helpDM = true] - Active direct message to default command help
	* @param {boolean} [options.helpEnable = true] - Enable/disable default command help
	* @param {boolean} [options.devLogs = false] - Enable/disable aghanim dev logs
	*/
	constructor(token, options = {}) {
		// Attempt to load options from aghanim.config.js(on) file
		try {
			options = require(`${process.cwd()}/aghanim.config`) /* eslint import/no-dynamic-require: "off", global-require : "off", no-param-reassign : "off" */
			logger.info('Loaded: aghanim.config.js(on)')
		} catch (err) { } /* eslint no-empty: "off" */

		super(token, options)
		options.devLogs = options.devLogs || false
		logger._config.ignoredLevels = [options.devLogs ? '' : 'dev'] /* eslint no-underscore-dangle: "off" */

		/** @prop {string} - The prefix the bot will respond to in guilds
		 * for which there is no other confguration. */
		this.prefix = options.prefix || ''
		/** @prop {Command[]} - An array of commands the bot will respond to. */
		this.commands = []
		/** @prop {Category[]} - Categories for commands. */
		this.categories = []
		/** @prop {Object<Component>} - Components. */
		this.components = {}
		/** @prop {Object} - Setup */
		this.setup = {}
		// /** @prop {Object} - Context Extension */
		// this.contextExtension = {}

		this._ready = false
		this._commandsRequirements = {}

		// /** @prop {boolean} - Whether or not the bot can respond to messages
		//  * starting with a mention of the bot. Defaults to true. */
		// this.allowMention = options.allowMention === null ? false : options.allowMention
		/** @prop {boolean} - Whether or not the bot ignores messages
		 sent from bot accounts. Defaults to true.
		 * @default true */
		this.ignoreBots = options.ignoreBots !== undefined ? options.ignoreBots : true
		/** @prop {boolean} - Ignore self
		* @default true */
		this.ignoreSelf = options.ignoreSelf !== undefined ? options.ignoreSelf : true

		this.once('ready', () => {
			if (this._ready) { return }
			this._ready = true
			/**
			 * @prop {RegExp} - The RegExp used to tell whether or not a message starts
			 *     with a mention of the bot. Only present after the 'ready' event.
			 */
			this.mentionPrefixRegExp = new RegExp(`^<@!?${this.user.id}>\\s?`)

			this.getOAuthApplication().then((app) => {
				/**
				 * @prop {object} - The OAuth application information returned by
				 *     Discord. Present some time after the ready event.
				 * @prop {string} description - Discord App description
				 * @prop {string} name - Discord App name
				 * @prop {string} owner - Discord App owner
				 * @prop {string} owner.id - Owner ID
				 * @prop {string} owner.username - Owner username
				 * @prop {string} owner.discriminator - Owner discriminator
				 * @prop {string} owner.avatar - Owner avatar
				 * @prop {boolean} bot_public - If app is public
				 * @prop {boolean} bot_require_code_grant -
				 * @prop {string} id - Discord App id
				 * @prop {string} icon - Discord App icon
				 */
				this.app = app
				/**
				 * Owner description
				 * @prop {string} id - Owner ID
				 * @prop {string} username - Owner username
				 * @prop {string} discriminator - Owner discriminator
				 * @prop {string} avatar - Owner avatar
				 * @prop {Ownersend} send - Send a message to Owner
				 */
				this.owner = Object.assign({}, this.app.owner)
				this.getDMChannel(this.owner.id).then((channel) => {
					/**
					 * Function to send messages to owner
					 * @callback Ownersend
					 * @param  {string|EmbedMessageObject} content - Message content to send
					 * @param  {object} file - File to send
					 */
					this.owner.send = function sendOwner(content, file) {
						channel.createMessage(content, file)
					}
				})

				this.handleEvent('ready')()
			})
		}).on('error', (err) => {
			logger.error(err)
			/**
			 * Fired when there are an error
			 * @event Client#aghanim:error
			 * @param {object} err - Error
			 * @param {Client} client - Client instance
			 */
			this.emit('aghanim:error', err, this)
		}).on('messageCreate', this.handleMessage)
			.on('messageReactionAdd', this.handleEvent('messageReactionAdd'))
			.on('messageReactionRemove', this.handleEvent('messageReactionRemove'))
			.on('guildCreate', this.handleEvent('guildCreate'))
			.on('guildDelete', this.handleEvent('guildDelete'))
			.on('guildMemberAdd', this.handleEvent('guildMemberAdd'))
			.on('guildMemberRemove', this.handleEvent('guildMemberRemove'))

		this.setup.helpMessage = `${options.helpMessage || '**Help**'}\n\n`
		this.setup.helpMessageAfterCategories = `${options.helpMessageAfterCategories || `**Note**: Use \`${this.prefix}help <category>\` to see those commands`}\n\n`
		this.setup.helpDM = options.helpDM || false
		if (!options.disableHelp) {
			// Add default help command to bot
			this.addCommand(new Command('help', {}, async (msg, args, client, command) => { /* eslint no-unused-vars: "off" */
				const categories = client.categories.map(c => c.name.toLowerCase())
				const query = args.from(1).toLowerCase()
				let { helpMessage } = client.setup
				if (categories.includes(query)) {
					const cmds = client.getCommandsOfCategories(query)
					if (!cmds) {
						helpMessage += client.categories.filter(c => !c.hide) /* eslint prefer-template: "off", max-len: "off" */
							.map(c => `**${c.name}** \`${client.prefix}help ${c.name.toLowerCase()}\` - ${c.help}`)
							.join('\n') + '\n\n' + client.setup.helpMessageAfterCategories
					} else {
						helpMessage += cmds.filter(c => !c.hide).map(c => `\`${client.prefix}${c.name}${c.args ? ' ' + c.args : ''}\` - ${c.help}${c.childs.length ? '\n' + c.childs.filter(s => !s.hide).map(s => `  · \`${s.name}${s.args ? ' ' + s.args : ''}\` - ${s.help}`).join('\n') : ''}`)
							.join('\n')
					}
				} else if (categories.length) {
					helpMessage += client.categories.filter(c => !c.hide).map(c => `**${c.name}** \`${client.prefix}help ${c.name.toLowerCase()}\` - ${c.help}`).join('\n') + '\n\n' + client.setup.helpMessageAfterCategories
				} else {
					const cmds = client.getCommandsOfCategories('Default')
					if (!cmds) {
						helpMessage += 'No commands'
					} else {
						helpMessage += cmds.filter(c => !c.hide).map(c => `\`${client.prefix}${c.name}${c.args ? ' ' + c.args : ''}\` - ${c.help}${c.childs.length ? '\n' + c.childs.filter(s => !s.hide).map(s => `  · \`${s.name}${s.args ? ' ' + s.args : ''}\` - ${s.help}`).join('\n') : ''}`)
							.join('\n')
					}
				}
				if (!client.setup.helpDM) {
					return msg.channel.createMessage(helpMessage)
				}
				return msg.author.getDMChannel().then(channel => channel.createMessage(helpMessage))
			}))
		}
	}

	/**
	 * Given a message, see if there is a command and process it if so.
	 * @param {Object} msg - The message object recieved from Eris.
	 * @return {*} - Returns
	 */
	async handleMessage(msg) {
		if (!this._ready) return
		if (!this.triggerMessageCreate(msg, this)) { return }
		this.handleEvent('messageCreate')(msg)

		if (this.ignoreBots && msg.author.bot) return
		if (this.ignoreSelf && msg.author.id === this.user.id) return

		const args = this.createCommandArgs(msg)
		if (!args) { return }
		const command = this.getCommandByName(args[0], args[1])
		if (!command) { return }
		// const context = Object.assign({
		// 	command,
		// 	client: this
		// }, this.contextExtension)
		try {
			/**
			 * Fired before a command is executed. Can't stop command of running
			 * @event Client#aghanim:command:prereq
			 * @param {object} msg - Eris Message object
			 * @param {args} args - Args object
			 * @param {Client} client - Client instance
			 * @param {Command} command - Command
			 */
			this.emit('aghanim:command:prereq', msg, args, this, command)
			await command.runHook('prereq', msg, args, this, command)
			const notpass = !(await this.checkRequirements(msg, args, this, command))
			if (notpass) return
			/**
			 * Fired before a command is executed. Can't stop command of running
			 * @event Client#aghanim:command:prerun
			 * @param {object} msg - Eris Message object
			 * @param {args} args - Args object
			 * @param {Client} client - Client instance
			 * @param {Command} command - Command
			 */
			this.emit('aghanim:command:prerun', msg, args, this, command)
			await command.runHook('prerun', msg, args, this, command)
			if (command.response) {
				switch (typeof command.response) {
				case 'string': {
					await msg.channel.createMessage(command.response)
					break
				}
				case 'function': {
					const response = command.response(msg, args, this, command)
					await msg.channel.createMessage(response)
					break
				}
				case 'object': {
					await msg.channel.createMessage(command.response)
					break
				}
				default: {}
				}
			} else if (command.responseDM) {
				switch (typeof command.responseDM) {
				case 'string': {
					await msg.channel.createMessage(command.responseDM)
					break
				}
				case 'function': {
					const responseDM = command.responseDM(msg, args, this, command)
					await msg.channel.createMessage(responseDM)
					break
				}
				case 'object': {
					await msg.channel.createMessage(command.responseDM)
					break
				}
				default: {}
				}
			} else {
				await command.run(msg, args, this, command)
			}
			/**
			 * Fired after a command is executed. Don't cant stop command of running
			 * @event Client#aghanim:command:executed
			 * @param {object} msg - Eris Message object
			 * @param {args} args - Args object
			 * @param {Client} client - Client instance
			 * @param {Command} command - Command
			 */
			this.emit('aghanim:command:executed', msg, args, this, command)
			await command.runHook('executed', msg, args, this, command)
		} catch (err) {
			/**
			 * Fired when a command got an error executing the run function
			 * @event Client#aghanim:command:error
			 * @param {object} err - Error
			 * @param {object} msg - Eris Message object
			 * @param {args} args - Args object
			 * @param {Client} client - Client instance
			 * @param {Command} command - Command
			 */
			logger.commandrunerror(`${command.name} - ${err} - ${err.stack}`)
			this.emit('aghanim:command:error', err, msg, args, this, command)
			try {
				await command.runHook('error', msg, args, this, command, err)
			} catch (errhook) {
				logger.commandrunerror(`${command.name} - ${errhook} - ${errhook.stack}`)
				this.emit('aghanim:command:error', errhook, msg, args, this, command)
			}
		}
	}

	handleEvent(eventname) {
		return (...args) => {
			Object.keys(this.components)
				.map(componentName => this.components[componentName])
				.filter(component => component[eventname] && component.enable)
				.forEach(async (component) => {
					try {
						await component[eventname](...args, this)
					} catch (err) {
						logger.componentrunerror(`${component.constructor.name} (${eventname}) - ${err}`)
						/**
						 * Fired when a component get an error to be executed
						 * @event Client#aghanim:component:error
						 * @param {object} err - Error
						 * @param {string} eventname - Name of Eris event
						 * @param {Client} client - Client instance
						 * @param {Component} component - Component
						 */
						this.emit('aghanim:component:error', err, eventname, this, component)
					}
				})
		}
	}

	/**
	 * Extend default args object.
	 * @param {args} args - Args object.
	 * @param {msg} msg - Eris Message.
	 * @param {Client} client - Client instance.
	 */
	extendCommandArgs(args, msg, client) { } /* eslint class-methods-use-this: "off" */

	_addFromDirectory(dirname, func) {
		if (!dirname.endsWith('/')) dirname += '/'
		const pattern = `${dirname}*.js`
		const filenames = glob.sync(pattern)
		filenames.forEach(filename => func(filename))
	}

	/**
	 * Register a command to the client.
	 * @param {Command | object} command - The command to add to the bot.
	 * @returns {Command} - Command added
	 */
	addCommand(command) { /* eslint consistent-return:"off" */
		if (!(command instanceof Command) && typeof command === 'object') { // allow command as object and create it
			command = new Command(command)
		}
		if (!(command instanceof Command)) throw new TypeError('Not a command') // throw error if not a Command instance or class extending of command
		if (!this.categories.find(c => c.name === command.category)) { // Check category exists or assing default category
			command.category = DEFAULT_CATEGORY
			logger.warn(`Category not found for ${command.name}. Established as ${DEFAULT_CATEGORY}`)
		}
		command.client = this // inject client on command
		const { requirements } = command
		command.requirements = [] // reset command.requirements
		mapCommandRequirement(this, command, requirements) /* eslint no-use-before-define: "off" */

		// Check if command exists already and throw error or add to client
		if (!command.childOf) {
			const commandExists = this.commands.find(c => c.names.some(cname => [command.name, ...command.aliases].includes(cname)))
			if (commandExists) {
				logger.commandadderror(`Command exists: ${command.name}`)
			} else {
				this.commands.push(command)
				logger.dev(`Command added: ${command.name}`)
				return command
			}
		} else { // Find parent command and add to client
			const parent = this.commands.find(c => c.names.includes(command.childOf))
			if (!parent) {
				throw new Error(`Parent command ${command.childOf} not found for ${command.name}`)
			} else {
				if (command.category !== parent.category) { // Set category as parent category if is different
					command.category = parent.category
					logger.warn(`${command.category} not same upcomand! Established as ${parent.category}`)
				}
				command.parent = parent
				parent.childs.push(command)
				logger.dev(`Subcommand added: ${command.name} from ${parent.name}`)
				return command
			}
		}
	}

	/**
	 * Load all the JS files in a directory and attempt to load them each as commands.
	 * @param {string} dirname - The location of the directory.
	 */
	addCommandDir(dirname) {
		this._addFromDirectory(dirname, filename => this.addCommandFile(filename))
	}

	/**
	 * Load a command exported from a file.
	 * @param {string} filename - The location of the file.
	 * @returns {Command} - Command added.
	 */
	addCommandFile(filename) {
		try {
			const commandLoaded = reload(filename)
			const command = this.addCommand(commandLoaded)
			if (command) {
				command.filename = filename
			}
			return command
		} catch (err) {
			logger.commandadderror(`${err.stack} on ${filename}`)
		}
	}

	/**
	 * Add a Command {@link Category}
	 * @param {string} name - Name for Category
	 * @param {string} help - Help Message
	 * @param {object} options - Options
	 * @param {object} options.hide - Options
	 * @param {object} options.restrict - Options
	 */
	addCategory(name, help, options) {
		const category = new Category(name, help, options)
		if (this.categories.find(c => c.name === category.name)) {
			logger.categoryadderror(`${category.name} exists`)
		} else {
			this.categories.push(category);
			logger.dev(`Category added: ${category.name}`)
		}
	}

	/**
	 * Add a Component
	 * @param {Component | object} component - Component {@link Component}
	 * @returns {Component} - Component added
	 */
	addComponent(component, options) {
		if (!(component instanceof Component) && typeof component === 'object') { // allow load components as object
			const componentObject = component
			if (!componentObject.name) throw new TypeError(`Component as object require an name => ${JSON.stringify(componentObject)}`)
			component = class extends Component {
				constructor(client, options) {
					super(client, options)
					if (typeof componentObject.constructor === 'function'){
						componentObject.constructor(client, options)
					}
				}
			}
			Object.defineProperty(component, 'name', { value: componentObject.name })
			Object.keys(componentObject).filter(key => key !== 'name').forEach(key => component.prototype[key] = componentObject[key])
		}
		if (!(component.prototype instanceof Component)) throw new TypeError(`Not a Component => ${component}`)
		if (this.components[component.name]) { throw new Error(`Component exists => ${component.name}`) }
		try {
			const instanceComponent = new component(this, options) /* eslint new-cap: "off" */
			if (instanceComponent.enable) {
				instanceComponent.name = component.name
				this.components[component.name] = instanceComponent
				logger.dev(`Component Added: ${component.name}`)
				return this.components[component.name]
			} else {
				logger.warn(`Component Disabled: ${component.name}`)
			}
		} catch (err) {
			logger.componentadderror(`${component.name} - ${err}`)
		}
	}

	/**
	 * Add component from file
	 * @param {string} filename Path to file
	 * @returns {Component} Component added
	 */
	addComponentFile(filename) {
		try {
			const componentClass = reload(filename)
			const component = this.addComponent(componentClass)
			if (component) {
				component.filename = filename
			}
			return component
		} catch (err) {
			logger.componentadderror(`${err} on ${filename}`)
		}
	}

	/**
	 * Add components from a directory
	 * @param {string} dirname Path to load components
	 */
	addComponentDir(dirname) {
		this._addFromDirectory(dirname, filename => this.addComponentFile(filename))
	}

	/**
	 * Define a requirement that can be added by commands
	 * @param  {(CommandRequirementObject|CommandRequirementFunction)} requirement - Requirement to define
	 */
	addCommandRequirement(requirement) {
		if (typeof requirement === 'object' && requirement.type) {
			this._commandsRequirements[requirement.type] = requirement
			return requirement
		} else if (typeof requirement === 'function') {
			this._commandsRequirements[requirement.name] = requirement
			return requirement
		} else {
			logger.error('Error adding command requirement')
		}
	}

	/**
	 * Add command requirement from file
	 * @param {string} filename Path to file
	 * @returns {CommandRequirement} CommandRequirement added
	 */
	addCommandRequirementFile(filename) {
		try {
			const requirementLoaded = reload(filename)
			if (typeof requirementLoaded === 'function') Object.defineProperty(requirementLoaded, 'name', { value: path.basename(filename, '.js') })
			const requirement = this.addCommandRequirement(requirementLoaded)
			if (requirement) {
				requirement.filename = filename
			}
			return requirement
		} catch (err) {
			logger.commandadderror(`${err} on ${filename}`)
		}
	}

	/**
	 * Add command requirements from a directory
	 * @param {string} dirname Path to load command requirements
	 */
	addCommandRequirementDir(dirname) {
		this._addFromDirectory(dirname, filename => this.addCommandRequirementFile(filename))
	}

	/**
	 * Reloads all commands that were loaded via `addCommandFile` and
	 * `addCommandDir`. Useful for development to hot-reload commands as you work
	 * on them.
	 */
	reloadCommands() { 
		logger.dev('Reloading commands...')
		const commands = this.commands.reduce((filenames, command) => {
			filenames.push(command.filename ? command.filename : command)
			if (command.childs.length > 0) {
				command.childs.forEach((subcommand) => {
					filenames.push(subcommand.filename ? subcommand.filename : subcommand)
				})
			}
			return filenames
		}, [])
		this.commands = []
		commands.forEach(command => typeof command === 'string' ? this.addCommandFile(command) : this.addCommand(command)) /* eslint no-confusing-arrow: 'off' */
	}

	/**
	 * Reloads all components that were loaded via `addComponentFile` and
	 * `addCommponentDir`. Useful for development to hot-reload commands as you work
	 * on them.
	 */
	reloadComponents() { 
		logger.dev('Reloading components...')
		const components = Object.keys(this.components).map(key => this.components[key]).reduce((filenames, component) => {
			if (component.filename) {
				filenames.push([component.filename, component.name || component.constructor.name])
			}
			return filenames
		}, [])

		components.forEach(([filename, name]) => {
			delete this.components[name]
			this.addComponentFile(filename)
		})
		this.handleEvent('ready')()
	}

	reloadCommandRequirements() {
		logger.dev('Reloading command requirements...')
		const filenamesRequirement = Object.keys(this._commandsRequirements).map(key => this._commandsRequirements[key]).reduce((filenames, requirement) => {
			if (requirement.filename) {
				filenames.push(requirement.filename)
			}
			return filenames
		}, [])
		// this._commandsRequirements = {}
		filenamesRequirement.forEach(filename => this.addCommandRequirementFile(filename))
	}

	/**
	 * Checks the list of registered commands and returns one whch is known by a
	 * given name, either as the command's name or an alias of the command.
	 * @param {string} command - The name of the command to look for.
	 * @param {string} subcommand - The name of the subcommand to look for.
	 * @return {Command|undefined}
	 */
	getCommandByName(command, subcommand) {
		const cmd = this.commands.find(c => c.names.includes(command))
		if (!cmd) return
		if (subcommand) {
			const scmd = cmd.childs.find(c => c.names.includes(subcommand))
			return scmd || cmd
		}
		return cmd
	}

	/***
	 * Returns the appropriate prefix string to use for commands based on a
	 * certain message.
	 * @param {Object} msg - The message to check the prefix of.
	 * @return {string}
	 */
	getPrefixForMessage(msg) {
		return this.prefix
	}

	/***
	 * Takes a message, gets the prefix based on the config of any guild it was
	 * sent in, and returns the message's content without the prefix if the
	 * prefix matches, and `null` if it doesn't.
	 * @param {Object} msg - The message to process
	 * @return {Array<String|null>}
	 **/
	splitPrefixFromContent(msg) {
		// Traditional prefix handling - if there is no prefix, skip this rule
		const prefix = this.getPrefixForMessage(msg) // TODO: guild config
		if (prefix !== undefined && msg.content.startsWith(prefix)) {
			return { prefix, content: msg.content.substr(prefix.length) }
		}
		// Allow mentions to be used as prefixes according to config
		const match = msg.content.match(this.mentionPrefixRegExp)
		if (this.allowMention && match) { // TODO: guild config
			return { prefix: match[0], content: msg.content.substr(match[0].length) }
		}
		// we got nothing
		return { prefix: undefined, content: msg.content }
	}

	/**
	 * Get Commands from a Category
	 * @param  {(string|string[])} categories - Category or list of these to search commands
	 * @return {Command[]|undefined} - Array of {@link Command} (include childs commands aka subcommands)
	 */
	getCommandsOfCategories(categories) {
		if (!Array.isArray(categories)) {
			categories = [categories]
		}
		categories = categories.map(c => c.toLowerCase())
		const cmds = this.commands.filter(c => categories.includes(c.category.toLowerCase()))
		return cmds.length > 0 ? cmds : undefined
	}

	/**
	 * If returns true, allow default commands management and messageCreate Components functions.
	 * @param  {Eris.message} msg - Eris Message object
	 * @param  {Client} client - Client instance
	 * @return {boolean} - true = allow, false = omit
	 */
	triggerMessageCreate(msg, client) {
		return true
	}

	/**
	 * @typedef EmbedMessageObject
	 * @see {@link https://abal.moe/Eris/docs/TextChannel#function-createMessage EmbedMessageObject}
	 */

	/**
	 * Creators for Command Requirements
	 * @typedef {function} CommandRequirementsCreators
	 * @param {object} config - Object with requirement config
	 * @see {@link https://desvelao.github.io/aghanim/tutorial-6command-requirements.html Command Requirements Creators} config - Object with requirement config
	 * @returns {CommandRequirementObject}
	 */

	/**
	 * Create args object and find command. Returns both.
	 * @typedef parseCommand
	 * @prop  {args} args - args object
	 * @prop  {Command|undefined} command - command
	 */

	/**
	 * Create args object and find command. Returns both.
	 * @param  {Eris.message} msg - Eris Message object
	 * @returns {parseCommand} - 
	 */
	createCommandArgs(msg) {
		const { prefix, content } = this.splitPrefixFromContent(msg)
		if (typeof prefix !== 'string' || typeof content !== 'string') return

		const args = content.split(' ').map(word => word.trim())

		/**
		 * Message is spit for spaces (' ')
		 * @typedef args
		 * @prop {string} prefix - Message prefix
		 * @prop {string} content - Message content
		 * @prop {function} from - Splice message content from argument number to end message. fn(arg:number)
		 * @prop {function} until - Splice message content from begin until argument number. fn(arg:number)
		 * @prop {function} after - Same content. fn()
		 * @prop {Client} client - Client instance
		 * @prop {string} command - Command name
		 * @prop {(string|undefined)} subcommand - Subcommand name if exists
		 * @prop {array} - Each word form message is in a slot
		 */
		args.prefix = prefix
		args.content = content
		args.from = arg => args.slice(arg).join(' ')
		args.until = arg => args.prefix + args.slice(0, arg).join(' ')
		args.after = args.from(1)
		args.client = this
		args.command = args[0]
		args.subcommand = args[1]
		this.extendCommandArgs(args, msg, this)
		return args
	}

	async checkRequirements(msg, args, client, command) {
		if (!command.enable) { return false }
		return command.requirements.reduce(async (result, requirement) => {
			if (!(await result)) { return Promise.resolve(false) }
			if (typeof requirement === 'object') {
				const pass = await requirement.validate(msg, args, client, command, requirement)
				if (pass === null) { // ignore response/responseDM/run methods
					return Promise.resolve(false)
				} else if (!pass) { // false/undefined do response/responseDM/run methods
					if (["string", "object"].includes(typeof(requirement.response))) {
						await msg.channel.createMessage(requirement.response) // Response to message
					} else if (typeof requirement.response === "function") {
						const res = await requirement.response(msg, args, client, command, requirement) 
						await msg.channel.createMessage(res) // Response to message
					} else if (["string", "object"].includes(typeof(requirement.responseDM))) {
						await msg.author.getDMChannel().then(channel => channel.createMessage(requirement.responseDM)) // Response with a dm
					} else if (typeof requirement.responseDM === "function") {
						const res = await requirement.responseDM(msg, args, client, command, requirement) 
						await msg.author.getDMChannel().then(channel => channel.createMessage(res)) // Response with a dm
					} else if (typeof requirement.run === "function") {
						await requirement.run(msg, args, client, command, requirement) // Custom
					}
					return Promise.resolve(false)
				}
			}
			return Promise.resolve(true) // result
		}, Promise.resolve(true))
	}
	// /**
	//  * Creates a message. If the specified message content is longer than 2000
	//  * characters, splits the message intelligently into chunks until each chunk
	//  * is less than 2000 characters, then sends each chunk as its own message.
	//  * Embeds and files are sent with the last message and are otherwise
	//  * unaffected.
	//  * @param content
	//  * @param
	//  * @TODO everything
	//  */
	// _createMessageChunked (channelId, content, file, maxLength = 2000) {
	//   let embed
	//   if (typeof content === 'object') {
	//     embed = content.embed
	//     content = content.content
	//   } else {
	//     embed = null
	//   }
	//   let self = this
	//   ;(function sendChunk (left) {
	//     console.log(left.length)
	//     if (left.length < maxLength) return self.createMessage(channelId, {content, embed}, file)
	//     let newlineIndex = left.substr(0, maxLength).lastIndexOf('\n')
	//     if (newlineIndex < 1) newlineIndex = maxLength - 1
	//     console.log(newlineIndex)
	//     left = left.split('')
	//     const chunk = left.splice(0, newlineIndex)
	//     if (!left.length) {
	//       // Interesting, the message was exactly good. We'll put the embed and stuff in now.
	//       return self.createMessage(channelId, {content: chunk, embed: embed}, file)
	//     }
	//     sendChunk(left.join(''), maxLength)
	//   }(content))
	// }
}


function getCommandRequirement(client, command, req) {
	if (typeof req === 'string') {
		if (builtinCommandRequirements[req]) {
			const requirement = builtinCommandRequirements[req]({command, client})
			requirement.type = req
			return requirement
		} else if (client._commandsRequirements[req]) {
			if (typeof client._commandsRequirements[req] === "object") {
				return client._commandsRequirements[req]
			}else if(typeof client._commandsRequirements[req]  === "function"){
				return client._commandsRequirements[req]({ command, client })
			}
		} else {
			throw new Error(`String command requirement not found: ${req}`)
		}
	} else if (typeof req === 'object') {
		if (builtinCommandRequirements[req.type]) {
			const requirement = builtinCommandRequirements[req.type]({ ...req, command, client })
			requirement.type = req.type
			return requirement
		} else {
			return req
		}
	} else {
		throw new TypeError(`Requirement: ${req} on ${command.name}`)
	}
}

function mapCommandRequirement(client, command, reqs){
	reqs.forEach(req => {
		const requirement = getCommandRequirement(client, command, req)
		if(Array.isArray(requirement)){
			mapCommandRequirement(client, command, requirement)
		}else{
			command.addRequirement(requirement)
		}
	})
}

module.exports = Client