///|
pub(all) enum QuerySort {
  SizeDescending
  SizeAscending
  PathAscending
  ModifiedNewest
  ModifiedOldest
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct SnapshotQuery {
  path_pattern : String
  extensions : Array[String]
  categories : Array[FileCategory]
  kinds : Array[EntryKind]
  minimum_size : Int64
  maximum_size : Int64
  modified_after : Int64
  modified_before : Int64
  sort : QuerySort
  limit : Int
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn SnapshotQuery::default() -> SnapshotQuery {
  SnapshotQuery::{
    path_pattern: "*",
    extensions: [],
    categories: [],
    kinds: [Regular],
    minimum_size: 0L,
    maximum_size: -1L,
    modified_after: -1L,
    modified_before: -1L,
    sort: SizeDescending,
    limit: 100,
  }
}

///|
pub fn query_sort_from_name(name : String) -> QuerySort raise {
  match name.to_lower() {
    "size-desc" => SizeDescending
    "size-asc" => SizeAscending
    "path" => PathAscending
    "newest" => ModifiedNewest
    "oldest" => ModifiedOldest
    value => raise Failure("Unknown query sort: " + value)
  }
}

///|
fn normalized_query_extensions(extensions : Array[String]) -> Array[String] {
  extensions.map(fn(extension) {
    let lower = extension.to_lower()
    if lower.has_prefix(".") {
      lower[1:].to_owned()
    } else {
      lower
    }
  })
}

///|
fn query_has_kind(kinds : Array[EntryKind], kind : EntryKind) -> Bool {
  kinds.is_empty() || kinds.any(fn(candidate) { candidate == kind })
}

///|
fn query_has_category(
  categories : Array[FileCategory],
  category : FileCategory,
) -> Bool {
  categories.is_empty() ||
  categories.any(fn(candidate) { candidate == category })
}

///|
pub fn entry_matches_query(entry : Entry, query : SnapshotQuery) -> Bool {
  if !query_has_kind(query.kinds, entry.kind) {
    return false
  }
  let pattern = normalize_relative_path(query.path_pattern)
  if pattern != "" &&
    pattern != "*" &&
    !wildcard_match(pattern, entry.path) &&
    !wildcard_match(pattern, entry.name()) {
    return false
  }
  let extensions = normalized_query_extensions(query.extensions)
  if !extensions.is_empty() &&
    !extensions.any(fn(extension) { extension == entry.extension() }) {
    return false
  }
  if entry.is_file() &&
    !query_has_category(query.categories, file_category(entry)) {
    return false
  }
  if entry.size < query.minimum_size {
    return false
  }
  if query.maximum_size >= 0L && entry.size > query.maximum_size {
    return false
  }
  if query.modified_after >= 0L && entry.modified_at <= query.modified_after {
    return false
  }
  if query.modified_before >= 0L && entry.modified_at >= query.modified_before {
    return false
  }
  true
}

///|
fn compare_query_entries(a : Entry, b : Entry, sort : QuerySort) -> Int {
  match sort {
    SizeDescending =>
      if a.size > b.size {
        -1
      } else if a.size < b.size {
        1
      } else if a.path < b.path {
        -1
      } else {
        1
      }
    SizeAscending =>
      if a.size < b.size {
        -1
      } else if a.size > b.size {
        1
      } else if a.path < b.path {
        -1
      } else {
        1
      }
    PathAscending =>
      if a.path < b.path {
        -1
      } else if a.path > b.path {
        1
      } else {
        0
      }
    ModifiedNewest =>
      if a.modified_at > b.modified_at {
        -1
      } else if a.modified_at < b.modified_at {
        1
      } else if a.path < b.path {
        -1
      } else {
        1
      }
    ModifiedOldest =>
      if a.modified_at < b.modified_at {
        -1
      } else if a.modified_at > b.modified_at {
        1
      } else if a.path < b.path {
        -1
      } else {
        1
      }
  }
}

///|
pub fn query_snapshot(
  snapshot : Snapshot,
  query : SnapshotQuery,
) -> Array[Entry] {
  let result = snapshot.entries.filter(fn(entry) {
    entry_matches_query(entry, query)
  })
  result.sort_by(fn(a, b) { compare_query_entries(a, b, query.sort) })
  let limit = if query.limit < 0 { 0 } else { query.limit }
  if result.length() > limit {
    result[:limit].to_owned()
  } else {
    result
  }
}

///|
pub fn query_total_size(entries : Array[Entry]) -> Int64 {
  let mut total = 0L
  for entry in entries {
    if entry.is_file() {
      total += entry.size
    }
  }
  total
}

///|
pub fn render_query_results(entries : Array[Entry]) -> String {
  let lines : Array[String] = [
    "MoonDisk snapshot query",
    "Matches: " + entries.length().to_string(),
    "Matched size: " + format_bytes(query_total_size(entries)),
    "",
  ]
  for entry in entries {
    lines.push(
      pad_left(format_bytes(entry.size), 12) +
      "  " +
      pad_right(entry_kind_name(entry.kind), 9) +
      "  " +
      entry.path,
    )
  }
  lines.join("\n")
}