///|
using @cirru_parser {type Cirru}

///|
pub(all) suberror EdnCommonError {
  EdnCommonError(String)
} derive(Eq, Hash, Show)

///|
pub impl @strconv.FromStr for Edn with from_str(s) {
  Edn::parse(s.to_string())
}

///|
/// parse Cirru code into data
pub fn Edn::parse(s : String) -> Edn raise @strconv.StrConvError {
  let xs = try! Cirru::parse(s)
  if xs.length() == 1 {
    match xs[0] {
      Leaf(s) =>
        raise @strconv.StrConvError("expected expr for data, got leaf: " + s)
      List(_) => extract_cirru_edn(xs[0])
    }
  } else {
    raise @strconv.StrConvError(
      "Expected 1 expr for edn, got length \{xs.length()}",
    )
  }
}

///|
fn extract_cirru_edn(
  node : @cirru_parser.Cirru,
) -> Edn raise @strconv.StrConvError {
  match node {
    Leaf(s) =>
      match s {
        "nil" => Nil
        "true" => Bool(true)
        "false" => Bool(false)
        "" => raise @strconv.StrConvError("empty string is invalid for edn")
        s1 =>
          match s1.get_char(0).unwrap() {
            '\'' => Symbol(try! s1[1:].to_string())
            ':' => Tag(try! s1[1:].to_string())
            '"' | '|' => Str(try! s1[1:].to_string())
            _ =>
              try
                @strconv.parse_double(s1.trim(char_set=" ").to_string())
              catch {
                e =>
                  match e {
                    @strconv.StrConvError(s) =>
                      raise @strconv.StrConvError(
                        "failed to parse number: " + s,
                      )
                  }
              } noraise {
                f => Number(f)
              }
          }
      }
    List(xs) =>
      if xs.is_empty() {
        raise @strconv.StrConvError("empty expr is invalid for edn")
      } else {
        // println("extracting list: " + xs.to_string())
        match xs[0] {
          Leaf(s) =>
            match s {
              "quote" =>
                if xs.length() == 2 {
                  Quote(xs[1])
                } else {
                  raise @strconv.StrConvError("missing edn quote value")
                }
              "do" => {
                let mut ret : Edn? = None
                for x in xs[1:] {
                  if is_comment(x) {
                    continue
                  }
                  match ret {
                    Some(_) =>
                      raise @strconv.StrConvError("multiple values in do")
                    None => ret = Some(extract_cirru_edn(x))
                  }
                }
                match ret {
                  None => raise @strconv.StrConvError("missing edn do value")
                  Some(_v) => ()
                }
                ret.unwrap_or_error(
                  @strconv.StrConvError("missing edn do value"),
                )
              }
              "atom" =>
                if xs.length() == 2 {
                  Atom(extract_cirru_edn(xs[1]))
                } else {
                  raise @strconv.StrConvError("missing edn atom value")
                }
              "::" => {
                let mut tag : Edn? = None
                let extra : Array[Edn] = []
                for x in xs[1:] {
                  if is_comment(x) {
                    continue
                  }
                  match tag {
                    Some(_) => {
                      extra.push(extract_cirru_edn(x))
                      continue
                    }
                    None => tag = Some(extract_cirru_edn(x))
                  }
                }
                match tag {
                  Some(x0) => Tuple({ tag: x0, extra })
                  None =>
                    raise @strconv.StrConvError("missing edn :: fst value")
                }
              }
              "[]" => {
                let ys : Array[Edn] = []
                for x in xs[1:] {
                  if is_comment(x) {
                    continue
                  }
                  try extract_cirru_edn(x) catch {
                    v => raise @strconv.StrConvError(v.to_string())
                  } noraise {
                    v => ys.push(v)
                  }
                }
                List(EdnListView(ys))
              }
              "#{}" => {
                let ys : @hashset.HashSet[Edn] = @hashset.new()
                for x in xs[1:] {
                  if is_comment(x) {
                    continue
                  }
                  try extract_cirru_edn(x) catch {
                    v => raise @strconv.StrConvError(v.to_string())
                  } noraise {
                    v => ys.add(v)
                  }
                }
                Edn::Set(EdnSetView(ys))
              }
              "{}" => {
                let zs : Map[Edn, Edn] = {}
                for x in xs[1:] {
                  if is_comment(x) {
                    continue
                  }
                  match x {
                    Leaf(s) =>
                      raise @strconv.StrConvError(
                        "expected a pair, invalid map entry: " + s,
                      )
                    List(ys) =>
                      if ys.length() == 2 {
                        match
                          (
                            try? extract_cirru_edn(ys[0]),
                            try? extract_cirru_edn(ys[1]),
                          ) {
                          (Ok(k), Ok(v)) => zs.set(k, v)
                          (Err(e), _) =>
                            raise @strconv.StrConvError(
                              "invalid map entry `\{e}` from `\{ys[0]}`",
                            )
                          (Ok(k), Err(e)) =>
                            raise @strconv.StrConvError(
                              "invalid map entry for `\{k}`, got \{e}",
                            )
                        }
                      }
                  }
                }
                Edn::Map(EdnMapView(zs))
              }
              "%{}" =>
                if xs.length() >= 3 {
                  let name = match xs[1] {
                    Leaf(s) =>
                      EdnTag::new(s.trim_start(char_set=":").to_string())
                    List(e) =>
                      raise @strconv.StrConvError(
                        "expected record name in string: " + e.to_string(),
                      )
                  }
                  let entries : Array[(EdnTag, Edn)] = []
                  for x in xs[2:] {
                    if is_comment(x) {
                      continue
                    }
                    match x {
                      Leaf(s) =>
                        raise @strconv.StrConvError(
                          "expected record, invalid record entry: " + s,
                        )
                      List(ys) =>
                        if ys.length() == 2 {
                          match (ys[0], try? extract_cirru_edn(ys[1])) {
                            (Leaf(s), Ok(v)) =>
                              entries.push(
                                (
                                  EdnTag::new(
                                    s.trim_start(char_set=":").to_string(),
                                  ),
                                  v,
                                ),
                              )
                            (Leaf(s), Err(e)) =>
                              raise @strconv.StrConvError(
                                "invalid record value for `\{s}`, got: \{e}",
                              )
                            (List(zs), _) =>
                              raise @strconv.StrConvError(
                                "invalid list as record key: \{zs}",
                              )
                          }
                        } else {
                          raise @strconv.StrConvError(
                            "expected pair of 2: " + ys.to_string(),
                          )
                        }
                    }
                  }
                  if entries.is_empty() {
                    raise @strconv.StrConvError("empty record is invalid")
                  }
                  Edn::Record({ tag: name, extra: entries })
                } else {
                  raise @strconv.StrConvError(
                    "insufficient items for edn record",
                  )
                }
              "buf" => {
                let ys : Array[UInt] = []
                for x in xs[1:] {
                  if is_comment(x) {
                    continue
                  }
                  match x {
                    Leaf(y) =>
                      if y.length() == 2 {
                        try @strconv.parse_int(y) catch {
                          e =>
                            match e {
                              @strconv.StrConvError(s) =>
                                raise @strconv.StrConvError(
                                  "expected length 2 hex string in buffer, got: \{y} \{s}",
                                )
                            }
                        } noraise {
                          b => ys.push(b.reinterpret_as_uint())
                        }
                      } else {
                        raise @strconv.StrConvError(
                          "expected length 2 hex string in buffer, got: \{y}",
                        )
                      }
                    _ =>
                      raise @strconv.StrConvError(
                        "expected hex string in buffer, got: " + x.to_string(),
                      )
                  }
                }
                Edn::Buffer(ys)
              }
              a => raise @strconv.StrConvError("invalid operator for edn: " + a)
            }
          List(a) =>
            raise @strconv.StrConvError(
              "invalid nodes for edn: " + a.to_string(),
            )
        }
      }
  }
}

///|
fn is_comment(node : @cirru_parser.Cirru) -> Bool {
  match node {
    Leaf(_) => false
    List(xs) => xs[0] == @cirru_parser.Cirru::Leaf(";")
  }
}

///|
fn assemble_cirru_node(data : Edn) -> @cirru_parser.Cirru raise EdnCommonError {
  match data {
    Nil => Leaf("nil")
    Bool(v) => Leaf(v.to_string())
    Number(n) => Leaf(n.to_string())
    Symbol(s) => Leaf("'\{s}")
    Tag(s) => Leaf(":" + s.0)
    Str(s) => Leaf("|" + s)
    Quote(v) => List([Leaf("quote"), v])
    List(xs) => {
      let ys : Array[@cirru_parser.Cirru] = []
      ys.push(Leaf("[]"))
      for x in xs {
        ys.push(assemble_cirru_node(x))
      }
      List(ys)
    }
    Set(xs) => {
      let ys : Array[@cirru_parser.Cirru] = []
      ys.push(@cirru_parser.Cirru::Leaf("#{}"))
      let items = xs.0
      // items.sort()
      for x in items {
        ys.push(assemble_cirru_node(x))
      }
      @cirru_parser.Cirru::List(ys)
    }
    Map(xs) => {
      let ys : Array[@cirru_parser.Cirru] = []
      ys.push(@cirru_parser.Cirru::Leaf("{}"))
      let items = Array::from_iter(xs.0.iter())
      items.sort_by(fn(left, right) {
        let (a1, a2) = left
        let (b1, b2) = right
        match
          (a1.is_literal(), b1.is_literal(), a2.is_literal(), b2.is_literal()) {
          (true, true, true, false) => -1
          (true, true, false, true) => 1
          (true, false, _, _) => -1
          (false, true, _, _) => 1
          // _ => a1.cmp(b1)
          _ => a1.compare(b1)
        }
      })
      for pair in items.iter() {
        let (k, v) = pair
        ys.push(List([assemble_cirru_node(k), assemble_cirru_node(v)]))
      }
      List(ys)
    }
    Record({ tag: name, extra: entries }) => {
      let ys : Array[@cirru_parser.Cirru] = []
      ys.push(Leaf("%{}"))
      ys.push(Leaf(":" + name.0))
      let ordered_entries = entries
      ordered_entries.sort_by(fn(left, right) {
        let (_a1, a2) = left
        let (_b1, b2) = right
        match (a2.is_literal(), b2.is_literal()) {
          (true, false) => -1
          (false, true) => 1
          _ => 0
        }
      })
      for entry in ordered_entries {
        let v = entry.1
        ys.push(List([Leaf(":" + entry.0.0), assemble_cirru_node(v)]))
      }
      List(ys)
    }
    Tuple({ tag, extra }) => {
      let ys : Array[@cirru_parser.Cirru] = [
        Leaf("::"),
        assemble_cirru_node(tag),
      ]
      for item in extra {
        ys.push(assemble_cirru_node(item))
      }
      List(ys)
    }
    Buffer(buf) => {
      let ys : Array[@cirru_parser.Cirru] = []
      ys.push(Leaf("buf"))
      for b in buf.0 {
        ys.push(
          Leaf(BigInt::from_int(b.reinterpret_as_int()).to_string(radix=16)),
        )
      }
      List(ys)
    }
    // AnyRef(..) => unreachable!("AnyRef is not serializable"),
    Atom(v) => {
      let ys : Array[@cirru_parser.Cirru] = []
      ys.push(Leaf("atom"))
      ys.push(assemble_cirru_node(v))
      List(ys)
    }
  }
}

///|
/// generate Cirru code from Edn
pub fn Edn::format(
  data : Self,
  use_inline? : Bool = false,
) -> String raise EdnCommonError {
  let ret = match assemble_cirru_node(data) {
    Leaf(s) => try? Cirru::format([List([Leaf("do"), Leaf(s)])], use_inline~)
    List(xs) => try? Cirru::format([List(xs)], use_inline~)
  }
  match ret {
    Ok(s) => s
    Err(e) => raise EdnCommonError(e.to_string())
  }
}