///|
/// How a profile compiles, without its callback: one unit for each file, or one unit for the whole project.
#derive(Eq, Debug)
pub(all) enum CompileKind {
  EachFile
  WholeProject
}

///|
pub extend CompileKind with Eq::{equal, not_equal}

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

///|
/// A source profile without its compile callback. `capabilities` and `unit_limits` are `None` for a frontend from
/// `register_frontend`.
pub(all) struct ProfileOffer {
  id : @ids.ProfileId
  compile : CompileKind
  capabilities : SourceCapabilities?
  evidence : SourceEvidence
  unit_limits : @data.Limits?
}

///|
/// One source language that one provider offers. Two providers of one language give two offers; planning then
/// needs `frontend.provider`.
pub(all) struct LanguageOffer {
  language : @ids.LanguageId
  provider : @ids.ProviderId
  extensions : Array[@ids.FileExtension]
  default_profile : @ids.ProfileId?
  profiles : Array[ProfileOffer]
}

///|
fn kind(compile : SourceCompile) -> CompileKind {
  match compile {
    PerFile(_) => EachFile
    PerProject(_) => WholeProject
  }
}

///|
/// The offer of a frontend from `register_frontend` or `register_unit_frontend`. Their names are not typed yet
/// (`morphir-ukvn.5.32`), so a name that is not a valid id gives no offer.
fn Frontend::offer(self : Frontend, name : String) -> LanguageOffer? {
  try {
    let extensions = []
    for e in self.extensions {
      extensions.push(@ids.FileExtension::parse(e))
    }
    let (compile, unit_limits) = match self.compiler {
      FileCompiler(_) => (EachFile, None)
      UnitCompiler(_, limits) => (EachFile, Some(limits))
      ProjectCompiler(_, limits) => (WholeProject, Some(limits))
    }
    let default_profile = match self.profile {
      Some(text) => Some(@ids.ProfileId::parse(text))
      None => None
    }
    let profiles : Array[ProfileOffer] = match default_profile {
      Some(id) =>
        [
          {
            id,
            compile,
            capabilities: self.capabilities.map(c => c.snapshot()),
            evidence: self.evidence,
            unit_limits,
          },
        ]
      None => []
    }
    Some({
      language: @ids.LanguageId::parse(name),
      provider: @ids.ProviderId::parse(self.provider),
      extensions,
      default_profile,
      profiles,
    })
  } catch {
    _ => None
  }
}

///|
/// Every source language the engine can compile: registered providers, and frontends from `register_frontend`
/// and `register_unit_frontend`, including the built-in `scheme`. The `ir-json` frontend and the data formats are
/// not source languages. One offer per language and provider, sorted by language, then provider. No offer holds a
/// compile callback, so the list is safe to give to any caller and to convert to JSON.
pub fn Engine::languages(self : Engine) -> Array[LanguageOffer] {
  let offers : Array[LanguageOffer] = []
  for _, provider in self.source_providers {
    for language in provider.languages {
      offers.push({
        language: language.id,
        provider: provider.id,
        extensions: language.extensions.copy(),
        default_profile: Some(language.default_profile),
        profiles: language.profiles.map(p => {
          (
            {
              id: p.id,
              compile: kind(p.compile),
              capabilities: Some(p.capabilities.snapshot()),
              evidence: p.evidence,
              unit_limits: Some(p.unit_limits),
            } : ProfileOffer)
        }),
      })
    }
  }
  for name, frontend in self.frontends {
    if name == "ir-json" || self.data_formats.contains(name) {
      continue
    }
    if frontend.offer(name) is Some(offer) {
      offers.push(offer)
    }
  }
  offers.sort_by((a, b) => {
    let by_language = a.language
      .to_string()
      .lexical_compare(b.language.to_string())
    if by_language != 0 {
      by_language
    } else {
      a.provider.to_string().lexical_compare(b.provider.to_string())
    }
  })
  offers
}