///|
/// Atom 1.0 feed rendering (RFC 4287).
///
/// Atom fields are derived from the shared `FeedConfig` / `FeedItem` model:
///
/// **Feed level:**
/// * `title`           → ``
/// * `description`     → `<subtitle>`
/// * `feed_url`        → `<link rel="self">` and `<id>`
/// * `site_url`        → `<link rel="alternate">`
/// * `hub`             → `<link rel="hub">`
/// * `last_build_date` → `<updated>`
/// * `author`          → `<author><name>…</name></author>`
/// * `copyright`       → `<rights>`
/// * `generator`       → `<generator>`
/// * `categories`      → `<category term="…">`
/// * `image.url`       → `<logo>`
///
/// **Entry level:**
/// * `title`          → `<title>`
/// * `url`            → `<link rel="alternate">` and `<id>` (fallback)
/// * `guid`           → `<id>` (preferred)
/// * `description`    → `<summary>`
/// * `content`        → `<content type="html">`
/// * `author`         → `<author>` (falls back to channel author)
/// * `pub_date`       → `<published>` and `<updated>`
/// * `categories`     → `<category term="…">`
/// * `enclosure`      → `<link rel="enclosure">`

///|
fn atom_link(
  w : XmlWriter,
  href : String,
  rel : String?,
  type_ : String?,
  length : String?,
) -> Unit {
  let attrs : Array[(String, String)] = [("href", href)]
  match rel {
    Some(r) => attrs.push(("rel", r))
    None => ()
  }
  match type_ {
    Some(t) => attrs.push(("type", t))
    None => ()
  }
  match length {
    Some(l) => attrs.push(("length", l))
    None => ()
  }
  w.self_closing("link", attrs)
}

///|
fn atom_author_xml(w : XmlWriter, author : Author) -> Unit {
  w.open("author", [])
  w.leaf("name", author.name)
  w.opt_leaf("email", author.email)
  w.close("author")
}

///|
fn atom_category_xml(w : XmlWriter, category : FeedCategory) -> Unit {
  let attrs : Array[(String, String)] = [("term", category.value)]
  match category.domain {
    Some(d) => attrs.push(("scheme", d))
    None => ()
  }
  w.self_closing("category", attrs)
}

///|
fn is_reserved_atom_namespace_prefix(prefix : String) -> Bool {
  prefix == "atom" || prefix == "geo"
}

///|
fn atom_entry_xml(
  errors : Array[FeedError],
  w : XmlWriter,
  item : FeedItem,
  channel_author : Author?,
  feed_date : FeedDate?,
) -> Unit {
  w.open("entry", [])
  w.leaf("title", item.title)
  match item.url {
    Some(url) => atom_link(w, url, Some("alternate"), Some("text/html"), None)
    None => ()
  }
  let id = match (item.guid, item.url) {
    (Some(g), _) => g
    (None, Some(u)) => u
    (None, None) => ""
  }
  w.leaf("id", id)
  match item.pub_date {
    Some(date) => w.opt_leaf("published", optional_date_to_rfc3339(Some(date)))
    None => ()
  }
  match item.pub_date {
    Some(date) => w.opt_leaf("updated", optional_date_to_rfc3339(Some(date)))
    None =>
      match feed_date {
        Some(date) =>
          w.opt_leaf("updated", optional_date_to_rfc3339(Some(date)))
        None => ()
      }
  }
  match item.description {
    Some(d) => w.leaf_cdata_with_attrs("summary", [("type", "html")], d)
    None => ()
  }
  match item.content {
    Some(c) => w.leaf_cdata_with_attrs("content", [("type", "html")], c)
    None => ()
  }
  match (item.author, channel_author) {
    (Some(a), _) => atom_author_xml(w, a)
    (None, Some(a)) => atom_author_xml(w, a)
    (None, None) => ()
  }
  for cat in item.categories {
    atom_category_xml(w, cat)
  }
  match item.enclosure {
    Some(enclosure) => {
      enclosure.validate(errors)
      let mime = enclosure.effective_mime()
      atom_link(
        w,
        enclosure.url,
        Some("enclosure"),
        Some(mime),
        Some(enclosure.size.to_string()),
      )
    }
    None => ()
  }
  match item.lat {
    Some(v) =>
      if validate_geo_value(v, -90.0, 90.0, "latitude", errors) {
        w.leaf("geo:lat", format_geo(v))
      }
    None => ()
  }
  match item.long {
    Some(v) =>
      if validate_geo_value(v, -180.0, 180.0, "longitude", errors) {
        w.leaf("geo:long", format_geo(v))
      }
    None => ()
  }
  for elem in item.custom_elements {
    custom_elem_xml(errors, w, elem)
  }
  w.close("entry")
}

///|
/// Render the feed as an Atom 1.0 XML string. Returns `Err` if `title` or
/// `description` is empty, or if any custom element has an invalid tag
/// name; all accumulated errors are reported without halting rendering.
///
/// When `indent` is `true`, produces indented output; otherwise minified.
pub fn FeedConfig::to_atom(
  self : FeedConfig,
  indent? : Bool = false,
) -> Result[String, Array[FeedError]] {
  let errors : Array[FeedError] = []
  let feed_date : FeedDate? = match self.last_build_date {
    Some(d) => Some(d)
    None => self.pub_date
  }
  validate_common_fields(self, errors, is_reserved_atom_namespace_prefix)
  if feed_date is None {
    errors.push(MissingRequiredField("updated"))
  }
  if self.author is None &&
    self.items.iter().any(fn(item) { item.author is None }) {
    errors.push(MissingRequiredField("author"))
  }
  for item in self.items {
    if item.guid is None && item.url is None {
      errors.push(MissingRequiredField("item.id (guid or url)"))
    }
  }
  // Atom uses 2 spaces per level: feed children at depth 1, entry children at 2.
  let w = XmlWriter::new(indent)
  let geo_rss = self.items
    .iter()
    .any(fn(item) { item.lat is Some(_) || item.long is Some(_) })
  w.write_raw("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
  w.newline()
  w.write_raw("<feed xmlns=\"http://www.w3.org/2005/Atom\"")
  w.write_namespace_attrs(self.custom_namespaces)
  if geo_rss {
    w.write_raw(" xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\"")
  }
  w.write_raw(">")
  w.newline()
  // depth is 0 here; increment to 1 so feed children are indented.
  w.indent()
  w.leaf("title", self.title)
  w.leaf("subtitle", self.description)
  match self.feed_url {
    Some(url) => {
      atom_link(w, url, Some("self"), Some("application/atom+xml"), None)
      w.leaf("id", url)
    }
    None => w.leaf("id", self.site_url)
  }
  atom_link(w, self.site_url, Some("alternate"), Some("text/html"), None)
  match self.hub {
    Some(h) => atom_link(w, h, Some("hub"), None, None)
    None => ()
  }
  match feed_date {
    Some(date) => w.opt_leaf("updated", optional_date_to_rfc3339(Some(date)))
    None => ()
  }
  match self.author {
    Some(a) => atom_author_xml(w, a)
    None => ()
  }
  match self.copyright {
    Some(c) => w.leaf("rights", c)
    None => ()
  }
  w.leaf("generator", self.generator)
  match self.image {
    Some(img) => w.leaf("logo", img.url)
    None => ()
  }
  for cat in self.categories {
    atom_category_xml(w, cat)
  }
  for elem in self.custom_elements {
    custom_elem_xml(errors, w, elem)
  }
  for item in self.items {
    atom_entry_xml(errors, w, item, self.author, feed_date)
  }
  if !errors.is_empty() {
    return Err(errors)
  }
  // depth is still 1; back to 0 before closing tag.
  w.dedent()
  w.write_raw("</feed>")
  w.newline()
  Ok(w.to_string())
}
</code></pre>
  <script>
    let moonbitLanguageFn = hljs => {
      return {
        case_insensitive: true,
        keywords: {
          keyword: 'func fn enum struct type if else match return continue break while let var interface pub priv readonly',
          literal: 'true false',
          type: "Int Int64 Double String Bool Char Bytes Option Array Result",
          built_in: 'lsl lsr asr shl shr land lor lxor Show Debug Hash Eq Compare Some None'
        },
        contains: [
          {
            scope: "char",
            begin: "'", end: "'"
          },
          {
            scope: "string",
            begin: "\"", end: "\""
          },
          {
            scope: "number",
            begin: "\\b\\d+(\\.\\d+)?\\b"
          },
          {
            scope: "codelink",
            match: /\<a href\="(?<link>[^<>]+?)"\>(?<code>[^\/<>]+?)\<\/a\>/g
          },
          hljs.COMMENT(
            '//', // begin
            '\n', // end
          )
        ]
      }
    }

    hljs.registerLanguage('moonbit', moonbitLanguageFn);
    hljs.highlightAll();
    hljs.initLineNumbersOnLoad();

    const number = window.location.href.split('#')[1];

    function waitForLineNumbers() {
      setTimeout(function () {
        const target = document.querySelector(`.hljs-ln-line[data-line-number="${number}"]`);
        if (target == null) waitForLineNumbers();
        else target.scrollIntoView();
      }, 50);
    }

    waitForLineNumbers()

  </script>
  <style>
    .hljs-ln-numbers {
      -webkit-touch-callout: none;
      -webkit-user-select: none;
      -khtml-user-select: none;
      -moz-user-select: none;
      -ms-user-select: none;
      user-select: none;
    }

    .hljs-ln-n {
      color: #ccc;
      border-right: 1px solid #dfdddd;
      margin-right: 1em;
      text-align: center;
      vertical-align: top;
      padding-right: 0.5em;
    }

    .hljs {
      background: none;
    }

    body {
      background-color: #fafafa;
    }
  </style>
</body>

</html>