// In-memory indexing primitives for repeated glob queries.
//
// The index deliberately uses compact arrays instead of a hidden filesystem
// cache.  This keeps the API deterministic, makes it useful for build tools,
// and lets callers refresh a batch of paths without depending on OS watchers.

///|
/// The kind of filesystem entry represented by a path record.
pub enum PathKind {
  File
  Directory
} derive(Debug, Eq)

///|
/// A normalized path plus metadata used by filtering and reporting tools.
pub(all) struct PathRecord {
  path : String
  kind : PathKind
  depth : Int
  basename : String
  extension : String?
  hidden : Bool
} derive(Debug, Eq)

///|
/// Summary statistics for an in-memory path index.
pub(all) struct IndexStats {
  total : Int
  files : Int
  directories : Int
  hidden : Int
  max_depth : Int
} derive(Debug, Eq)

///|
/// An ordered, duplicate-free collection of normalized path records.
pub(all) struct PathIndex {
  entries : Array[PathRecord]
  generation : Int
} derive(Debug, Eq)

///|
fn trim_trailing_separator(path : String) -> String {
  let normalized = normalize_path(path)
  let mut end = normalized.length()
  while end > 0 && normalized[end - 1].to_int().unsafe_to_char() == '/' {
    end = end - 1
  }
  if end == 0 && !normalized.is_empty() {
    "/"
  } else {
    normalized[:end].to_owned()
  }
}

///|
fn make_record(path : String, kind : PathKind) -> PathRecord {
  let normalized = trim_trailing_separator(path)
  {
    path: normalized,
    kind,
    depth: path_depth(normalized),
    basename: basename(normalized),
    extension: extension(normalized),
    hidden: is_hidden_path(normalized),
  }
}

///|
/// Creates a file record.
pub fn PathRecord::file(path : String) -> PathRecord {
  make_record(path, PathKind::File)
}

///|
/// Creates a directory record.
pub fn PathRecord::directory(path : String) -> PathRecord {
  make_record(path, PathKind::Directory)
}

///|
/// Creates a record with an explicit kind.
pub fn PathRecord::new(path : String, kind : PathKind) -> PathRecord {
  make_record(path, kind)
}

///|
pub fn PathRecord::is_file(self : PathRecord) -> Bool {
  self.kind == PathKind::File
}

///|
pub fn PathRecord::is_directory(self : PathRecord) -> Bool {
  self.kind == PathKind::Directory
}

///|
/// Returns true when the record path matches a compiled glob.
pub fn PathRecord::matches(
  self : PathRecord,
  pattern : CompiledPattern,
) -> Bool {
  pattern.matches(self.path)
}

///|
/// Starts with an empty index.
pub fn PathIndex::new() -> PathIndex {
  { entries: [], generation: 0 }
}

///|
/// Builds an index while removing duplicate normalized paths.
pub fn PathIndex::from_entries(entries : Array[PathRecord]) -> PathIndex {
  let mut index = PathIndex::new()
  for entry in entries {
    index = index.add_record(entry)
  }
  index
}

///|
fn PathIndex::add_record(self : PathIndex, entry : PathRecord) -> PathIndex {
  let entries = self.entries.copy()
  let mut replaced = false
  let mut i = 0
  while i < entries.length() {
    if entries[i].path == entry.path {
      entries[i] = entry
      replaced = true
      break
    }
    i = i + 1
  }
  if !replaced {
    entries.push(entry)
  }
  { entries, generation: self.generation + 1 }
}

///|
/// Inserts or replaces a file record.
pub fn PathIndex::add_file(self : PathIndex, path : String) -> PathIndex {
  self.add_record(PathRecord::file(path))
}

///|
/// Inserts or replaces a directory record.
pub fn PathIndex::add_directory(self : PathIndex, path : String) -> PathIndex {
  self.add_record(PathRecord::directory(path))
}

///|
/// Inserts a batch of records in input order.
pub fn PathIndex::add_many(
  self : PathIndex,
  entries : Array[PathRecord],
) -> PathIndex {
  let mut result = self
  for entry in entries {
    result = result.add_record(entry)
  }
  result
}

///|
/// Returns the number of records.
pub fn PathIndex::length(self : PathIndex) -> Int {
  self.entries.length()
}

///|
pub fn PathIndex::is_empty(self : PathIndex) -> Bool {
  self.entries.is_empty()
}

///|
/// Returns the current mutation generation, useful for cache invalidation.
pub fn PathIndex::generation(self : PathIndex) -> Int {
  self.generation
}

///|
pub fn PathIndex::contains(self : PathIndex, path : String) -> Bool {
  let normalized = trim_trailing_separator(path)
  for entry in self.entries {
    if entry.path == normalized {
      return true
    }
  }
  false
}

///|
/// Returns the record for a normalized path, if indexed.
pub fn PathIndex::get(self : PathIndex, path : String) -> PathRecord? {
  let normalized = trim_trailing_separator(path)
  for entry in self.entries {
    if entry.path == normalized {
      return Some(entry)
    }
  }
  None
}

///|
/// Removes one path and returns the resulting index.
pub fn PathIndex::remove(self : PathIndex, path : String) -> PathIndex {
  let normalized = trim_trailing_separator(path)
  let entries : Array[PathRecord] = []
  for entry in self.entries {
    if entry.path != normalized {
      entries.push(entry)
    }
  }
  { entries, generation: self.generation + 1 }
}

///|
/// Removes all records while advancing the generation.
pub fn PathIndex::clear(self : PathIndex) -> PathIndex {
  { entries: [], generation: self.generation + 1 }
}

///|
/// Returns records in insertion order.
pub fn PathIndex::records(self : PathIndex) -> Array[PathRecord] {
  self.entries.copy()
}

///|
pub fn PathIndex::files(self : PathIndex) -> Array[PathRecord] {
  let result : Array[PathRecord] = []
  for entry in self.entries {
    if entry.is_file() {
      result.push(entry)
    }
  }
  result
}

///|
pub fn PathIndex::directories(self : PathIndex) -> Array[PathRecord] {
  let result : Array[PathRecord] = []
  for entry in self.entries {
    if entry.is_directory() {
      result.push(entry)
    }
  }
  result
}

///|
/// Returns records at or below a normalized directory prefix.
pub fn PathIndex::under(self : PathIndex, prefix : String) -> Array[PathRecord] {
  let normalized = trim_trailing_separator(prefix)
  let result : Array[PathRecord] = []
  for entry in self.entries {
    if normalized == "." ||
      normalized.is_empty() ||
      entry.path == normalized ||
      entry.path.has_prefix(normalized + "/") {
      result.push(entry)
    }
  }
  result
}

///|
/// Returns direct children of a directory, excluding deeper descendants.
pub fn PathIndex::children(
  self : PathIndex,
  parent : String,
) -> Array[PathRecord] {
  let normalized = trim_trailing_separator(parent)
  let result : Array[PathRecord] = []
  for entry in self.entries {
    if dirname(entry.path) == normalized {
      result.push(entry)
    }
  }
  result
}

///|
/// Matches all indexed records with one compiled pattern.
pub fn PathIndex::search_compiled(
  self : PathIndex,
  pattern : CompiledPattern,
) -> Array[PathRecord] {
  let result : Array[PathRecord] = []
  for entry in self.entries {
    if pattern.matches(entry.path) {
      result.push(entry)
    }
  }
  result
}

///|
/// Compiles and applies one pattern to the index.
pub fn PathIndex::query(
  self : PathIndex,
  pattern : String,
) -> Result[Array[PathRecord], GlobError] {
  match compile_pattern(pattern) {
    Err(err) => Err(err)
    Ok(compiled) => Ok(self.search_compiled(compiled))
  }
}

///|
/// Applies include/exclude rules to the indexed paths.
pub fn PathIndex::apply_rules(
  self : PathIndex,
  rules : GlobRules,
) -> Array[PathRecord] {
  let result : Array[PathRecord] = []
  for entry in self.entries {
    if rules.allows(entry.path, entry.is_directory()) {
      result.push(entry)
    }
  }
  result
}

///|
/// Computes stable aggregate statistics.
pub fn PathIndex::stats(self : PathIndex) -> IndexStats {
  let mut files = 0
  let mut directories = 0
  let mut hidden = 0
  let mut max_depth = 0
  for entry in self.entries {
    if entry.is_file() {
      files = files + 1
    } else {
      directories = directories + 1
    }
    if entry.hidden {
      hidden = hidden + 1
    }
    if entry.depth > max_depth {
      max_depth = entry.depth
    }
  }
  { total: self.entries.length(), files, directories, hidden, max_depth }
}

///|
/// Counts records with one extension, without treating directories specially.
pub fn PathIndex::count_extension(self : PathIndex, wanted : String) -> Int {
  let mut count = 0
  for entry in self.entries {
    match entry.extension {
      Some(value) => if value == wanted { count = count + 1 }
      None => ()
    }
  }
  count
}

///|
/// Returns records matching a pattern and optional rule set.
pub fn PathIndex::query_with_rules(
  self : PathIndex,
  pattern : String,
  rules : GlobRules,
) -> Result[Array[PathRecord], GlobError] {
  match self.query(pattern) {
    Err(err) => Err(err)
    Ok(records) => {
      let result : Array[PathRecord] = []
      for entry in records {
        if rules.allows(entry.path, entry.is_directory()) {
          result.push(entry)
        }
      }
      Ok(result)
    }
  }
}