///|
/// Canonical JSON interchange between this library and the Rust oracle.
///
/// The field names and shapes match `oracle/src/main.rs`; both sides must
/// stay in sync for differential tests to be meaningful.

///|
fn jstr(s : String) -> Json {
  Json::string(s)
}

///|
fn jopt(s : String?) -> Json {
  match s {
    Some(v) => Json::string(v)
    None => Json::null()
  }
}

///|
fn jstrs(values : Array[String]) -> Json {
  let out : Array[Json] = []
  for v in values {
    out.push(Json::string(v))
  }
  Json::array(out)
}

///|
fn jcategory(c : Category) -> Json {
  let obj : Map[String, Json] = Map([])
  obj["domain"] = jopt(c.domain)
  obj["name"] = jstr(c.name)
  Json::object(obj)
}

///|
fn jcategories(cs : Array[Category]) -> Json {
  let out : Array[Json] = []
  for c in cs {
    out.push(jcategory(c))
  }
  Json::array(out)
}

///|
fn jextensions(exts : ExtensionMap) -> Json {
  let outer : Map[String, Json] = Map([])
  for ns, inner in exts {
    let mid : Map[String, Json] = Map([])
    for name, list in inner {
      let arr : Array[Json] = []
      for ext in list {
        arr.push(jextension(ext))
      }
      mid[name] = Json::array(arr)
    }
    outer[ns] = Json::object(mid)
  }
  Json::object(outer)
}

///|
fn jextension(ext : Extension) -> Json {
  let attrs : Map[String, Json] = Map([])
  for k, v in ext.attrs {
    attrs[k] = jstr(v)
  }
  let children : Map[String, Json] = Map([])
  for name, list in ext.children {
    let arr : Array[Json] = []
    for child in list {
      arr.push(jextension(child))
    }
    children[name] = Json::array(arr)
  }
  let obj : Map[String, Json] = Map([])
  obj["name"] = jstr(ext.name)
  obj["value"] = jopt(ext.value)
  obj["attrs"] = Json::object(attrs)
  obj["children"] = Json::object(children)
  Json::object(obj)
}

///|
fn jatom_links(links : Array[@atom.Link]) -> Json {
  let arr : Array[Json] = []
  for link in links {
    let obj : Map[String, Json] = Map([])
    obj["href"] = jstr(link.href)
    obj["rel"] = jopt(link.rel)
    obj["type"] = jopt(link.media_type)
    obj["hreflang"] = jopt(link.hreflang)
    obj["title"] = jopt(link.title)
    obj["length"] = jopt(link.length)
    arr.push(Json::object(obj))
  }
  Json::array(arr)
}

///|
fn jitunes_channel(ext : ITunesChannelExtension?) -> Json {
  match ext {
    None => Json::null()
    Some(ext) => {
      let obj : Map[String, Json] = Map([])
      obj["author"] = jopt(ext.author)
      obj["block"] = jopt(ext.block)
      let cats : Array[Json] = []
      for category in ext.categories {
        let cobj : Map[String, Json] = Map([])
        cobj["text"] = jstr(category.text)
        cobj["subcategory"] = match category.subcategory {
          Some(sub) => {
            let sobj : Map[String, Json] = Map([])
            sobj["text"] = jstr(sub.text)
            sobj["subcategory"] = Json::null()
            Json::object(sobj)
          }
          None => Json::null()
        }
        cats.push(Json::object(cobj))
      }
      obj["categories"] = Json::array(cats)
      obj["image"] = jopt(ext.image)
      obj["explicit"] = jopt(ext.explicit)
      obj["complete"] = jopt(ext.complete)
      obj["newFeedUrl"] = jopt(ext.new_feed_url)
      obj["owner"] = match ext.owner {
        Some(owner) => {
          let oobj : Map[String, Json] = Map([])
          oobj["name"] = jopt(owner.name)
          oobj["email"] = jopt(owner.email)
          Json::object(oobj)
        }
        None => Json::null()
      }
      obj["subtitle"] = jopt(ext.subtitle)
      obj["summary"] = jopt(ext.summary)
      obj["keywords"] = jopt(ext.keywords)
      obj["type"] = jopt(ext.itunes_type)
      Json::object(obj)
    }
  }
}

///|
fn jitunes_item(ext : ITunesItemExtension?) -> Json {
  match ext {
    None => Json::null()
    Some(ext) => {
      let obj : Map[String, Json] = Map([])
      obj["author"] = jopt(ext.author)
      obj["block"] = jopt(ext.block)
      obj["image"] = jopt(ext.image)
      obj["duration"] = jopt(ext.duration)
      obj["explicit"] = jopt(ext.explicit)
      obj["closedCaptioned"] = jopt(ext.closed_captioned)
      obj["order"] = jopt(ext.order)
      obj["subtitle"] = jopt(ext.subtitle)
      obj["summary"] = jopt(ext.summary)
      obj["keywords"] = jopt(ext.keywords)
      obj["episode"] = jopt(ext.episode)
      obj["season"] = jopt(ext.season)
      obj["episodeType"] = jopt(ext.episode_type)
      Json::object(obj)
    }
  }
}

///|
fn jdublincore(ext : DublinCoreExtension?) -> Json {
  match ext {
    None => Json::null()
    Some(ext) => {
      let obj : Map[String, Json] = Map([])
      obj["contributors"] = jstrs(ext.contributors)
      obj["coverages"] = jstrs(ext.coverages)
      obj["creators"] = jstrs(ext.creators)
      obj["dates"] = jstrs(ext.dates)
      obj["descriptions"] = jstrs(ext.descriptions)
      obj["formats"] = jstrs(ext.formats)
      obj["identifiers"] = jstrs(ext.identifiers)
      obj["languages"] = jstrs(ext.languages)
      obj["publishers"] = jstrs(ext.publishers)
      obj["relations"] = jstrs(ext.relations)
      obj["rights"] = jstrs(ext.rights)
      obj["sources"] = jstrs(ext.sources)
      obj["subjects"] = jstrs(ext.subjects)
      obj["titles"] = jstrs(ext.titles)
      obj["types"] = jstrs(ext.types)
      Json::object(obj)
    }
  }
}

///|
fn jsyndication(ext : SyndicationExtension?) -> Json {
  match ext {
    None => Json::null()
    Some(ext) => {
      let obj : Map[String, Json] = Map([])
      obj["period"] = jstr(ext.period.to_string())
      obj["frequency"] = Json::number(ext.frequency.to_double())
      obj["base"] = jstr(ext.base)
      Json::object(obj)
    }
  }
}

///|
fn jitem(item : Item) -> Json {
  let obj : Map[String, Json] = Map([])
  obj["title"] = jopt(item.title)
  obj["link"] = jopt(item.link)
  obj["description"] = jopt(item.description)
  obj["author"] = jopt(item.author)
  obj["categories"] = jcategories(item.categories)
  obj["comments"] = jopt(item.comments)
  obj["enclosure"] = match item.enclosure {
    Some(e) => {
      let eobj : Map[String, Json] = Map([])
      eobj["url"] = jstr(e.url)
      eobj["length"] = jstr(e.length)
      eobj["mimeType"] = jstr(e.mime_type)
      Json::object(eobj)
    }
    None => Json::null()
  }
  obj["guid"] = match item.guid {
    Some(g) => {
      let gobj : Map[String, Json] = Map([])
      gobj["isPermalink"] = Json::boolean(g.is_permalink)
      gobj["value"] = jstr(g.value)
      Json::object(gobj)
    }
    None => Json::null()
  }
  obj["pubDate"] = jopt(item.pub_date)
  obj["source"] = match item.source {
    Some(s) => {
      let sobj : Map[String, Json] = Map([])
      sobj["url"] = jstr(s.url)
      sobj["title"] = jopt(s.title)
      Json::object(sobj)
    }
    None => Json::null()
  }
  obj["content"] = jopt(item.content)
  obj["extensions"] = jextensions(item.extensions)
  obj["atomLinks"] = jatom_links(item.atom_links)
  obj["itunes"] = jitunes_item(item.itunes_ext)
  obj["dublincore"] = jdublincore(item.dublin_core_ext)
  Json::object(obj)
}

///|
/// Convert a parsed channel to the canonical JSON form.
pub fn Channel::to_canon(self : Channel) -> Json {
  let obj : Map[String, Json] = Map([])
  obj["title"] = jstr(self.title)
  obj["link"] = jstr(self.link)
  obj["description"] = jstr(self.description)
  obj["language"] = jopt(self.language)
  obj["copyright"] = jopt(self.copyright)
  obj["managingEditor"] = jopt(self.managing_editor)
  obj["webMaster"] = jopt(self.webmaster)
  obj["pubDate"] = jopt(self.pub_date)
  obj["lastBuildDate"] = jopt(self.last_build_date)
  obj["categories"] = jcategories(self.categories)
  obj["generator"] = jopt(self.generator)
  obj["docs"] = jopt(self.docs)
  obj["cloud"] = match self.cloud {
    Some(c) => {
      let cobj : Map[String, Json] = Map([])
      cobj["domain"] = jstr(c.domain)
      cobj["port"] = jstr(c.port)
      cobj["path"] = jstr(c.path)
      cobj["registerProcedure"] = jstr(c.register_procedure)
      cobj["protocol"] = jstr(c.protocol)
      Json::object(cobj)
    }
    None => Json::null()
  }
  obj["rating"] = jopt(self.rating)
  obj["ttl"] = jopt(self.ttl)
  obj["image"] = match self.image {
    Some(i) => {
      let iobj : Map[String, Json] = Map([])
      iobj["url"] = jstr(i.url)
      iobj["title"] = jstr(i.title)
      iobj["link"] = jstr(i.link)
      iobj["width"] = jopt(i.width)
      iobj["height"] = jopt(i.height)
      iobj["description"] = jopt(i.description)
      Json::object(iobj)
    }
    None => Json::null()
  }
  obj["textInput"] = match self.text_input {
    Some(t) => {
      let tobj : Map[String, Json] = Map([])
      tobj["title"] = jstr(t.title)
      tobj["description"] = jstr(t.description)
      tobj["name"] = jstr(t.name)
      tobj["link"] = jstr(t.link)
      Json::object(tobj)
    }
    None => Json::null()
  }
  obj["skipHours"] = jstrs(self.skip_hours)
  obj["skipDays"] = jstrs(self.skip_days)
  let items : Array[Json] = []
  for item in self.items {
    items.push(jitem(item))
  }
  obj["items"] = Json::array(items)
  obj["extensions"] = jextensions(self.extensions)
  obj["atomLinks"] = jatom_links(self.atom_links)
  obj["itunes"] = jitunes_channel(self.itunes_ext)
  obj["dublincore"] = jdublincore(self.dublin_core_ext)
  obj["syndication"] = jsyndication(self.syndication_ext)
  let nss : Map[String, Json] = Map([])
  for k, v in self.namespaces {
    nss[k] = jstr(v)
  }
  obj["namespaces"] = Json::object(nss)
  Json::object(obj)
}

///|
/// Recursively sort JSON object keys so that output is byte-identical to
/// serde_json's BTreeMap-based serialisation used by the oracle.
pub fn sort_json_keys(j : Json) -> Json {
  match j {
    Object(map) => {
      let out : Map[String, Json] = Map([])
      for k in sorted_keys(map) {
        out[k] = sort_json_keys(map[k])
      }
      Json::object(out)
    }
    Array(arr) => {
      let out : Array[Json] = []
      for item in arr {
        out.push(sort_json_keys(item))
      }
      Json::array(out)
    }
    other => other
  }
}

///|
/// Compact, deterministic serialisation: object keys are emitted in sorted
/// order (matching serde_json over BTreeMaps).
pub fn canonical_stringify(j : Json) -> String {
  let sb = StringBuilder()
  write_canonical(sb, j)
  sb.to_string()
}

///|
fn write_canonical(sb : StringBuilder, j : Json) -> Unit {
  match j {
    Null => sb.write_string("null")
    True => sb.write_string("true")
    False => sb.write_string("false")
    String(s) => write_json_string(sb, s)
    Number(_, repr~) =>
      match repr {
        Some(r) => sb.write_string(r)
        None => sb.write_string(j.stringify())
      }
    Array(arr) => {
      sb.write_string("[")
      for i, item in arr {
        if i > 0 {
          sb.write_string(",")
        }
        write_canonical(sb, item)
      }
      sb.write_string("]")
    }
    Object(map) => {
      sb.write_string("{")
      for i, k in sorted_keys(map) {
        if i > 0 {
          sb.write_string(",")
        }
        write_json_string(sb, k)
        sb.write_string(":")
        write_canonical(sb, map[k])
      }
      sb.write_string("}")
    }
  }
}

///|
fn write_json_string(sb : StringBuilder, s : String) -> Unit {
  sb.write_string("\"")
  for c in s {
    match c {
      '"' => sb.write_string("\\\"")
      '\\' => sb.write_string("\\\\")
      '\n' => sb.write_string("\\n")
      '\r' => sb.write_string("\\r")
      '\t' => sb.write_string("\\t")
      c =>
        if c.to_int() < 0x20 {
          let hex = "0123456789abcdef"
          let v = c.to_int()
          sb.write_string(
            "\\u\{hex[(v >> 12) & 15]}\{hex[(v >> 8) & 15]}\{hex[(v >> 4) & 15]}\{hex[v & 15]}",
          )
        } else {
          sb.write_char(c)
        }
    }
  }
  sb.write_string("\"")
}