///|
priv struct TagContext {
  handles : Map[String, String]
  coerce_binary : Bool
  mut seen_yaml : Bool
}

///|
fn TagContext::new() -> TagContext {
  let handles : Map[String, String] = {}
  handles["!!"] = "tag:yaml.org,2002:"
  handles["!"] = "!"
  { handles, coerce_binary: true, seen_yaml: false, }
}

///|
fn TagContext::new_raw() -> TagContext {
  let handles : Map[String, String] = {}
  handles["!!"] = "tag:yaml.org,2002:"
  handles["!"] = "!"
  { handles, coerce_binary: false, seen_yaml: false, }
}

///|
fn TagContext::set_handle(
  self : TagContext,
  handle : String,
  prefix : String,
) -> Unit {
  self.handles[handle] = prefix
}

///|
fn TagContext::expand(
  self : TagContext,
  full_tag : String,
) -> String raise ParseError {
  if !full_tag.has_prefix("!") {
    return full_tag
  }
  match full_tag.rev_find("!") {
    None => decode_tag_uri(full_tag)
    Some(idx) => {
      let handle = full_tag.view(end_offset=idx + 1).to_string()
      let suffix = full_tag.view(start_offset=idx + 1).to_string()
      match self.handles.get(handle) {
        Some(prefix) => decode_tag_uri(prefix + suffix)
        None => raise ParseError::InvalidTagValue(tag="tag", value=full_tag)
      }
    }
  }
}

///|
fn decode_tag_uri(text : String) -> String {
  let chars = text.to_array()
  let sb = StringBuilder::new()
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if c is '%' && i + 2 < chars.length() {
      let hi = tag_hex_value(chars[i + 1])
      let lo = tag_hex_value(chars[i + 2])
      if hi >= 0 && lo >= 0 {
        let code = hi * 16 + lo
        match code.to_char() {
          Some(decoded) => sb.write_char(decoded)
          None => {
            sb.write_char(c)
            sb.write_char(chars[i + 1])
            sb.write_char(chars[i + 2])
          }
        }
        i = i + 3
        continue
      }
    }
    sb.write_char(c)
    i = i + 1
  }
  sb.to_string()
}

///|
fn tag_hex_value(c : Char) -> Int32 {
  if c >= '0' && c <= '9' {
    c.to_int() - '0'.to_int()
  } else if c >= 'A' && c <= 'F' {
    c.to_int() - 'A'.to_int() + 10
  } else if c >= 'a' && c <= 'f' {
    c.to_int() - 'a'.to_int() + 10
  } else {
    -1
  }
}