/* https://github.com/axtgr/wildcard-match */

/**
 * Escapes a character if it has a special meaning in regular expressions
 * and returns the character as is if it doesn't
 */
function escapeRegExpChar(char: string) {
  if (
    char === "-" ||
    char === "^" ||
    char === "$" ||
    char === "+" ||
    char === "." ||
    char === "(" ||
    char === ")" ||
    char === "|" ||
    char === "[" ||
    char === "]" ||
    char === "{" ||
    char === "}" ||
    char === "*" ||
    char === "?" ||
    char === "\\"
  ) {
    return `\\${char}`;
  } else {
    return char;
  }
}

/**
 * Escapes all characters in a given string that have a special meaning in regular expressions
 */
function escapeRegExpString(str: string) {
  let result = "";
  for (const char of str) {
    result += escapeRegExpChar(char);
  }
  return result;
}

/**
 * Transforms one or more glob patterns into a RegExp pattern
 */
function transform(
  pattern: string | string[],
  separator: string | boolean = true,
): string {
  if (Array.isArray(pattern)) {
    const regExpPatterns = pattern.map((p) => `^${transform(p, separator)}$`);
    return `(?:${regExpPatterns.join("|")})`;
  }

  let separatorSplitter = "";
  let separatorMatcher = "";
  let wildcard = ".";

  if (separator === true) {
    // In this case forward slashes in patterns match both forward and backslashes in samples:
    //
    // `foo/bar` will match `foo/bar`
    //           will match `foo\bar`
    //
    separatorSplitter = "/";
    separatorMatcher = "[/\\\\]";
    wildcard = "[^/\\\\]";
  } else if (separator) {
    separatorSplitter = separator;
    separatorMatcher = escapeRegExpString(separatorSplitter);

    if (separatorMatcher.length > 1) {
      separatorMatcher = `(?:${separatorMatcher})`;
      wildcard = `((?!${separatorMatcher}).)`;
    } else {
      wildcard = `[^${separatorMatcher}]`;
    }
  }

  // When a separator is explicitly specified in a pattern,
  // it MUST match ONE OR MORE separators in a sample:
  //
  // `foo/bar/` will match  `foo//bar///`
  //            won't match `foo/bar`
  //
  // When a pattern doesn't have a trailing separator,
  // a sample can still optionally have them:
  //
  // `foo/bar` will match `foo/bar//`
  //
  // So we use different quantifiers depending on the index of a segment.
  const requiredSeparator = separator ? `${separatorMatcher}+?` : "";
  const optionalSeparator = separator ? `${separatorMatcher}*?` : "";

  const segments = separator ? pattern.split(separatorSplitter) : [pattern];
  let result = "";

  for (let s = 0; s < segments.length; s++) {
    const segment = segments[s]!;
    const nextSegment = segments[s + 1]!;
    let currentSeparator = "";

    if (!segment && s > 0) {
      continue;
    }

    if (separator) {
      if (s === segments.length - 1) {
        currentSeparator = optionalSeparator;
      } else if (nextSegment !== "**") {
        currentSeparator = requiredSeparator;
      } else {
        currentSeparator = "";
      }
    }

    if (separator && segment === "**") {
      if (currentSeparator) {
        result += s === 0 ? "" : currentSeparator;
        result += `(?:${wildcard}*?${currentSeparator})*?`;
      }
      continue;
    }

    for (let c = 0; c < segment.length; c++) {
      const char = segment[c]!;

      if (char === "\\") {
        if (c < segment.length - 1) {
          result += escapeRegExpChar(segment[c + 1]!);
          c++;
        }
      } else if (char === "?") {
        result += wildcard;
      } else if (char === "*") {
        result += `${wildcard}*?`;
      } else {
        result += escapeRegExpChar(char);
      }
    }

    result += currentSeparator;
  }

  return result;
}

export default transform;

interface WildcardMatchOptions {
  /** Separator to be used to split patterns and samples into segments */
  separator?: string | boolean;

  /** Flags to pass to the RegExp */
  flags?: string;
}

// This overrides the function's signature because for the end user
// the function is always bound to a RegExp
interface isMatch {
  /**
   * Tests if a sample string matches the pattern(s)
   *
   * ```js
   * isMatch('foo') //=> true
   * ```
   */
  (sample: string): boolean;

  /** Compiled regular expression */
  regexp: RegExp;

  /** Original pattern or array of patterns that was used to compile the RegExp */
  pattern: string | string[];

  /** Options that were used to compile the RegExp */
  options: WildcardMatchOptions;
}

function isMatch(regexp: RegExp, sample: string) {
  if (typeof sample !== "string") {
    throw new TypeError(`Sample must be a string, but ${typeof sample} given`);
  }

  return regexp.test(sample);
}

/**
 * Compiles one or more glob patterns into a RegExp and returns an isMatch function.
 * The isMatch function takes a sample string as its only argument and returns `true`
 * if the string matches the pattern(s).
 *
 * ```js
 * wildcardMatch('src/*.js')('src/index.js') //=> true
 * ```
 *
 * ```js
 * const isMatch = wildcardMatch('*.example.com', '.')
 * isMatch('foo.example.com') //=> true
 * isMatch('foo.bar.com') //=> false
 * ```
 */
function wildcardMatch(
  pattern: string | string[],
  options?: string | boolean | WildcardMatchOptions,
) {
  if (typeof pattern !== "string" && !Array.isArray(pattern)) {
    throw new TypeError(
      `The first argument must be a single pattern string or an array of patterns, but ${typeof pattern} given`,
    );
  }

  if (typeof options === "string" || typeof options === "boolean") {
    options = { separator: options };
  }

  if (
    arguments.length === 2 &&
    !(
      typeof options === "undefined" ||
      (typeof options === "object" && !Array.isArray(options))
    )
  ) {
    throw new TypeError(
      `The second argument must be an options object or a string/boolean separator, but ${typeof options} given`,
    );
  }

  options = options ?? {};

  if (options.separator === "\\") {
    throw new Error(
      "\\ is not a valid separator because it is used for escaping. Try setting the separator to `true` instead",
    );
  }

  const regexpPattern = transform(pattern, options.separator);
  const regexp = new RegExp(`^${regexpPattern}$`, options.flags);

  const fn = isMatch.bind(null, regexp) as isMatch;
  fn.options = options;
  fn.pattern = pattern;
  fn.regexp = regexp;
  return fn;
}

export { wildcardMatch, isMatch };
