///|

///|
/// Sorted map keys, matching the crate's BTreeMap iteration order.

///|
/// Lexicographic (code-unit wise) string comparison, matching the byte-wise
/// `Ord` of Rust strings used by the oracle's BTreeMap ordering.
pub fn str_lexicographic(a : String, b : String) -> Int {
  let n = if a.length() < b.length() { a.length() } else { b.length() }
  for i in 0.. Array[String] {
  let keys : Array[String] = []
  for k, _ in map {
    keys.push(k)
  }
  keys.sort_by(str_lexicographic)
  keys
}

///|
fn sorted_extension_names(map : Map[String, Array[Extension]]) -> Array[String] {
  sorted_keys(map)
}

///|
/// Namespaces required by this item's content.
fn Item::used_namespaces(self : Item) -> Map[String, String] {
  let namespaces : Map[String, String] = Map([])
  if self.content is Some(_) {
    namespaces["content"] = "http://purl.org/rss/1.0/modules/content/"
  }
  if self.atom_links.length() > 0 {
    namespaces["atom"] = ATOM_NAMESPACE
  }
  if self.itunes_ext is Some(_) {
    namespaces["itunes"] = ITUNES_NAMESPACE
  }
  if self.dublin_core_ext is Some(_) {
    namespaces["dc"] = DUBLIN_CORE_NAMESPACE
  }
  namespaces
}

///|
/// Namespaces required to write this channel, computed bottom-up like the
/// crate's `used_namespaces`.
fn Channel::used_namespaces(self : Channel) -> Map[String, String] {
  let namespaces : Map[String, String] = Map([])
  for item in self.items {
    for k, v in item.used_namespaces() {
      namespaces[k] = v
    }
  }
  if self.itunes_ext is Some(_) {
    namespaces["itunes"] = ITUNES_NAMESPACE
  }
  if self.dublin_core_ext is Some(_) {
    namespaces["dc"] = DUBLIN_CORE_NAMESPACE
  }
  if self.atom_links.length() > 0 {
    namespaces["atom"] = ATOM_NAMESPACE
  }
  namespaces
}

///|
fn @xml.Writer::write_text_element(
  self : @xml.Writer,
  name : String,
  content : String,
) -> Unit {
  self.open(name)
  self.text(content)
  self.close(name)
}

///|
/// Count non-overlapping occurrences of `needle` in `s`.
fn count_occurrences(s : String, needle : String) -> Int {
  let n = s.length()
  let m = needle.length()
  if m == 0 {
    return 0
  }
  let mut count = 0
  let mut i = 0
  while i + m <= n {
    if s[i] == needle[0] {
      let mut ok = true
      for k in 1.. Unit {
  // Content containing more than one `]]>` cannot be represented losslessly
  // with CDATA section splitting (the Rust crate emits broken XML there);
  // fall back to escaped text, which always roundtrips.
  if count_occurrences(content, "]]>") > 1 {
    self.write_text_element(name, content)
  } else {
    self.open(name)
    self.cdata(content)
    self.close(name)
  }
}

///|
fn Extension::write(self : Extension, w : @xml.Writer) -> Unit {
  let attrs : Array[(String, String)] = []
  for key in sorted_keys(self.attrs) {
    attrs.push((key, self.attrs[key]))
  }
  w.open_with(self.name, attrs~)
  if self.value is Some(value) {
    w.text(value)
  }
  for name in sorted_extension_names(self.children) {
    for child in self.children[name] {
      child.write(w)
    }
  }
  w.close(self.name)
}

///|
fn Category::write(self : Category, w : @xml.Writer) -> Unit {
  let attrs : Array[(String, String)] = []
  if self.domain is Some(domain) {
    attrs.push(("domain", domain))
  }
  w.open_with("category", attrs~)
  w.text(self.name)
  w.close("category")
}

///|
fn Cloud::write(self : Cloud, w : @xml.Writer) -> Unit {
  w.empty("cloud", attrs=[
    ("domain", self.domain),
    ("port", self.port),
    ("path", self.path),
    ("registerProcedure", self.register_procedure),
    ("protocol", self.protocol),
  ])
}

///|
fn Enclosure::write(self : Enclosure, w : @xml.Writer) -> Unit {
  w.empty("enclosure", attrs=[
    ("url", self.url),
    ("length", self.length),
    ("type", self.mime_type),
  ])
}

///|
fn Guid::write(self : Guid, w : @xml.Writer) -> Unit {
  if self.is_permalink {
    w.open("guid")
  } else {
    w.open_with("guid", attrs=[("isPermaLink", "false")])
  }
  w.text(self.value)
  w.close("guid")
}

///|
fn Image::write(self : Image, w : @xml.Writer) -> Unit {
  w.open("image")
  w.write_text_element("url", self.url)
  w.write_text_element("title", self.title)
  w.write_text_element("link", self.link)
  if self.width is Some(width) {
    w.write_text_element("width", width)
  }
  if self.height is Some(height) {
    w.write_text_element("height", height)
  }
  if self.description is Some(description) {
    w.write_text_element("description", description)
  }
  w.close("image")
}

///|
fn Source::write(self : Source, w : @xml.Writer) -> Unit {
  w.open_with("source", attrs=[("url", self.url)])
  if self.title is Some(title) {
    w.text(title)
  }
  w.close("source")
}

///|
fn TextInput::write(self : TextInput, w : @xml.Writer) -> Unit {
  w.open("textInput")
  w.write_text_element("title", self.title)
  w.write_text_element("description", self.description)
  w.write_text_element("name", self.name)
  w.write_text_element("link", self.link)
  w.close("textInput")
}

///|
fn ITunesCategory::write(self : ITunesCategory, w : @xml.Writer) -> Unit {
  w.open_with("itunes:category", attrs=[("text", self.text)])
  if self.subcategory is Some(sub) {
    sub.write(w)
  }
  w.close("itunes:category")
}

///|
fn ITunesOwner::write(self : ITunesOwner, w : @xml.Writer) -> Unit {
  w.open("itunes:owner")
  if self.name is Some(name) {
    w.write_text_element("itunes:name", name)
  }
  if self.email is Some(email) {
    w.write_text_element("itunes:email", email)
  }
  w.close("itunes:owner")
}

///|
fn ITunesChannelExtension::write(
  self : ITunesChannelExtension,
  w : @xml.Writer,
) -> Unit {
  if self.author is Some(v) {
    w.write_text_element("itunes:author", v)
  }
  if self.block is Some(v) {
    w.write_text_element("itunes:block", v)
  }
  for category in self.categories {
    category.write(w)
  }
  if self.image is Some(image) {
    w.empty("itunes:image", attrs=[("href", image)])
  }
  if self.explicit is Some(v) {
    w.write_text_element("itunes:explicit", v)
  }
  if self.complete is Some(v) {
    w.write_text_element("itunes:complete", v)
  }
  if self.new_feed_url is Some(v) {
    w.write_text_element("itunes:new-feed-url", v)
  }
  if self.owner is Some(owner) {
    owner.write(w)
  }
  if self.subtitle is Some(v) {
    w.write_text_element("itunes:subtitle", v)
  }
  if self.summary is Some(v) {
    w.write_text_element("itunes:summary", v)
  }
  if self.keywords is Some(v) {
    w.write_text_element("itunes:keywords", v)
  }
  if self.itunes_type is Some(v) {
    w.write_text_element("itunes:type", v)
  }
}

///|
fn ITunesItemExtension::write(
  self : ITunesItemExtension,
  w : @xml.Writer,
) -> Unit {
  if self.author is Some(v) {
    w.write_text_element("itunes:author", v)
  }
  if self.block is Some(v) {
    w.write_text_element("itunes:block", v)
  }
  if self.image is Some(image) {
    w.open_with("itunes:image", attrs=[("href", image)])
    w.close("itunes:image")
  }
  if self.duration is Some(v) {
    w.write_text_element("itunes:duration", v)
  }
  if self.explicit is Some(v) {
    w.write_text_element("itunes:explicit", v)
  }
  if self.closed_captioned is Some(v) {
    w.write_text_element("itunes:isClosedCaptioned", v)
  }
  if self.order is Some(v) {
    w.write_text_element("itunes:order", v)
  }
  if self.subtitle is Some(v) {
    w.write_text_element("itunes:subtitle", v)
  }
  if self.summary is Some(v) {
    w.write_text_element("itunes:summary", v)
  }
  if self.keywords is Some(v) {
    w.write_text_element("itunes:keywords", v)
  }
  if self.episode is Some(v) {
    w.write_text_element("itunes:episode", v)
  }
  if self.season is Some(v) {
    w.write_text_element("itunes:season", v)
  }
  if self.episode_type is Some(v) {
    w.write_text_element("itunes:episodeType", v)
  }
}

///|
fn DublinCoreExtension::write(
  self : DublinCoreExtension,
  w : @xml.Writer,
) -> Unit {
  fn elements(name : String, values : Array[String]) -> Unit {
    for value in values {
      w.write_text_element(name, value)
    }
  }

  elements("dc:contributor", self.contributors)
  elements("dc:coverage", self.coverages)
  elements("dc:creator", self.creators)
  elements("dc:date", self.dates)
  elements("dc:description", self.descriptions)
  elements("dc:format", self.formats)
  elements("dc:identifier", self.identifiers)
  elements("dc:language", self.languages)
  elements("dc:publisher", self.publishers)
  elements("dc:relation", self.relations)
  elements("dc:rights", self.rights)
  elements("dc:source", self.sources)
  elements("dc:subject", self.subjects)
  elements("dc:title", self.titles)
  elements("dc:type", self.types)
}

///|
fn SyndicationExtension::write(
  self : SyndicationExtension,
  namespaces : Map[String, String],
  w : @xml.Writer,
) -> Unit {
  for prefix in sorted_keys(namespaces) {
    if namespaces[prefix] == SYNDICATION_NAMESPACE {
      w.write_text_element("\{prefix}:updatePeriod", self.period.to_string())
      w.write_text_element(
        "\{prefix}:updateFrequency",
        self.frequency.to_string(),
      )
      w.write_text_element("\{prefix}:updateBase", self.base)
    }
  }
}

///|
fn Item::write(self : Item, w : @xml.Writer) -> Unit {
  w.open("item")
  if self.title is Some(v) {
    w.write_text_element("title", v)
  }
  if self.link is Some(v) {
    w.write_text_element("link", v)
  }
  if self.description is Some(v) {
    w.write_cdata_element("description", v)
  }
  if self.author is Some(v) {
    w.write_text_element("author", v)
  }
  for category in self.categories {
    category.write(w)
  }
  if self.comments is Some(v) {
    w.write_text_element("comments", v)
  }
  if self.enclosure is Some(enclosure) {
    enclosure.write(w)
  }
  if self.guid is Some(guid) {
    guid.write(w)
  }
  if self.pub_date is Some(v) {
    w.write_text_element("pubDate", v)
  }
  if self.source is Some(source) {
    source.write(w)
  }
  if self.content is Some(v) {
    w.write_cdata_element("content:encoded", v)
  }
  for _ns in sorted_keys(self.extensions) {
    let inner = self.extensions[_ns]
    for name in sorted_extension_names(inner) {
      for ext in inner[name] {
        ext.write(w)
      }
    }
  }
  for link in self.atom_links {
    atom_link_write(link, w)
  }
  if self.itunes_ext is Some(ext) {
    ext.write(w)
  }
  if self.dublin_core_ext is Some(ext) {
    ext.write(w)
  }
  w.close("item")
}

///|
/// Write an `atom:link` extension element (href and rel are always emitted).
fn atom_link_write(link : @atom.Link, w : @xml.Writer) -> Unit {
  let attrs : Array[(String, String)] = [
    ("href", link.href),
    ("rel", link.rel.unwrap_or("alternate")),
  ]
  if link.hreflang is Some(v) {
    attrs.push(("hreflang", v))
  }
  if link.media_type is Some(v) {
    attrs.push(("type", v))
  }
  if link.title is Some(v) {
    attrs.push(("title", v))
  }
  if link.length is Some(v) {
    attrs.push(("length", v))
  }
  w.empty("atom:link", attrs~)
}

///|

///|
/// Serialise the channel as RSS 2.0 XML.
pub fn Channel::to_xml(self : Channel) -> String {
  channel_to_xml(self)
}

///|
pub impl Show for Channel with fn output(self, logger) -> Unit {
  logger.write_string(channel_to_xml(self))
}

///|
/// Serialise the channel as RSS 2.0 XML. Writing follows the crate:
/// only RSS 2.0 output is produced regardless of the version parsed.
fn channel_to_xml(channel : Channel) -> String {
  let w = @xml.Writer::new()
  w.write_decl()
  // Merge computed namespaces with explicitly-declared ones; declared wins.
  let merged : Map[String, String] = Map([])
  for k, v in channel.used_namespaces() {
    merged[k] = v
  }
  for k, v in channel.namespaces {
    merged[k] = v
  }
  let root_attrs : Array[(String, String)] = [("version", "2.0")]
  for prefix in sorted_keys(merged) {
    root_attrs.push(("xmlns:\{prefix}", merged[prefix]))
  }
  w.open_with("rss", attrs=root_attrs)

  w.open("channel")
  w.write_text_element("title", channel.title)
  w.write_text_element("link", channel.link)
  w.write_text_element("description", channel.description)
  if channel.language is Some(v) {
    w.write_text_element("language", v)
  }
  if channel.copyright is Some(v) {
    w.write_text_element("copyright", v)
  }
  if channel.managing_editor is Some(v) {
    w.write_text_element("managingEditor", v)
  }
  if channel.webmaster is Some(v) {
    w.write_text_element("webMaster", v)
  }
  if channel.pub_date is Some(v) {
    w.write_text_element("pubDate", v)
  }
  if channel.last_build_date is Some(v) {
    w.write_text_element("lastBuildDate", v)
  }
  for category in channel.categories {
    category.write(w)
  }
  if channel.generator is Some(v) {
    w.write_text_element("generator", v)
  }
  if channel.rating is Some(v) {
    w.write_text_element("rating", v)
  }
  if channel.docs is Some(v) {
    w.write_text_element("docs", v)
  }
  if channel.cloud is Some(cloud) {
    cloud.write(w)
  }
  if channel.ttl is Some(v) {
    w.write_text_element("ttl", v)
  }
  if channel.image is Some(image) {
    image.write(w)
  }
  if channel.text_input is Some(text_input) {
    text_input.write(w)
  }
  if channel.skip_hours.length() > 0 {
    w.open("skipHours")
    for hour in channel.skip_hours {
      w.write_text_element("hour", hour)
    }
    w.close("skipHours")
  }
  if channel.skip_days.length() > 0 {
    w.open("skipDays")
    for day in channel.skip_days {
      w.write_text_element("day", day)
    }
    w.close("skipDays")
  }
  for _ns in sorted_keys(channel.extensions) {
    let inner = channel.extensions[_ns]
    for name in sorted_extension_names(inner) {
      for ext in inner[name] {
        ext.write(w)
      }
    }
  }
  for link in channel.atom_links {
    atom_link_write(link, w)
  }
  if channel.itunes_ext is Some(ext) {
    ext.write(w)
  }
  if channel.dublin_core_ext is Some(ext) {
    ext.write(w)
  }
  if channel.syndication_ext is Some(ext) {
    ext.write(channel.namespaces, w)
  }
  for item in channel.items {
    item.write(w)
  }
  w.close("channel")
  w.close("rss")
  w.to_string()
}