///|
/// The result of a lookup that can match nothing, one item or several items.
pub(all) enum Lookup[T] {
  NotFound
  Found(T)
  Ambiguous(ReadOnlyArray[T])
} derive(Eq, Debug)

///|
pub fn[T] Lookup::to_option(self : Lookup[T]) -> T? {
  match self {
    Found(x) => Some(x)
    _ => None
  }
}

///|
fn[T] lookup_of(items : Array[T]) -> Lookup[T] {
  match items {
    [] => NotFound
    [x] => Found(x)
    _ => Ambiguous(ReadOnlyArray::from_array(items))
  }
}

///|
/// A set of bundles found under one directory, and the Markdown files that
/// belong to no bundle.
struct Knowledge {
  bundles : ReadOnlyArray[Bundle]
  strays : ReadOnlyArray[StorePath]
  options : Options
}

///|
/// Knowledge with the given bundles.
pub fn Knowledge::Knowledge(
  bundles : ArrayView[Bundle],
  strays~ : ArrayView[StorePath] = [],
  options~ : Options = Options::default(),
) -> Knowledge {
  {
    bundles: ReadOnlyArray::from_array(bundles.to_owned()),
    strays: ReadOnlyArray::from_array(strays.to_owned()),
    options,
  }
}

///|
/// Finds each bundle under the directory `under`: each directory whose
/// `index.md` frontmatter contains `okf_version`. It does not look for bundles
/// inside a bundle. Markdown files outside every bundle, except `README.md`,
/// are strays.
pub fn discover(
  set : DocumentSet,
  under~ : StoreDir = StoreDir::root(),
  options~ : Options = Options::default(),
  profiles~ : ArrayView[&Profile] = [],
) -> Knowledge {
  let roots : Array[StoreDir] = []
  for entry in set.entries() {
    guard under.contains(entry.path) else { continue }
    guard entry.path.name().to_lower() == "index.md" else { continue }
    let header = split_header(entry.content.text())
    let declares = match header.shape {
      Block(raw~) => scan_entries(raw).any(e => e.key == "okf_version")
      _ => false
    }
    if declares {
      roots.push(entry.path.parent())
    }
  }
  // Outer roots first, so that a bundle inside a bundle is skipped.
  roots.sort_by((a, b) => {
    let (la, lb) = (a.segments().length(), b.segments().length())
    if la != lb {
      la.compare(lb)
    } else {
      a.compare(b)
    }
  })
  let accepted : Array[StoreDir] = []
  for root in roots {
    if !accepted.any(outer => outer != root && outer.encloses(root)) {
      accepted.push(root)
    }
  }
  accepted.sort()
  let depth = under.segments().length()
  let bundles = accepted.map(root => {
    let segments = root.segments()
    // A bundle at the discovery root, or one level below it, has no group.
    let group = if segments.length() <= depth + 1 {
      []
    } else {
      segments.exact_view(start=depth, end=segments.length() - 1).to_owned()
    }
    let name = root.name().unwrap_or(".")
    load_bundle(
      set,
      root~,
      label=BundleLabel::of_parts(group, name),
      options~,
      profiles~,
    )
  })
  let strays = []
  for entry in set.entries() {
    guard under.contains(entry.path) else { continue }
    guard is_markdown_name(entry.path.name()) else { continue }
    guard entry.path.name().to_lower() != "readme.md" else { continue }
    if !accepted.any(root => root.contains(entry.path)) {
      strays.push(entry.path)
    }
  }
  Knowledge(bundles, strays~, options~)
}

///|
pub fn Knowledge::bundles(self : Knowledge) -> ReadOnlyArray[Bundle] {
  self.bundles
}

///|
pub fn Knowledge::strays(self : Knowledge) -> ReadOnlyArray[StorePath] {
  self.strays
}

///|
pub fn Knowledge::options(self : Knowledge) -> Options {
  self.options
}

///|
/// Finds a bundle by its full label, or by its name when the label is bare and
/// no bundle has it as a full label.
pub fn Knowledge::bundle(
  self : Knowledge,
  label : BundleLabel,
) -> Lookup[Bundle] {
  let exact = self.bundles.iter().filter(b => b.label() == label).collect()
  if !exact.is_empty() || !label.is_bare() {
    return lookup_of(exact)
  }
  lookup_of(self.bundles.iter().filter(b => b.name() == label.name()).collect())
}

///|
/// Every concept of every bundle, with its bundle.
pub fn Knowledge::concepts(self : Knowledge) -> Array[(Bundle, Document)] {
  let out = []
  for bundle in self.bundles {
    for doc in bundle.concepts() {
      out.push((bundle, doc))
    }
  }
  out
}

///|
/// Every document of every bundle, with its bundle.
pub fn Knowledge::documents(self : Knowledge) -> Array[(Bundle, Document)] {
  let out = []
  for bundle in self.bundles {
    for doc in bundle.documents() {
      out.push((bundle, doc))
    }
  }
  out
}

///|
/// The bundle that contains a store path, if any. The innermost bundle wins.
pub fn Knowledge::bundle_at(self : Knowledge, path : StorePath) -> Bundle? {
  let mut best : Bundle? = None
  for bundle in self.bundles {
    if bundle.root().contains(path) {
      match best {
        Some(b) if b.root().segments().length() >=
          bundle.root().segments().length() => ()
        _ => best = Some(bundle)
      }
    }
  }
  best
}

///|
/// The document at a store path, with its bundle.
pub fn Knowledge::document_at(
  self : Knowledge,
  path : StorePath,
) -> (Bundle, Document)? {
  match self.bundle_at(path) {
    Some(bundle) =>
      match bundle.bundle_path_of(path) {
        Some(rel) =>
          match bundle.document(rel) {
            Some(doc) => Some((bundle, doc))
            None => None
          }
        None => None
      }
    None => None
  }
}

///|
/// A document key: the bundle label and the bundle path.
pub(all) struct DocKey {
  bundle : BundleLabel
  path : BundlePath
} derive(Eq, Hash, Debug)

///|
pub impl Compare for DocKey with fn compare(self, other) {
  let c = self.bundle.compare(other.bundle)
  if c != 0 {
    c
  } else {
    self.path.compare(other.path)
  }
}

///|
pub fn DocKey::of(bundle : Bundle, doc : Document) -> DocKey {
  { bundle: bundle.label(), path: doc.path(), }
}

///|
pub fn DocKey::to_string(self : DocKey) -> String {
  self.bundle.to_string() + ":" + self.path.to_string()
}

///|
/// The document for a key. The bundle label must match exactly.
pub fn Knowledge::document(self : Knowledge, key : DocKey) -> Document? {
  match self.bundles.iter().find_first(b => b.label() == key.bundle) {
    Some(bundle) => bundle.document(key.path)
    None => None
  }
}

///|
pub extend Lookup with Eq::{not_equal, equal}

///|
pub extend Lookup with @debug.Debug::{to_repr}

///|
pub extend DocKey with Eq::{not_equal, equal}

///|

///|
pub extend DocKey with Hash::{hash, hash_combine}

///|
pub extend DocKey with @debug.Debug::{to_repr}

///|
pub extend DocKey with Compare::{op_lt, op_le, op_ge, compare, op_gt}