/* eslint-disable class-methods-use-this */
/**
 * Converts usage strings into objects to compare against later
 */
class ParsedUsage {

  /**
   * @param {KomadaClient} client The Komada client
   * @param {Command} command The command this parsed usage is for
   */
  constructor(client, command) {
    /**
     * The client this CommandMessage was created with.
     * @name ParsedUsage#client
     * @type {KomadaClient}
     * @readonly
     */
    Object.defineProperty(this, "client", { value: client });

    /**
     * All names and aliases for the command
     * @type {string[]}
     */
    this.names = [command.help.name, ...command.conf.aliases];

    /**
      * The compiled string for all names/aliases in a usage string
      * @type {string}
      */
    this.commands = this.names.length === 1 ? this.names[0] : `(${this.names.join("|")})`;

    /**
     * The usage string re-deliminated with the usageDelim
     * @type {string}
     */
    this.deliminatedUsage = command.help.usage !== "" ? ` ${command.help.usage.split(" ").join(command.help.usageDelim)}` : "";

    /**
     * The usage string
     * @type {string}
     */
    this.usageString = command.help.usage;

    /**
     * The usage object to compare against later
     * @type {Object[]}
     */
    this.parsedUsage = this.parseUsage();

    /**
     * The concatenated string of this.commands and this.deliminatedUsage
     * @type {string}
     */
    this.nearlyFullUsage = `${this.commands}${this.deliminatedUsage}`;
  }

  /**
   * Creates a full usage string including prefix and commands/aliases for documentation/help purposes
   * @param {external:Message} msg a message to check to get the current prefix
   * @returns {string}
   */
  fullUsage(msg) {
    const prefix = msg.guildSettings.prefix || this.client.config.prefix;
    return `${prefix.length !== 1 ? `${prefix} ` : prefix}${this.nearlyFullUsage}`;
  }

  /**
   * Method responsible for building the usage object to check against
   * @private
   * @returns {Object}
   */
  parseUsage() {
    let usage = {
      tags: [],
      opened: 0,
      current: "",
      openReq: false,
      last: false,
      char: 0,
      from: 0,
      at: "",
      fromto: "",
    };

    this.usageString.split("").forEach((com, i) => {
      usage.char = i + 1;
      usage.from = usage.char - usage.current.length;
      usage.at = `at char #${usage.char} '${com}'`;
      usage.fromto = `from char #${usage.from} to #${usage.char} '${usage.current}'`;

      if (usage.last && com !== " ") {
        throw `${usage.at}: there can't be anything else after the repeat tag.`;
      }

      if (this[com]) {
        usage = this[com](usage);
      } else {
        usage.current += com;
      }
    });

    if (usage.opened) throw `from char #${this.usageString.length - usage.current.length} '${this.usageString.substr(-usage.current.length - 1)}' to end: a tag was left open`;
    if (usage.current) throw `from char #${(this.usageString.length + 1) - usage.current.length} to end '${usage.current}' a literal was found outside a tag.`;

    return usage.tags;
  }

  ["<"](usage) {
    if (usage.opened) throw `${usage.at}: you might not open a tag inside another tag.`;
    if (usage.current) throw `${usage.fromto}: there can't be a literal outside a tag`;
    usage.opened++;
    usage.openReq = true;
    return usage;
  }

  [">"](usage) {
    if (!usage.opened) throw `${usage.at}: invalid close tag found`;
    if (!usage.openReq) throw `${usage.at}: Invalid closure of '[${usage.current}' with '>'`;
    usage.opened--;
    if (usage.current) {
      usage.tags.push({
        type: "required",
        possibles: this.parseTag(usage.current, usage.tags.length + 1),
      });
      usage.current = "";
    } else { throw `${usage.at}: empty tag found`; }
    return usage;
  }

  ["["](usage) {
    if (usage.opened) throw `${usage.at}: you might not open a tag inside another tag.`;
    if (usage.current) throw `${usage.fromto}: there can't be a literal outside a tag`;
    usage.opened++;
    usage.openReq = false;
    return usage;
  }

  ["]"](usage) {
    if (!usage.opened) throw `${usage.at}: invalid close tag found`;
    if (usage.openReq) throw `${usage.at}: Invalid closure of '<${usage.current}' with ']'`;
    usage.opened--;
    if (usage.current === "...") {
      if (usage.tags.length < 1) { throw `${usage.fromto}: there can't be a loop at the begining`; }
      usage.tags.push({ type: "repeat" });
      usage.last = true;
      usage.current = "";
    } else if (usage.current) {
      usage.tags.push({
        type: "optional",
        possibles: this.parseTag(usage.current, usage.tags.length + 1),
      });
      usage.current = "";
    } else { throw `${usage.at}: empty tag found`; }
    return usage;
  }

  [" "](usage) {
    if (usage.opened) throw `${usage.at}: spaces aren't allowed inside a tag`;
    if (usage.current) throw `${usage.fromto}: there can't be a literal outside a tag.`;
    return usage;
  }

  ["\n"](usage) {
    throw `${usage.at}: there can't be a line break in the command!`;
  }

  parseTag(tag, count) {
    const literals = [];
    const types = [];
    const toRet = [];

    const members = tag.split("|");

    members.forEach((elemet, i) => {
      const current = `at tag #${count} at bound #${i + 1}`;

      const result = /^([^:]+)(?::([^{}]+))?(?:{([^,]+)?(?:,(.+))?})?$/i.exec(elemet);

      if (!result) throw `${current}: invalid syntax, non specific`;

      const fill = {
        name: result[1],
        type: result[2] ? result[2].toLowerCase() : "literal",
      };

      if (result[3]) {
        const proto = " in the type length (min): ";

        if (fill.type === "literal") throw `${current + proto}you cannot set a length for a literal type`;

        if (Number.isNaN(result[3])) throw `${current + proto}must be a number`;

        const temp = parseFloat(result[3]);
        if ((fill.type === "string" || fill.type === "str") && temp % 1 !== 0) throw `${current + proto}the string type must have an integer length`;

        fill.min = temp;
      }

      if (result[4]) {
        const proto = " in the type length (max): ";
        if (fill.type === "literal") throw `${current + proto}you canno't set a length for a literal type`;

        if (Number.isNaN(result[4])) throw `${current + proto}must be a number`;

        const temp = parseFloat(result[4]);

        if ((fill.type === "string" || fill.type === "str") && temp % 1 !== 0) throw `${current + proto}the string type must have an integer length`;
        fill.max = temp;
      }

      if (fill.type === "literal") {
        if (literals.includes(fill.name)) throw `${current}: there can't be two literals with the same text.`;

        literals.push(fill.name);
      } else if (members.length > 1) {
        if (fill.type === "string" && members.length - 1 !== i) throw `${current}: the String type is vague, you must specify it at the last bound`;
        if (types.includes(fill.type)) throw `${current}: there can't be two bounds with the same type (${fill.type})`;
        types.push(fill.type);
      }

      toRet.push(fill);
    });

    return toRet;
  }

}

module.exports = ParsedUsage;