import { parse } from "https://deno.land/std@0.106.0/flags/mod.ts";
import * as $ from "./utils.js";

const ALL = "__all__";
const DEF = "__default__";

class Sade {
  constructor(name, isOne) {
    const [bin, ...rest] = name.split(/\s+/);
    isOne = isOne || rest.length > 0;

    this.bin = bin;
    this.ver = "0.0.0";
    this.default = "";
    this.tree = {};
    // set internal shapes;
    this.command(ALL);
    this.command([DEF].concat(isOne ? rest : "<command>").join(" "));
    this.single = isOne;
    this.curr = ""; // reset
  }

  command(str, desc, opts = {}) {
    if (this.single) {
      throw new Error('Disable "single" mode to add commands');
    }

    // All non-([|<) are commands
    let cmd = [], usage = [];
    const rgx = /(\[|<)/;
    str.split(/\s+/).forEach((x) => {
      (rgx.test(x.charAt(0)) ? usage : cmd).push(x);
    });

    // Back to string~!
    cmd = cmd.join(" ");

    if (cmd in this.tree) {
      throw new Error(`Command already exists: ${cmd}`);
    }

    // re-include `cmd` for commands
    cmd.includes("__") || usage.unshift(cmd);
    usage = usage.join(" "); // to string

    this.curr = cmd;
    if (opts.default) this.default = cmd;

    this.tree[cmd] = {
      usage,
      alibi: [],
      options: [],
      alias: {},
      default: {},
      examples: [],
    };
    if (opts.alias) this.alias(opts.alias);
    if (desc) this.describe(desc);

    return this;
  }

  describe(str) {
    this.tree[this.curr || DEF].describe = Array.isArray(str)
      ? str
      : $.sentences(str);
    return this;
  }

  alias(...names) {
    if (this.single) throw new Error('Cannot call `alias()` in "single" mode');
    if (!this.curr) {
      throw new Error("Cannot call `alias()` before defining a command");
    }
    const arr = this.tree[this.curr].alibi = this.tree[this.curr].alibi.concat(
      ...names,
    );
    arr.forEach((key) => this.tree[key] = this.curr);
    return this;
  }

  option(str, desc, val) {
    const cmd = this.tree[this.curr || ALL];

    let [flag, alias] = $.parse(str);
    if (alias && alias.length > 1) [flag, alias] = [alias, flag];

    str = `--${flag}`;
    if (alias && alias.length > 0) {
      str = `-${alias}, ${str}`;
      const old = cmd.alias[alias];
      cmd.alias[alias] = (old || []).concat(flag);
    }

    const arr = [str, desc || ""];

    if (val !== void 0) {
      arr.push(val);
      cmd.default[flag] = val;
    } else if (!alias) {
      cmd.default[flag] = void 0;
    }

    cmd.options.push(arr);
    return this;
  }

  action(handler) {
    this.tree[this.curr || DEF].handler = handler;
    return this;
  }

  example(str) {
    this.tree[this.curr || DEF].examples.push(str);
    return this;
  }

  version(str) {
    this.ver = str;
    return this;
  }

  parse(arr, opts = {}) {
    arr = ["", ""].concat(arr);
    let offset = 2, tmp, idx, isVoid, cmd;

    const alias = { h: "help", v: "version" };
    const argv = parse(arr.slice(offset), { alias });
    const isSingle = this.single;

    let bin = this.bin;
    let name = "";

    if (isSingle) {
      cmd = this.tree[DEF];
    } else {
      // Loop thru possible command(s)
      let i = 1, xyz;
      const len = argv._.length + 1;

      for (; i < len; i++) {
        tmp = argv._.slice(0, i).join(" ");
        xyz = this.tree[tmp];
        if (typeof xyz === "string") {
          idx = (name = xyz).split(" ");
          arr.splice(arr.indexOf(argv._[0]), i, ...idx);
          i += (idx.length - i);
        } else if (xyz) {
          name = tmp;
        } else if (name) {
          break;
        }
      }

      cmd = this.tree[name];
      isVoid = (cmd === void 0);

      if (isVoid) {
        if (this.default) {
          name = this.default;
          cmd = this.tree[name];
          arr.unshift(name);
          offset++;
        } else if (tmp) {
          return $.error(bin, `Invalid command: ${tmp}`);
        } //=> else: cmd not specified, wait for now...
      }
    }

    // show main help if relied on "default" for multi-cmd
    if (argv.help) return this.help(!isSingle && !isVoid && name);
    if (argv.version) return this._version();

    if (!isSingle && cmd === void 0) {
      return $.error(bin, "No command specified.");
    }

    const all = this.tree[ALL];
    // merge all objects :: params > command > all
    opts.alias = Object.assign(all.alias, cmd.alias, opts.alias);
    opts.default = Object.assign(all.default, cmd.default, opts.default);

    tmp = name.split(" ");
    idx = arr.indexOf(tmp[0], 2);
    if (~idx) arr.splice(idx, tmp.length);

    const vals = parse(arr.slice(offset), opts);
    if (!vals || typeof vals === "string") {
      return $.error(bin, vals || "Parsed unknown option flag(s)!");
    }

    const segs = cmd.usage.split(/\s+/);
    const reqs = segs.filter((x) => x.charAt(0) === "<");
    const args = vals._.splice(0, reqs.length);

    if (args.length < reqs.length) {
      if (name) bin += ` ${name}`; // for help text
      return $.error(bin, "Insufficient arguments!");
    }

    segs.filter((x) => x.charAt(0) === "[").forEach((_) => {
      args.push(vals._.shift()); // adds `undefined` per [slot] if no more
    });

    args.push(vals); // flags & co are last
    const handler = cmd.handler;
    return opts.lazy ? { args, name, handler } : handler.apply(null, args);
  }

  help(str) {
    console.log(
      $.help(this.bin, this.tree, str || DEF, this.single),
    );
  }

  _version() {
    console.log(`${this.bin}, ${this.ver}`);
  }
}

export default (str, isOne) => new Sade(str, isOne);
