/**
 * The commands API is meant for registering commands that can be used in the chat message
 * bar, mimicing Discord's slash command user interface. For Vizality commands, you may set
 * a custom command prefix, which can be found in Vizality's Dashboard Settings.
 * @module Commands
 * @memberof API
 * @namespace API.Commands
 * @version 1.0.0
 */

/**
 * Vizality command object.
 * @typedef VizalityCommand
 * @property {string} command Command name
 * @property {Function} executor Command executor
 * @property {Array<string>} [aliases] Command aliases
 * @property {string} [icon] Command icon. This can be either an icon name or an image URL.
 * @property {string} [description] Command description
 * @property {Function} [autocomplete] Autocompletion method
 * @property {Array<VizalityCommand>|undefined} [subcommands] Command subcommands
 * @property {string} [source] Source text to the right in the autocomplete
 * @property {boolean} [showTyping=false] Whether typing status should be shown or not
 * @property {string} caller Addon ID of command registrar. This property is set automatically.
 */

/**
 * Vizality subcommand object.
 * @typedef VizalitySubcommand
 * @property {string} command Command name
 * @property {Function} executor Command executor
 * @property {Array<string>} [aliases] Command aliases
 * @property {string} [icon] Command icon. This can be either an icon name or an image URL.
 * @property {string} [description] Command description
 * @property {Array<VizalityCommand>|undefined} [subcommands] Command subcommands
 * @property {string} [source] Source text to the right in the autocomplete
 */

import { assertString, isUrl } from '@vizality/util/string';
import { assertObject } from '@vizality/util/object';
import { assertArray } from '@vizality/util/array';
import { getCaller } from '@vizality/util/file';
import { Events } from '@vizality/constants';
import { Icon } from '@vizality/components';
import { API } from '@vizality/entities';

/**
 * @extends API
 * @extends Events
 */
export default class Commands extends API {
  constructor () {
    super();
    /**
     * All currently registered actions.
     */
    this.commands = [];
  }

  /**
   * Shuts down the API, removing all listeners and stored objects.
   */
  stop () {
    this.unregisterAllCommands();
    this.removeAllListeners();
    delete vizality.api.commands;
  }

  /**
   * Gets the custom command prefix.
   */
  get prefix () {
    try {
      return vizality.settings.get('commandPrefix', '.');
    } catch (err) {
      return this.error(this._labels.concat('prefix'), err);
    }
  }

  /**
   * Registers a command.
   * @param {VizalityCommand} command Command to register
   * @emits Commands#Events.VIZALITY_COMMAND_ADD
   */
  registerCommand (command) {
    try {
      assertObject(command);
      assertString(command.command);
      command.command = command.command.toLowerCase();
      if (this.isCommand(command.command)) {
        throw new Error(`Command "${command.command}" is already registered!`);
      }
      if (!command.executor) {
        throw new Error('Command must contain an executor!');
      }
      if (typeof command.executor !== 'function') {
        throw new TypeError('Command executor must be a function!');
      }
      if (command.icon) {
        assertString(command.icon);
        if (!Icon.Names.includes(command.icon)) {
          if (!isUrl(command.icon)) {
            throw new Error(`Command icon "${command.icon}" asset not found! Must be a valid Vizality icon asset name or a valid URL.`);
          }
        }
      }
      if (command.aliases) {
        assertArray(command.aliases);
        command.aliases = command.aliases.map(alias => alias.toLowerCase());
      }
      command.caller = getCaller();
      this.commands.push(command);
      this.emit(Events.VIZALITY_COMMAND_ADD, command.command);
    } catch (err) {
      return this.error(this._labels.concat('registerCommand'), err);
    }
  }

  /**
   * Executes a command programmatically.
   * @param {string} commandName Command name
   */
  async invokeCommand (commandName) {
    try {
      assertString(commandName);
      if (!this.isCommand(commandName)) {
        throw new Error(`Command "${commandName}" could not be found!`);
      }
      try {
        await this.getCommandByName(commandName).executor();
      } catch (err) {
        return this.error(this._labels.concat('invokeCommand'), `There was a problem invoking command "${commandName}" executor!`, err);
      }
    } catch (err) {
      return this.error(this._labels.concat('invokeCommand'), err);
    }
  }

  /**
   * Checks if a command is registered.
   * @param {string} commandName Command name
   * @returns {boolean} Whether a command with a given name is registered
   */
  isCommand (commandName) {
    try {
      assertString(commandName);
      return Boolean(this.getCommandByName(commandName));
    } catch (err) {
      return this.error(this._labels.concat('isCommand'), err);
    }
  }

  /**
   * Checks if a command has subcommands.
   * @param {string} commandName Command name
   * @returns {boolean} Whether a command with a given name has subcommands
   */
  hasSubcommands (commandName) {
    try {
      assertString(commandName);
      return Boolean(this.getCommandByName(commandName).subcommands?.length);
    } catch (err) {
      return this.error(this._labels.concat('hasSubcommands'), err);
    }
  }

  /**
   * Gets the first command found matching a given filter.
   * @param {Function} filter Function to use to filter commands by
   * @returns {object|null} Command matching a given filter
   */
  getCommand (filter) {
    try {
      if (!filter?.length) return null;
      return this.commands.find(filter);
    } catch (err) {
      return this.error(this._labels.concat('getCommand'), err);
    }
  }

  /**
   * Gets a command matching a given name.
   * @param {string} commandName Command name
   * @returns {object|null} Command matching a given name
   */
  getCommandByName (commandName) {
    try {
      assertString(commandName);
      return this.commands.find(command => command.command === commandName);
    } catch (err) {
      return this.error(this._labels.concat('getCommandByName'), err);
    }
  }

  /**
   * Gets all commands found matching a given filter.
   * @param {Function} filter Function to use to filter commands by
   * @returns {Array<object|null>} Commands matching a given filter
   */
  getCommands (filter) {
    try {
      if (!filter?.length) return null;
      return this.commands.filter(filter);
    } catch (err) {
      return this.error(this._labels.concat('getCommands'), err);
    }
  }

  /**
   * Gets all commands matching a given caller.
   * @param {string} addonId Addon ID
   * @returns {Array<object|null>} Commands matching a given caller
   */
  getCommandsByCaller (addonId) {
    try {
      assertString(addonId);
      return this.commands.filter(command => command.caller?.id === addonId);
    } catch (err) {
      return this.error(this._labels.concat('getCommandsByCaller'), err);
    }
  }

  /**
   * Gets all commands.
   * @returns {Array<object|null>} All commands
   */
  getAllCommands () {
    try {
      return this.commands;
    } catch (err) {
      return this.error(this._labels.concat('getAllCommands'), err);
    }
  }

  /**
   * Unregisters a command.
   * @param {string} commandName Command name
   * @emits Commands#Events.VIZALITY_COMMAND_REMOVE
   */
  unregisterCommand (commandName) {
    try {
      assertString(commandName);
      if (!this.isCommand(commandName)) {
        throw new Error(`Command "${commandName}" is not registered, so it cannot be unregistered!`);
      }
      this.commands = this.getCommands(command => command.command !== commandName);
      this.emit(Events.VIZALITY_COMMAND_REMOVE, commandName);
    } catch (err) {
      return this.error(this._labels.concat('unregisterCommand'), err);
    }
  }

  /**
   * Unregisters all commands matching a given caller.
   * @param {string} addonId Addon ID
   * @emits Commands#Events.VIZALITY_COMMAND_REMOVE_ALL_BY_CALLER
   */
  unregisterCommandsByCaller (addonId) {
    try {
      assertString(addonId);
      this.commands = this.commands.filter(command => command.caller?.id !== addonId);
      this.emit(Events.VIZALITY_COMMAND_REMOVE_ALL_BY_CALLER, addonId);
    } catch (err) {
      return this.error(this._labels.concat('unregisterCommandsByCaller'), err);
    }
  }

  /**
   * Unregisters all commands.
   * @emits Commands#Events.VIZALITY_COMMAND_REMOVE_ALL
   */
  unregisterAllCommands () {
    try {
      this.commands = [];
      this.emit(Events.VIZALITY_COMMAND_REMOVE_ALL);
    } catch (err) {
      return this.error(this._labels.concat('unregisterAllCommands'), err);
    }
  }
}
