///|
fn feed_image_xml(w : XmlWriter, image : FeedImage) -> Unit {
  w.open("image", [])
  w.leaf("url", image.url)
  w.leaf("title", image.title)
  w.leaf("link", image.link)
  w.opt_int_leaf("width", image.width)
  w.opt_int_leaf("height", image.height)
  w.opt_leaf("description", image.description)
  w.close("image")
}

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

///|
fn validate_rss_fields(config : FeedConfig, errors : Array[FeedError]) -> Unit {
  match config.ttl {
    Some(ttl) =>
      if ttl < 0 {
        errors.push(InvalidValue("ttl is negative: \{ttl}"))
      }
    None => ()
  }
  match config.image {
    Some(img) => {
      match img.width {
        Some(width) =>
          if width <= 0 || width > 144 {
            errors.push(InvalidValue("image width out of range: \{width}"))
          }
        None => ()
      }
      match img.height {
        Some(height) =>
          if height <= 0 || height > 400 {
            errors.push(InvalidValue("image height out of range: \{height}"))
          }
        None => ()
      }
    }
    None => ()
  }
}

///|
/// Render the feed as an 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 of
/// the remaining elements.
///
/// When `indent` is `true`, produces indented output; otherwise minified.
pub fn FeedConfig::to_xml(
  self : FeedConfig,
  indent? : Bool = false,
) -> Result[String, Array[FeedError]] {
  let errors : Array[FeedError] = []
  validate_common_fields(self, errors, is_reserved_rss_namespace_prefix)
  validate_rss_fields(self, errors)
  // RSS uses 4 spaces per level:  at depth 1, its children at depth 2.
  let w = XmlWriter::new_with_size(indent, 4)
  let geo_rss = self.items
    .iter()
    .any(fn(item) { item.lat is Some(_) || item.long is Some(_) })
  // Write the RSS header manually;  and  are not managed by
  // open()/close() so that we can emit the multi-attribute opening tag.
  w.write_raw("")
  w.newline()
  w.write_raw("")
  w.newline()
  // depth is 0 here; manually indent to depth 1 for 
  w.indent()
  w.pad()
  w.write_raw("")
  w.newline()
  // depth is now 1; children will be at depth 2
  w.indent()
  w.leaf_cdata("title", self.title)
  w.leaf_cdata("description", self.description)
  w.leaf("link", self.site_url)
  match self.image {
    Some(img) => feed_image_xml(w, img)
    None => ()
  }
  w.leaf("generator", self.generator)
  w.opt_leaf("lastBuildDate", optional_date_to_rfc822(self.last_build_date))
  match self.feed_url {
    Some(url) =>
      w.self_closing("atom:link", [
        ("href", url),
        ("rel", "self"),
        ("type", "application/rss+xml"),
      ])
    None => ()
  }
  match self.author {
    Some(a) => w.leaf_cdata("dc:creator", a.name)
    None => ()
  }
  w.opt_leaf("pubDate", optional_date_to_rfc822(self.pub_date))
  w.opt_leaf_cdata("copyright", self.copyright)
  w.opt_leaf_cdata("language", self.language)
  w.opt_leaf_cdata("managingEditor", self.managing_editor)
  w.opt_leaf_cdata("webMaster", self.web_master)
  w.opt_leaf("docs", self.docs)
  w.opt_int_leaf("ttl", self.ttl)
  for cat in self.categories {
    feed_category_xml(w, cat)
  }
  match self.hub {
    Some(h) => w.self_closing("atom:link", [("href", h), ("rel", "hub")])
    None => ()
  }
  for elem in self.custom_elements {
    custom_elem_xml(errors, w, elem)
  }
  for item in self.items {
    feed_item_xml(errors, w, item, self.author)
  }
  if !errors.is_empty() {
    return Err(errors)
  }
  // Close  at depth 1, then  at depth 0.
  w.dedent()
  w.pad()
  w.write_raw("")
  w.newline()
  w.dedent()
  w.write_raw("")
  w.newline()
  Ok(w.to_string())
}