///|
fn alphanumeric(value : String, plus~ : Bool = false) -> Bool {
  !value.is_empty() &&
  value
  .iter()
  .all(c => {
    (c >= 'a' && c <= 'z') ||
    (c >= 'A' && c <= 'Z') ||
    (c >= '0' && c <= '9') ||
    (plus && c == '+')
  })
}

///|
fn parts(path : String) -> Array[String] raise TraceError {
  let parts = path.split("/").map(v => v.to_owned()).to_array()
  if parts.any(p => {
      p.is_empty() ||
      p == "." ||
      p == ".." ||
      p.contains("\\") ||
      p.contains(":")
    }) {
    reject(
      "invalid_path", path, "Use a dataset-relative slash-separated path without empty, dot, parent, drive or backslash components.",
    )
  }
  parts
}

///|
fn entity_value(
  key : String,
  value : String,
  path : String,
) -> String raise TraceError {
  match key {
    "run" | "echo" | "chunk" => {
      if value.is_empty() || !value.iter().all(c => c >= '0' && c <= '9') {
        reject(
          "invalid_entity",
          path,
          key + " requires a nonnegative decimal index.",
        )
      }
      let mut start = 0
      while start + 1 < value.length() && value[start] == '0' {
        start += 1
      }
      value[start:].to_owned()
    }
    "sub" | "ses" | "task" | "acq" | "ce" | "rec" | "dir" | "part" => {
      if !alphanumeric(value, plus=true) {
        reject(
          "invalid_entity",
          path,
          key +
          " requires a nonempty ASCII alphanumeric label (plus is permitted).",
        )
      }
      value
    }
    _ => {
      reject(
        "unsupported_entity",
        path,
        "Entity " +
        key +
        " is outside the raw-MRI profile; no metadata result was produced.",
      )
      value
    }
  }
}

///|
fn selector(path : String, sidecar~ : Bool) -> Selector raise TraceError {
  let components = parts(path)
  let filename = components[components.length() - 1]
  let extension = if sidecar {
    ".json"
  } else if filename.has_suffix(".nii.gz") {
    ".nii.gz"
  } else {
    ".nii"
  }
  if !filename.has_suffix(extension) {
    reject(
      "unsupported_extension", path, "Expected JSON sidecar or .nii/.nii.gz data path for this profile.",
    )
  }
  let stem = filename[:filename.length() - extension.length()].to_owned()
  let tokens = stem.split("_").map(v => v.to_owned()).to_array()
  let suffix = tokens[tokens.length() - 1]
  let datatype = match suffix {
    "bold" => "func"
    "T1w" => "anat"
    "dwi" => "dwi"
    _ => {
      reject(
        "unsupported_suffix", path, "Only bold, T1w and dwi are supported; select an explicit supported raw-MRI input.",
      )
      ""
    }
  }
  let entities : Map[String, String] = Map([])
  let order = [
    "sub", "ses", "task", "acq", "ce", "rec", "dir", "run", "echo", "part", "chunk",
  ]
  let mut previous = -1
  for token in tokens[:tokens.length() - 1] {
    let pair = token.split("-").map(v => v.to_owned()).to_array()
    if pair.length() != 2 {
      reject(
        "invalid_entity",
        path,
        "Expected key-label entity, found " + token,
      )
    }
    let key = pair[0]
    let value = entity_value(key, pair[1], path)
    if entities.contains(key) {
      reject("duplicate_entity", path, "Repeated filename entity " + key)
    }
    let rank = order.search(key).unwrap()
    if rank <= previous {
      reject(
        "entity_order", path, "Filename entities must follow the documented raw-MRI order.",
      )
    }
    previous = rank
    entities[key] = value
  }
  let directories = components[:components.length() - 1].to_owned()
  let mut subject : String? = None
  let mut session : String? = None
  let mut has_datatype = false
  for i, directory in directories {
    if i == 0 && directory.has_prefix("sub-") {
      subject = Some(entity_value("sub", directory[4:].to_owned(), path))
    } else if i == 1 && subject is Some(_) && directory.has_prefix("ses-") {
      session = Some(entity_value("ses", directory[4:].to_owned(), path))
    } else if directory == datatype &&
      subject is Some(_) &&
      i == directories.length() - 1 {
      has_datatype = true
    } else {
      reject(
        "unsupported_scope", path, "Use root/sub-