///|
priv struct SelectedPrimary {
  path : @config.FileTreePath
  supplemental : SupplementalFile?
}

///|
priv struct PrimaryDocument {
  selected : SelectedPrimary
  value : @config.ConfigValue
}

///|
/// Establish presence and ambiguity before requesting any file contents.
fn Scan::existing(self : Scan, paths : Array[@config.FileTreePath]) -> Array[@config.FileTreePath]? raise @config.ConfigError {
  let existing = []
  let mut known = true
  for path in paths {
    match self.context.tree.is_file(path) {
      Found(true) => if !existing.contains(path) { existing.push(path) }
      Found(false) | Missing => ()
      Unknown => { add_need(self.needs, self.context.tree.file_request(path)); known = false }
    }
  }
  guard known else { return None }
  if existing.length() > 1 {
    raise @config.Ambiguous(display=self.conventions.display,
      location=@config.FileTreePath::common_dir(paths).map(path => path.to_string()).unwrap_or("."),
      candidates=texts(existing))
  }
  Some(existing)
}

///|
/// Only selected physical identities enter this cache. A supplemental transform
/// runs with its parser once in a pass and is reused by classification and load.
fn Scan::read_document(
  self : Scan,
  path : @config.FileTreePath,
  supplemental? : SupplementalFile,
) -> @config.Lookup[@config.ConfigValue] raise @config.ConfigError {
  let path = normalize(path)
  if self.parsed.get(path) is Some(value) { return Found(value) }
  match self.context.tree.file(path) {
    Found(text) => {
      let value = @config.parse_file(self.context.formats, path.to_string(), text)
      let value = match supplemental { Some(file) => (file.transform)(value); None => value }
      self.parsed[path] = value
      Found(value)
    }
    Missing => Missing
    Unknown => { add_need(self.needs, ReadFile(path)); Unknown }
  }
}

///|
fn Scan::primary_paths(self : Scan, dir : @config.FileTreePath) -> Array[SelectedPrimary]? raise @config.ConfigError {
  guard self.conventions.layers is Some(layers) else { return Some([]) }
  let selected = []
  let mut known = true
  for group in layers.groups {
    if group.supplemental is Some(file) {
      let path = dir.child(file.name)
      match self.existing([path]) {
        Some([path]) => selected.push({ path, supplemental: Some(file) })
        Some(_) => ()
        None => known = false
      }
    }
    let paths = self.conventions.candidates(dir, group.extensions.map(extension => extension.text))
    match self.existing(paths) {
      Some([path]) => selected.push({ path, supplemental: None })
      Some(_) => ()
      None => known = false
    }
  }
  if known { Some(selected) } else { None }
}

///|
fn Scan::primary_documents(self : Scan, dir : @config.FileTreePath) -> Array[PrimaryDocument]? raise @config.ConfigError {
  let dir = normalize(dir)
  if self.documents.get(dir) is Some(documents) { return Some(documents) }
  guard self.primary_paths(dir) is Some(paths) else { return None }
  let documents = []
  let mut known = true
  for selected in paths {
    match self.read_document(selected.path, supplemental?=selected.supplemental) {
      Found(value) => documents.push({ selected, value })
      Missing => raise @config.FileNotFound(path=selected.path.to_string())
      Unknown => known = false
    }
  }
  guard known else { return None }
  self.documents[dir] = documents
  Some(documents)
}

///|
fn Scan::document_source(self : Scan, document : PrimaryDocument, role : @config.FileRole) -> @config.ConfigSource raise @config.ConfigError {
  @config.ConfigSource::parsed_file(document.selected.path.to_string(), document.value, role~, name=self.conventions.display)
}

///|
fn Scan::found(self : Scan, file : ProjectFile, value : @config.ConfigValue) -> Enclosing raise @config.ConfigError {
  let primaries = if self.conventions.layers is Some(_) && file.kind is Discovered {
    self.documents.get(file.root).unwrap_or([]).map(document => self.document_source(document, Workspace))
  } else {
    [@config.ConfigSource::parsed_file(file.path.to_string(), value,
      name=self.conventions.display, role=match file.kind {
        LegacyFile => Legacy
        ExplicitFile => Explicit
        Discovered => Workspace
      })]
  }
  { file, value, primaries }
}

///|
fn Scan::source_for(
  self : Scan,
  paths : Array[@config.FileTreePath],
  role : @config.FileRole,
  supplemental? : SupplementalFile,
  omit_selected? : Bool = true,
  as_member? : Bool = false,
) -> Array[@config.ConfigSource]? raise @config.ConfigError {
  guard self.existing(paths) is Some(existing) else { return None }
  guard existing is [path] else {
    return Some([@config.ConfigSource::candidates(texts(paths), display=self.conventions.display, role~, name=self.conventions.display)])
  }
  match self.read_document(path, supplemental?) {
    Found(value) => {
      let source = @config.ConfigSource::parsed_file(path.to_string(), value, role~, name=self.conventions.display)
      Some(if omit_selected { self.keep_source(path, source, as_member~) } else { [source] })
    }
    Missing => raise @config.FileNotFound(path=path.to_string())
    Unknown => None
  }
}

///|
fn Scan::ordered_layer(
  self : Scan,
  root : @config.FileTreePath,
  project : ProjectInputs,
  as_member~ : Bool,
) -> Layer? raise @config.ConfigError {
  guard self.conventions.layers is Some(layers) else { return None }
  guard self.primary_documents(root) is Some(documents) else { return None }
  let role : @config.FileRole = if as_member { Member } else { Workspace }
  let primaries = []
  let mut known = true
  for group in layers.groups {
    if group.supplemental is Some(file) {
      match self.source_for([root.child(file.name)], role, supplemental=file) {
        Some(sources) => primaries.append(sources)
        None => known = false
      }
    }
    let paths = self.conventions.candidates(root, group.extensions.map(extension => extension.text))
    match self.source_for(paths, role) {
      Some(sources) => primaries.append(sources)
      None => known = false
    }
  }
  guard known else { return None }
  if documents.is_empty() { return Some({ primaries, adjacent: [], paths: [], controls: [] }) }
  // Only ordinary layouts supply adjacent stems. A supplemental-only root uses
  // the standard application layout rather than a supplemental-file stem.
  let paths = documents.filter_map(document => if document.selected.supplemental is None { Some(document.selected.path) } else { None })
  if paths.is_empty() {
    paths.push(root.child("\{self.conventions.name}.\{layers.groups[0].extensions[0].text}"))
  }
  let anchors = self.conventions.anchors(paths, project.extensions)
  let adjacent = self.ordered_overrides(anchors, project, as_member~)
  guard known && adjacent is Some(adjacent) else { return None }
  let controls = if as_member || self.conventions.profiles is None { [] } else { documents.map(document => self.control_layer(document.selected.path, document.value, Workspace)) }
  Some({ primaries, adjacent, paths, controls })
}

///|
fn Scan::control_layer(self : Scan, path : @config.FileTreePath, value : @config.ConfigValue, role : @config.FileRole) -> @profiles.ControlLayer raise @config.ConfigError {
  @profiles.ControlLayer::new(value, { kind: File(role), source: self.conventions.display, priority: 300, location: Some(path.to_string()) })
}

///|
fn Scan::ordered_profile_sources(
  self : Scan,
  tier : Tier,
  resolution : @config.ProfileResolution,
) -> Array[@config.ConfigSource] raise @config.ConfigError {
  let sources = []
  for name in resolution.active().names() {
    for is_member in [false, true] {
      let paths = tier.locations.filter_map(location => if location.1 == is_member { Some(location.0) } else { None })
      let anchors = self.conventions.anchors(paths, self.extensions)
      if anchors.is_empty() { continue }
      guard self.adjacent_sources(anchors, "profile.\{name}", ProfileFile(name), as_member=is_member) is Some(stage) else { continue }
      let filters = if is_member { key_paths(self.conventions.member_scope) } else if tier.has_member { key_paths(self.conventions.inherited) } else { [] }
      sources.append(stage.map(source => filtered(source, filters, warn=is_member)))
    }
  }
  sources
}

///|
fn Conventions::anchors(self : Conventions, paths : Array[@config.FileTreePath], extensions : Array[String]) -> Array[(@config.FileTreePath, String)] {
  let anchors = []
  for path in paths {
    if self.adjacent(path, extensions) is Some(anchor) && !anchors.contains(anchor) {
      anchors.push(anchor)
    }
  }
  anchors
}

///|
fn Scan::adjacent_sources(
  self : Scan,
  anchors : Array[(@config.FileTreePath, String)],
  suffix : String,
  role : @config.FileRole,
  as_member? : Bool = false,
) -> Array[@config.ConfigSource]? raise @config.ConfigError {
  let groups = self.conventions.layers.map(layers => layers.groups.map(group => group.extensions.map(extension => extension.text))).unwrap_or([self.extensions])
  let sources = []
  let mut known = true
  for extensions in groups {
    let paths = []
    for (dir, stem) in anchors {
      for extension in extensions {
        let path = dir.child("\{stem}.\{suffix}.\{extension}")
        if !paths.contains(path) { paths.push(path) }
      }
    }
    match self.source_for(paths, role, as_member~) {
      Some(stage) => sources.append(stage)
      None => known = false
    }
  }
  if known { Some(sources) } else { None }
}

///|
fn Scan::ordered_overrides(
  self : Scan,
  anchors : Array[(@config.FileTreePath, String)],
  project : ProjectInputs,
  as_member? : Bool = false,
) -> Array[Array[@config.ConfigSource]]? raise @config.ConfigError {
  let stages = []
  let mut known = true
  match project.environment {
    None => stages.push([])
    Some(name) => match self.adjacent_sources(anchors, name, EnvironmentFile, as_member~) {
      Some(sources) => stages.push(sources)
      None => { stages.push([]); known = false }
    }
  }
  for (part, role, suffix) in [(LocalOverrides, @config.FileRole::LocalOverride, "local"), (UserOverrides, UserOverride, "user")] {
    if !self.conventions.has(part) { stages.push([]); continue }
    if project.options.isolated {
      let count = self.conventions.layers.map(layers => layers.groups.length()).unwrap_or(1)
      let sources = Array::makei(count, _ => @config.ConfigSource::skipped(File(role), name=self.conventions.display))
      stages.push(sources)
      continue
    }
    match self.adjacent_sources(anchors, suffix, role, as_member~) {
      Some(sources) => stages.push(sources)
      None => { stages.push([]); known = false }
    }
  }
  if known { Some(stages) } else { None }
}

///|
/// Reuse existing directory policy, then divide each enabled stage by the
/// explicit format order. Supplemental files remain below ordinary candidates.
fn Scan::machine_sources(self : Scan, options : DiscoveryOptions) -> Array[MachineSource] raise @config.ConfigError {
  let ordinary = self.conventions.machine_sources(options, self.context, self.extensions, self.notes)
  guard self.conventions.layers is Some(layers) else {
    if self.conventions.selected_file is Primary { return ordinary }
    let prepared = []
    for item in ordinary {
      if item.candidates.is_empty() { prepared.push(item); continue }
      guard item.source.kind() is File(role) else { continue }
      if self.source_for(item.candidates, role, omit_selected=false) is Some(sources) {
        for source in sources { prepared.push({ source, candidates: item.candidates }) }
      }
    }
    return prepared
  }
  let sources = []
  for item in ordinary {
    if item.candidates.is_empty() { sources.push(item); continue }
    guard item.source.kind() is File(role) else { continue }
    let directories = []
    for path in item.candidates {
      if path.parent() is Some(dir) && !directories.contains(dir) { directories.push(dir) }
    }
    for group in layers.groups {
      if group.supplemental is Some(file) {
        let paths = directories.map(dir => dir.child(file.name))
        if self.source_for(paths, role, supplemental=file, omit_selected=false) is Some(stage) {
          for source in stage { sources.push({ source, candidates: paths }) }
        }
      }
      let paths = item.candidates.filter(path => path.name() is Some(name) && group.extensions.any(extension => name.has_suffix("." + extension.text)))
      if self.source_for(paths, role, omit_selected=false) is Some(stage) {
        for source in stage { sources.push({ source, candidates: paths }) }
      }
    }
  }
  sources
}