// This file is ported from globrex@0.1.2
// MIT License
// Copyright (c) 2018 Terkel Gjervig Nielsen
/** This module is browser compatible. */

import { isWindows as isWin } from "./_constants.ts";

const SEP = isWin ? `(?:\\\\|\\/)` : `\\/`;
const SEP_ESC = isWin ? `\\\\` : `/`;
const SEP_RAW = isWin ? `\\` : `/`;
const GLOBSTAR = `(?:(?:[^${SEP_ESC}/]*(?:${SEP_ESC}|\/|$))*)`;
const WILDCARD = `(?:[^${SEP_ESC}/]*)`;
const GLOBSTAR_SEGMENT = `((?:[^${SEP_ESC}/]*(?:${SEP_ESC}|\/|$))*)`;
const WILDCARD_SEGMENT = `(?:[^${SEP_ESC}/]*)`;

export interface GlobrexOptions {
  /** Allow ExtGlob features.
   * @default false */
  extended?: boolean;
  /** Support globstar.
   * @remarks When globstar is `true`, '/foo/**' is equivalent
   * to '/foo/*' when globstar is `false`.
   * Having globstar set to `true` is the same usage as
   * using wildcards in bash.
   * @default false */
  globstar?: boolean;
  /** Be laissez-faire about mutiple slashes.
   * @default true */
  strict?: boolean;
  /** Parse as filepath for extra path related features.
   * @default false */
  filepath?: boolean;
  /** Flag to use in the generated RegExp. */
  flags?: string;
}

export interface GlobrexResult {
  regex: RegExp;
  path?: {
    regex: RegExp;
    segments: RegExp[];
    globstar?: RegExp;
  };
}

/**
 * Convert any glob pattern to a JavaScript Regexp object
 * @param glob Glob pattern to convert
 * @param opts Configuration object
 * @returns Converted object with string, segments and RegExp object
 */
export function globrex(
  glob: string,
  {
    extended = false,
    globstar = false,
    strict = false,
    filepath = false,
    flags = "",
  }: GlobrexOptions = {},
): GlobrexResult {
  const sepPattern = new RegExp(`^${SEP}${strict ? "" : "+"}$`);
  let regex = "";
  let segment = "";
  let pathRegexStr = "";
  const pathSegments = [];

  // If we are doing extended matching, this boolean is true when we are inside
  // a group (eg {*.html,*.js}), and false otherwise.
  let inGroup = false;
  let inRange = false;

  // extglob stack. Keep track of scope
  const ext = [];

  interface AddOptions {
    split?: boolean;
    last?: boolean;
    only?: string;
  }

  // Helper function to build string and segments
  function add(
    str: string,
    options: AddOptions = { split: false, last: false, only: "" },
  ): void {
    const { split, last, only } = options;
    if (only !== "path") regex += str;
    if (filepath && only !== "regex") {
      pathRegexStr += str.match(sepPattern) ? SEP : str;
      if (split) {
        if (last) segment += str;
        if (segment !== "") {
          // change it 'includes'
          if (!flags.includes("g")) segment = `^${segment}$`;
          pathSegments.push(new RegExp(segment, flags));
        }
        segment = "";
      } else {
        segment += str;
      }
    }
  }

  let c, n;
  for (let i = 0; i < glob.length; i++) {
    c = glob[i];
    n = glob[i + 1];

    if (["\\", "$", "^", ".", "="].includes(c)) {
      add(`\\${c}`);
      continue;
    }

    if (c.match(sepPattern)) {
      add(SEP, { split: true });
      if (n != null && n.match(sepPattern) && !strict) regex += "?";
      continue;
    }

    if (c === "(") {
      if (ext.length) {
        add(`${c}?:`);
        continue;
      }
      add(`\\${c}`);
      continue;
    }

    if (c === ")") {
      if (ext.length) {
        add(c);
        const type: string | undefined = ext.pop();
        if (type === "@") {
          add("{1}");
        } else if (type === "!") {
          add(WILDCARD);
        } else {
          add(type as string);
        }
        continue;
      }
      add(`\\${c}`);
      continue;
    }

    if (c === "|") {
      if (ext.length) {
        add(c);
        continue;
      }
      add(`\\${c}`);
      continue;
    }

    if (c === "+") {
      if (n === "(" && extended) {
        ext.push(c);
        continue;
      }
      add(`\\${c}`);
      continue;
    }

    if (c === "@" && extended) {
      if (n === "(") {
        ext.push(c);
        continue;
      }
    }

    if (c === "!") {
      if (extended) {
        if (inRange) {
          add("^");
          continue;
        }
        if (n === "(") {
          ext.push(c);
          add("(?!");
          i++;
          continue;
        }
        add(`\\${c}`);
        continue;
      }
      add(`\\${c}`);
      continue;
    }

    if (c === "?") {
      if (extended) {
        if (n === "(") {
          ext.push(c);
        } else {
          add(".");
        }
        continue;
      }
      add(`\\${c}`);
      continue;
    }

    if (c === "[") {
      if (inRange && n === ":") {
        i++; // skip [
        let value = "";
        while (glob[++i] !== ":") value += glob[i];
        if (value === "alnum") add("(?:\\w|\\d)");
        else if (value === "space") add("\\s");
        else if (value === "digit") add("\\d");
        i++; // skip last ]
        continue;
      }
      if (extended) {
        inRange = true;
        add(c);
        continue;
      }
      add(`\\${c}`);
      continue;
    }

    if (c === "]") {
      if (extended) {
        inRange = false;
        add(c);
        continue;
      }
      add(`\\${c}`);
      continue;
    }

    if (c === "{") {
      if (extended) {
        inGroup = true;
        add("(?:");
        continue;
      }
      add(`\\${c}`);
      continue;
    }

    if (c === "}") {
      if (extended) {
        inGroup = false;
        add(")");
        continue;
      }
      add(`\\${c}`);
      continue;
    }

    if (c === ",") {
      if (inGroup) {
        add("|");
        continue;
      }
      add(`\\${c}`);
      continue;
    }

    if (c === "*") {
      if (n === "(" && extended) {
        ext.push(c);
        continue;
      }
      // Move over all consecutive "*"'s.
      // Also store the previous and next characters
      const prevChar = glob[i - 1];
      let starCount = 1;
      while (glob[i + 1] === "*") {
        starCount++;
        i++;
      }
      const nextChar = glob[i + 1];
      if (!globstar) {
        // globstar is disabled, so treat any number of "*" as one
        add(".*");
      } else {
        // globstar is enabled, so determine if this is a globstar segment
        const isGlobstar = starCount > 1 && // multiple "*"'s
          // from the start of the segment
          [SEP_RAW, "/", undefined].includes(prevChar) &&
          // to the end of the segment
          [SEP_RAW, "/", undefined].includes(nextChar);
        if (isGlobstar) {
          // it's a globstar, so match zero or more path segments
          add(GLOBSTAR, { only: "regex" });
          add(GLOBSTAR_SEGMENT, { only: "path", last: true, split: true });
          i++; // move over the "/"
        } else {
          // it's not a globstar, so only match one path segment
          add(WILDCARD, { only: "regex" });
          add(WILDCARD_SEGMENT, { only: "path" });
        }
      }
      continue;
    }

    add(c);
  }

  // When regexp 'g' flag is specified don't
  // constrain the regular expression with ^ & $
  if (!flags.includes("g")) {
    regex = `^${regex}$`;
    segment = `^${segment}$`;
    if (filepath) pathRegexStr = `^${pathRegexStr}$`;
  }

  const result: GlobrexResult = { regex: new RegExp(regex, flags) };

  // Push the last segment
  if (filepath) {
    pathSegments.push(new RegExp(segment, flags));
    result.path = {
      regex: new RegExp(pathRegexStr, flags),
      segments: pathSegments,
      globstar: new RegExp(
        !flags.includes("g") ? `^${GLOBSTAR_SEGMENT}$` : GLOBSTAR_SEGMENT,
        flags,
      ),
    };
  }

  return result;
}
