///|
/// Paths are relative to a host-owned root. list recursively returns file paths.
pub(all) struct Host {
  read : (String) -> String raise
  exists : (String) -> Bool raise
  list : (String) -> Array[String] raise
}

///|
/// An immutable snapshot is useful for editors, tests, WASM and remote hosts.
pub fn Host::memory(files : Map[String, String]) -> Host raise {
  let snapshot : Map[String, String] = Map([])
  for path, content in files {
    let normalized = normalize(path)
    if snapshot.contains(normalized) {
      raise @scheme.Invalid("Duplicate host path: " + normalized)
    }
    snapshot[normalized] = content
  }
  {
    read: fn(path) raise {
      match snapshot.get(path) {
        Some(content) => content
        None => raise @scheme.Invalid("File not found: " + path)
      }
    },
    exists: fn(path) { snapshot.contains(path) },
    list: fn(root) {
      snapshot.keys().filter(path => within(root, path)).collect()
    },
  }
}

///|
pub(all) enum Input {
  File(String)
  Directory(String)
  Project(String)
  Workspace(String)
}

///|
pub(all) struct Source {
  path : String
  relative_path : String
  content : String
  package_name : String
  module_name : String
}

///|
/// One backend output per input. Hosts decide when and where to persist artifacts.
pub(all) struct Artifact {
  path : String
  source : String
  content : String
}

///|
/// A generation problem at one IR node: its code, the node path and a detail.
#derive(Debug)
pub(all) struct GenerationProblem {
  code : String
  node_path : String
  detail : String
}

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

///|
/// The structured cause of a diagnostic, when the engine knows it. `Source` comes from a frontend and has the
/// source path (relative to the source directory) and range; `Generation` comes from a generation backend.
#derive(Debug)
pub(all) enum Problem {
  Source(SourceProblem)
  Generation(GenerationProblem)
}

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

///|
#derive(Debug)
pub(all) struct Diagnostic {
  path : String
  stage : String
  message : String
  details : @diagnostics.Warning?
  problem : Problem?
}

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

///|
/// The problem code, for a diagnostic with a known cause.
pub fn Diagnostic::code(self : Diagnostic) -> String? {
  match self.problem {
    Some(Source(problem)) => Some(problem.code)
    Some(Generation(problem)) => Some(problem.code)
    None => None
  }
}

///|
/// The source range, for a frontend problem that has one.
pub fn Diagnostic::location(self : Diagnostic) -> @ir.SourceLocation? {
  match self.problem {
    Some(Source(problem)) => problem.location
    _ => None
  }
}

///|
/// A unit that reached the backend stage of `execute_content`, and the report path of its source.
#derive(Debug)
pub(all) struct CompiledUnit {
  source : String
  unit : @data.IRUnit
}

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

///|
pub(all) struct Report {
  artifacts : Array[Artifact]
  diagnostics : Array[Diagnostic]
  processed : Int32
}

///|
pub fn Report::successful(self : Report) -> Bool {
  self.diagnostics.is_empty()
}

///|
struct Frontend {
  extensions : Array[String]
  compiler : SourceCompiler
  profile : String?
  dependencies : Map[String, String]
  provider : String
  default_profile : Bool
  available_profiles : Array[String]
  capabilities : SourceCapabilities?
  evidence : SourceEvidence
}

///|
struct Backend {
  extension : String
  emit : (@ir.IRFile) -> String raise
  rich_units : Bool
  /// Generation from a rich unit; the second argument is the source path the engine reports. Files are relative
  /// to the job destination.
  emit_unit : ((@data.IRUnit, String) -> Array[GeneratedFile] raise)?
}

///|
/// Registries are explicit; every run creates isolated script sessions.
pub struct Engine {
  priv frontends : Map[String, Frontend]
  priv source_providers : Map[String, SourceProvider]
  priv backends : Map[String, Backend]
  priv generators : Map[String, GenerationBackend]
  priv components : Map[String, Component]
  priv metadata_policies : Map[String, @data.MetadataPolicy]
  priv data_formats : Map[String, DataFormat]
  priv limits : @scheme.Limits
  priv static_metadata_authority : Bool
  priv mut project_metadata_provider : ((ProjectMetadataScope) -> @linked_metadata.Capabilities raise)?
}

///|
pub fn Engine::new(
  limits~ : @scheme.Limits = @scheme.Limits::default(),
  metadata_capabilities~ : @linked_metadata.Capabilities = @linked_metadata.Capabilities::unavailable(),
) -> Engine {
  let engine = {
    static_metadata_authority: metadata_capabilities.restore_library is Some(_) ||
    metadata_capabilities.load_context is Some(_) ||
    metadata_capabilities.target_names_interpreter,
    project_metadata_provider: None,
    frontends: Map([]),
    source_providers: Map([]),
    components: Map::from_array([
      (
        "json-identity",
        Component::JSON(fn(text) { { content: text, lineage: None, } }),
      ),
    ]),
    metadata_policies: Map::from_array([
      ("retain", @data.MetadataPolicy::retain()),
      ("copy-single-origin", @data.MetadataPolicy::copy_single_origin()),
    ]),
    backends: Map([]),
    generators: Map::from_array([
      (
        "moonbit",
        GenerationBackend::{
          generate: fn(unit, options, cancelled) raise {
            moonbit_generation(unit, options, cancelled, metadata_capabilities)
          },
          write_bytes: true,
          validate_options: moonbit_validate_options,
          targets: ["wasm", "wasm-gc", "js", "native"],
        },
      ),
      (
        "elm",
        GenerationBackend::{
          generate: elm_generation,
          write_bytes: false,
          validate_options: elm_validate_options,
          targets: [],
        },
      ),
    ]),
    limits,
    data_formats: Map::from_array([
      (
        "morphir-json",
        DataFormat::{
          codec: "morphir-json",
          extensions: [".json"],
          suffix: ".json",
          binary: false,
        },
      ),
      (
        "ion-text",
        DataFormat::{
          codec: "ion-text",
          extensions: [".ion"],
          suffix: ".ion",
          binary: false,
        },
      ),
      (
        "ion-binary",
        DataFormat::{
          codec: "ion-binary",
          extensions: [".ionb", ".10n"],
          suffix: ".ionb",
          binary: true,
        },
      ),
    ]),
  }
  engine.register_frontend("scheme", [".scm", ".ss"], fn(source) raise {
    @frontend.compile_file(
      source.content,
      package_name=canonical_path(source.package_name),
      module_name=canonical_path(source.module_name),
    )
  })
  engine.register_frontend("ir-json", [".json"], fn(source) raise {
    @migration.to_current(@versioned_json.read(source.content))
  })
  engine.register_unit_backend(
    "scheme", ".scm", @backend.emit_file, scheme_unit_generation,
  )
  engine.register_backend("ir-json", ".ir.json", @ir_json.write)
  engine
}

///|
pub fn Engine::register_frontend(
  self : Engine,
  name : String,
  extensions : Array[String],
  compile : (Source) -> @ir.IRFile raise,
) -> Unit {
  self.frontends[name] = {
    extensions: extensions.copy(),
    compiler: FileCompiler(compile),
    profile: None,
    dependencies: Map([]),
    provider: name,
    default_profile: true,
    available_profiles: [],
    capabilities: None,
    evidence: Declared,
  }
}

///|
pub fn Engine::register_backend(
  self : Engine,
  name : String,
  extension : String,
  emit : (@ir.IRFile) -> String raise,
  rich_units~ : Bool = false,
) -> Unit {
  self.backends[name] = { extension, emit, rich_units, emit_unit: None, }
}

///|
/// A backend that also generates from a rich unit. `emit` serves file frontends as with `register_backend`;
/// `emit_unit` serves rich frontends and keeps the unit's metadata in the form the backend documents, so the
/// engine reports no `backend.metadata_dropped`. Its files are relative to the job destination, and a plan with
/// such a job requires a byte publisher.
pub fn Engine::register_unit_backend(
  self : Engine,
  name : String,
  extension : String,
  emit : (@ir.IRFile) -> String raise,
  emit_unit : (@data.IRUnit, String) -> Array[GeneratedFile] raise,
) -> Unit {
  self.backends[name] = {
    extension,
    emit,
    rich_units: true,
    emit_unit: Some(emit_unit),
  }
}