///|
/// Convert an RSS channel to an Atom 1.0 feed.
///
/// Mapping (documented, lossy by nature — Atom has no cloud/textInput/etc.):
/// - `title`       → `` (plain text)
/// - `link`        → `<link rel="alternate">`
/// - `description` → `<subtitle>`
/// - item guid     → entry `<id>`, falling back to the item link
/// - item title/link/description/categories map 1:1
pub fn Channel::to_atom(self : Channel) -> @atom.Feed {
  let feed = @atom.Feed::new(
    id=self.atom_id(),
    title=self.title,
    updated=self.atom_updated(),
  )
  let authors : Array[@atom.Person] = match self.managing_editor {
    Some(email) => [{ name: "", email: Some(email), uri: None }]
    None => []
  }
  let entries : Array[@atom.Entry] = []
  for item in self.items {
    let id = match item.guid {
      Some(g) => g.value
      None => item.link.unwrap_or("")
    }
    let links : Array[@atom.Link] = match item.link {
      Some(link) => [@atom.Link::alternate(link)]
      None => []
    }
    let entry_categories : Array[@atom.Category] = []
    for category in item.categories {
      entry_categories.push({
        term: category.name,
        scheme: category.domain,
        label: None,
      })
    }
    let item_authors : Array[@atom.Person] = match item.author {
      // An RSS author is an email address; Atom wants a name as well.
      Some(email) => [{ name: "", email: Some(email), uri: None }]
      None => []
    }
    entries.push({
      ..@atom.Entry::new(
        id~,
        title=item.title.unwrap_or(""),
        updated=item.atom_updated(),
      ),
      links,
      summary: item.description.map(fn(d) { @atom.Text::plain(d) }),
      content: item.content.map(fn(c) { @atom.Content::Html(c) }),
      categories: entry_categories,
      authors: item_authors,
    })
  }
  {
    ..feed,
    subtitle: Some(@atom.Text::plain(self.description)),
    links: [@atom.Link::alternate(self.link)],
    authors,
    entries,
  }
}

///|
fn Channel::atom_id(self : Channel) -> String {
  self.link
}

///|
fn Channel::atom_updated(self : Channel) -> String {
  self.last_build_date.unwrap_or(self.pub_date.unwrap_or(""))
}

///|
fn Item::atom_updated(self : Item) -> String {
  self.pub_date.unwrap_or("")
}

///|
fn first_alternate(links : Array[@atom.Link]) -> @atom.Link? {
  for link in links {
    if link.rel is None || link.rel is Some("alternate") {
      return Some(link)
    }
  }
  None
}

///|
fn enclosure_of(links : Array[@atom.Link]) -> Enclosure? {
  for link in links {
    if link.rel is Some("enclosure") {
      return Some({
        url: link.href,
        length: link.length.unwrap_or("0"),
        mime_type: link.media_type.unwrap_or("application/octet-stream"),
      })
    }
  }
  None
}

///|
fn content_to_string(c : @atom.Content) -> String {
  match c {
    @atom.Content::Text(v) => v
    @atom.Content::Html(v) => v
    @atom.Content::Xhtml(v) => v
    @atom.Content::Src(..) => ""
  }
}

///|
/// Convert an Atom feed into an RSS channel (the inverse of [to_atom]),
/// so Atom feeds can be consumed with the same API.
pub fn Channel::from_atom(feed : @atom.Feed) -> Channel raise RssError {
  let channel_link = match first_alternate(feed.links) {
    Some(link) => link.href
    None => ""
  }
  let description = match feed.subtitle {
    Some(subtitle) => subtitle.value
    None => ""
  }
  let copyright : String? = match feed.rights {
    Some(rights) => Some(rights.value)
    None => None
  }
  let generator : String? = feed.generator.map(fn(g) { g.value.unwrap_or("") })
  let channel : Channel = {
    title: feed.title.value,
    link: channel_link,
    description,
    language: None,
    copyright,
    managing_editor: None,
    webmaster: None,
    pub_date: None,
    last_build_date: Some(feed.updated),
    categories: [],
    generator,
    docs: None,
    cloud: None,
    rating: None,
    ttl: None,
    image: None,
    text_input: None,
    skip_hours: [],
    skip_days: [],
    items: [],
    extensions: {},
    atom_links: [],
    itunes_ext: None,
    dublin_core_ext: None,
    syndication_ext: None,
    namespaces: {},
  }
  for category in feed.categories {
    channel.categories.push({ domain: category.scheme, name: category.term })
  }
  for entry in feed.entries {
    let alternate_link = first_alternate(entry.links)
    let enclosure = enclosure_of(entry.links)
    let item_categories : Array[Category] = []
    for category in entry.categories {
      item_categories.push({ domain: category.scheme, name: category.term })
    }
    // Authors: prefer entry-level, fall back to feed-level (RFC 4287 §4.1.2).
    let authors = if entry.authors.length() > 0 {
      entry.authors
    } else {
      feed.authors
    }
    let author : String? = if authors.length() > 0 {
      let person = authors[0]
      match person.email {
        Some(email) => Some(email)
        None => Some(person.name)
      }
    } else {
      None
    }
    channel.items.push({
      title: Some(entry.title.value),
      link: alternate_link.map(fn(l) { l.href }),
      description: entry.summary.map(fn(s) { s.value }),
      author,
      categories: item_categories,
      comments: None,
      enclosure,
      guid: Some({ is_permalink: false, value: entry.id }),
      pub_date: Some(entry.updated),
      source: None,
      content: entry.content.map(fn(c) { content_to_string(c) }),
      extensions: {},
      atom_links: [],
      itunes_ext: None,
      dublin_core_ext: None,
    })
  }
  channel
}
</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>