text: 'use strict';
  
  const Assert = require('assert');
  const Path = require('path');
  const OS = require('os');
  const File = require('wmcc-file');
  const HOME = OS.homedir ? OS.homedir() : '/';
  
  /**
   * Config Parser
   */
  class Config {
    /**
     * Create a config.
     * @constructor
     * @param {String} module - Module name (e.g. `wmcc`).
     * @param {Object?} options
     */
    constructor(module, options) {
      Assert(typeof module === 'string');
      Assert(module.length > 0);
  
      this.module = module;
      this.prefix = Path.join(HOME, `.${module}`);
      this.suffix = null;
      this.fallback = null;
      this.alias = Object.create(null);
  
      this.options = Object.create(null);
      this.data = Object.create(null);
      this.env = Object.create(null);
      this.args = Object.create(null);
      this.argv = [];
      this.pass = [];
      this.query = Object.create(null);
      this.hash = Object.create(null);
  
      if (options)
        this.init(options);
    }
  
    /**
     * Initialize options.
     * @private
     * @param {Object} options
     */
    init(options) {
      Assert(options && typeof options === 'object');
  
      if (options.suffix != null) {
        Assert(typeof options.suffix === 'string');
        this.suffix = options.suffix;
      }
  
      if (options.fallback != null) {
        Assert(typeof options.fallback === 'string');
        this.fallback = options.fallback;
      }
  
      if (options.alias) {
        Assert(typeof options.alias === 'object');
        for (const key of Object.keys(options.alias)) {
          const value = options.alias[key];
          Assert(typeof value === 'string');
          this.alias[key] = value;
        }
      }
    }
  
    /**
     * Inject options.
     * @param {Object} options
     */
    inject(options) {
      for (const key of Object.keys(options)) {
        const value = options[key];
  
        switch (key) {
          case 'hash':
          case 'query':
          case 'env':
          case 'argv':
          case 'config':
            continue;
        }
  
        this.set(key, value);
      }
    }
  
    /**
     * Load options from hash, query, env, or args.
     * @param {Object} options
     */
    load(options) {
      if (options.hash)
        this.parseHash(options.hash);
  
      if (options.query)
        this.parseQuery(options.query);
  
      if (options.env)
        this.parseEnv(options.env);
  
      if (options.argv)
        this.parseArg(options.argv);
  
      this.prefix = this.getPrefix();
    }
  
    /**
     * Open a config file.
     * @param {String} file - e.g. `wmcc.conf`.
     * @throws on IO error
     */
    open(file) {
      if (File.unsupported)
        return;
  
      const path = this.getFile(file);
  
      let text;
      try {
        text = File.readFileSync(path, 'utf8');
      } catch (e) {
        if (e.code === 'ENOENT')
          return;
        throw e;
      }
  
      this.parseConfig(text);
  
      this.prefix = this.getPrefix();
    }
  
    /**
     * Create a child config. Filter by plugin name.
     * @param {String} name
     * @returns {Config}
     */
    filter(name) {
      Assert(typeof name === 'string');
  
      const child = new Config(this.module);
  
      child.prefix = this.prefix;
      child.suffix = this.suffix;
      child.fallback = this.fallback;
      child.argv = this.argv;
      child.pass = this.pass;
  
      _filter(name, this.env, child.env);
      _filter(name, this.args, child.args);
      _filter(name, this.query, child.query);
      _filter(name, this.hash, child.hash);
  
      return child;
    }
  
    /**
     * Set default option.
     * @param {String} key
     * @param {Object} value
     */
    set(key, value) {
      Assert(typeof key === 'string', 'Key must be a string.');
  
      if (value == null)
        return;
  
      key = key.replace(/-/g, '');
      key = key.toLowerCase();
  
      this.options[key] = value;
    }
  
    /**
     * Test whether a config option is present.
     * @param {String} key
     * @returns {Boolean}
     */
    has(key) {
      if (typeof key === 'number') {
        Assert(key >= 0, 'Index must be positive.');
        if (key >= this.argv.length)
          return false;
        return true;
      }
  
      Assert(typeof key === 'string', 'Key must be a string.');
  
      key = key.replace(/-/g, '');
      key = key.toLowerCase();
  
      if (this.hash[key] != null)
        return true;
  
      if (this.query[key] != null)
        return true;
  
      if (this.args[key] != null)
        return true;
  
      if (this.env[key] != null)
        return true;
  
      if (this.data[key] != null)
        return true;
  
      if (this.options[key] != null)
        return true;
  
      return false;
    }
  
    /**
     * Get a config option.
     * @param {String} key
     * @param {Object?} fallback
     * @returns {Object|null}
     */
    get(key, fallback) {
      if (fallback === undefined)
        fallback = null;
  
      if (Array.isArray(key)) {
        const keys = key;
        for (const key of keys) {
          const value = this.get(key);
          if (value !== null)
            return value;
        }
        return fallback;
      }
  
      if (typeof key === 'number') {
        Assert(key >= 0, 'Index must be positive.');
  
        if (key >= this.argv.length)
          return fallback;
  
        if (this.argv[key] != null)
          return this.argv[key];
  
        return fallback;
      }
  
      Assert(typeof key === 'string', 'Key must be a string.');
  
      key = key.replace(/-/g, '');
      key = key.toLowerCase();
  
      if (this.hash[key] != null)
        return this.hash[key];
  
      if (this.query[key] != null)
        return this.query[key];
  
      if (this.args[key] != null)
        return this.args[key];
  
      if (this.env[key] != null)
        return this.env[key];
  
      if (this.data[key] != null)
        return this.data[key];
  
      if (this.options[key] != null)
        return this.options[key];
  
      return fallback;
    }
  
    /**
     * Get a value's type.
     * @param {String} key
     * @returns {String}
     */
    typeOf(key) {
      const value = this.get(key);
  
      if (value === null)
        return 'null';
  
      return typeof value;
    }
  
    /**
     * Get a config option (as a string).
     * @param {String} key
     * @param {Object?} fallback
     * @returns {String|null}
     */
    str(key, fallback) {
      const value = this.get(key);
  
      if (fallback === undefined)
        fallback = null;
  
      if (value === null)
        return fallback;
  
      if (typeof value !== 'string')
        throw new Error(`${fmt(key)} must be a string.`);
  
      return value;
    }
  
    /**
     * Get a config option (as an integer).
     * @param {String} key
     * @param {Object?} fallback
     * @returns {Number|null}
     */
    int(key, fallback) {
      const value = this.get(key);
  
      if (fallback === undefined)
        fallback = null;
  
      if (value === null)
        return fallback;
  
      if (typeof value !== 'string') {
        if (typeof value !== 'number')
          throw new Error(`${fmt(key)} must be an int.`);
  
        if (!Number.isSafeInteger(value))
          throw new Error(`${fmt(key)} must be an int.`);
  
        return value;
      }
  
      if (!/^\-?\d+$/.test(value))
        throw new Error(`${fmt(key)} must be an int.`);
  
      const num = parseInt(value, 10);
  
      if (!Number.isSafeInteger(num))
        throw new Error(`${fmt(key)} must be an int.`);
  
      return num;
    }
  
    /**
     * Get a config option (as a unsigned integer).
     * @param {String} key
     * @param {Object?} fallback
     * @returns {Number|null}
     */
    uint(key, fallback) {
      const value = this.int(key);
  
      if (fallback === undefined)
        fallback = null;
  
      if (value === null)
        return fallback;
  
      if (value < 0)
        throw new Error(`${fmt(key)} must be a uint.`);
  
      return value;
    }
  
    /**
     * Get a config option (as a float).
     * @param {String} key
     * @param {Object?} fallback
     * @returns {Number|null}
     */
    float(key, fallback) {
      const value = this.get(key);
  
      if (fallback === undefined)
        fallback = null;
  
      if (value === null)
        return fallback;
  
      if (typeof value !== 'string') {
        if (typeof value !== 'number')
          throw new Error(`${fmt(key)} must be a float.`);
  
        if (!isFinite(value))
          throw new Error(`${fmt(key)} must be a float.`);
  
        return value;
      }
  
      if (!/^\-?\d*(?:\.\d*)?$/.test(value))
        throw new Error(`${fmt(key)} must be a float.`);
  
      if (!/\d/.test(value))
        throw new Error(`${fmt(key)} must be a float.`);
  
      const num = parseFloat(value);
  
      if (!isFinite(num))
        throw new Error(`${fmt(key)} must be a float.`);
  
      return num;
    }
  
    /**
     * Get a config option (as a positive float).
     * @param {String} key
     * @param {Object?} fallback
     * @returns {Number|null}
     */
    ufloat(key, fallback) {
      const value = this.float(key);
  
      if (fallback === undefined)
        fallback = null;
  
      if (value === null)
        return fallback;
  
      if (value < 0)
        throw new Error(`${fmt(key)} must be a positive float.`);
  
      return value;
    }
  
    /**
     * Get a value (as a fixed number).
     * @param {String} key
     * @param {Number?} exp
     * @param {Object?} fallback
     * @returns {Number|null}
     */
    fixed(key, exp, fallback) {
      const value = this.float(key);
  
      if (fallback === undefined)
        fallback = null;
  
      if (value === null)
        return fallback;
  
      try {
        return fromFloat(value, exp || 0);
      } catch (e) {
        throw new Error(`${fmt(key)} must be a fixed number.`);
      }
    }
  
    /**
     * Get a value (as a positive fixed number).
     * @param {String} key
     * @param {Number?} exp
     * @param {Object?} fallback
     * @returns {Number|null}
     */
    ufixed(key, exp, fallback) {
      const value = this.fixed(key, exp);
  
      if (fallback === undefined)
        fallback = null;
  
      if (value === null)
        return fallback;
  
      if (value < 0)
        throw new Error(`${fmt(key)} must be a positive fixed number.`);
  
      return value;
    }
  
    /**
     * Get a config option (as a boolean).
     * @param {String} key
     * @param {Object?} fallback
     * @returns {Boolean|null}
     */
    bool(key, fallback) {
      const value = this.get(key);
  
      if (fallback === undefined)
        fallback = null;
  
      if (value === null)
        return fallback;
  
      // Bitcoin Core compat.
      if (typeof value === 'number') {
        if (value === 1)
          return true;
  
        if (value === 0)
          return false;
      }
  
      if (typeof value !== 'string') {
        if (typeof value !== 'boolean')
          throw new Error(`${fmt(key)} must be a boolean.`);
        return value;
      }
  
      if (value === 'true' || value === '1')
        return true;
  
      if (value === 'false' || value === '0')
        return false;
  
      throw new Error(`${fmt(key)} must be a boolean.`);
    }
  
    /**
     * Get a config option (as a buffer).
     * @param {String} key
     * @param {Object?} fallback
     * @returns {Buffer|null}
     */
    buf(key, fallback, enc) {
      const value = this.get(key);
  
      if (!enc)
        enc = 'hex';
  
      if (fallback === undefined)
        fallback = null;
  
      if (value === null)
        return fallback;
  
      if (typeof value !== 'string') {
        if (!Buffer.isBuffer(value))
          throw new Error(`${fmt(key)} must be a buffer.`);
        return value;
      }
  
      const data = Buffer.from(value, enc);
  
      if (data.length !== Buffer.byteLength(value, enc))
        throw new Error(`${fmt(key)} must be a ${enc} string.`);
  
      return data;
    }
  
    /**
     * Get a config option (as an array of strings).
     * @param {String} key
     * @param {Object?} fallback
     * @returns {String[]|null}
     */
    array(key, fallback) {
      const value = this.get(key);
  
      if (fallback === undefined)
        fallback = null;
  
      if (value === null)
        return fallback;
  
      if (typeof value !== 'string') {
        if (!Array.isArray(value))
          throw new Error(`${fmt(key)} must be an array.`);
        return value;
      }
  
      const parts = value.trim().split(/\s*,\s*/);
      const result = [];
  
      for (const part of parts) {
        if (part.length === 0)
          continue;
  
        result.push(part);
      }
  
      return result;
    }
  
    /**
     * Get a config option (as an object).
     * @param {String} key
     * @param {Object?} fallback
     * @returns {Object|null}
     */
    obj(key, fallback) {
      const value = this.get(key);
  
      if (fallback === undefined)
        fallback = null;
  
      if (value === null)
        return fallback;
  
      if (typeof value !== 'object' || Array.isArray(value))
        throw new Error(`${fmt(key)} must be an object.`);
  
      return value;
    }
  
    /**
     * Get a config option (as a function).
     * @param {String} key
     * @param {Object?} fallback
     * @returns {Function|null}
     */
    func(key, fallback) {
      const value = this.get(key);
  
      if (fallback === undefined)
        fallback = null;
  
      if (value === null)
        return fallback;
  
      if (typeof value !== 'function')
        throw new Error(`${fmt(key)} must be a function.`);
  
      return value;
    }
  
    /**
     * Get a config option (as a string).
     * @param {String} key
     * @param {Object?} fallback
     * @returns {String|null}
     */
    path(key, fallback) {
      let value = this.str(key);
  
      if (fallback === undefined)
        fallback = null;
  
      if (value === null)
        return fallback;
  
      if (value.length === 0)
        return fallback;
  
      switch (value[0]) {
        case '~': // home dir
          value = Path.join(HOME, value.substring(1));
          break;
        case '@': // prefix
          value = Path.join(this.prefix, value.substring(1));
          break;
        default: // cwd
          break;
      }
  
      return Path.normalize(value);
    }
  
    /**
     * Get a config option (in MB).
     * @param {String} key
     * @param {Object?} fallback
     * @returns {Number|null}
     */
    mb(key, fallback) {
      const value = this.uint(key);
  
      if (fallback === undefined)
        fallback = null;
  
      if (value === null)
        return fallback;
  
      return value * 1024 * 1024;
    }
  
    /**
     * Grab suffix from config data.
     * @returns {String}
     */
    getSuffix() {
      if (!this.suffix)
        throw new Error('No suffix presented.');
  
      const suffix = this.str(this.suffix, this.fallback);
  
      Assert(isAlpha(suffix), 'Bad suffix.');
  
      return suffix;
    }
  
    /**
     * Grab prefix from config data.
     * @private
     * @returns {String}
     */
    getPrefix() {
      let prefix = this.str('prefix');
  
      if (prefix) {
        if (prefix[0] === '~')
          prefix = Path.join(HOME, prefix.substring(1));
      } else {
        prefix = Path.join(HOME, `.${this.module}`);
      }
  
      if (this.suffix) {
        const suffix = this.str(this.suffix);
  
        if (suffix) {
          Assert(isAlpha(suffix), 'Bad suffix.');
          if (this.fallback && suffix !== this.fallback)
            prefix = Path.join(prefix, suffix);
        }
      }
  
      return Path.normalize(prefix);
    }
  
    /**
     * Grab config filename from config data.
     * @private
     * @param {String} file
     * @returns {String}
     */
    getFile(file) {
      const name = this.str('config');
  
      if (name)
        return name;
  
      return Path.join(this.prefix, file);
    }
  
    /**
     * Create a file path using `prefix`.
     * @param {String} file
     * @returns {String}
     */
    location(file) {
      return Path.join(this.prefix, file);
    }
  
    /**
     * Parse config text.
     * @private
     * @param {String} text
     */
    parseConfig(text) {
      Assert(typeof text === 'string', 'Config must be text.');
  
      if (text.charCodeAt(0) === 0xfeff)
        text = text.substring(1);
  
      text = text.replace(/\r\n/g, '\n');
      text = text.replace(/\r/g, '\n');
      text = text.replace(/\\\n/g, '');
  
      let num = 0;
  
      for (const chunk of text.split('\n')) {
        const line = chunk.trim();
  
        num += 1;
  
        if (line.length === 0)
          continue;
  
        if (line[0] === '#')
          continue;
  
        const index = line.indexOf(':');
  
        if (index === -1)
          throw new Error(`Expected ':' on line ${num}: "${line}".`);
  
        let key = line.substring(0, index).trim();
  
        key = key.replace(/\-/g, '');
  
        if (!isLowerKey(key))
          throw new Error(`Invalid option on line ${num}: ${key}.`);
  
        const value = line.substring(index + 1).trim();
  
        if (value.length === 0)
          continue;
  
        const alias = this.alias[key];
  
        if (alias)
          key = alias;
  
        this.data[key] = value;
      }
    }
  
    /**
     * Parse arguments.
     * @private
     * @param {Array?} argv
     */
    parseArg(argv) {
      if (!argv || typeof argv !== 'object')
        argv = process.argv;
  
      Assert(Array.isArray(argv));
  
      let last = null;
      let pass = false;
  
      for (let i = 2; i < argv.length; i++) {
        const arg = argv[i];
  
        Assert(typeof arg === 'string');
  
        if (arg === '--') {
          pass = true;
          continue;
        }
  
        if (pass) {
          this.pass.push(arg);
          continue;
        }
  
        if (arg.length === 0) {
          last = null;
          continue;
        }
  
        if (arg.indexOf('--') === 0) {
          const index = arg.indexOf('=');
  
          let key = null;
          let value = null;
          let empty = false;
  
          if (index !== -1) {
            // e.g. --opt=val
            key = arg.substring(2, index);
            value = arg.substring(index + 1);
            last = null;
            empty = false;
          } else {
            // e.g. --opt
            key = arg.substring(2);
            value = 'true';
            last = null;
            empty = true;
          }
  
          key = key.replace(/\-/g, '');
  
          if (!isLowerKey(key))
            throw new Error(`Invalid argument: --${key}.`);
  
          if (value.length === 0)
            continue;
  
          // Do not allow one-letter aliases.
          if (key.length > 1) {
            const alias = this.alias[key];
            if (alias)
              key = alias;
          }
  
          this.args[key] = value;
  
          if (empty)
            last = key;
  
          continue;
        }
  
        if (arg[0] === '-') {
          // e.g. -abc
          last = null;
  
          for (let j = 1; j < arg.length; j++) {
            let key = arg[j];
  
            if ((key < 'a' || key > 'z')
                && (key < 'A' || key > 'Z')
                && (key < '0' || key > '9')
                && key !== '?') {
              throw new Error(`Invalid argument: -${key}.`);
            }
  
            const alias = this.alias[key];
  
            if (alias)
              key = alias;
  
            this.args[key] = 'true';
  
            last = key;
          }
  
          continue;
        }
  
        // e.g. foo
        const value = arg;
  
        if (value.length === 0) {
          last = null;
          continue;
        }
  
        if (last) {
          this.args[last] = value;
          last = null;
        } else {
          this.argv.push(value);
        }
      }
    }
  
    /**
     * Parse environment variables.
     * @private
     * @param {Object?} env
     * @returns {Object}
     */
    parseEnv(env) {
      let prefix = this.module;
  
      prefix = prefix.toUpperCase();
      prefix = prefix.replace(/-/g, '_');
      prefix += '_';
  
      if (!env || typeof env !== 'object')
        env = process.env;
  
      Assert(env && typeof env === 'object');
  
      for (let key of Object.keys(env)) {
        const value = env[key];
  
        Assert(typeof value === 'string');
  
        if (key.indexOf(prefix) !== 0)
          continue;
  
        key = key.substring(prefix.length);
        key = key.replace(/_/g, '');
  
        if (!isUpperKey(key))
          continue;
  
        if (value.length === 0)
          continue;
  
        key = key.toLowerCase();
  
        // Do not allow one-letter aliases.
        if (key.length > 1) {
          const alias = this.alias[key];
          if (alias)
            key = alias;
        }
  
        this.env[key] = value;
      }
    }
  
    /**
     * Parse uri querystring variables.
     * @private
     * @param {String} query
     */
    parseQuery(query) {
      if (typeof query !== 'string') {
        if (!global.location)
          return {};
  
        query = global.location.search;
  
        if (typeof query !== 'string')
          return {};
      }
  
      return this.parseForm(query, '?', this.query);
    }
  
    /**
     * Parse uri hash variables.
     * @private
     * @param {String} hash
     */
    parseHash(hash) {
      if (typeof hash !== 'string') {
        if (!global.location)
          return {};
  
        hash = global.location.hash;
  
        if (typeof hash !== 'string')
          return {};
      }
  
      return this.parseForm(hash, '#', this.hash);
    }
  
    /**
     * Parse form-urlencoded variables.
     * @private
     * @param {String} query
     * @param {String} ch
     * @param {Object} map
     */
    parseForm(query, ch, map) {
      Assert(typeof query === 'string');
  
      if (query.length === 0)
        return;
  
      if (query[0] === ch)
        query = query.substring(1);
  
      for (const pair of query.split('&')) {
        const index = pair.indexOf('=');
  
        let key, value;
        if (index !== -1) {
          key = pair.substring(0, index);
          value = pair.substring(index + 1);
        } else {
          key = pair;
          value = 'true';
        }
  
        key = unescape(key);
        key = key.replace(/\-/g, '');
  
        if (!isLowerKey(key))
          continue;
  
        value = unescape(value);
  
        if (value.length === 0)
          continue;
  
        const alias = this.alias[key];
  
        if (alias)
          key = alias;
  
        map[key] = value;
      }
    }
  }
  
  /*
   * Helpers
   */
  function fmt(key) {
    if (Array.isArray(key))
      key = key[0];
  
    if (typeof key === 'number')
      return `Argument #${key}`;
  
    return key;
  }
  
  function unescape(str) {
    try {
      str = decodeURIComponent(str);
      str = str.replace(/\+/g, ' ');
    } catch (e) {
      ;
    }
    str = str.replace(/\0/g, '');
    return str;
  }
  
  function isAlpha(str) {
    return /^[a-z0-9_\-]+$/i.test(str);
  }
  
  function isKey(key) {
    return /^[a-zA-Z0-9]+$/.test(key);
  }
  
  function isLowerKey(key) {
    if (!isKey(key))
      return false;
  
    return !/[A-Z]/.test(key);
  }
  
  function isUpperKey(key) {
    if (!isKey(key))
      return false;
  
    return !/[a-z]/.test(key);
  }
  
  function _filter(name, a, b) {
    for (const key of Object.keys(a)) {
      if (key.length > name.length && key.indexOf(name) === 0) {
        const sub = key.substring(name.length);
        b[sub] = a[key];
      }
    }
  }
  
  function fromFloat(num, exp) {
    Assert(typeof num === 'number' && isFinite(num));
    Assert(Number.isSafeInteger(exp));
  
    let str = num.toFixed(exp);
    let sign = 1;
  
    if (str.length > 0 && str[0] === '-') {
      str = str.substring(1);
      sign = -1;
    }
  
    let hi = str;
    let lo = '0';
  
    const index = str.indexOf('.');
  
    if (index !== -1) {
      hi = str.substring(0, index);
      lo = str.substring(index + 1);
    }
  
    hi = hi.replace(/^0+/, '');
    lo = lo.replace(/0+$/, '');
  
    Assert(hi.length <= 16 - exp,
      'Fixed number string exceeds 2^53-1.');
  
    Assert(lo.length <= exp,
      'Too many decimal places in fixed number string.');
  
    if (hi.length === 0)
      hi = '0';
  
    while (lo.length < exp)
      lo += '0';
  
    if (lo.length === 0)
      lo = '0';
  
    Assert(/^\d+$/.test(hi) && /^\d+$/.test(lo),
      'Non-numeric characters in fixed number string.');
  
    hi = parseInt(hi, 10);
    lo = parseInt(lo, 10);
  
    const mult = Math.pow(10, exp);
    const maxLo = Number.MAX_SAFE_INTEGER % mult;
    const maxHi = (Number.MAX_SAFE_INTEGER - maxLo) / mult;
  
    Assert(hi < maxHi || (hi === maxHi && lo <= maxLo),
      'Fixed number string exceeds 2^53-1.');
  
    return sign * (hi * mult + lo);
  }
  
  /*
   * Expose
   */
  module.exports = Config;