///|
fn write_varuint(out : Array[Byte], value : Int) -> Unit {
  let mut n = value
  if n < 0 {
    n = 0
  }
  let tmp : Array[Int] = []
  if n == 0 {
    tmp.push(0)
  } else {
    while n > 0 {
      tmp.push(n & 0x7F)
      n = n >> 7
    }
  }
  let mut i = tmp.length() - 1
  while i >= 0 {
    if i == 0 {
      push_u8(out, tmp[i])
    } else {
      push_u8(out, tmp[i] | 0x80)
    }
    i -= 1
  }
}

///|
fn write_varint(out : Array[Byte], value : Int) -> Unit {
  let negative = value < 0
  let mut n = if negative { 0 - value } else { value }
  if n == 0 {
    push_u8(out, 0)
  } else {
    let low = n & 0x3F
    n = n >> 6
    if n == 0 {
      if negative {
        push_u8(out, low | 0x40)
      } else {
        push_u8(out, low)
      }
    } else {
      let first = if negative { low | 0xC0 } else { low | 0x80 }
      push_u8(out, first)
      write_varuint(out, n)
    }
  }
}

///|
fn write_type_desc(out : Array[Byte], tid : Int, len : Int) -> Unit {
  if len < 14 {
    push_u8(out, (tid << 4) | len)
  } else {
    push_u8(out, (tid << 4) | 14)
    write_varuint(out, len)
  }
}

///|
fn write_null(out : Array[Byte], tid : Int) -> Unit {
  push_u8(out, (tid << 4) | 15)
}

///|
fn magnitude_bytes(value : BigInt) -> Array[Byte] {
  bytes_to_array(value.digits)
}

///|
fn encode_int(out : Array[Byte], value : BigInt) -> Unit {
  if value.is_zero() {
    write_type_desc(out, TID_POSINT, 0)
  } else {
    let mag = magnitude_bytes(value)
    let tid = if value.negative { TID_NEGINT } else { TID_POSINT }
    write_type_desc(out, tid, mag.length())
    for b in mag {
      out.push(b)
    }
  }
}

///|
fn encode_string_bytes(out : Array[Byte], tid : Int, raw : Bytes) -> Unit {
  let arr = bytes_to_array(raw)
  write_type_desc(out, tid, arr.length())
  for b in arr {
    out.push(b)
  }
}

///|
fn encode_value_body(
  out : Array[Byte],
  table : SymbolTable,
  value : IonValue,
) -> Unit {
  if value.annotations.length() > 0 {
    encode_annotated(out, table, value)
  } else {
    encode_datum(out, table, value.datum)
  }
}

///|
fn encode_annotated(
  out : Array[Byte],
  table : SymbolTable,
  value : IonValue,
) -> Unit {
  let inner : Array[Byte] = []
  let annots : Array[Byte] = []
  for a in value.annotations {
    write_varuint(annots, table.intern(a))
  }
  write_varuint(inner, annots.length())
  for b in annots {
    inner.push(b)
  }
  encode_datum(inner, table, value.datum)
  write_type_desc(out, TID_ANNOT, inner.length())
  for b in inner {
    out.push(b)
  }
}

///|
fn encode_datum(
  out : Array[Byte],
  table : SymbolTable,
  datum : IonDatum,
) -> Unit {
  match datum {
    NullD(t) => {
      let tid = match t {
        NullT => TID_NULL
        BoolT => TID_BOOL
        IntT => TID_POSINT
        FloatT => TID_FLOAT
        DecimalT => TID_DECIMAL
        TimestampT => TID_TIMESTAMP
        SymbolT => TID_SYMBOL
        StringT => TID_STRING
        ClobT => TID_CLOB
        BlobT => TID_BLOB
        ListT => TID_LIST
        SexpT => TID_SEXP
        StructT => TID_STRUCT
      }
      write_null(out, tid)
    }
    BoolD(v) => write_type_desc(out, TID_BOOL, if v { 1 } else { 0 })
    IntD(v) => encode_int(out, v)
    FloatD(v) =>
      if v == 0.0 && !is_nan(v) && !is_inf(v) {
        write_type_desc(out, TID_FLOAT, 0)
      } else {
        let raw = bytes_to_array(f64_to_be_bytes(v))
        write_type_desc(out, TID_FLOAT, 8)
        for b in raw {
          out.push(b)
        }
      }
    DecimalD(v) => encode_decimal(out, v)
    TimestampD(v) => encode_timestamp(out, v)
    SymbolD(v) => encode_symbol(out, table, v)
    StringD(v) => encode_string_bytes(out, TID_STRING, utf8_encode(v))
    ClobD(v) => encode_string_bytes(out, TID_CLOB, v)
    BlobD(v) => encode_string_bytes(out, TID_BLOB, v)
    ListD(v) => encode_seq(out, table, TID_LIST, v)
    SexpD(v) => encode_seq(out, table, TID_SEXP, v)
    StructD(v) => encode_struct(out, table, v)
  }
}

///|
fn encode_symbol(out : Array[Byte], table : SymbolTable, name : String) -> Unit {
  let sid = table.intern(name)
  if sid == 0 {
    write_type_desc(out, TID_SYMBOL, 0)
  } else {
    let tmp : Array[Byte] = []
    let mut n = sid
    if n == 0 {
      tmp.push((0).to_byte())
    } else {
      let stack : Array[Byte] = []
      while n > 0 {
        stack.push((n & 255).to_byte())
        n = n >> 8
      }
      let mut i = stack.length() - 1
      while i >= 0 {
        tmp.push(stack[i])
        i -= 1
      }
    }
    write_type_desc(out, TID_SYMBOL, tmp.length())
    for b in tmp {
      out.push(b)
    }
  }
}

///|
fn encode_decimal(out : Array[Byte], value : IonDecimal) -> Unit {
  if value.coefficient.is_zero() && value.exponent == 0 {
    write_type_desc(out, TID_DECIMAL, 0)
  } else {
    let inner : Array[Byte] = []
    write_varint(inner, value.exponent)
    let mag = magnitude_bytes(value.coefficient)
    if mag.length() == 0 {
      if value.coefficient.negative {
        push_u8(inner, 0x80)
      } else {
        push_u8(inner, 0)
      }
    } else {
      for b in mag {
        inner.push(b)
      }
      if value.coefficient.negative {
        inner[inner.length() - mag.length()] = (inner[inner.length() -
        mag.length()].to_int() |
        0x80).to_byte()
      }
    }
    write_type_desc(out, TID_DECIMAL, inner.length())
    for b in inner {
      out.push(b)
    }
  }
}

///|
fn encode_timestamp(out : Array[Byte], ts : IonTimestamp) -> Unit {
  let inner : Array[Byte] = []
  if ts.offset_unknown {
    write_varint(inner, -0)
  } else {
    write_varint(inner, ts.offset_minutes)
  }
  write_varuint(inner, ts.year)
  match ts.precision {
    Year => ()
    Month => write_varuint(inner, ts.month)
    Day => {
      write_varuint(inner, ts.month)
      write_varuint(inner, ts.day)
    }
    Minute => {
      write_varuint(inner, ts.month)
      write_varuint(inner, ts.day)
      write_varuint(inner, ts.hour)
      write_varuint(inner, ts.minute)
    }
    Second => {
      write_varuint(inner, ts.month)
      write_varuint(inner, ts.day)
      write_varuint(inner, ts.hour)
      write_varuint(inner, ts.minute)
      write_varuint(inner, ts.second)
    }
    Fractional => {
      write_varuint(inner, ts.month)
      write_varuint(inner, ts.day)
      write_varuint(inner, ts.hour)
      write_varuint(inner, ts.minute)
      write_varuint(inner, ts.second)
      write_decimal_payload(inner, ts.fraction)
    }
  }
  write_type_desc(out, TID_TIMESTAMP, inner.length())
  for b in inner {
    out.push(b)
  }
}

///|
fn encode_seq(
  out : Array[Byte],
  table : SymbolTable,
  tid : Int,
  values : Array[IonValue],
) -> Unit {
  let inner : Array[Byte] = []
  for v in values {
    encode_value_body(inner, table, v)
  }
  write_type_desc(out, tid, inner.length())
  for b in inner {
    out.push(b)
  }
}

///|
fn encode_struct(
  out : Array[Byte],
  table : SymbolTable,
  fields : Array[IonField],
) -> Unit {
  let inner : Array[Byte] = []
  for f in fields {
    write_varuint(inner, table.intern(f.name))
    encode_value_body(inner, table, f.value)
  }
  write_type_desc(out, TID_STRUCT, inner.length())
  for b in inner {
    out.push(b)
  }
}

///|
fn encode_local_table(out : Array[Byte], table : SymbolTable) -> Unit {
  let names = table.local_names()
  if names.length() == 0 {
    ()
  } else {
    let symbols : Array[IonValue] = []
    for name in names {
      symbols.push(ion_string(name))
    }
    let st = ion_struct([ion_field("symbols", ion_list(symbols))]).with_annotation(
      "$ion_symbol_table",
    )
    let scratch = symbol_table_new()
    encode_value_body(out, scratch, st)
  }
}

///|
pub fn dumps_binary_all(values : Array[IonValue]) -> Bytes {
  let table = symbol_table_new()
  collect_symbols(table, values)
  let out : Array[Byte] = []
  push_u8(out, 0xE0)
  push_u8(out, 0x01)
  push_u8(out, 0x00)
  push_u8(out, 0xEA)
  encode_local_table(out, table)
  for v in values {
    encode_value_body(out, table, v)
  }
  array_to_bytes(out)
}

///|
pub fn dumps_binary(value : IonValue) -> Bytes {
  dumps_binary_all([value])
}

///|
fn collect_symbols(table : SymbolTable, values : Array[IonValue]) -> Unit {
  for v in values {
    collect_value_symbols(table, v)
  }
}

///|
fn collect_value_symbols(table : SymbolTable, value : IonValue) -> Unit {
  for a in value.annotations {
    let _ = table.intern(a)
  }
  match value.datum {
    SymbolD(s) => {
      let _ = table.intern(s)
    }
    ListD(xs) => collect_symbols(table, xs)
    SexpD(xs) => collect_symbols(table, xs)
    StructD(fs) =>
      for f in fs {
        let _ = table.intern(f.name)
        collect_value_symbols(table, f.value)
      }
    _ => ()
  }
}

///|
fn write_decimal_payload(out : Array[Byte], value : IonDecimal) -> Unit {
  write_varint(out, value.exponent)
  let mag = magnitude_bytes(value.coefficient)
  if mag.length() == 0 {
    push_u8(out, if value.coefficient.negative { 0x80 } else { 0 })
  } else {
    for b in mag {
      out.push(b)
    }
    if value.coefficient.negative {
      let idx = out.length() - mag.length()
      out[idx] = (out[idx].to_int() | 0x80).to_byte()
    }
  }
}