///|
/// Decodes Java Modified UTF-8, also accepting canonical four-byte UTF-8.
/// Unlike lossy decoding, invalid sequences never silently change identifiers.
pub fn decode_hprof_text(data : Bytes) -> String raise ParseError {
  let units : Array[Int] = []
  let mut pos = 0
  while pos < data.length() {
    let offset = pos
    let first = data[pos].to_int()
    pos += 1
    if first < 128 {
      units.push(first)
      continue
    }
    let count = if first >= 0xc0 && first <= 0xdf {
      1
    } else if first >= 0xe0 && first <= 0xef {
      2
    } else if first >= 0xf0 && first <= 0xf4 {
      3
    } else {
      raise Invalid(offset, "invalid UTF-8 leading byte")
    }
    if count > data.length() - pos {
      raise Invalid(offset, "truncated UTF-8")
    }
    let mut value = first &
      (if count == 1 { 31 } else if count == 2 { 15 } else { 7 })
    for _ in 0.. 191 {
        raise Invalid(pos, "invalid UTF-8 continuation")
      }
      pos += 1
      value = (value << 6) | (next & 63)
    }
    if (count == 1 && value < 128 && value != 0) ||
      (count == 2 && value < 2048) ||
      (count == 3 && (value < 65536 || value > 0x10ffff)) {
      raise Invalid(offset, "overlong or out-of-range UTF-8")
    }
    units.push(value)
  }
  let out = StringBuilder()
  let mut i = 0
  while i < units.length() {
    let mut value = units[i]
    i += 1
    if value >= 0xd800 && value <= 0xdbff {
      if i >= units.length() || units[i] < 0xdc00 || units[i] > 0xdfff {
        raise Invalid(i - 1, "unpaired UTF-16 high surrogate")
      }
      value = 0x10000 + ((value - 0xd800) << 10) + units[i] - 0xdc00
      i += 1
    } else if value >= 0xdc00 && value <= 0xdfff {
      raise Invalid(i - 1, "unpaired UTF-16 low surrogate")
    }
    match value.to_char() {
      Some(char) => out.write_char(char)
      None => raise Invalid(i - 1, "invalid Unicode scalar")
    }
  }
  out.to_string()
}

///|
/// Converts a JVM array descriptor to a display name without changing identity.
pub fn display_class_name(name : String) -> String {
  let chars = name.to_array()
  let mut dimensions = 0
  while dimensions < chars.length() && chars[dimensions] == '[' {
    dimensions += 1
  }
  if dimensions == 0 {
    return name.replace_all(old="/", new=".")
  }
  let body = String::from_array(chars[dimensions:])
  let component = match body {
    "Z" => "boolean"
    "B" => "byte"
    "C" => "char"
    "S" => "short"
    "I" => "int"
    "J" => "long"
    "F" => "float"
    "D" => "double"
    _ =>
      if body.has_prefix("L") && body.has_suffix(";") {
        String::from_array(chars[dimensions + 1:chars.length() - 1]).replace_all(
          old="/",
          new=".",
        )
      } else {
        return name
      }
  }
  component + "[]".repeat(dimensions)
}

///|
/// RFC 4180 cell escaping. Newlines inside cells remain quoted data.
pub fn csv_cell(value : String) -> String {
  if value.contains(",") ||
    value.contains("\"") ||
    value.contains("\r") ||
    value.contains("\n") {
    "\"" + value.replace_all(old="\"", new="\"\"") + "\""
  } else {
    value
  }
}

///|
fn encode_unit(units : Array[Byte], value : Int) -> Unit {
  if value > 0 && value < 128 {
    units.push(value.to_byte())
  } else if value < 2048 {
    units.push((0xc0 | (value >> 6)).to_byte())
    units.push((0x80 | (value & 63)).to_byte())
  } else {
    units.push((0xe0 | (value >> 12)).to_byte())
    units.push((0x80 | ((value >> 6) & 63)).to_byte())
    units.push((0x80 | (value & 63)).to_byte())
  }
}

///|
/// Encodes Java Modified UTF-8, including two-byte NUL and surrogate pairs.
pub fn encode_hprof_text(text : String) -> Bytes {
  let units = []
  for char in text {
    let value = char.to_int()
    if value > 65535 {
      encode_unit(units, 0xd800 + ((value - 0x10000) >> 10))
      encode_unit(units, 0xdc00 + ((value - 0x10000) & 1023))
    } else {
      encode_unit(units, value)
    }
  }
  Bytes::from_array(units)
}