///|
/// An OKF bundle: a directory tree of Markdown documents and other files.
struct Bundle {
  root : StoreDir
  label : BundleLabel
  documents : ReadOnlyArray[Document]
  by_path : Map[BundlePath, Int32]
  assets : ReadOnlyArray[BundlePath]
  ignored_logs : ReadOnlyArray[BundlePath]
  options : Options
}

///|
/// Loads the bundle whose root is the directory `root` of a document set.
/// Each Markdown file under the root becomes a document; other files become
/// assets. Without `label`, the label is the name of the root directory, or
/// `.` for the store root.
pub fn load_bundle(
  set : DocumentSet,
  root~ : StoreDir = StoreDir::root(),
  label? : BundleLabel,
  options~ : Options = Options::default(),
  profiles~ : ArrayView[&Profile] = [],
) -> Bundle {
  let documents : Array[Document] = []
  let assets : Array[BundlePath] = []
  let ignored : Array[BundlePath] = []
  for entry in set.entries() {
    guard root.contains(entry.path) else { continue }
    let rel = entry.path
      .segments()
      .exact_view(start=root.segments().length())
      .to_owned()
    let path = BundlePath::of_segments(rel)
    if !is_markdown_name(path.name()) {
      assets.push(path)
      continue
    }
    if options.nested_logs is RootOnly &&
      kind_of(path) is Log &&
      path.depth() > 0 {
      ignored.push(path)
      continue
    }
    let doc = Document(path, entry.path, entry.content.text())
    let doc = if doc.has_frontmatter_block() {
      doc
    } else {
      match derive_with(profiles, doc) {
        Some(fields) => doc.with_derived(fields)
        None => doc
      }
    }
    documents.push(doc)
  }
  let by_path : Map[BundlePath, Int32] = Map([])
  for i, doc in documents {
    by_path[doc.path()] = i
  }
  let label = match label {
    Some(l) => l
    None => BundleLabel::of_parts([], root.name().unwrap_or("."))
  }
  {
    root,
    label,
    documents: ReadOnlyArray::from_array(documents),
    by_path,
    assets: ReadOnlyArray::from_array(assets),
    ignored_logs: ReadOnlyArray::from_array(ignored),
    options,
  }
}

///|
/// The store directory of the bundle root.
pub fn Bundle::root(self : Bundle) -> StoreDir {
  self.root
}

///|
/// The bundle label: its group and its name, such as `morphir/ir`.
pub fn Bundle::label(self : Bundle) -> BundleLabel {
  self.label
}

///|
/// The bundle name, the last segment of its label.
pub fn Bundle::name(self : Bundle) -> String {
  self.label.name()
}

///|
/// The grouping directories between the discovery root and the bundle.
pub fn Bundle::group(self : Bundle) -> ReadOnlyArray[String] {
  self.label.group()
}

///|
/// The options that the bundle was loaded with.
pub fn Bundle::options(self : Bundle) -> Options {
  self.options
}

///|
/// Every document, sorted by path.
pub fn Bundle::documents(self : Bundle) -> ReadOnlyArray[Document] {
  self.documents
}

///|
pub fn Bundle::concepts(self : Bundle) -> Array[Document] {
  self.documents.iter().filter(d => d.kind() is Concept).collect()
}

///|
/// The root index and the sub-indexes.
pub fn Bundle::indexes(self : Bundle) -> Array[Document] {
  self.documents.iter().filter(d => d.kind().is_index()).collect()
}

///|
pub fn Bundle::logs(self : Bundle) -> Array[Document] {
  self.documents.iter().filter(d => d.kind() is Log).collect()
}

///|
pub fn Bundle::root_index(self : Bundle) -> Document? {
  self.documents.iter().find_first(d => d.kind() is RootIndex)
}

///|
/// The bundle-root `log.md`.
pub fn Bundle::root_log(self : Bundle) -> Document? {
  self.documents
  .iter()
  .find_first(d => d.kind() is Log && d.path().depth() == 0)
}

///|
/// Non-Markdown files.
pub fn Bundle::assets(self : Bundle) -> ReadOnlyArray[BundlePath] {
  self.assets
}

///|
/// Nested `log.md` files that the `RootOnly` option left out.
pub fn Bundle::ignored_logs(self : Bundle) -> ReadOnlyArray[BundlePath] {
  self.ignored_logs
}

///|
pub fn Bundle::document(self : Bundle, path : BundlePath) -> Document? {
  match self.by_path.get(path) {
    Some(i) => Some(self.documents[i])
    None => None
  }
}

///|
/// The concept with this Concept ID.
pub fn Bundle::concept(self : Bundle, id : ConceptId) -> Document? {
  match self.document(id.to_path()) {
    Some(doc) if doc.kind() is Concept => Some(doc)
    _ => None
  }
}

///|
/// The root index frontmatter: `okf_version` and profile bundle keys.
pub fn Bundle::config(self : Bundle) -> Metadata {
  match self.root_index() {
    Some(doc) => doc.metadata()
    None => Metadata::empty()
  }
}

///|
/// The `okf_version` of the root index.
pub fn Bundle::okf_version(self : Bundle) -> OkfVersion? {
  self.config().text("okf_version").map(OkfVersion::parse)
}

///|
/// True when this library knows the declared OKF version, or none is declared.
pub fn Bundle::knows_version(self : Bundle) -> Bool {
  match self.okf_version() {
    Some(v) => v.is_known()
    None => true
  }
}

///|
/// The store path of a bundle path.
pub fn Bundle::store_path_of(self : Bundle, path : BundlePath) -> StorePath {
  let segments = self.root.segments().iter().collect()
  for s in path.segments() {
    segments.push(s)
  }
  StorePath::of_segments(segments)
}

///|
/// The bundle path of a store path, when it is inside this bundle.
pub fn Bundle::bundle_path_of(self : Bundle, path : StorePath) -> BundlePath? {
  if self.root.contains(path) {
    Some(
      BundlePath::of_segments(
        path
        .segments()
        .exact_view(start=self.root.segments().length())
        .to_owned(),
      ),
    )
  } else {
    None
  }
}