///|
fn main {
  let args = @env.args()
  let result : Result[Unit, Error] = Ok(run(args)) catch { err => Err(err) }
  match result {
    Ok(_) => ()
    Err(err) => {
      println(err.to_string())
      panic()
    }
  }
}

///|
fn run(args : Array[String]) -> Unit raise {
  let idx = if args.length() > 0 { 1 } else { 0 }
  if args.length() - idx < 2 {
    fail("usage: yamlsuite  ")
  }
  let mode = args[idx]
  let payload = args[idx + 1]
  let bytes = @yaml.decode_base64("base64", payload)
  let input = @encoding/utf8.decode(bytes)
  if mode is "tree" {
    let docs = @yaml.parse_yaml_stream_raw(input)
    let json = Json::array(docs.map(yaml_to_tree_json))
    println(json.stringify())
  } else if mode is "json" {
    let docs = @yaml.parse_yaml_stream_raw(input).map(@yaml.resolve_aliases)
    if docs.length() == 1 {
      let json = yaml_to_json_value(docs[0])
      println(json.stringify())
    } else {
      let json = Json::array(docs.map(yaml_to_json_value))
      println(json.stringify())
    }
  } else {
    fail("unknown mode: " + mode)
  }
}

///|
fn yaml_to_tree_json(value : @yaml.Yaml) -> Json {
  match value {
    Null => tree_obj("Null", [])
    Bool(b) => tree_obj("Bool", [("value", Json::boolean(b))])
    Int(i) => tree_obj("Int", [("value", Json::string(i.to_string()))])
    Float(f) => tree_obj("Float", [("value", float_to_json_value(f))])
    String(s) => tree_obj("String", [("value", Json::string(s))])
    Binary(bytes) =>
      tree_obj("String", [("value", Json::string(@yaml.encode_base64(bytes)))])
    Seq(items) =>
      tree_obj("Seq", [("items", Json::array(items.map(yaml_to_tree_json)))])
    Map(items) =>
      tree_obj("Map", [("items", Json::array(items.map(pair_to_json)))])
    Alias(name) => tree_obj("Alias", [("name", Json::string(name))])
    Anchor(name~, value~) =>
      tree_obj("Anchor", [
        ("name", Json::string(name)),
        ("value", yaml_to_tree_json(value)),
      ])
    Tagged(tag~, value~) =>
      tree_obj("Tagged", [
        ("tag", Json::string(tag)),
        ("value", yaml_to_tree_json(value)),
      ])
  }
}

///|
fn pair_to_json(pair : (@yaml.Yaml, @yaml.Yaml)) -> Json {
  let (key, value) = pair
  Json::array([yaml_to_tree_json(key), yaml_to_tree_json(value)])
}

///|
fn tree_obj(kind : String, fields : Array[(String, Json)]) -> Json {
  let obj : Map[String, Json] = Map([])
  obj["type"] = Json::string(kind)
  for field in fields {
    let (key, value) = field
    obj[key] = value
  }
  Json::object(obj)
}

///|
fn float_to_json_value(value : Double) -> Json {
  if value.is_nan() {
    Json::string("nan")
  } else if value.is_pos_inf() {
    Json::string("inf")
  } else if value.is_neg_inf() {
    Json::string("-inf")
  } else {
    Json::number(value)
  }
}

///|
fn yaml_to_json_value(value : @yaml.Yaml) -> Json raise @yaml.ParseError {
  match value {
    Null => Json::null()
    Bool(b) => Json::boolean(b)
    Int(i) => Json::number(Double::from_int(i))
    Float(f) => float_to_json_value(f)
    String(s) => Json::string(s)
    Binary(bytes) => Json::string(@yaml.encode_base64(bytes))
    Seq(items) => Json::array(items.map(yaml_to_json_value))
    Map(items) => map_to_json(items)
    Anchor(name~, value~) => {
      ignore(name)
      yaml_to_json_value(value)
    }
    Tagged(tag~, value~) =>
      if is_set_tag(tag) {
        match value {
          Map(items) => set_map_to_json(items)
          _ => yaml_to_json_value(value)
        }
      } else if is_str_tag(tag) || is_non_specific_tag(tag) {
        Json::string(scalar_to_string(strip_props(value)))
      } else {
        ignore(tag)
        yaml_to_json_value(value)
      }
    Alias(name) =>
      raise @yaml.ParseError::InvalidTagValue(tag="alias", value=name)
  }
}

///|
fn map_to_json(
  items : Array[(@yaml.Yaml, @yaml.Yaml)],
) -> Json raise @yaml.ParseError {
  let obj : Map[String, Json] = Map([])
  for pair in items {
    let (key, value) = pair
    match key_to_string(key) {
      Some(s) => obj[s] = yaml_to_json_value(value)
      None =>
        raise @yaml.ParseError::InvalidTagValue(
          tag="json",
          value="non-string key",
        )
    }
  }
  Json::object(obj)
}

///|
fn set_map_to_json(
  items : Array[(@yaml.Yaml, @yaml.Yaml)],
) -> Json raise @yaml.ParseError {
  let obj : Map[String, Json] = Map([])
  for pair in items {
    let (key, _value) = pair
    match key_to_string(key) {
      Some(s) => obj[s] = Json::null()
      None =>
        raise @yaml.ParseError::InvalidTagValue(
          tag="json",
          value="non-string key",
        )
    }
  }
  Json::object(obj)
}

///|
fn is_set_tag(tag : String) -> Bool {
  tag is "tag:yaml.org,2002:set" ||
  tag is "!!set" ||
  tag is "!"
}

///|
fn is_str_tag(tag : String) -> Bool {
  tag is "tag:yaml.org,2002:str" ||
  tag is "!!str" ||
  tag is "!"
}

///|
fn is_non_specific_tag(tag : String) -> Bool {
  tag is "!"
}

///|
fn scalar_to_string(value : @yaml.Yaml) -> String {
  match value {
    String(s) => s
    Null => ""
    Bool(b) => b.to_string()
    Int(i) => i.to_string()
    Float(f) => f.to_string()
    Binary(bytes) => @yaml.encode_base64(bytes)
    Anchor(name~, value~) => {
      ignore(name)
      scalar_to_string(value)
    }
    Tagged(tag~, value~) => {
      ignore(tag)
      scalar_to_string(value)
    }
    Seq(_) => ""
    Map(_) => ""
    Alias(_) => ""
  }
}

///|
fn key_to_string(value : @yaml.Yaml) -> String? {
  match value {
    String(s) => Some(s)
    Anchor(name~, value~) => {
      ignore(name)
      key_to_string(value)
    }
    Tagged(tag~, value~) =>
      if is_str_tag(tag) || is_non_specific_tag(tag) {
        Some(scalar_to_string(strip_props(value)))
      } else {
        key_to_string(value)
      }
    _ => None
  }
}

///|
fn strip_props(value : @yaml.Yaml) -> @yaml.Yaml {
  match value {
    Anchor(name~, value~) => {
      ignore(name)
      strip_props(value)
    }
    Tagged(tag~, value~) => {
      ignore(tag)
      strip_props(value)
    }
    _ => value
  }
}