///|
/// Parse an RSS feed from any of the supported versions
/// (RSS 0.90, 0.91, 0.92, 1.0 and 2.0).
///
/// Mirrors `Channel::read_from`. As a best effort for invalid feeds,
/// elements the RSS 2.0 spec declares required default to an empty string.
pub fn Channel::read_from(input : String) -> Channel raise RssError {
  Channel::read_from_impl(input)
}

///|
fn Channel::read_from_impl(input : String) -> Channel raise RssError {
  let r = RssReader::new(input)
  // Find the opening element.
  let mut root_attrs : Array[@xml.Attribute] = []
  for ;; {
    match r.next() {
      @xml.Start(name~, attrs~) =>
        if name is "rss" || name is "rdf:RDF" {
          root_attrs = attrs
          break
        } else {
          raise RssError::InvalidStartTag
        }
      @xml.Eof | @xml.End(_) => raise RssError::InvalidStartTag
      _ => continue
    }
  }
  let namespaces = read_namespaces(root_attrs, {})
  let mut channel : Channel? = None
  let extra_items : Array[Item] = []
  let mut has_extra_items = false
  let mut top_image : Image? = None
  let mut top_text_input : TextInput? = None
  for ;; {
    match r.next() {
      @xml.Start(name~, attrs~) =>
        match @xml.local_name(name) {
          "channel" => channel = Some(read_channel(r, attrs, namespaces))
          "item" => {
            extra_items.push(read_item(r, attrs, namespaces))
            has_extra_items = true
          }
          "image" => top_image = Some(read_image(r))
          "textinput" | "textInput" => top_text_input = Some(read_text_input(r))
          _ => skip_element(r)
        }
      @xml.End(_) | @xml.Eof => break
      _ => ()
    }
  }
  match channel {
    Some(ch) => {
      // RSS 0.9 / 1.0 keep items (and sometimes image/textinput) as siblings
      // of ; merge them in.
      if has_extra_items {
        for item in extra_items {
          ch.items.push(item)
        }
      }
      let image = if top_image is Some(_) { top_image } else { ch.image }
      let text_input = if top_text_input is Some(_) {
        top_text_input
      } else {
        ch.text_input
      }
      { ..ch, image, text_input, namespaces }
    }
    None => raise RssError::Eof
  }
}

///|
fn read_channel(
  r : RssReader,
  attrs : Array[@xml.Attribute],
  parent_ns : Map[String, String],
) -> Channel raise RssError {
  let scope_ns = read_namespaces(attrs, parent_ns)
  let mut title = ""
  let mut link = ""
  let mut description = ""
  let mut language : String? = None
  let mut copyright : String? = None
  let mut managing_editor : String? = None
  let mut webmaster : String? = None
  let mut pub_date : String? = None
  let mut last_build_date : String? = None
  let categories : Array[Category] = []
  let mut generator : String? = None
  let mut docs : String? = None
  let mut cloud : Cloud? = None
  let mut rating : String? = None
  let mut ttl : String? = None
  let mut image : Image? = None
  let mut text_input : TextInput? = None
  let skip_hours : Array[String] = []
  let skip_days : Array[String] = []
  let items : Array[Item] = []
  let extensions : ExtensionMap = Map([])
  let atom_links : Array[@atom.Link] = []
  for ;; {
    match r.next() {
      @xml.Start(name~, attrs=el_attrs) =>
        match name {
          "category" => categories.push(read_category(r, el_attrs))
          "cloud" => cloud = Some(read_cloud(r, el_attrs))
          "image" => image = Some(read_image(r))
          "textInput" => text_input = Some(read_text_input(r))
          "item" => items.push(read_item(r, el_attrs, scope_ns))
          "title" => if element_text(r) is Some(content) { title = content }
          "link" => if element_text(r) is Some(content) { link = content }
          "description" =>
            if element_text(r) is Some(content) {
              description = content
            }
          "language" => language = element_text(r)
          "copyright" => copyright = element_text(r)
          "managingEditor" => managing_editor = element_text(r)
          "webMaster" => webmaster = element_text(r)
          "pubDate" => pub_date = element_text(r)
          "lastBuildDate" => last_build_date = element_text(r)
          "generator" => generator = element_text(r)
          "rating" => rating = element_text(r)
          "docs" => docs = element_text(r)
          "ttl" => ttl = element_text(r)
          "skipHours" => read_skip_list(r, skip_hours, "hour")
          "skipDays" => read_skip_list(r, skip_days, "day")
          _ =>
            match extension_name(name) {
              Some((prefix, ename)) => {
                let inner_ns = read_namespaces(el_attrs, scope_ns)
                let ext = parse_extension_element(r, el_attrs)
                collect_extension(
                  extensions, atom_links, inner_ns, prefix, ename, ext,
                )
              }
              None => skip_element(r)
            }
        }
      @xml.End(_) => break
      @xml.Eof => raise unexpected_eof(r)
      _ => ()
    }
  }
  let (itunes, dc, syn) = extract_extensions(extensions)
  {
    title,
    link,
    description,
    language,
    copyright,
    managing_editor,
    webmaster,
    pub_date,
    last_build_date,
    categories,
    generator,
    docs,
    cloud,
    rating,
    ttl,
    image,
    text_input,
    skip_hours,
    skip_days,
    items,
    extensions,
    atom_links,
    itunes_ext: itunes,
    dublin_core_ext: dc,
    syndication_ext: syn,
    namespaces: scope_ns,
  }
}

///|
fn read_skip_list(
  r : RssReader,
  out : Array[String],
  entry : String,
) -> Unit raise RssError {
  for ;; {
    match r.next() {
      @xml.Start(name=tag, ..) =>
        if tag == entry && element_text(r) is Some(content) {
          out.push(content)
        } else {
          skip_element(r)
        }
      @xml.End(_) | @xml.Eof => break
      _ => ()
    }
  }
}

///|
fn read_item(
  r : RssReader,
  attrs : Array[@xml.Attribute],
  parent_ns : Map[String, String],
) -> Item raise RssError {
  let scope_ns = read_namespaces(attrs, parent_ns)
  let mut title : String? = None
  let mut link : String? = None
  let mut description : String? = None
  let mut author : String? = None
  let categories : Array[Category] = []
  let mut comments : String? = None
  let mut enclosure : Enclosure? = None
  let mut guid : Guid? = None
  let mut pub_date : String? = None
  let mut source : Source? = None
  let mut content : String? = None
  let extensions : ExtensionMap = Map([])
  let atom_links : Array[@atom.Link] = []
  for ;; {
    match r.next() {
      @xml.Start(name~, attrs=el_attrs) =>
        match name {
          "category" => categories.push(read_category(r, el_attrs))
          "guid" => guid = Some(read_guid(r, el_attrs))
          "enclosure" => enclosure = Some(read_enclosure(r, el_attrs))
          "source" => source = Some(read_source(r, el_attrs))
          "title" => title = element_text(r)
          "link" =>
            // Mirrors the crate: empty links are dropped on items.
            if element_text(r) is Some(value) {
              link = Some(value)
            }
          "description" => description = element_text(r)
          "author" => author = element_text(r)
          "comments" => comments = element_text(r)
          "pubDate" => pub_date = element_text(r)
          "content:encoded" => content = element_text(r)
          _ =>
            match extension_name(name) {
              Some((prefix, ename)) => {
                let inner_ns = read_namespaces(el_attrs, scope_ns)
                let ext = parse_extension_element(r, el_attrs)
                collect_extension(
                  extensions, atom_links, inner_ns, prefix, ename, ext,
                )
              }
              None => skip_element(r)
            }
        }
      @xml.End(_) => break
      @xml.Eof => raise unexpected_eof(r)
      _ => ()
    }
  }
  let (itunes, dc) = extract_item_extensions(extensions)
  {
    title,
    link,
    description,
    author,
    categories,
    comments,
    enclosure,
    guid,
    pub_date,
    source,
    content,
    extensions,
    atom_links,
    itunes_ext: itunes,
    dublin_core_ext: dc,
  }
}

///|
fn read_category(
  r : RssReader,
  attrs : Array[@xml.Attribute],
) -> Category raise RssError {
  {
    domain: attribute_value(attrs, "domain"),
    name: element_text(r).unwrap_or(""),
  }
}

///|
fn read_guid(
  r : RssReader,
  attrs : Array[@xml.Attribute],
) -> Guid raise RssError {
  let permalink = match attribute_value(attrs, "isPermaLink") {
    // Mirrors the crate: any value other than `false` counts as a permalink.
    Some(v) => !(v is "false")
    None => true
  }
  { is_permalink: permalink, value: element_text(r).unwrap_or("") }
}

///|
fn read_cloud(
  r : RssReader,
  attrs : Array[@xml.Attribute],
) -> Cloud raise RssError {
  // Consume the synthetic end tag of a self-closing element plus children.
  skip_element(r)
  {
    domain: attribute_value(attrs, "domain").unwrap_or(""),
    port: attribute_value(attrs, "port").unwrap_or(""),
    path: attribute_value(attrs, "path").unwrap_or(""),
    register_procedure: attribute_value(attrs, "registerProcedure").unwrap_or(
      "",
    ),
    protocol: attribute_value(attrs, "protocol").unwrap_or(""),
  }
}

///|
fn read_enclosure(
  r : RssReader,
  attrs : Array[@xml.Attribute],
) -> Enclosure raise RssError {
  skip_element(r)
  {
    url: attribute_value(attrs, "url").unwrap_or(""),
    length: attribute_value(attrs, "length").unwrap_or(""),
    mime_type: attribute_value(attrs, "type").unwrap_or(""),
  }
}

///|
fn read_source(
  r : RssReader,
  attrs : Array[@xml.Attribute],
) -> Source raise RssError {
  { url: attribute_value(attrs, "url").unwrap_or(""), title: element_text(r) }
}

///|
/// Read an `` element whose start tag has been consumed.
fn read_image(r : RssReader) -> Image raise RssError {
  let mut url = ""
  let mut title = ""
  let mut link = ""
  let mut width : String? = None
  let mut height : String? = None
  let mut img_description : String? = None
  for ;; {
    match r.next() {
      @xml.Start(name=tag, ..) =>
        match tag {
          "url" => if element_text(r) is Some(content) { url = content }
          "title" => if element_text(r) is Some(content) { title = content }
          "link" => if element_text(r) is Some(content) { link = content }
          "width" => width = element_text(r)
          "height" => height = element_text(r)
          "description" => img_description = element_text(r)
          _ => skip_element(r)
        }
      @xml.End(_) | @xml.Eof => break
      _ => ()
    }
  }
  { url, title, link, width, height, description: img_description }
}

///|
/// Read a `` (or RSS 1.0 ``) element whose start tag
/// has been consumed.
fn read_text_input(r : RssReader) -> TextInput raise RssError {
  let mut title = ""
  let mut description = ""
  let mut input_name = ""
  let mut link = ""
  for ;; {
    match r.next() {
      @xml.Start(name=tag, ..) =>
        match tag {
          "title" => if element_text(r) is Some(content) { title = content }
          "description" =>
            if element_text(r) is Some(content) {
              description = content
            }
          "name" => if element_text(r) is Some(content) { input_name = content }
          "link" => if element_text(r) is Some(content) { link = content }
          _ => skip_element(r)
        }
      @xml.End(_) | @xml.Eof => break
      _ => ()
    }
  }
  { title, description, name: input_name, link }
}