///|
/// A structured error raised while reading, compiling, or validating a
/// gettext catalog.
pub(all) suberror GettextError {
  PoSyntax(line~ : Int, column~ : Int, message~ : String)
  PluralSyntax(position~ : Int, message~ : String)
  MoFormat(offset~ : Int, message~ : String)
  Validation(message~ : String)
} derive(Debug, Eq)

///|
/// The semantic kind of a comment attached to a PO entry.
///
/// GNU gettext distinguishes comments by the marker immediately following
/// `#`: no marker for translator comments, `.` for extracted comments, `:`
/// for source references, `,` for flags, and `|` for previous values.
pub(all) enum CommentKind {
  Translator
  Extracted
  Reference
  Flag
  Previous
} derive(Debug, Eq)

///|
/// A comment attached to a PO entry.
pub(all) struct PoComment {
  kind : CommentKind
  text : String
} derive(Debug, Eq)

///|
/// Construct a PO comment.
pub fn PoComment::new(kind : CommentKind, text : String) -> PoComment {
  { kind, text }
}

///|
/// One message entry in a PO or POT catalog.
///
/// `translations` stores `msgstr` as element zero for singular entries and
/// stores `msgstr[N]` values by numeric index for plural entries. Missing
/// indexes are represented by empty strings so indexes remain stable.
pub(all) struct PoEntry {
  comments : Array[PoComment]
  context : String?
  msgid : String
  msgid_plural : String?
  translations : Array[String]
  obsolete : Bool
} derive(Debug, Eq)

///|
/// Construct a singular entry without comments.
pub fn PoEntry::singular(
  msgid : String,
  translation? : String = "",
  context? : String,
) -> PoEntry {
  {
    comments: [],
    context,
    msgid,
    msgid_plural: None,
    translations: [translation],
    obsolete: false,
  }
}

///|
/// Construct a plural entry without comments.
pub fn PoEntry::plural(
  msgid : String,
  msgid_plural : String,
  translations : Array[String],
  context? : String,
) -> PoEntry {
  {
    comments: [],
    context,
    msgid,
    msgid_plural: Some(msgid_plural),
    translations,
    obsolete: false,
  }
}

///|
/// Return true when this entry is the conventional metadata header.
pub fn PoEntry::is_header(self : PoEntry) -> Bool {
  self.context is None && self.msgid == ""
}

///|
/// Return true when a flag comment contains the exact comma-separated flag.
pub fn PoEntry::has_flag(self : PoEntry, flag : String) -> Bool {
  for comment in self.comments {
    if comment.kind == Flag {
      for item in comment.text.split(",") {
        if item.trim().to_owned() == flag {
          return true
        }
      }
    }
  }
  false
}

///|
/// Return true when this entry has GNU gettext's `fuzzy` flag.
pub fn PoEntry::is_fuzzy(self : PoEntry) -> Bool {
  self.has_flag("fuzzy")
}

///|
/// Return a translation by plural index, or `None` if it is absent or empty.
pub fn PoEntry::translation(self : PoEntry, index : Int) -> String? {
  guard index >= 0 && index < self.translations.length() else { return None }
  let value = self.translations[index]
  if value == "" {
    None
  } else {
    Some(value)
  }
}

///|
/// A parsed PO or POT file.
pub(all) struct PoFile {
  entries : Array[PoEntry]
} derive(Debug, Eq)

///|
/// Construct a catalog document from entries.
pub fn PoFile::new(entries : Array[PoEntry]) -> PoFile {
  { entries, }
}

///|
/// Return the first conventional metadata header entry, if present.
pub fn PoFile::header(self : PoFile) -> PoEntry? {
  for entry in self.entries {
    if entry.is_header() {
      return Some(entry)
    }
  }
  None
}

///|
/// Byte order used when writing or reading a GNU MO file.
pub(all) enum Endian {
  Little
  Big
} derive(Debug, Eq)