// Copyright 2018-2026 the Deno authors. MIT license.
// translated primarily from: https://github.com/nodejs/node/blob/2acc8bc6a9a830b38d101ac70390b8c5c9a14bf3/lib/internal/fs/glob.js#L258
// with glob() and globSync() from: https://github.com/nodejs/node/blob/2acc8bc6a9a830b38d101ac70390b8c5c9a14bf3/lib/fs.js#L3167
import { core, primordials } from "ext:core/mod.js";
import { op_require_real_path, op_require_stat } from "ext:core/ops";

const {
  validateBoolean,
  validateObject,
  validateString,
  validateStringArray,
} = core.loadExtScript("ext:deno_node/internal/validators.mjs");
const { isMacOS, isWindows } = core.loadExtScript("ext:deno_node/_util/os.ts");
const { kEmptyObject } = core.loadExtScript("ext:deno_node/internal/util.mjs");
const lazyProcess = core.createLazyLoader("node:process");
const lazyReaddir = core.createLazyLoader(
  "ext:deno_node/_fs/_fs_readdir.ts",
);
const {
  lstatPromise: lstat,
  lstatSync,
} = core.loadExtScript("ext:deno_node/_fs/_fs_lstat.ts");

const {
  basename,
  dirname,
  isAbsolute,
  join,
  resolve,
} = core.loadExtScript("ext:deno_node/path/mod.ts");
import {
  type Dirent,
  DirentFromStats,
} from "ext:deno_node/internal/fs/utils.mjs";

const {
  ERR_INVALID_ARG_TYPE,
  hideStackFrames,
} = core.loadExtScript("ext:deno_node/internal/errors.ts");

const { default: assert } = core.loadExtScript("ext:deno_node/assert.ts");

import type { ErrnoException } from "ext:deno_node/_global.d.ts";
const { toPathIfFileURL } = core.loadExtScript(
  "ext:deno_node/internal/url.ts",
);

interface GlobOptionsBase {
  /**
   * Current working directory.
   * @default process.cwd()
   */
  cwd?: string | undefined;
  /**
   * `true` if the glob should return paths as `Dirent`s, `false` otherwise.
   * @default false
   * @since v22.2.0
   */
  withFileTypes?: boolean | undefined;
  /**
   * Whether to follow symbolic links.
   * @default true
   * @since v26.0.0
   */
  followSymlinks?: boolean | undefined;
  /**
   * Function to filter out files/directories. Return true to exclude the item, false to include it.
   */
  // deno-lint-ignore no-explicit-any
  exclude?: ((fileName: any) => boolean) | undefined;
}
export interface GlobOptionsWithFileTypes extends GlobOptionsBase {
  exclude?: ((fileName: Dirent) => boolean) | undefined;
  withFileTypes: true;
}
export interface GlobOptionsWithoutFileTypes extends GlobOptionsBase {
  exclude?: ((fileName: string) => boolean) | undefined;
  withFileTypes?: false | undefined;
}
export interface GlobOptions extends GlobOptionsBase {
  exclude?: ((fileName: Dirent | string) => boolean) | undefined;
}

export type GlobOptionsU =
  | GlobOptionsWithFileTypes
  | GlobOptionsWithoutFileTypes
  | GlobOptions;

export type GlobCallback<Args extends unknown[]> = (
  e: ErrnoException | null,
  ...args: Args
) => unknown;

const nop = () => {};

const {
  ArrayFrom,
  ArrayFromAsync,
  ArrayIsArray,
  ArrayPrototypeAt,
  ArrayPrototypeFlatMap,
  ArrayPrototypeMap,
  ArrayPrototypePop,
  ArrayPrototypePush,
  ArrayPrototypeSome,
  MapPrototypeDelete,
  MapPrototypeForEach,
  MapPrototypeGet,
  MapPrototypeGetSize,
  MapPrototypeSet,
  PromisePrototype,
  PromisePrototypeThen,
  PromiseResolve,
  SafeMap,
  SafeMapIterator,
  SafeSet,
  SafeSetIterator,
  SetPrototypeForEach,
  StringPrototypeEndsWith,
  ReflectApply,
  ObjectPrototypeIsPrototypeOf,
} = primordials;

const lazyMinimatch = core.createLazyLoader("ext:deno_node/deps/minimatch.js");

/**
 * @param {string} path
 * @returns {Promise<DirentFromStats|null>}
 */
async function getDirent(path) {
  let stat;
  try {
    stat = await lstat(path);
  } catch {
    return null;
  }
  return new DirentFromStats(basename(path), stat, dirname(path));
}

/**
 * @param {string} path
 * @returns {DirentFromStats|null}
 */
function getDirentSync(path) {
  try {
    const stat = lstatSync(path, { throwIfNoEntry: false });
    if (stat === undefined) {
      return null;
    }
    return new DirentFromStats(basename(path), stat, dirname(path));
  } catch (err) {
    if (err.code === "ENOTDIR") {
      return null;
    }
    throw err;
  }
}

function getFollowDirent(path) {
  try {
    const result = op_require_stat(path);
    if (result === -1) return null;
    return { isDirectory: () => result === 1 };
  } catch {
    return null;
  }
}

function getFollowDirentSync(path) {
  try {
    const result = op_require_stat(path);
    if (result === -1) return null;
    return { isDirectory: () => result === 1 };
  } catch {
    return null;
  }
}

function getRealpathString(path) {
  try {
    return op_require_real_path(path);
  } catch {
    return null;
  }
}

function getRealpathStringSync(path) {
  try {
    return op_require_real_path(path);
  } catch {
    return null;
  }
}

/**
 * @callback validateStringArrayOrFunction
 * @param {*} value
 * @param {string} name
 */
const validateStringArrayOrFunction = hideStackFrames((value, name) => {
  if (ArrayIsArray(value)) {
    for (let i = 0; i < value.length; ++i) {
      if (typeof value[i] !== "string") {
        throw new ERR_INVALID_ARG_TYPE(`${name}[${i}]`, "string", value[i]);
      }
    }
    return;
  }
  if (typeof value !== "function") {
    throw new ERR_INVALID_ARG_TYPE(name, ["string[]", "function"], value);
  }
});

/**
 * @param {string} pattern
 * @param {options} options
 * @returns {Minimatch}
 */
function createMatcher(pattern, options = kEmptyObject) {
  const opts = {
    __proto__: null,
    nocase: isWindows || isMacOS,
    windowsPathsNoEscape: true,
    nonegate: true,
    nocomment: true,
    optimizationLevel: 2,
    platform: lazyProcess().default.platform,
    nocaseMagicOnly: true,
    ...options,
  };
  return new (lazyMinimatch().default.Minimatch)(pattern, opts);
}

class Cache {
  #cache = new SafeMap();
  #statsCache = new SafeMap();
  #followStatsCache = new SafeMap();
  #readdirCache = new SafeMap();
  #realpathCache = new SafeMap();

  stat(path) {
    const cached = this.#statsCache.get(path);
    if (cached) {
      return cached;
    }
    const promise = getDirent(path);
    this.#statsCache.set(path, promise);
    return promise;
  }
  statSync(path) {
    const cached = this.#statsCache.get(path);
    // Do not return a promise from a sync function.
    if (cached && !ObjectPrototypeIsPrototypeOf(PromisePrototype, cached)) {
      return cached;
    }
    const val = getDirentSync(path);
    this.#statsCache.set(path, val);
    return val;
  }
  addToStatCache(path, val) {
    this.#statsCache.set(path, val);
  }
  followStat(path) {
    const cached = this.#followStatsCache.get(path);
    if (cached) {
      return cached;
    }
    const promise = PromiseResolve(getFollowDirent(path));
    this.#followStatsCache.set(path, promise);
    return promise;
  }
  followStatSync(path) {
    const cached = this.#followStatsCache.get(path);
    if (cached && !ObjectPrototypeIsPrototypeOf(PromisePrototype, cached)) {
      return cached;
    }
    const val = getFollowDirentSync(path);
    this.#followStatsCache.set(path, val);
    return val;
  }
  realpath(path) {
    const cached = this.#realpathCache.get(path);
    if (cached) {
      return cached;
    }
    const promise = PromiseResolve(getRealpathString(path));
    this.#realpathCache.set(path, promise);
    return promise;
  }
  realpathSync(path) {
    const cached = this.#realpathCache.get(path);
    if (cached && !ObjectPrototypeIsPrototypeOf(PromisePrototype, cached)) {
      return cached;
    }
    const val = getRealpathStringSync(path);
    this.#realpathCache.set(path, val);
    return val;
  }
  readdir(path) {
    const cached = this.#readdirCache.get(path);
    if (cached) {
      return cached;
    }
    const promise = PromisePrototypeThen(
      lazyReaddir().readdirPromise(path, {
        __proto__: null,
        withFileTypes: true,
      }),
      null,
      () => [],
    );
    this.#readdirCache.set(path, promise);
    return promise;
  }
  readdirSync(path) {
    const cached = this.#readdirCache.get(path);
    if (cached) {
      return cached;
    }
    let val;
    try {
      val = lazyReaddir().readdirSync(path, {
        __proto__: null,
        withFileTypes: true,
      });
    } catch {
      val = [];
    }
    this.#readdirCache.set(path, val);
    return val;
  }
  add(path, pattern) {
    let cache = this.#cache.get(path);
    if (!cache) {
      cache = new SafeSet();
      this.#cache.set(path, cache);
    }
    const originalSize = cache.size;
    SetPrototypeForEach(
      pattern.indexes,
      (index) => cache.add(pattern.cacheKey(index)),
    );
    return cache.size !== originalSize + pattern.indexes.size;
  }
  seen(path, pattern, index) {
    return this.#cache.get(path)?.has(pattern.cacheKey(index));
  }
}

class Pattern {
  #pattern;
  #globStrings;
  indexes;
  symlinks;
  realpaths;
  last;

  constructor(
    pattern,
    globStrings,
    indexes,
    symlinks,
    realpaths = new SafeSet(),
  ) {
    this.#pattern = pattern;
    this.#globStrings = globStrings;
    this.indexes = indexes;
    this.symlinks = symlinks;
    this.realpaths = realpaths;
    this.last = pattern.length - 1;
  }

  isLast(isDirectory) {
    return this.indexes.has(this.last) ||
      (this.partAt(-1) === "" && isDirectory &&
        this.indexes.has(this.last - 1) &&
        this.partAt(-2) === lazyMinimatch().default.GLOBSTAR);
  }
  isFirst() {
    return this.indexes.has(0);
  }
  get hasSeenSymlinks() {
    return ArrayPrototypeSome(
      ArrayFrom(this.indexes),
      (i) => !this.symlinks.has(i),
    );
  }
  partAt(index) {
    return ArrayPrototypeAt(this.#pattern, index);
  }
  child(indexes, symlinks = new SafeSet(), realpaths = this.realpaths) {
    return new Pattern(
      this.#pattern,
      this.#globStrings,
      indexes,
      symlinks,
      realpaths,
    );
  }
  test(index, path) {
    if (index > this.#pattern.length) {
      return false;
    }
    const pattern = this.#pattern[index];
    if (pattern === lazyMinimatch().default.GLOBSTAR) {
      return true;
    }
    if (typeof pattern === "string") {
      return pattern === path;
    }
    if (typeof pattern?.test === "function") {
      return pattern.test(path);
    }
    return false;
  }

  cacheKey(index) {
    let key = "";
    for (let i = index; i < this.#globStrings.length; i++) {
      key += this.#globStrings[i];
      if (i !== this.#globStrings.length - 1) {
        key += "/";
      }
    }
    return key;
  }
}

class ResultSet extends SafeSet {
  #root = ".";
  #isExcluded = () => false;

  setup(root, isExcludedFn) {
    this.#root = root;
    this.#isExcluded = isExcludedFn;
  }

  add(value) {
    if (this.#isExcluded(resolve(this.#root, value))) {
      return false;
    }
    super.add(value);
    return true;
  }
}

export class Glob {
  #root;
  #exclude;
  #cache = new Cache();
  #results = new ResultSet();
  #queue = [];
  #subpatterns = new SafeMap();
  #patterns;
  #withFileTypes;
  #followSymlinks = false;
  #isExcluded = () => false;
  constructor(pattern, options = kEmptyObject) {
    validateObject(options, "options");
    const { exclude, cwd, withFileTypes, followSymlinks } = options;
    if (followSymlinks != null) {
      validateBoolean(followSymlinks, "options.followSymlinks");
      this.#followSymlinks = followSymlinks;
    }
    this.#root = toPathIfFileURL(cwd) ?? ".";
    this.#withFileTypes = !!withFileTypes;
    if (exclude != null) {
      validateStringArrayOrFunction(exclude, "options.exclude");
      if (ArrayIsArray(exclude)) {
        assert(typeof this.#root === "string");
        // Convert the path part of exclude patterns to absolute paths for
        // consistent comparison before instantiating matchers.
        const matchers = ArrayPrototypeMap(
          ArrayPrototypeMap(exclude, (pattern) => resolve(this.#root, pattern)),
          (pattern) => createMatcher(pattern),
        );
        this.#isExcluded = (value) =>
          ArrayPrototypeSome(matchers, (matcher) => {
            // No primordial exists for Minimatch.prototype.match.
            // deno-lint-ignore deno-internal/prefer-primordials
            return matcher.match(value);
          });
        this.#results.setup(this.#root, this.#isExcluded);
      } else {
        this.#exclude = exclude;
      }
    }
    let patterns;
    if (typeof pattern === "object") {
      validateStringArray(pattern, "patterns");
      patterns = pattern;
    } else {
      validateString(pattern, "patterns");
      patterns = [pattern];
    }
    this.matchers = ArrayPrototypeMap(
      patterns,
      (pattern) => createMatcher(pattern),
    );
    this.#patterns = ArrayPrototypeFlatMap(
      this.matchers,
      (matcher) =>
        ArrayPrototypeMap(
          matcher.set,
          (pattern, i) =>
            new Pattern(
              pattern,
              matcher.globParts[i],
              new SafeSet().add(0),
              new SafeSet(),
            ),
        ),
    );
  }

  #isDirectorySync(path, stat, pattern) {
    if (stat?.isDirectory()) return true;
    if (!stat?.isSymbolicLink()) return false;
    if (this.#followSymlinks) {
      return !!this.#cache.followStatSync(path)?.isDirectory();
    }
    return pattern.hasSeenSymlinks;
  }
  async #isDirectory(path, stat, pattern) {
    if (stat?.isDirectory()) return true;
    if (!stat?.isSymbolicLink()) return false;
    if (this.#followSymlinks) {
      return !!(await this.#cache.followStat(path))?.isDirectory();
    }
    return pattern.hasSeenSymlinks;
  }
  #isCyclicSync(path, isDirectory, pattern) {
    if (!this.#followSymlinks || !isDirectory) return false;
    const real = this.#cache.realpathSync(path);
    return real !== null && pattern.realpaths.has(real);
  }
  async #isCyclic(path, isDirectory, pattern) {
    if (!this.#followSymlinks || !isDirectory) return false;
    const real = await this.#cache.realpath(path);
    return real !== null && pattern.realpaths.has(real);
  }
  #nextRealpathsSync(path, isDirectory, pattern) {
    if (!this.#followSymlinks || !isDirectory) return pattern.realpaths;
    const real = this.#cache.realpathSync(path);
    if (real === null) return pattern.realpaths;
    const realpaths = new SafeSet();
    SetPrototypeForEach(pattern.realpaths, (v) => realpaths.add(v));
    realpaths.add(real);
    return realpaths;
  }
  async #nextRealpaths(path, isDirectory, pattern) {
    if (!this.#followSymlinks || !isDirectory) return pattern.realpaths;
    const real = await this.#cache.realpath(path);
    if (real === null) return pattern.realpaths;
    const realpaths = new SafeSet();
    SetPrototypeForEach(pattern.realpaths, (v) => realpaths.add(v));
    realpaths.add(real);
    return realpaths;
  }
  globSync() {
    ArrayPrototypePush(this.#queue, {
      __proto__: null,
      path: ".",
      patterns: this.#patterns,
    });
    while (this.#queue.length > 0) {
      const item = ArrayPrototypePop(this.#queue);
      for (let i = 0; i < item.patterns.length; i++) {
        this.#addSubpatterns(item.path, item.patterns[i]);
      }
      MapPrototypeForEach(
        this.#subpatterns,
        (patterns, path) =>
          ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns }),
      );
      this.#subpatterns.clear();
    }
    return ArrayFrom(
      this.#results,
      this.#withFileTypes
        ? (path) =>
          this.#cache.statSync(isAbsolute(path) ? path : join(this.#root, path))
        : undefined,
    );
  }
  #addSubpattern(path, pattern) {
    if (this.#isExcluded(path)) {
      return;
    }
    const fullpath = resolve(this.#root, path);

    // If path is a directory, add trailing slash and test patterns again.
    if (
      this.#cache.statSync(fullpath).isDirectory() &&
      this.#isExcluded(`${fullpath}/`)
    ) {
      return;
    }

    if (this.#exclude) {
      if (this.#withFileTypes) {
        const stat = this.#cache.statSync(path);
        if (stat !== null) {
          if (this.#exclude(stat)) {
            return;
          }
        }
      } else if (this.#exclude(path)) {
        return;
      }
    }
    if (!this.#subpatterns.has(path)) {
      this.#subpatterns.set(path, [pattern]);
    } else {
      ArrayPrototypePush(this.#subpatterns.get(path), pattern);
    }
  }
  #addSubpatterns(path, pattern) {
    const seen = this.#cache.add(path, pattern);
    if (seen) {
      return;
    }
    const fullpath = resolve(this.#root, path);
    const stat = this.#cache.statSync(fullpath);
    const last = pattern.last;
    const isDirectory = this.#isDirectorySync(fullpath, stat, pattern);
    const isLast = pattern.isLast(isDirectory);
    const isFirst = pattern.isFirst();

    if (this.#isExcluded(fullpath)) {
      return;
    }
    if (
      isFirst && isWindows && typeof pattern.partAt(0) === "string" &&
      StringPrototypeEndsWith(pattern.partAt(0), ":")
    ) {
      // Absolute path, go to root
      this.#addSubpattern(
        `${pattern.partAt(0)}\\`,
        pattern.child(new SafeSet().add(1)),
      );
      return;
    }
    if (isFirst && pattern.partAt(0) === "") {
      // Absolute path, go to root
      this.#addSubpattern("/", pattern.child(new SafeSet().add(1)));
      return;
    }
    if (isFirst && pattern.partAt(0) === "..") {
      // Start with .., go to parent
      this.#addSubpattern("../", pattern.child(new SafeSet().add(1)));
      return;
    }
    if (isFirst && pattern.partAt(0) === ".") {
      // Start with ., proceed
      this.#addSubpattern(".", pattern.child(new SafeSet().add(1)));
      return;
    }

    if (isLast && typeof pattern.partAt(-1) === "string") {
      // Add result if it exists
      const p = pattern.partAt(-1);
      const stat = this.#cache.statSync(join(fullpath, p));
      if (stat && (p || isDirectory)) {
        this.#results.add(join(path, p));
      }
      if (pattern.indexes.size === 1 && pattern.indexes.has(last)) {
        return;
      }
    } else if (
      isLast && pattern.partAt(-1) === lazyMinimatch().default.GLOBSTAR &&
      (path !== "." || pattern.partAt(0) === "." || (last === 0 && stat))
    ) {
      // If pattern ends with **, add to results
      // if path is ".", add it only if pattern starts with "." or pattern is exactly "**"
      this.#results.add(path);
    }

    if (!isDirectory || this.#isCyclicSync(fullpath, isDirectory, pattern)) {
      return;
    }

    const nextRealpaths = this.#nextRealpathsSync(
      fullpath,
      isDirectory,
      pattern,
    );

    let children;
    const firstPattern = pattern.indexes.size === 1 &&
      pattern.partAt(ArrayFrom(pattern.indexes)[0]);
    if (typeof firstPattern === "string") {
      const stat = this.#cache.statSync(join(fullpath, firstPattern));
      if (stat) {
        stat.name = firstPattern;
        children = [stat];
      } else {
        return;
      }
    } else {
      children = this.#cache.readdirSync(fullpath);
    }

    for (let i = 0; i < children.length; i++) {
      const entry = children[i];
      const entryPath = join(path, entry.name);
      const entryFullpath = join(fullpath, entry.name);
      this.#cache.addToStatCache(entryFullpath, entry);
      const entryIsDirectory = entry.isDirectory() ||
        (this.#followSymlinks && entry.isSymbolicLink() &&
          !!this.#cache.followStatSync(entryFullpath)?.isDirectory());

      const subPatterns = new SafeSet();
      const nSymlinks = new SafeSet();
      for (const index of new SafeSetIterator(pattern.indexes)) {
        const current = pattern.partAt(index);
        const nextIndex = index + 1;
        const next = pattern.partAt(nextIndex);
        const fromSymlink = !this.#followSymlinks &&
          pattern.symlinks.has(index);

        if (current === lazyMinimatch().default.GLOBSTAR) {
          const isDot = entry.name[0] === ".";

          const nextMatches = pattern.test(nextIndex, entry.name);

          let nextNonGlobIndex = nextIndex;
          while (
            pattern.partAt(nextNonGlobIndex) ===
              lazyMinimatch().default.GLOBSTAR
          ) {
            nextNonGlobIndex++;
          }

          const matchesDot = isDot &&
            pattern.test(nextNonGlobIndex, entry.name);

          if (
            (isDot && !matchesDot) ||
            (this.#exclude &&
              this.#exclude(this.#withFileTypes ? entry : entry.name))
          ) {
            continue;
          }
          if (!fromSymlink && entryIsDirectory) {
            // If directory, add ** to its potential patterns
            subPatterns.add(index);
          } else if (!fromSymlink && index === last) {
            // If ** is last, add to results
            this.#results.add(entryPath);
          }

          // Any pattern after ** is also a potential pattern
          // so we can already test it here
          if (nextMatches && nextIndex === last && !isLast) {
            // If next pattern is the last one, add to results
            this.#results.add(entryPath);
          } else if (nextMatches && entryIsDirectory) {
            // Pattern matched, meaning two patterns forward
            // are also potential patterns
            // e.g **/b/c when entry is a/b - add c to potential patterns
            subPatterns.add(index + 2);
          }
          if (
            (nextMatches || pattern.partAt(0) === ".") &&
            (entryIsDirectory || entry.isSymbolicLink()) &&
            !fromSymlink
          ) {
            // If pattern after ** matches, or pattern starts with "."
            // and entry is a directory or symlink, add to potential patterns
            subPatterns.add(nextIndex);
          }

          if (!this.#followSymlinks && entry.isSymbolicLink()) {
            nSymlinks.add(index);
          }

          if (next === ".." && entryIsDirectory) {
            // In case pattern is "**/..",
            // both parent and current directory should be added to the queue
            // if this is the last pattern, add to results instead
            const parent = join(path, "..");
            if (nextIndex < last) {
              if (
                !this.#subpatterns.has(path) &&
                !this.#cache.seen(path, pattern, nextIndex + 1)
              ) {
                this.#subpatterns.set(path, [
                  pattern.child(new SafeSet().add(nextIndex + 1)),
                ]);
              }
              if (
                !this.#subpatterns.has(parent) &&
                !this.#cache.seen(parent, pattern, nextIndex + 1)
              ) {
                this.#subpatterns.set(parent, [
                  pattern.child(new SafeSet().add(nextIndex + 1)),
                ]);
              }
            } else {
              if (!this.#cache.seen(path, pattern, nextIndex)) {
                this.#cache.add(
                  path,
                  pattern.child(new SafeSet().add(nextIndex)),
                );
                this.#results.add(path);
              }
              if (
                !this.#cache.seen(path, pattern, nextIndex) ||
                !this.#cache.seen(parent, pattern, nextIndex)
              ) {
                this.#cache.add(
                  parent,
                  pattern.child(new SafeSet().add(nextIndex)),
                );
                this.#results.add(parent);
              }
            }
          }
        }
        if (typeof current === "string") {
          if (pattern.test(index, entry.name) && index !== last) {
            // If current pattern matches entry name
            // the next pattern is a potential pattern
            subPatterns.add(nextIndex);
          } else if (current === "." && pattern.test(nextIndex, entry.name)) {
            // If current pattern is ".", proceed to test next pattern
            if (nextIndex === last) {
              this.#results.add(entryPath);
            } else {
              subPatterns.add(nextIndex + 1);
            }
          }
        }
        if (typeof current === "object" && pattern.test(index, entry.name)) {
          // If current pattern is a regex that matches entry name (e.g *.js)
          // add next pattern to potential patterns, or to results if it's the last pattern
          if (index === last) {
            this.#results.add(entryPath);
          } else if (entryIsDirectory) {
            subPatterns.add(nextIndex);
          }
        }
      }
      if (subPatterns.size > 0) {
        // If there are potential patterns, add to queue
        this.#addSubpattern(
          entryPath,
          pattern.child(subPatterns, nSymlinks, nextRealpaths),
        );
      }
    }
  }

  async *glob() {
    ArrayPrototypePush(this.#queue, {
      __proto__: null,
      path: ".",
      patterns: this.#patterns,
    });
    while (this.#queue.length > 0) {
      const item = ArrayPrototypePop(this.#queue);
      for (let i = 0; i < item.patterns.length; i++) {
        const iter = this.#iterateSubpatterns(item.path, item.patterns[i]);
        while (true) {
          // deno-lint-ignore deno-internal/prefer-primordials
          const { done, value } = await iter.next();
          if (done) break;
          yield value;
        }
      }
      MapPrototypeForEach(
        this.#subpatterns,
        (patterns, path) =>
          ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns }),
      );
      this.#subpatterns.clear();
    }
  }
  async *#iterateSubpatterns(path, pattern) {
    const seen = this.#cache.add(path, pattern);
    if (seen) {
      return;
    }
    const fullpath = resolve(this.#root, path);
    const stat = await this.#cache.stat(fullpath);
    const last = pattern.last;
    const isDirectory = await this.#isDirectory(fullpath, stat, pattern);
    const isLast = pattern.isLast(isDirectory);
    const isFirst = pattern.isFirst();

    if (this.#isExcluded(fullpath)) {
      return;
    }
    if (
      isFirst && isWindows && typeof pattern.partAt(0) === "string" &&
      StringPrototypeEndsWith(pattern.partAt(0), ":")
    ) {
      // Absolute path, go to root
      this.#addSubpattern(
        `${pattern.partAt(0)}\\`,
        pattern.child(new SafeSet().add(1)),
      );
      return;
    }

    if (isFirst && pattern.partAt(0) === "") {
      // Absolute path, go to root
      this.#addSubpattern("/", pattern.child(new SafeSet().add(1)));
      return;
    }
    if (isFirst && pattern.partAt(0) === "..") {
      // Start with .., go to parent
      this.#addSubpattern("../", pattern.child(new SafeSet().add(1)));
      return;
    }
    if (isFirst && pattern.partAt(0) === ".") {
      // Start with ., proceed
      this.#addSubpattern(".", pattern.child(new SafeSet().add(1)));
      return;
    }

    if (isLast && typeof pattern.partAt(-1) === "string") {
      // Add result if it exists
      const p = pattern.partAt(-1);
      const stat = await this.#cache.stat(join(fullpath, p));
      if (stat && (p || isDirectory)) {
        const result = join(path, p);
        if (!this.#results.has(result)) {
          if (this.#results.add(result)) {
            yield this.#withFileTypes ? stat : result;
          }
        }
      }
      if (pattern.indexes.size === 1 && pattern.indexes.has(last)) {
        return;
      }
    } else if (
      isLast && pattern.partAt(-1) === lazyMinimatch().default.GLOBSTAR &&
      (path !== "." || pattern.partAt(0) === "." || (last === 0 && stat))
    ) {
      // If pattern ends with **, add to results
      // if path is ".", add it only if pattern starts with "." or pattern is exactly "**"
      if (!this.#results.has(path)) {
        if (this.#results.add(path)) {
          yield this.#withFileTypes ? stat : path;
        }
      }
    }

    if (!isDirectory || await this.#isCyclic(fullpath, isDirectory, pattern)) {
      return;
    }

    const nextRealpaths = await this.#nextRealpaths(
      fullpath,
      isDirectory,
      pattern,
    );

    let children;
    const firstPattern = pattern.indexes.size === 1 &&
      pattern.partAt(ArrayFrom(pattern.indexes)[0]);
    if (typeof firstPattern === "string") {
      const stat = await this.#cache.stat(join(fullpath, firstPattern));
      if (stat) {
        stat.name = firstPattern;
        children = [stat];
      } else {
        return;
      }
    } else {
      children = await this.#cache.readdir(fullpath);
    }

    for (let i = 0; i < children.length; i++) {
      const entry = children[i];
      const entryPath = join(path, entry.name);
      const entryFullpath = join(fullpath, entry.name);
      this.#cache.addToStatCache(entryFullpath, entry);
      const entryIsDirectory = entry.isDirectory() ||
        (this.#followSymlinks && entry.isSymbolicLink() &&
          !!(await this.#cache.followStat(entryFullpath))?.isDirectory());

      const subPatterns = new SafeSet();
      const nSymlinks = new SafeSet();
      for (const index of new SafeSetIterator(pattern.indexes)) {
        const current = pattern.partAt(index);
        const nextIndex = index + 1;
        const next = pattern.partAt(nextIndex);
        const fromSymlink = !this.#followSymlinks &&
          pattern.symlinks.has(index);

        if (current === lazyMinimatch().default.GLOBSTAR) {
          const isDot = entry.name[0] === ".";

          const nextMatches = pattern.test(nextIndex, entry.name);

          let nextNonGlobIndex = nextIndex;
          while (
            pattern.partAt(nextNonGlobIndex) ===
              lazyMinimatch().default.GLOBSTAR
          ) {
            nextNonGlobIndex++;
          }

          const matchesDot = isDot &&
            pattern.test(nextNonGlobIndex, entry.name);

          if (
            (isDot && !matchesDot) ||
            (this.#exclude &&
              this.#exclude(this.#withFileTypes ? entry : entry.name))
          ) {
            continue;
          }
          if (!fromSymlink && entryIsDirectory) {
            // If directory, add ** to its potential patterns
            subPatterns.add(index);
          } else if (!fromSymlink && index === last) {
            // If ** is last, add to results
            if (!this.#results.has(entryPath) && this.#results.add(entryPath)) {
              yield this.#withFileTypes ? entry : entryPath;
            }
          }

          // Any pattern after ** is also a potential pattern
          // so we can already test it here
          if (nextMatches && nextIndex === last && !isLast) {
            // If next pattern is the last one, add to results
            if (!this.#results.has(entryPath) && this.#results.add(entryPath)) {
              yield this.#withFileTypes ? entry : entryPath;
            }
          } else if (nextMatches && entryIsDirectory) {
            // Pattern matched, meaning two patterns forward
            // are also potential patterns
            // e.g **/b/c when entry is a/b - add c to potential patterns
            subPatterns.add(index + 2);
          }
          if (
            (nextMatches || pattern.partAt(0) === ".") &&
            (entryIsDirectory || entry.isSymbolicLink()) &&
            !fromSymlink
          ) {
            // If pattern after ** matches, or pattern starts with "."
            // and entry is a directory or symlink, add to potential patterns
            subPatterns.add(nextIndex);
          }

          if (!this.#followSymlinks && entry.isSymbolicLink()) {
            nSymlinks.add(index);
          }

          if (next === ".." && entryIsDirectory) {
            // In case pattern is "**/..",
            // both parent and current directory should be added to the queue
            // if this is the last pattern, add to results instead
            const parent = join(path, "..");
            if (nextIndex < last) {
              if (
                !this.#subpatterns.has(path) &&
                !this.#cache.seen(path, pattern, nextIndex + 1)
              ) {
                this.#subpatterns.set(path, [
                  pattern.child(new SafeSet().add(nextIndex + 1)),
                ]);
              }
              if (
                !this.#subpatterns.has(parent) &&
                !this.#cache.seen(parent, pattern, nextIndex + 1)
              ) {
                this.#subpatterns.set(parent, [
                  pattern.child(new SafeSet().add(nextIndex + 1)),
                ]);
              }
            } else {
              if (!this.#cache.seen(path, pattern, nextIndex)) {
                this.#cache.add(
                  path,
                  pattern.child(new SafeSet().add(nextIndex)),
                );
                if (!this.#results.has(path)) {
                  if (this.#results.add(path)) {
                    yield this.#withFileTypes
                      ? this.#cache.statSync(fullpath)
                      : path;
                  }
                }
              }
              if (
                !this.#cache.seen(path, pattern, nextIndex) ||
                !this.#cache.seen(parent, pattern, nextIndex)
              ) {
                this.#cache.add(
                  parent,
                  pattern.child(new SafeSet().add(nextIndex)),
                );
                if (!this.#results.has(parent)) {
                  if (this.#results.add(parent)) {
                    yield this.#withFileTypes
                      ? this.#cache.statSync(join(this.#root, parent))
                      : parent;
                  }
                }
              }
            }
          }
        }
        if (typeof current === "string") {
          if (pattern.test(index, entry.name) && index !== last) {
            // If current pattern matches entry name
            // the next pattern is a potential pattern
            subPatterns.add(nextIndex);
          } else if (current === "." && pattern.test(nextIndex, entry.name)) {
            // If current pattern is ".", proceed to test next pattern
            if (nextIndex === last) {
              if (!this.#results.has(entryPath)) {
                if (this.#results.add(entryPath)) {
                  yield this.#withFileTypes ? entry : entryPath;
                }
              }
            } else {
              subPatterns.add(nextIndex + 1);
            }
          }
        }
        if (typeof current === "object" && pattern.test(index, entry.name)) {
          // If current pattern is a regex that matches entry name (e.g *.js)
          // add next pattern to potential patterns, or to results if it's the last pattern
          if (index === last) {
            if (!this.#results.has(entryPath)) {
              if (this.#results.add(entryPath)) {
                yield this.#withFileTypes ? entry : entryPath;
              }
            }
          } else if (entryIsDirectory) {
            subPatterns.add(nextIndex);
          }
        }
      }
      if (subPatterns.size > 0) {
        // If there are potential patterns, add to queue
        this.#addSubpattern(
          entryPath,
          pattern.child(subPatterns, nSymlinks, nextRealpaths),
        );
      }
    }
  }
}

/**
 * Check if a path matches a glob pattern
 * @param path the path to check
 * @param pattern the glob pattern to match
 * @param windows whether the path is on a Windows system, defaults to `isWindows`
 */
// Compiling a glob pattern into a `Minimatch` matcher (which builds a regex)
// is expensive relative to running it. `matchesGlob()`/`matchGlobPattern()` are
// typically called repeatedly with the same pattern, so we cache the compiled
// matcher keyed by the pattern and reuse it. This avoids recompiling on every
// call (the slow "no-reuse" path). Separate caches per platform let us key by
// the raw pattern string, avoiding a per-call key allocation on the hot path.
const MATCHER_CACHE_LIMIT = 256;
const posixMatcherCache = new SafeMap();
const windowsMatcherCache = new SafeMap();

function cachedMatcher(pattern: string, windows: boolean) {
  const cache = windows ? windowsMatcherCache : posixMatcherCache;
  let matcher = MapPrototypeGet(cache, pattern);
  if (matcher === undefined) {
    matcher = createMatcher(pattern, {
      platform: windows ? "win32" : "posix",
    });
    if (MapPrototypeGetSize(cache) >= MATCHER_CACHE_LIMIT) {
      // Evict the oldest entry (SafeMap preserves insertion order).
      for (const { 0: oldest } of new SafeMapIterator(cache)) {
        MapPrototypeDelete(cache, oldest);
        break;
      }
    }
    MapPrototypeSet(cache, pattern, matcher);
  }
  return matcher;
}

export function matchGlobPattern(
  path: string,
  pattern: string,
  windows = isWindows,
): boolean {
  validateString(path, "path");
  validateString(pattern, "pattern");
  // deno-lint-ignore deno-internal/prefer-primordials
  return cachedMatcher(pattern, windows).match(path);
}

export default {
  Glob,
  matchGlobPattern,
};

//
// extra
//

// normally-imported helper on the nodejs side
function makeCallback(cb) {
  return (...args) => ReflectApply(cb, this, args);
}

export function globSync(pattern: string | string[]): string[];
export function globSync(
  pattern: string | string[],
  options: GlobOptionsWithFileTypes,
): Dirent[];
export function globSync(
  pattern: string | string[],
  options: GlobOptionsWithoutFileTypes,
): string[];
export function globSync(
  pattern: string | string[],
  options: GlobOptions,
): Dirent[] | string[];
export function globSync(
  pattern: string | string[],
  options: GlobOptionsU = kEmptyObject,
): Dirent[] | string[] {
  return new Glob(pattern, options).globSync();
}

/**
 * Retrieves the files matching the specified pattern.
 */
export function glob(
  pattern: string | string[],
  callback: GlobCallback<[string[]]>,
): void;
export function glob(
  pattern: string | string[],
  options: GlobOptionsWithFileTypes,
  callback: GlobCallback<[Dirent[]]>,
): void;
export function glob(
  pattern: string | string[],
  options: GlobOptionsWithoutFileTypes,
  callback: GlobCallback<[string[]]>,
): void;
export function glob(
  pattern: string | string[],
  options: GlobOptions,
  callback: GlobCallback<[Dirent[] | string[]]>,
): void;
export function glob(
  pattern: string | string[],
  options: GlobOptionsU | GlobCallback<[string[]]> | undefined,
  callback: GlobCallback<[Dirent[]]> | GlobCallback<[string[]]> = nop,
): void {
  if (typeof options === "function") {
    callback = options;
    options = undefined;
  }
  callback = makeCallback(callback);

  // Mirror Node's lib/fs.js glob(): dispatch via Promise.then so a callback
  // that throws is not retried via the rejection branch.
  PromisePrototypeThen(
    ArrayFromAsync(new Glob(pattern, options).glob()),
    (res) => callback(null, res),
    callback,
  );
}

export function globPromise(pattern, options) {
  return new Glob(pattern, options).glob();
}
