class CommandMessage {

/* eslint-disable no-underscore-dangle, no-throw-literal, newline-per-chained-call */
/**
 * @param {external:Message} msg The message this command message is for
 * @param {Command} cmd The command being run
 * @param {string} prefix The prefix the user used to run the command
 * @param {number} prefixLength The length of the prefix the user used to run the command
 */
  constructor(msg, cmd, prefix, prefixLength) {
    /**
    * The client this CommandMessage was created with.
    * @name CommandMessage#client
    * @type {KomadaClient}
    * @readonly
    */
    Object.defineProperty(this, "client", { value: msg.client });

    /**
    * The message this command message is for
    * @type {external:Message}
    */
    this.msg = msg;

    /**
    * The command being run
    * @type {Command}
    */
    this.cmd = cmd;

    /**
    * The prefix used
    * @type {string}
    */
    this.prefix = prefix;

    /**
    * The length of the prefix used
    * @type {number}
    */
    this.prefixLength = prefixLength;

    /**
    * The string arguments derived from the usageDelim of the command
    * @type {string[]}
    */
    this.args = this.constructor.getArgs(this);

    /**
    * The parameters resolved by this class
    * @type {any[]}
    */
    this.params = [];

    /**
    * If the command reprompted for missing args
    * @type {boolean}
    */
    this.reprompted = false;

    /**
    * A cache of the current usage while validating
    * @private
    * @type {Object}
    */
    this._currentUsage = {};

    /**
    * Whether the current usage is a repeating arg
    * @private
    * @type {boolean}
    */
    this._repeat = false;
  }


  /**
   * Validates and resolves args into parameters
   * @private
   * @returns {any[]} The resolved parameters
   */
  async validateArgs() {
    if (this.params.length >= this.cmd.usage.parsedUsage.length && this.params.length >= this.args.length) {
      return this.params;
    } else if (this.cmd.usage.parsedUsage[this.params.length]) {
      if (this.cmd.usage.parsedUsage[this.params.length].type !== "repeat") {
        this._currentUsage = this.cmd.usage.parsedUsage[this.params.length];
      } else if (this.cmd.usage.parsedUsage[this.params.length].type === "repeat") {
        this._currentUsage.type = "optional";
        this._repeat = true;
      }
    } else if (!this._repeat) {
      return this.params;
    }
    if (this._currentUsage.type === "optional" && (this.args[this.params.length] === undefined || this.args[this.params.length] === "")) {
      if (this.cmd.usage.parsedUsage.slice(this.params.length).some(usage => usage.type === "required")) {
        this.args.splice(this.params.length, 0, undefined);
        this.args.splice(this.params.length, 1, null);
        throw this.client.funcs.newError("Missing one or more required arguments after end of input.", 1);
      } else {
        return this.params;
      }
    } else if (this._currentUsage.type === "required" && this.args[this.params.length] === undefined) {
      this.args.splice(this.params.length, 1, null);
      throw this.client.funcs.newError(this._currentUsage.possibles.length === 1 ?
        `${this._currentUsage.possibles[0].name} is a required argument.` :
        `Missing a required option: (${this._currentUsage.possibles.map(poss => poss.name).join(", ")})`, 1);
    } else if (this._currentUsage.possibles.length === 1) {
      if (this.client.argResolver[this._currentUsage.possibles[0].type]) {
        return this.client.argResolver[this._currentUsage.possibles[0].type](this.args[this.params.length], this._currentUsage, 0, this._repeat, this.msg)
          .catch((err) => {
            this.args.splice(this.params.length, 1, null);
            throw this.client.funcs.newError(err, 1);
          })
          .then((res) => {
            if (res !== null) {
              this.params.push(res);
              return this.validateArgs();
            }
            this.args.splice(this.params.length, 0, undefined);
            this.params.push(undefined);
            return this.validateArgs();
          });
      }
      this.client.emit("log", "Unknown Argument Type encountered", "warn");
      return this.validateArgs();
    } else {
      return this.multiPossibles(0, false);
    }
  }

  /**
   * Validates and resolves args into parameters, when multiple types of usage is defined
   * @param {number} possible The id of the possible usage currently being checked
   * @param {boolean} validated Escapes the recursive function if the previous iteration validated the arg into a parameter
   * @private
   * @returns {any[]} The resolved parameters
   */
  async multiPossibles(possible, validated) {
    if (validated) {
      return this.validateArgs();
    } else if (possible >= this._currentUsage.possibles.length) {
      if (this._currentUsage.type === "optional" && !this._repeat) {
        this.args.splice(this.params.length, 0, undefined);
        this.params.push(undefined);
        return this.validateArgs();
      }
      this.args.splice(this.params.length, 1, null);
      throw this.client.funcs.newError(`Your option didn't match any of the possibilities: (${this._currentUsage.possibles.map(poss => poss.name).join(", ")})`, 1);
    } else if (this.client.argResolver[this._currentUsage.possibles[possible].type]) {
      return this.client.argResolver[this._currentUsage.possibles[possible].type](this.args[this.params.length], this._currentUsage, possible, this._repeat, this.msg)
        .then((res) => {
          if (res !== null) {
            this.params.push(res);
            return this.multiPossibles(++possible, true);
          }
          return this.multiPossibles(++possible, validated);
        })
        .catch(() => this.multiPossibles(++possible, validated));
    } else {
      this.client.emit("log", "Unknown Argument Type encountered", "warn");
      return this.multiPossibles(++possible, validated);
    }
  }


  /**
   * Parses a message into string args
   * @param {CommandMessage} cmdMsg this command message
   * @private
   * @returns {string[]}
   */
  static getArgs(cmdMsg) {
    const args = cmdMsg.msg.content.slice(cmdMsg.prefixLength).trim().split(" ").slice(1).join(" ").split(cmdMsg.cmd.help.usageDelim !== "" ? cmdMsg.cmd.help.usageDelim : undefined);
    if (args[0] === "") return [];
    return args;
  }

}

module.exports = CommandMessage;