///|
/// Where one viewer asset is served from: a file on disk, the copy built into
/// this binary (`generated_assets.mbt`), or nowhere.
priv enum AssetSource {
  File(String)
  Embedded(String)
  Missing
}

///|
/// The viewer shell: the first existing disk candidate, else the embedded copy.
async fn resolve_shell(candidates : Array[String]) -> AssetSource {
  match first_existing(candidates) {
    Some(path) => File(path)
    None => embedded_or_missing(embedded_index_html)
  }
}

///|
/// The frontend bundle: the disk candidate `locate_bundle` picks, else the
/// embedded copy.
async fn resolve_bundle(candidates : Array[String]) -> AssetSource {
  match locate_bundle(candidates) {
    Some(path) => File(path)
    None => embedded_or_missing(embedded_viz_app_js)
  }
}

///|
fn embedded_or_missing(content : String) -> AssetSource {
  if content == "" {
    Missing
  } else {
    Embedded(content)
  }
}

///|
/// The asset's text, or `None` when it is missing.
async fn AssetSource::text(self : AssetSource) -> String? {
  match self {
    File(path) => Some(read_text_lossy(path))
    Embedded(content) => Some(content)
    Missing => None
  }
}

///|
/// For the startup log: where the asset is being served from.
fn AssetSource::describe(self : AssetSource) -> String {
  match self {
    File(path) => "'\{path}'"
    Embedded(_) => "the copy built into this binary"
    Missing => "nowhere (not found)"
  }
}

///|
test "an empty embedded asset counts as missing" {
  assert_true(embedded_or_missing("") is Missing)
  assert_true(embedded_or_missing("") is Embedded(""))
}