///|
fn jget(j : Json, key : String) -> Json {
  match j {
    Object(map) =>
      match map.get(key) {
        Some(v) => v
        None => Json::null()
      }
    _ => Json::null()
  }
}

///|
fn s_at(j : Json, key : String) -> String? {
  match jget(j, key) {
    String(s) => Some(s)
    _ => None
  }
}

///|
fn s_at_or(j : Json, key : String, fallback~ : String) -> String {
  match s_at(j, key) {
    Some(v) => v
    None => fallback
  }
}

///|
fn arr_at(j : Json, key : String) -> Array[Json] {
  match jget(j, key) {
    Array(arr) => arr
    _ => []
  }
}

///|
fn obj_at(j : Json, key : String) -> Json? {
  match jget(j, key) {
    Object(_) => Some(jget(j, key))
    _ => None
  }
}

///|
fn categories_from(cats : Array[Json]) -> Array[Category] {
  let out : Array[Category] = []
  for c in cats {
    out.push({
      domain: s_at(c, "domain"),
      name: s_at_or(c, "name", fallback=""),
    })
  }
  out
}

///|
fn extension_from(j : Json) -> Extension {
  let attrs : Map[String, String] = Map([])
  match jget(j, "attrs") {
    Object(map) =>
      for k, v in map {
        match v {
          String(s) => attrs[k] = s
          _ => ()
        }
      }
    _ => ()
  }
  let children : Map[String, Array[Extension]] = Map([])
  match jget(j, "children") {
    Object(map) =>
      for name, list in map {
        match list {
          Array(arr) => {
            let exts : Array[Extension] = []
            for item in arr {
              exts.push(extension_from(item))
            }
            children[name] = exts
          }
          _ => ()
        }
      }
    _ => ()
  }
  {
    name: s_at_or(j, "name", fallback=""),
    value: s_at(j, "value"),
    attrs,
    children,
  }
}

///|
fn extensions_from(j : Json) -> ExtensionMap {
  let exts : ExtensionMap = Map([])
  match j {
    Object(map) =>
      for ns, inner in map {
        match inner {
          Object(mid) =>
            for name, list in mid {
              match list {
                Array(arr) => {
                  let ext_list = extension_entry(exts, ns, name)
                  for item in arr {
                    ext_list.push(extension_from(item))
                  }
                }
                _ => ()
              }
            }
          _ => ()
        }
      }
    _ => ()
  }
  exts
}

///|
fn atom_links_from(j : Json) -> Array[@atom.Link] {
  let links : Array[@atom.Link] = []
  for l in arr_at(j, "atomLinks") {
    links.push({
      href: s_at_or(l, "href", fallback=""),
      rel: s_at(l, "rel"),
      media_type: s_at(l, "type"),
      hreflang: s_at(l, "hreflang"),
      title: s_at(l, "title"),
      length: s_at(l, "length"),
    })
  }
  links
}

///|
fn item_from(j : Json) -> Item raise RssError {
  let enclosure : Enclosure? = match obj_at(j, "enclosure") {
    Some(e) =>
      Some({
        url: s_at_or(e, "url", fallback=""),
        length: s_at_or(e, "length", fallback=""),
        mime_type: s_at_or(e, "mimeType", fallback=""),
      })
    None => None
  }
  let guid : Guid? = match obj_at(j, "guid") {
    Some(g) =>
      Some({
        is_permalink: match jget(g, "isPermalink") {
          False => false
          _ => true
        },
        value: s_at_or(g, "value", fallback=""),
      })
    None => None
  }
  let source : Source? = match obj_at(j, "source") {
    Some(src) =>
      Some({ url: s_at_or(src, "url", fallback=""), title: s_at(src, "title") })
    None => None
  }
  let itunes : ITunesItemExtension? = match obj_at(j, "itunes") {
    Some(e) =>
      Some({
        author: s_at(e, "author"),
        block: s_at(e, "block"),
        image: s_at(e, "image"),
        duration: s_at(e, "duration"),
        explicit: s_at(e, "explicit"),
        closed_captioned: s_at(e, "closedCaptioned"),
        order: s_at(e, "order"),
        subtitle: s_at(e, "subtitle"),
        summary: s_at(e, "summary"),
        keywords: s_at(e, "keywords"),
        episode: s_at(e, "episode"),
        season: s_at(e, "season"),
        episode_type: s_at(e, "episodeType"),
      })
    None => None
  }
  let dc = dublincore_from(jget(j, "dublincore"))
  let extensions = extensions_from(jget(j, "extensions"))
  let atom_links = atom_links_from(j)
  {
    title: s_at(j, "title"),
    link: s_at(j, "link"),
    description: s_at(j, "description"),
    author: s_at(j, "author"),
    categories: categories_from(arr_at(j, "categories")),
    comments: s_at(j, "comments"),
    enclosure,
    guid,
    pub_date: s_at(j, "pubDate"),
    source,
    content: s_at(j, "content"),
    extensions,
    atom_links,
    itunes_ext: itunes,
    dublin_core_ext: dc,
  }
}

///|
fn dublincore_from(j : Json) -> DublinCoreExtension? {
  fn strs(key : String) -> Array[String] {
    let out : Array[String] = []
    for v in arr_at(j, key) {
      match v {
        String(s) => out.push(s)
        _ => ()
      }
    }
    out
  }

  match j {
    Object(_) =>
      Some({
        contributors: strs("contributors"),
        coverages: strs("coverages"),
        creators: strs("creators"),
        dates: strs("dates"),
        descriptions: strs("descriptions"),
        formats: strs("formats"),
        identifiers: strs("identifiers"),
        languages: strs("languages"),
        publishers: strs("publishers"),
        relations: strs("relations"),
        rights: strs("rights"),
        sources: strs("sources"),
        subjects: strs("subjects"),
        titles: strs("titles"),
        types: strs("types"),
      })
    _ => None
  }
}

///|
fn itunes_channel_from(j : Json) -> ITunesChannelExtension? {
  match obj_at(j, "itunes") {
    None => None
    Some(e) => {
      let categories : Array[ITunesCategory] = []
      for c in arr_at(e, "categories") {
        let subcategory : ITunesCategory? = match obj_at(c, "subcategory") {
          Some(sub) =>
            Some({ text: s_at_or(sub, "text", fallback=""), subcategory: None })
          None => None
        }
        categories.push({ text: s_at_or(c, "text", fallback=""), subcategory })
      }
      let owner : ITunesOwner? = match obj_at(e, "owner") {
        Some(o) => Some({ name: s_at(o, "name"), email: s_at(o, "email") })
        None => None
      }
      Some({
        author: s_at(e, "author"),
        block: s_at(e, "block"),
        categories,
        image: s_at(e, "image"),
        explicit: s_at(e, "explicit"),
        complete: s_at(e, "complete"),
        new_feed_url: s_at(e, "newFeedUrl"),
        owner,
        subtitle: s_at(e, "subtitle"),
        summary: s_at(e, "summary"),
        keywords: s_at(e, "keywords"),
        itunes_type: s_at(e, "type"),
      })
    }
  }
}

///|
fn syndication_from(j : Json) -> SyndicationExtension? {
  match j {
    Object(_) => {
      let period = match s_at_or(j, "period", fallback="daily") {
        "hourly" => Hourly
        "weekly" => Weekly
        "monthly" => Monthly
        "yearly" => Yearly
        _ => Daily
      }
      let frequency = match jget(j, "frequency") {
        Number(n, ..) => {
          let n = n.to_int()
          if n > 0 {
            n
          } else {
            1
          }
        }
        _ => 1
      }
      Some({
        period,
        frequency,
        base: s_at_or(j, "base", fallback="1970-01-01T00:00:00Z"),
      })
    }
    _ => None
  }
}

///|
/// Reconstruct a channel from canonical JSON.
pub fn Channel::from_canon(json : Json) -> Channel raise RssError {
  let cloud : Cloud? = match obj_at(json, "cloud") {
    Some(c) =>
      Some({
        domain: s_at_or(c, "domain", fallback=""),
        port: s_at_or(c, "port", fallback=""),
        path: s_at_or(c, "path", fallback=""),
        register_procedure: s_at_or(c, "registerProcedure", fallback=""),
        protocol: s_at_or(c, "protocol", fallback=""),
      })
    None => None
  }
  let image : Image? = match obj_at(json, "image") {
    Some(i) =>
      Some({
        url: s_at_or(i, "url", fallback=""),
        title: s_at_or(i, "title", fallback=""),
        link: s_at_or(i, "link", fallback=""),
        width: s_at(i, "width"),
        height: s_at(i, "height"),
        description: s_at(i, "description"),
      })
    None => None
  }
  let text_input : TextInput? = match obj_at(json, "textInput") {
    Some(t) =>
      Some({
        title: s_at_or(t, "title", fallback=""),
        description: s_at_or(t, "description", fallback=""),
        name: s_at_or(t, "name", fallback=""),
        link: s_at_or(t, "link", fallback=""),
      })
    None => None
  }
  let extensions = extensions_from(jget(json, "extensions"))
  let atom_links = atom_links_from(json)
  let (itunes_ext, dublin_core_ext, syndication_ext) = (
    itunes_channel_from(json),
    dublincore_from(jget(json, "dublincore")),
    syndication_from(jget(json, "syndication")),
  )
  let items : Array[Item] = []
  for ij in arr_at(json, "items") {
    items.push(item_from(ij))
  }
  {
    title: s_at_or(json, "title", fallback=""),
    link: s_at_or(json, "link", fallback=""),
    description: s_at_or(json, "description", fallback=""),
    language: s_at(json, "language"),
    copyright: s_at(json, "copyright"),
    managing_editor: s_at(json, "managingEditor"),
    webmaster: s_at(json, "webMaster"),
    pub_date: s_at(json, "pubDate"),
    last_build_date: s_at(json, "lastBuildDate"),
    categories: categories_from(arr_at(json, "categories")),
    generator: s_at(json, "generator"),
    docs: s_at(json, "docs"),
    cloud,
    rating: s_at(json, "rating"),
    ttl: s_at(json, "ttl"),
    image,
    text_input,
    skip_hours: str_array(arr_at(json, "skipHours")),
    skip_days: str_array(arr_at(json, "skipDays")),
    items,
    extensions,
    atom_links,
    itunes_ext,
    dublin_core_ext,
    syndication_ext,
    namespaces: namespaces_from(jget(json, "namespaces")),
  }
}

///|
fn str_array(arr : Array[Json]) -> Array[String] {
  let out : Array[String] = []
  for v in arr {
    match v {
      String(s) => out.push(s)
      _ => ()
    }
  }
  out
}

///|
fn namespaces_from(j : Json) -> Map[String, String] {
  let out : Map[String, String] = Map([])
  match j {
    Object(map) =>
      for k, v in map {
        match v {
          String(s) => out[k] = s
          _ => ()
        }
      }
    _ => ()
  }
  out
}