///|
/// A reader adapter that mirrors the crate's `expand_empty_elements` setting:
/// `` surfaces as a [Start] immediately followed by its [End].
priv struct RssReader {
  inner : @xml.Reader
  mut pending_end : String?
}

///|
fn RssReader::new(input : String) -> RssReader raise RssError {
  let reader = @xml.Reader::new(input) catch {
    @xml.Syntax(pos~, msg~) => raise RssError::Xml(err=@xml.Syntax(pos~, msg~))
    _ => abort("rss: unexpected error")
  }
  { inner: reader, pending_end: None }
}

///|
fn RssReader::next(self : RssReader) -> @xml.Event raise RssError {
  if self.pending_end is Some(name) {
    self.pending_end = None
    return @xml.End(name)
  }
  let ev = self.inner.next() catch {
    @xml.Syntax(pos~, msg~) => raise RssError::Xml(err=@xml.Syntax(pos~, msg~))
    _ => abort("rss: unexpected error")
  }
  match ev {
    @xml.Empty(name~, attrs~) => {
      self.pending_end = Some(name)
      @xml.Start(name~, attrs~)
    }
    other => other
  }
}

///|
/// The code-unit offset of the underlying scanner (for diagnostics).
fn RssReader::position(self : RssReader) -> Int {
  self.inner.position()
}

///|
/// Port of `rss::util::element_text`: concatenated character content of the
/// currently-open element, trimmed; `None` when empty. Nested elements are
/// skipped.
fn element_text(r : RssReader) -> String? raise RssError {
  let sb = StringBuilder()
  for ;; {
    match r.next() {
      @xml.Text(t) => sb.write_string(t)
      @xml.CData(t) => sb.write_string(t)
      @xml.Start(..) => skip_element(r)
      @xml.End(_) | @xml.Eof => break
      _ => ()
    }
  }
  let content = @xml.trim(sb.to_string())
  if content == "" {
    None
  } else {
    Some(content)
  }
}

///|
/// Skip the remainder of the currently-open element (depth counting).
fn skip_element(r : RssReader) -> Unit raise RssError {
  let mut depth = 1
  while depth > 0 {
    match r.next() {
      @xml.Start(..) => depth += 1
      @xml.End(_) => depth -= 1
      @xml.Eof => return
      _ => ()
    }
  }
}

///|
/// Merge `xmlns:prefix` declarations of a start tag into `base`.
fn read_namespaces(
  attrs : Array[@xml.Attribute],
  base : Map[String, String],
) -> Map[String, String] {
  let out : Map[String, String] = Map([])
  let mut added = false
  for attr in attrs {
    if attr.key.length() > 6 && substring_eq(attr.key, 0, "xmlns:") {
      if !added {
        for k, v in base {
          out[k] = v
        }
        added = true
      }
      out[substring(attr.key, 6, attr.key.length())] = attr.value
    }
  }
  if added {
    out
  } else {
    base
  }
}

///|
fn substring_eq(s : String, offset : Int, lit : String) -> Bool {
  let n = lit.length()
  if offset + n > s.length() || offset < 0 {
    return false
  }
  for k in 0.. String {
  let sb = StringBuilder()
  for k in from.. (String, String)? {
  match @xml.split_qname(qname) {
    (Some(prefix), name) => Some((prefix, name))
    (None, _) => None
  }
}

///|
fn attribute_value(attrs : Array[@xml.Attribute], key : String) -> String? {
  for attr in attrs {
    if attr.key == key {
      return Some(attr.value)
    }
  }
  None
}

///|
/// Parse a generic namespaced element whose start tag has been consumed.
/// Mirrors `parse_extension_element`.
fn parse_extension_element(
  r : RssReader,
  start_attrs : Array[@xml.Attribute],
) -> Extension raise RssError {
  let attrs : Map[String, String] = Map([])
  for attr in start_attrs {
    attrs[attr.key] = attr.value
  }
  let children : Map[String, Array[Extension]] = Map([])
  let text = StringBuilder()
  let mut name = ""
  for ;; {
    match r.next() {
      @xml.Start(name=name2, attrs=attrs2) => {
        let child = parse_extension_element(r, attrs2)
        let lname = @xml.local_name(name2)
        let list = match children.get(lname) {
          Some(list) => list
          None => {
            let list : Array[Extension] = []
            children[lname] = list
            list
          }
        }
        list.push(child)
      }
      @xml.Text(t) => text.write_string(t)
      @xml.CData(t) => text.write_string(t)
      @xml.End(name2) => {
        name = name2
        break
      }
      @xml.Eof => raise unexpected_eof(r)
      _ => ()
    }
  }
  let trimmed = @xml.trim(text.to_string())
  {
    name,
    value: if trimmed == "" {
      None
    } else {
      Some(trimmed)
    },
    attrs,
    children,
  }
}

///|
fn unexpected_eof(r : RssReader) -> RssError {
  RssError::Xml(
    err=@xml.Syntax(pos=r.position(), msg="unexpected end of input"),
  )
}

///|
/// Route one namespaced element into the right bucket, mirroring the
/// namespace resolution of the crate's `channel.rs` / `item.rs`.
fn collect_extension(
  exts : ExtensionMap,
  atom_links : Array[@atom.Link],
  scope_ns : Map[String, String],
  prefix : String,
  name : String,
  ext : Extension,
) -> Unit {
  let resolved = scope_ns.get(prefix)
  if resolved is Some(ns) {
    if ns == ATOM_NAMESPACE {
      atom_links.push(atom_link_from(ext))
      return
    }
    if is_itunes_namespace(ns) {
      extension_entry(exts, ITUNES_NAMESPACE, name).push(ext)
      return
    }
    if ns == DUBLIN_CORE_NAMESPACE || ns == SYNDICATION_NAMESPACE {
      extension_entry(exts, ns, name).push(ext)
      return
    }
  }
  extension_entry(exts, prefix, name).push(ext)
}

///|
/// The Atom namespace used when bucketing extensions.
const ATOM_NAMESPACE : String = "http://www.w3.org/2005/Atom"

///|
/// Convert a generic `atom:link` extension element into an Atom link,
/// mirroring `AtomExtension::from_map`.
fn atom_link_from(ext : Extension) -> @atom.Link {
  {
    href: ext.attrs.get("href").unwrap_or(""),
    rel: Some(ext.attrs.get("rel").unwrap_or("alternate")),
    media_type: ext.attrs.get("type"),
    hreflang: ext.attrs.get("hreflang"),
    title: ext.attrs.get("title"),
    length: ext.attrs.get("length"),
  }
}

///|
/// Split extracted generic extensions into typed extension structs,
/// mirroring the tail of `Channel::from_xml` / `Item::from_xml`.
fn extract_extensions(
  exts : ExtensionMap,
) -> (ITunesChannelExtension?, DublinCoreExtension?, SyndicationExtension?) {
  let itunes = match exts.get(ITUNES_NAMESPACE) {
    Some(map) => Some(ITunesChannelExtension::from_map(map))
    None => None
  }
  exts.remove(ITUNES_NAMESPACE)
  let dc = match exts.get(DUBLIN_CORE_NAMESPACE) {
    Some(map) => Some(DublinCoreExtension::from_map(map))
    None => None
  }
  exts.remove(DUBLIN_CORE_NAMESPACE)
  let syn = match exts.get(SYNDICATION_NAMESPACE) {
    Some(map) => Some(SyndicationExtension::from_map(map))
    None => None
  }
  exts.remove(SYNDICATION_NAMESPACE)
  (itunes, dc, syn)
}

///|
/// Extract item-level extensions (no syndication on items).
fn extract_item_extensions(
  exts : ExtensionMap,
) -> (ITunesItemExtension?, DublinCoreExtension?) {
  let itunes = match exts.get(ITUNES_NAMESPACE) {
    Some(map) => Some(ITunesItemExtension::from_map(map))
    None => None
  }
  exts.remove(ITUNES_NAMESPACE)
  let dc = match exts.get(DUBLIN_CORE_NAMESPACE) {
    Some(map) => Some(DublinCoreExtension::from_map(map))
    None => None
  }
  exts.remove(DUBLIN_CORE_NAMESPACE)
  (itunes, dc)
}