/**
Copyright: 2024 Contrast Security, Inc
  Contact: support@contrastsecurity.com
  License: Commercial

  NOTICE: This Software and the patented inventions embodied within may only be
  used as part of Contrast Security’s commercial offerings. Even though it is
  made available through public repositories, use of this Software is subject to
  the applicable End User Licensing Agreement found at
  https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
  between Contrast Security and the End User. The Software may not be reverse
  engineered, modified, repackaged, sold, redistributed or otherwise used in a
  way not consistent with the End User License Agreement.
*/
'use strict';

const ESCAPE_REGEX = /([\^[\]()+*{}|?.\\-])/g;

const basePatterns = {
  quote: '(\'|")',
  whitespaces: '\\s*',
  whitespacesRequired: '\\s+'
};

const sqlPatterns = {
  blockComments: '(?:\\s)*(\\/*[\\s\\S]*\\*\\/)?(?:\\s)*',
  commentStart: '#|--|\\/\\*',
  inlineComment: '(?:\\s)*(#|--)+(.*)*'
};

const cmdPatterns = (() => {
  const anything = '.*';
  const switches = '[a-zA-Z\\-0-9\\.\\s]+'; // Do we want the */+?
  const spaces = '\\s+';
  const writeToFile = '(?:>|>>)(\\s*)/+(?:usr|tmp|opt|etc)/+';

  return {
    switches,
    writeToFile,

    // Commands
    anyUsrBin: '(?:/+usr)?/+s?bin/+[a-z]+',
    echoToFile: `echo${spaces}${anything}${writeToFile}`,
    catToFile: `cat${spaces}${anything}${writeToFile}`,
    netcat: `${'nc' + '(?:\\s+-e)?'}${spaces}${switches}`,
    netstat: `netstat${spaces}${switches}`,
    ping: `ping${spaces}${switches}`,
    uname: `uname${spaces}${switches}`,
    wget: `wget${spaces}${anything}--post-file`,
    whoami: 'whoami'
  };
})();

const javascriptPatterns = {
  blockOrInlineComments: '(?:\\s)*(\\/*[\\s\\S]*\\*\\/)?(?:\\s)*',
  braces: '\\{[\\s\\S]*\\}',
  brackets: '\\[[\\s\\S]*\\]',
  concatenation:
    '(\'|")(?:\\s*)(?:\\/\\*[\\s\\S]*\\*\\/)?(?:\\s*)\\+(?:\\s*)(?:\\/\\*[\\s\\S]*\\*\\/)?(?:\\s*)(\'|")',
  functionCall: '\\([\\s\\S]*\\)',
  identifier: '(?:[a-zA-Z$_][0-9a-zA-Z$_]*)',
  // this is not used but maybe should be, if it is correct
  invocation: '(?:\\([\\s\\S]*\\)|`[\\s\\S]*`)',
  inlineComment: '(?:\\s)*(\\/\\/.*)?',
  parens: '\\(.*\\)'
};

const chars = {
  any: '.',
  anyAndAll: '[\\s\\S]', // like ".", but you get "\n", "\r"
  digit: '\\d',
  numeric: '[0-9]',
  'alpha-lower': '[a-z]',
  'alpha-upper': '[A-Z]',
  alpha: '[a-zA-Z]',
  alphanumeric: '[a-zA-Z0-9]',
  word: '\\w'
};

const GROUPS = {
  NONE: ['', ''],
  NO_REMEMBER: ['(?:', ')'],
  REMEMBER: ['(', ')']
};

/**
 * Contructor function for pattern builder objects.
 */
const BuilderBase = function(options /*= {}*/) {
  options = options || {};

  this.flags = options.flags === undefined ? 'm' : options.flags;
  this.pattern = '';

  // Useful for debugging, and maybe other things.
  // Like adding `.pipe(console.log)` during method chaining will
  // dump the instance to the console so you can see if pattern,
  // grouping, etc look like you want at that time.
  this.pipe = function(fn) {
    fn(this);
    return this;
  };

  /** Helper aliases */
  this.zeroOrOneOf = this.between.bind(this, 0, 1); // ?
  this.zeroOrMoreOf = this.between.bind(this, 0, Infinity); // *
  this.oneOrMoreOf = this.between.bind(this, 1, Infinity); // +

  /** Sets min, most, and grouping defaults */
  this.reset();

  /** Add base-pattern methods */
  appendPatternWriters.call(this, basePatterns);
};

/**
 * Sets min/most values to be used for quantifier resolution
 * when a pattern sequence is written. Chainable.
 * @param {Number} m Min
 * @param {Number} n Most
 */
BuilderBase.prototype.between = function(m, n) {
  if (isNaN(m) || isNaN(n)) {
    throw new ArgumentErrorNaN();
  }
  this.min = m;
  this.most = n;
  return this;
};

/**
 * Sets up the qualifer to be {n}, by setting min / most values to
 * both be set to the provided non-negative integer value. Chainable.
 * @param {Number} n The number to match "exactly".
 */
BuilderBase.prototype.exactly = function(n) {
  if (isNaN(n)) {
    throw new ArgumentErrorNaN();
  }
  return this.between.call(this, n, n);
};

/**
 * Will add "." to the pattern. Uses preset grouping state and quantifiers. Chainable.
 * @param {Boolean} evenNewLines If `true`, will use "[\\s\\S]" instead of ".".
 */
BuilderBase.prototype.anything = function(evenNewLines) {
  this.writeSequence(evenNewLines ? chars.anyAndAll : chars.any);
  return this;
};

/**
 * Creates a patten that will look to match any pattern in the list - used as if
 * `literal()` is called and all values are ORed - "|". This groups by default, using
 * a non-remembering group (?: ). Use the `noEscape` and `remember` params to change
 * default behavior for skipping encoding or to remember group in matches. Chainable.
 * @param {String[]} listOfLiterals The list of literals used for constructing the
                                    pattern sequence.
 * @param {Boolean}  noEscape       Skip escaping step when adding to pattern.
 * @param {Boolean}  remember       If true will use ( ), instead of (?: ).
 */
BuilderBase.prototype.anyOf = function(listOfLiterals, noEscape, remember) {
  // This groups by default: (a|b|c) or (?:a|b|c)
  this.grouping = remember ? GROUPS.REMEMBER : GROUPS.NO_REMEMBER;
  const formattedListOfLiterals = listOfLiterals.map((str) =>
    noEscape ? str : escape(str)
  );
  const strOfLiterals = formattedListOfLiterals.join('|');
  return this.writeSequence(strOfLiterals);
};

/**
 * Writes the provided string to the pattern after encoding it, so that
 * special characters will be escaped so that they will be matched in the
 * input strings. Use the `noEscape` option to skip this and write verbatim
 * to the pattern - literally, literally :D. Chainable.
 */
BuilderBase.prototype.literally = function(patternStr, noEscape) {
  const formattedStr = noEscape ? patternStr : escape(patternStr);
  return this.writeSequence(formattedStr);
};

/**
 * Writes alphabetic characters (a-z) to the pattern as a character set. A casing
 * parameter can provide more specificity. Uses grouping state and whatever
 * quantifier is appropriate from min/most values. Chainable.
 * @param {String} casing Send 'lower' for [a-z], and 'upper' for [A-Z];
 *                        Otherwise the default will be [a-zA-Z].
 */
BuilderBase.prototype.alphabetic = function(casing) {
  casing = casing || '';

  const alphaWithCase = !casing ? 'alpha' : `alpha-${casing}`;

  if (alphaWithCase in chars) {
    return this.writeSequence(chars[alphaWithCase]);
  } else {
    throw Error('Unknown case descriptor. Use "lower" or "upper"');
  }
};

/**
 * Writes numeric chars to the pattern [0-9]. Uses grouping state and
 * whatever quantifier is appropriate from min/most values. Chainable.
 */
BuilderBase.prototype.numeric = function() {
  return this.writeSequence(chars.numeric);
};

/**
 * Writes alphanumeric chars to the pattern [a-zA-Z0-9]. Uses grouping state and
 * whatever quantifier is appropriate from min/most values. Chainable.
 */
BuilderBase.prototype.alphanumeric = function() {
  return this.writeSequence(chars.alphanumeric);
};

/**
 * Resets quantifier min/most values and sets grouping state to NONE.
 */
BuilderBase.prototype.reset = function() {
  // No quantifier
  this.min = 1;
  this.most = 1;
  // No grouping
  this.grouping = GROUPS.NONE;
};

/**
 * Builds a Regular Expression object from the pattern that has been constructed.
 * @returns {RegExp}
 */
BuilderBase.prototype.build = function() {
  return new RegExp(this.pattern, this.flags);
};

/**
 * Appends all of the provided strings directly to the pattern
 * property. No encoding, grouping or quantifiers are used. Chainable.
 */
BuilderBase.prototype.write = function(...args) {
  const pattern = args.join('');
  this.pattern += pattern;
  return this;
};

/**
 * From the provided pattern string, will contruct a pattern sequence
 * from the current set groups and quantifiers ( min, most ) and append
 * it to the builder's pattern. Chainable.
 * @param {String} pattern The pattern for which to construct the pattern sequence.
 */
BuilderBase.prototype.writeSequence = function(pattern) {
  const quantifier = this.resolveQuantifier();
  const groupStart = this.grouping[0];
  const groupEnd = this.grouping[1];

  this.reset();

  return this.write(groupStart, pattern, groupEnd, quantifier);
};

/**
 * Reads the instance's `min` and `most` values and will return the
 * most-appropriate quantifier.
 * Resolution table:
 *   1, 1         -> N/A - same as none.
 *   0, Infinity  -> *
 *   1, Infinity  -> +
 *   0, 1         -> ?
 *   m, m         -> {m}
 *   m, Infinity  -> {m,}
 *   m, n         -> {m,n}
 */
BuilderBase.prototype.resolveQuantifier = function() {
  const exactly = this.min === this.most && this.min !== 1; // Quantifier will be omitted if m = n = 1

  // Exactly: {m}
  return exactly
    ? `{${this.min}}`
    : this.most === Infinity
    ? // Infinite most value:  * , + , {m,}
      this.min === 0
      ? '*'
      : this.min === 1
      ? '+'
      : `{${this.min},}`
    : // Finite most value:  ? , {m,n} , or no quantifier
    this.most === 1
    ? this.min === 0
      ? '?'
      : ''
    : `{${this.min},${this.most}}`;
};

/**
 * When called, will provide a fluent API for JavaScript-specific
 * pattern construction. Chainable.
 */

BuilderBase.prototype.JS = function() {
  return appendPatternWriters.call(this, javascriptPatterns);
};

/**
 * When called, will provide a fluent API for SQL-specific
 * pattern construction. Chainable.
 */

BuilderBase.prototype.SQL = function() {
  return appendPatternWriters.call(this, sqlPatterns);
};

BuilderBase.prototype.CMD = function() {
  return appendPatternWriters.call(this, cmdPatterns);
};

function ArgumentErrorNaN() {
  this.message = 'Argument must be a number.';
}

/** Return the string escaped. */
function escape(string) {
  return string.replace(ESCAPE_REGEX, escapeChar);
}

/** Returns the input character escaped */
function escapeChar(char) {
  return `\\${char}`;
}

/**
 * A dictionary of pattern strings will provide, for each value, a
 * method that appends the string via `writeSequence`, so that grouping
 * and quantifier chaining can be used.
 */
function appendPatternWriters(patternDict) {
  // Remove pre-existing plugin methods

  Object.keys(this).forEach((name) => {
    if (this[name].plugin) {
      delete this[name];
    }
  });

  // Create methods from pattern dictionary
  Object.keys(patternDict).forEach((name) => {
    this[name] = () => {
      this.writeSequence(patternDict[name]);
      // ensure chainable API
      return this;
    };
    this[name].plugin = true;
  });

  // ensure chainable API
  return this;
}

module.exports = function Builder(options) {
  return new BuilderBase(options);
};

module.exports.toRegExpString = escape;
