// Public API of the Lxxbv/glob package.

///|
/// Compiles a glob pattern string into an AST.
pub fn compile(pattern : String) -> Result[AST, GlobError] {
  match tokenize(pattern) {
    Ok(tokens) => parse(tokens)
    Err(err) => Err(err)
  }
}

///|
/// Matches a path string against a glob pattern string.
pub fn match_pattern(
  pattern : String,
  path : String,
) -> Result[Bool, GlobError] {
  match compile(pattern) {
    Ok(ast) => Ok(match_path(ast, path))
    Err(err) => Err(err)
  }
}

///|
/// Filters an array of paths, returning only those that match the glob pattern.
pub fn filter(
  pattern : String,
  paths : Array[String],
) -> Result[Array[String], GlobError] {
  match compile(pattern) {
    Ok(ast) => {
      let result = []
      for path in paths {
        if match_path(ast, path) {
          result.push(path)
        }
      }
      Ok(result)
    }
    Err(err) => Err(err)
  }
}

///|
/// Filters an array of paths, returning only those that do NOT match the glob pattern.
pub fn filter_not(
  pattern : String,
  paths : Array[String],
) -> Result[Array[String], GlobError] {
  match compile(pattern) {
    Ok(ast) => {
      let result = []
      for path in paths {
        if !match_path(ast, path) {
          result.push(path)
        }
      }
      Ok(result)
    }
    Err(err) => Err(err)
  }
}

///|
fn filesystem_error(err : @fs.IOError) -> GlobError {
  GlobError::FilesystemError(err.to_repr().to_string())
}

///|
fn list_relative_paths(
  dir : String,
  prefix : String,
  result : Array[String],
  options : GlobOptions,
) -> Result[Unit, GlobError] {
  let current_dir = if prefix.is_empty() { dir } else { dir + "/" + prefix }
  let entries = @fs.read_dir(current_dir) catch {
    err => return Err(filesystem_error(err))
  }
  for entry in entries {
    let rel_path = if prefix.is_empty() { entry } else { prefix + "/" + entry }
    let full_path = dir + "/" + rel_path
    let is_directory = @fs.is_dir(full_path) catch {
      err => return Err(filesystem_error(err))
    }
    let is_hidden = is_hidden_path(rel_path)
    let depth = path_depth(rel_path)
    let within_depth = match options.max_depth {
      None => true
      Some(max_depth) => depth <= max_depth
    }
    if (!is_hidden || options.include_hidden) && within_depth {
      if (is_directory && options.include_directories) ||
        (!is_directory && options.include_files) {
        result.push(rel_path)
      }
    }
    let can_recurse = match options.max_depth {
      None => true
      Some(max_depth) => depth < max_depth
    }
    if is_directory && can_recurse && (!is_hidden || options.include_hidden) {
      match list_relative_paths(dir, rel_path, result, options) {
        Ok(_) => ()
        Err(err) => return Err(err)
      }
    }
  }
  Ok(())
}

///|
/// Searches the filesystem starting from `dir` for paths matching the glob `pattern`.
/// The returned paths are relative to `dir`.
pub fn glob(dir : String, pattern : String) -> Result[Array[String], GlobError] {
  glob_with_options(dir, pattern, GlobOptions::default())
}

///|
/// Searches the filesystem using explicit filtering and traversal options.
pub fn glob_with_options(
  dir : String,
  pattern : String,
  options : GlobOptions,
) -> Result[Array[String], GlobError] {
  match compile_pattern(pattern) {
    Ok(compiled) => {
      let plan = TraversalPlan::from_compiled(compiled)
      let paths = []
      let start_prefix = plan.start_prefix(dir)
      match list_relative_paths(dir, start_prefix, paths, options) {
        Ok(_) => ()
        Err(err) => return Err(err)
      }
      let result = []
      for path in paths {
        if plan.matches(path) {
          result.push(path)
        }
      }
      if options.sort_results {
        result.sort()
      }
      Ok(result)
    }
    Err(err) => Err(err)
  }
}