///|
/// An uncompiled message resource supplied by an application.
///
/// Catalog compilation parses every source string up front, so syntax errors
/// are reported during application initialization rather than during a request.
pub struct MessageResource {
  key : String
  source : String
} derive(Eq, Debug)

///|
pub fn MessageResource::new(key : String, source : String) -> MessageResource {
  { key, source }
}

///|
pub fn message_resource(key : String, source : String) -> MessageResource {
  { key, source }
}

///|
pub fn MessageResource::key(self : MessageResource) -> String {
  self.key
}

///|
pub fn MessageResource::source(self : MessageResource) -> String {
  self.source
}

///|
/// A parsed message associated with its stable application key.
pub struct CatalogMessage {
  key : String
  template : MessageTemplate
} derive(Eq, Debug)

///|
pub fn CatalogMessage::key(self : CatalogMessage) -> String {
  self.key
}

///|
pub fn CatalogMessage::template(self : CatalogMessage) -> MessageTemplate {
  self.template
}

///|
/// Errors found while compiling one locale's message resources.
pub(all) enum CatalogCompileError {
  EmptyMessageKey(Int)
  DuplicateMessageKey(String)
  InvalidMessage(String, MessageParseError)
} derive(Eq, Debug)

///|
pub fn CatalogCompileError::message(self : CatalogCompileError) -> String {
  match self {
    EmptyMessageKey(index) => "message key at index \{index} must not be empty"
    DuplicateMessageKey(key) => "duplicate message key: \{key}"
    InvalidMessage(key, error) => "invalid message '\{key}': \{error.message()}"
  }
}

///|
/// An immutable collection of compiled messages for one locale.
pub struct MessageCatalog {
  locale : Locale
  messages : Array[CatalogMessage]
} derive(Eq, Debug)

///|
fn catalog_has_key(messages : Array[CatalogMessage], key : String) -> Bool {
  for message in messages {
    if message.key == key {
      return true
    }
  }
  false
}

///|
/// Parses and validates all messages for a locale.
pub fn MessageCatalog::compile(
  locale : Locale,
  resources : Array[MessageResource],
) -> Result[MessageCatalog, CatalogCompileError] {
  let messages : Array[CatalogMessage] = []
  for index, resource in resources {
    if resource.key.length() == 0 {
      return Err(EmptyMessageKey(index))
    }
    if catalog_has_key(messages, resource.key) {
      return Err(DuplicateMessageKey(resource.key))
    }
    let template = match MessageTemplate::parse(resource.source) {
      Ok(value) => value
      Err(error) => return Err(InvalidMessage(resource.key, error))
    }
    messages.push({ key: resource.key, template })
  }
  Ok({ locale, messages })
}

///|
pub fn MessageCatalog::locale(self : MessageCatalog) -> Locale {
  self.locale
}

///|
pub fn MessageCatalog::length(self : MessageCatalog) -> Int {
  self.messages.length()
}

///|
pub fn MessageCatalog::is_empty(self : MessageCatalog) -> Bool {
  self.messages.is_empty()
}

///|
pub fn MessageCatalog::keys(self : MessageCatalog) -> Array[String] {
  self.messages.map(fn(message) { message.key })
}

///|
pub fn MessageCatalog::contains(self : MessageCatalog, key : String) -> Bool {
  catalog_has_key(self.messages, key)
}

///|
pub fn MessageCatalog::get(
  self : MessageCatalog,
  key : String,
) -> MessageTemplate? {
  for message in self.messages {
    if message.key == key {
      return Some(message.template)
    }
  }
  None
}

///|
/// Errors found while combining locale catalogs into one bundle.
pub(all) enum BundleBuildError {
  EmptyBundle
  DuplicateCatalog(String)
  MissingDefaultCatalog(String)
} derive(Eq, Debug)

///|
pub fn BundleBuildError::message(self : BundleBuildError) -> String {
  match self {
    EmptyBundle => "a message bundle must contain at least one catalog"
    DuplicateCatalog(tag) => "duplicate locale catalog: \{tag}"
    MissingDefaultCatalog(tag) => "default locale catalog is missing: \{tag}"
  }
}

///|
/// A message together with the locale that ultimately supplied it.
pub struct ResolvedMessage {
  requested : Locale
  resolved : Locale
  key : String
  template : MessageTemplate
} derive(Eq, Debug)

///|
pub fn ResolvedMessage::requested_locale(self : ResolvedMessage) -> Locale {
  self.requested
}

///|
pub fn ResolvedMessage::resolved_locale(self : ResolvedMessage) -> Locale {
  self.resolved
}

///|
pub fn ResolvedMessage::key(self : ResolvedMessage) -> String {
  self.key
}

///|
pub fn ResolvedMessage::template(self : ResolvedMessage) -> MessageTemplate {
  self.template
}

///|
/// Failures produced while resolving and formatting a bundled message.
pub(all) enum BundleFormatError {
  MissingMessage(String, Array[String])
  MessageFormattingFailed(String, String, MessageFormatError)
} derive(Eq, Debug)

///|
pub fn BundleFormatError::message(self : BundleFormatError) -> String {
  match self {
    MissingMessage(key, attempted) =>
      "message '\{key}' was not found; tried: \{attempted.join(", ")}"
    MessageFormattingFailed(key, tag, error) =>
      "failed to format message '\{key}' for \{tag}: \{error.message()}"
  }
}

///|
/// A set of locale catalogs with deterministic per-message fallback.
///
/// Resolution tries the request's exact fallback chain first, then the closest
/// catalog in the same language, and finally the default locale. If a catalog
/// exists but lacks the requested key, resolution continues to the next
/// candidate. This allows regional catalogs to override only selected strings.
pub struct MessageBundle {
  default_locale : Locale
  catalogs : Array[MessageCatalog]
} derive(Eq, Debug)

///|
fn find_catalog(
  catalogs : Array[MessageCatalog],
  locale : Locale,
) -> MessageCatalog? {
  for catalog in catalogs {
    if catalog.locale == locale {
      return Some(catalog)
    }
  }
  None
}

///|
fn locale_was_tried(tried : Array[Locale], locale : Locale) -> Bool {
  for current in tried {
    if current == locale {
      return true
    }
  }
  false
}

///|
fn push_locale_once(locales : Array[Locale], locale : Locale) -> Unit {
  if !locale_was_tried(locales, locale) {
    locales.push(locale)
  }
}

///|
pub fn MessageBundle::new(
  default_locale : Locale,
  catalogs : Array[MessageCatalog],
) -> Result[MessageBundle, BundleBuildError] {
  if catalogs.is_empty() {
    return Err(EmptyBundle)
  }
  for index, catalog in catalogs {
    for previous = 0; previous < index; previous = previous + 1 {
      if catalogs[previous].locale == catalog.locale {
        return Err(DuplicateCatalog(catalog.locale.tag()))
      }
    }
  }
  match find_catalog(catalogs, default_locale) {
    None => Err(MissingDefaultCatalog(default_locale.tag()))
    Some(_) => Ok({ default_locale, catalogs })
  }
}

///|
pub fn MessageBundle::default_locale(self : MessageBundle) -> Locale {
  self.default_locale
}

///|
pub fn MessageBundle::supported_locales(self : MessageBundle) -> Array[Locale] {
  self.catalogs.map(fn(catalog) { catalog.locale })
}

///|
pub fn MessageBundle::catalog_count(self : MessageBundle) -> Int {
  self.catalogs.length()
}

///|
fn MessageBundle::resolution_order(
  self : MessageBundle,
  requested : Locale,
) -> Array[Locale] {
  let order : Array[Locale] = []
  for locale in requested.fallback_chain() {
    match find_catalog(self.catalogs, locale) {
      Some(_) => push_locale_once(order, locale)
      None => ()
    }
  }
  match best_locale(requested, self.supported_locales()) {
    Some(locale) => push_locale_once(order, locale)
    None => ()
  }
  for locale in self.default_locale.fallback_chain() {
    match find_catalog(self.catalogs, locale) {
      Some(_) => push_locale_once(order, locale)
      None => ()
    }
  }
  push_locale_once(order, self.default_locale)
  order
}

///|
/// Resolves a key and reports which locale supplied the message.
pub fn MessageBundle::resolve(
  self : MessageBundle,
  requested : Locale,
  key : String,
) -> Result[ResolvedMessage, BundleFormatError] {
  let attempted : Array[String] = []
  for locale in self.resolution_order(requested) {
    attempted.push(locale.tag())
    match find_catalog(self.catalogs, locale) {
      Some(catalog) =>
        match catalog.get(key) {
          Some(template) =>
            return Ok({ requested, resolved: locale, key, template })
          None => ()
        }
      None => ()
    }
  }
  Err(MissingMessage(key, attempted))
}

///|
/// Resolves and formats one localized message.
pub fn MessageBundle::format(
  self : MessageBundle,
  requested : Locale,
  key : String,
  arguments : Array[MessageArgument],
) -> Result[String, BundleFormatError] {
  let resolved = match self.resolve(requested, key) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  match resolved.template.format(resolved.resolved, arguments) {
    Ok(value) => Ok(value)
    Err(error) =>
      Err(MessageFormattingFailed(key, resolved.resolved.tag(), error))
  }
}