///|
const TID_NULL : Int = 0

///|
const TID_BOOL : Int = 1

///|
const TID_POSINT : Int = 2

///|
const TID_NEGINT : Int = 3

///|
const TID_FLOAT : Int = 4

///|
const TID_DECIMAL : Int = 5

///|
const TID_TIMESTAMP : Int = 6

///|
const TID_SYMBOL : Int = 7

///|
const TID_STRING : Int = 8

///|
const TID_CLOB : Int = 9

///|
const TID_BLOB : Int = 10

///|
const TID_LIST : Int = 11

///|
const TID_SEXP : Int = 12

///|
const TID_STRUCT : Int = 13

///|
const TID_ANNOT : Int = 14

///|
priv struct BinCursor {
  data : Array[Byte]
  mut i : Int
  table : SymbolTable
}

///|
fn bin_eof(c : BinCursor) -> Bool {
  c.i >= c.data.length()
}

///|
fn bin_need(c : BinCursor, n : Int) -> Result[Unit, IonError] {
  if c.i + n > c.data.length() {
    Err(UnexpectedEof)
  } else {
    Ok(())
  }
}

///|
fn read_varuint_at(c : BinCursor) -> Result[Int64, IonError] {
  let mut n = 0L
  let mut count = 0
  let mut done = false
  while !done {
    match bin_need(c, 1) {
      Err(e) => return Err(e)
      Ok(_) => ()
    }
    let b = c.data[c.i].to_int()
    c.i += 1
    count += 1
    if count > 10 {
      return Err(InvalidBinary("VarUInt too long"))
    }
    n = (n << 7) | (b & 0x7F).to_int64()
    if (b & 0x80) == 0 {
      done = true
    }
  }
  Ok(n)
}

///|
fn read_varint_at(c : BinCursor) -> Result[Int64, IonError] {
  match bin_need(c, 1) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let b0 = c.data[c.i].to_int()
  c.i += 1
  let negative = (b0 & 0x40) != 0
  let mut n = (b0 & 0x3F).to_int64()
  let mut more = (b0 & 0x80) != 0
  let mut count = 1
  while more {
    match bin_need(c, 1) {
      Err(e) => return Err(e)
      Ok(_) => ()
    }
    let b = c.data[c.i].to_int()
    c.i += 1
    count += 1
    if count > 10 {
      return Err(InvalidBinary("VarInt too long"))
    }
    n = (n << 7) | (b & 0x7F).to_int64()
    more = (b & 0x80) != 0
  }
  if negative {
    Ok(0L - n)
  } else {
    Ok(n)
  }
}

///|
fn read_exact(c : BinCursor, n : Int) -> Result[Bytes, IonError] {
  match bin_need(c, n) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let out = slice_bytes(c.data, c.i, c.i + n)
  c.i += n
  Ok(out)
}

///|
fn parse_type_desc(c : BinCursor) -> Result[(Int, Int, Bool), IonError] {
  match bin_need(c, 1) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let td = c.data[c.i].to_int()
  c.i += 1
  let tid = (td >> 4) & 0x0F
  let low = td & 0x0F
  if low == 15 {
    Ok((tid, 0, true))
  } else if low == 14 {
    match read_varuint_at(c) {
      Ok(n) => Ok((tid, n.to_int(), false))
      Err(e) => Err(e)
    }
  } else {
    Ok((tid, low, false))
  }
}

///|
fn parse_binary_value(c : BinCursor) -> Result[IonValue, IonError] {
  match parse_type_desc(c) {
    Err(e) => Err(e)
    Ok((tid, len, is_null)) =>
      if tid == TID_NULL && is_null {
        Ok(ion_null(NullT))
      } else if tid == TID_NULL {
        c.i += len
        Ok(ion_null(NullT))
      } else if tid == TID_ANNOT {
        parse_annotation_wrapper(c, len)
      } else {
        parse_typed_payload(c, tid, len, is_null)
      }
  }
}

///|
fn parse_annotation_wrapper(
  c : BinCursor,
  len : Int,
) -> Result[IonValue, IonError] {
  let end = c.i + len
  let annot_len = match read_varuint_at(c) {
    Ok(n) => n.to_int()
    Err(e) => return Err(e)
  }
  let annot_end = c.i + annot_len
  if annot_end > end {
    return Err(InvalidBinary("annotation length"))
  }
  let anns : Array[String] = []
  while c.i < annot_end {
    let sid = match read_varuint_at(c) {
      Ok(n) => n.to_int()
      Err(e) => return Err(e)
    }
    match c.table.name_of(sid) {
      Ok(name) => anns.push(name)
      Err(e) => return Err(e)
    }
  }
  match parse_binary_value(c) {
    Ok(v) => {
      if c.i != end {
        return Err(InvalidBinary("annotation payload length"))
      }
      Ok({ annotations: anns, datum: v.datum, })
    }
    Err(e) => Err(e)
  }
}

///|
fn parse_typed_payload(
  c : BinCursor,
  tid : Int,
  len : Int,
  is_null : Bool,
) -> Result[IonValue, IonError] {
  if is_null {
    let t = match tid {
      1 => BoolT
      2 => IntT
      3 => IntT
      4 => FloatT
      5 => DecimalT
      6 => TimestampT
      7 => SymbolT
      8 => StringT
      9 => ClobT
      10 => BlobT
      11 => ListT
      12 => SexpT
      13 => StructT
      _ => NullT
    }
    Ok(ion_null(t))
  } else if tid == TID_BOOL {
    if len == 0 {
      Ok(ion_bool(false))
    } else if len == 1 {
      Ok(ion_bool(true))
    } else {
      Err(InvalidBinary("bool"))
    }
  } else if tid == TID_POSINT || tid == TID_NEGINT {
    if len == 0 {
      Ok(ion_int(0))
    } else {
      match read_exact(c, len) {
        Ok(mag) => Ok(ion_bigint(bigint_from_magnitude(tid == TID_NEGINT, mag)))
        Err(e) => Err(e)
      }
    }
  } else if tid == TID_FLOAT {
    if len == 0 {
      Ok(ion_float(0.0))
    } else if len == 4 {
      match read_exact(c, 4) {
        Ok(b) => {
          let a = bytes_to_array(b)
          Ok(
            ion_float(
              f64_from_be4(
                a[0].to_int(),
                a[1].to_int(),
                a[2].to_int(),
                a[3].to_int(),
              ),
            ),
          )
        }
        Err(e) => Err(e)
      }
    } else if len == 8 {
      match read_exact(c, 8) {
        Ok(b) => {
          let a = bytes_to_array(b)
          Ok(
            ion_float(
              f64_from_be_bytes(
                a[0].to_int(),
                a[1].to_int(),
                a[2].to_int(),
                a[3].to_int(),
                a[4].to_int(),
                a[5].to_int(),
                a[6].to_int(),
                a[7].to_int(),
              ),
            ),
          )
        }
        Err(e) => Err(e)
      }
    } else {
      Err(InvalidBinary("float length"))
    }
  } else if tid == TID_DECIMAL {
    parse_decimal_payload(c, len)
  } else if tid == TID_TIMESTAMP {
    parse_timestamp_payload(c, len)
  } else if tid == TID_SYMBOL {
    if len == 0 {
      Ok(ion_symbol(""))
    } else {
      match read_exact(c, len) {
        Ok(raw) => {
          let sid = bytes_to_int(raw)
          match c.table.name_of(sid) {
            Ok(name) => Ok(ion_symbol(name))
            Err(e) => Err(e)
          }
        }
        Err(e) => Err(e)
      }
    }
  } else if tid == TID_STRING {
    match read_exact(c, len) {
      Ok(raw) =>
        match utf8_decode(raw) {
          Ok(s) => Ok(ion_string(s))
          Err(e) => Err(e)
        }
      Err(e) => Err(e)
    }
  } else if tid == TID_CLOB {
    match read_exact(c, len) {
      Ok(raw) => Ok(ion_clob(raw))
      Err(e) => Err(e)
    }
  } else if tid == TID_BLOB {
    match read_exact(c, len) {
      Ok(raw) => Ok(ion_blob(raw))
      Err(e) => Err(e)
    }
  } else if tid == TID_LIST || tid == TID_SEXP {
    let end = c.i + len
    let items : Array[IonValue] = []
    while c.i < end {
      match parse_binary_value(c) {
        Ok(v) => items.push(v)
        Err(e) => return Err(e)
      }
    }
    if c.i != end {
      Err(InvalidBinary("container length"))
    } else if tid == TID_LIST {
      Ok(ion_list(items))
    } else {
      Ok(ion_sexp(items))
    }
  } else if tid == TID_STRUCT {
    parse_struct_payload(c, len)
  } else {
    Err(InvalidBinary("unknown type " + tid.to_string()))
  }
}

///|
fn bytes_to_int(raw : Bytes) -> Int {
  let arr = bytes_to_array(raw)
  let mut n = 0
  for b in arr {
    n = (n << 8) | b.to_int()
  }
  n
}

///|
fn parse_decimal_payload(
  c : BinCursor,
  len : Int,
) -> Result[IonValue, IonError] {
  if len == 0 {
    Ok(ion_decimal(decimal_from_int(0)))
  } else {
    let end = c.i + len
    let exp = match read_varint_at(c) {
      Ok(v) => v.to_int()
      Err(e) => return Err(e)
    }
    let mag = slice_bytes(c.data, c.i, end)
    c.i = end
    let arr = bytes_to_array(mag)
    let mut negative = false
    if arr.length() > 0 && (arr[0].to_int() & 0x80) != 0 {
      negative = true
      arr[0] = (arr[0].to_int() & 0x7F).to_byte()
    }
    Ok(
      ion_decimal(
        decimal_from_parts(
          bigint_from_magnitude(negative, array_to_bytes(arr)),
          exp,
        ),
      ),
    )
  }
}

///|
fn parse_timestamp_payload(
  c : BinCursor,
  len : Int,
) -> Result[IonValue, IonError] {
  let end = c.i + len
  let off = match read_varint_at(c) {
    Ok(v) => v.to_int()
    Err(e) => return Err(e)
  }
  let offset_unknown = false
  let year = match read_varuint_at(c) {
    Ok(v) => v.to_int()
    Err(e) => return Err(e)
  }
  let mut month = 1
  let mut day = 1
  let mut hour = 0
  let mut minute = 0
  let mut second = 0
  let mut precision = Year
  if c.i < end {
    month = match read_varuint_at(c) {
      Ok(v) => v.to_int()
      Err(e) => return Err(e)
    }
    precision = Month
  }
  if c.i < end {
    day = match read_varuint_at(c) {
      Ok(v) => v.to_int()
      Err(e) => return Err(e)
    }
    precision = Day
  }
  if c.i < end {
    hour = match read_varuint_at(c) {
      Ok(v) => v.to_int()
      Err(e) => return Err(e)
    }
    minute = match read_varuint_at(c) {
      Ok(v) => v.to_int()
      Err(e) => return Err(e)
    }
    precision = Minute
  }
  if c.i < end {
    second = match read_varuint_at(c) {
      Ok(v) => v.to_int()
      Err(e) => return Err(e)
    }
    precision = Second
  }
  let mut frac = decimal_from_int(0)
  if c.i < end {
    let rest = end - c.i
    match parse_decimal_payload(c, rest) {
      Ok(v) =>
        match v.datum {
          DecimalD(d) => {
            frac = d
            precision = Fractional
          }
          _ => ()
        }
      Err(e) => return Err(e)
    }
  }
  match
    timestamp_of(
      year, month, day, hour, minute, second, frac, off, offset_unknown, precision,
    ) {
    Ok(ts) => Ok(ion_timestamp(ts))
    Err(e) => Err(e)
  }
}

///|
fn parse_struct_payload(c : BinCursor, len : Int) -> Result[IonValue, IonError] {
  let end = c.i + len
  let fields : Array[IonField] = []
  while c.i < end {
    let sid = match read_varuint_at(c) {
      Ok(v) => v.to_int()
      Err(e) => return Err(e)
    }
    let name = match c.table.name_of(sid) {
      Ok(v) => v
      Err(e) => return Err(e)
    }
    match parse_binary_value(c) {
      Ok(v) => fields.push(ion_field(name, v))
      Err(e) => return Err(e)
    }
  }
  if c.i != end {
    Err(InvalidBinary("struct length"))
  } else {
    Ok(ion_struct(fields))
  }
}

///|
fn expect_bvm(c : BinCursor) -> Result[Unit, IonError] {
  match bin_need(c, 4) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  if c.data[c.i].to_int() == 0xE0 &&
    c.data[c.i + 1].to_int() == 0x01 &&
    c.data[c.i + 2].to_int() == 0x00 &&
    c.data[c.i + 3].to_int() == 0xEA {
    c.i += 4
    Ok(())
  } else {
    Err(InvalidBinary("missing binary version marker"))
  }
}

///|
pub fn loads_binary(data : Bytes) -> Result[Array[IonValue], IonError] {
  let c : BinCursor = {
    data: bytes_to_array(data),
    i: 0,
    table: symbol_table_new(),
  }
  match expect_bvm(c) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let values : Array[IonValue] = []
  while !bin_eof(c) {
    match parse_binary_value(c) {
      Ok(v) =>
        if v.is_symbol_table() {
          apply_local_table(c.table, v)
        } else {
          values.push(v)
        }
      Err(e) => return Err(e)
    }
  }
  Ok(values)
}

///|
fn apply_local_table(table : SymbolTable, value : IonValue) -> Unit {
  table.reset_locals()
  match value.field("symbols") {
    Some(list) =>
      match list.datum {
        ListD(items) =>
          for item in items {
            match item.datum {
              StringD(s) => table.define_local(s)
              SymbolD(s) => table.define_local(s)
              _ => ()
            }
          }
        _ => ()
      }
    None => ()
  }
}

///|
pub fn load_binary(data : Bytes) -> Result[IonValue, IonError] {
  match loads_binary(data) {
    Ok(vs) => if vs.length() == 0 { Err(EmptyDocument) } else { Ok(vs[0]) }
    Err(e) => Err(e)
  }
}