///|
/// Helper to encode the initial byte and integer argument (up to 64 bits)
fn encode_type_and_val(buf : Buffer, major : Int, val : UInt64) -> Unit {
  let m = major << 5
  if val < 24 {
    buf.write_byte((m | val.to_int()).to_byte())
  } else if val <= 0xFF {
    buf.write_byte((m | 24).to_byte())
    buf.write_byte(val.to_int().to_byte())
  } else if val <= 0xFFFF {
    buf.write_byte((m | 25).to_byte())
    buf.write_byte((val.to_int() >> 8).to_byte())
    buf.write_byte((val.to_int() & 0xFF).to_byte())
  } else if val <= 0xFFFFFFFF {
    buf.write_byte((m | 26).to_byte())
    buf.write_byte((val.to_int() >> 24).to_byte())
    buf.write_byte(((val.to_int() >> 16) & 0xFF).to_byte())
    buf.write_byte(((val.to_int() >> 8) & 0xFF).to_byte())
    buf.write_byte((val.to_int() & 0xFF).to_byte())
  } else {
    buf.write_byte((m | 27).to_byte())
    let v1 = (val >> 32).to_int()
    let v2 = (val & 0xFFFFFFFF).to_int()
    buf.write_byte((v1 >> 24).to_byte())
    buf.write_byte(((v1 >> 16) & 0xFF).to_byte())
    buf.write_byte(((v1 >> 8) & 0xFF).to_byte())
    buf.write_byte((v1 & 0xFF).to_byte())
    buf.write_byte((v2 >> 24).to_byte())
    buf.write_byte(((v2 >> 16) & 0xFF).to_byte())
    buf.write_byte(((v2 >> 8) & 0xFF).to_byte())
    buf.write_byte((v2 & 0xFF).to_byte())
  }
}

///|
fn write_uint16_be(buf : Buffer, value : UInt64) -> Unit {
  buf.write_byte((value >> 8).to_byte())
  buf.write_byte(value.to_byte())
}

///|
fn write_uint32_be(buf : Buffer, value : UInt) -> Unit {
  buf.write_byte((value >> 24).to_byte())
  buf.write_byte((value >> 16).to_byte())
  buf.write_byte((value >> 8).to_byte())
  buf.write_byte(value.to_byte())
}

///|
fn write_uint64_be(buf : Buffer, value : UInt64) -> Unit {
  buf.write_byte((value >> 56).to_byte())
  buf.write_byte((value >> 48).to_byte())
  buf.write_byte((value >> 40).to_byte())
  buf.write_byte((value >> 32).to_byte())
  buf.write_byte((value >> 24).to_byte())
  buf.write_byte((value >> 16).to_byte())
  buf.write_byte((value >> 8).to_byte())
  buf.write_byte(value.to_byte())
}

///|
fn preferred_half_bits(value : Double) -> UInt64? {
  if value.is_nan() {
    Some(0x7E00UL)
  } else if value.is_pos_inf() {
    Some(0x7C00UL)
  } else if value.is_neg_inf() {
    Some(0xFC00UL)
  } else {
    let sign = value.reinterpret_as_uint64() >> 63 << 15
    let absolute = value.abs()
    if absolute < 0.00006103515625 {
      let scaled = absolute * 16777216.0
      let fraction = scaled.to_uint64()
      if fraction.to_double() == scaled && fraction <= 0x3FFUL {
        Some(sign | fraction)
      } else {
        None
      }
    } else if absolute <= 65504.0 {
      let bits = absolute.reinterpret_as_uint64()
      let exponent = ((bits >> 52) & 0x7FFUL).to_int()
      let fraction = bits & 0xFFFFFFFFFFFFFUL
      let unbiased_exponent = exponent - 1023
      if unbiased_exponent >= -14 &&
        unbiased_exponent <= 15 &&
        (fraction & 0x3FFFFFFFFFFUL) == 0UL {
        let half_exponent = (unbiased_exponent + 15).to_uint64()
        let half_fraction = fraction >> 42
        Some(sign | (half_exponent << 10) | half_fraction)
      } else {
        None
      }
    } else {
      None
    }
  }
}

///|
fn encode_float(buf : Buffer, value : Double) -> Unit {
  match preferred_half_bits(value) {
    Some(bits) => {
      buf.write_byte(0xF9)
      write_uint16_be(buf, bits)
    }
    None => {
      let single = Float::from_double(value)
      if single.to_double() == value {
        buf.write_byte(0xFA)
        write_uint32_be(buf, single.reinterpret_as_uint())
      } else {
        buf.write_byte(0xFB)
        write_uint64_be(buf, value.reinterpret_as_uint64())
      }
    }
  }
}

///|
/// Helper to encode a string to UTF-8
fn encode_utf8(buf : Buffer, s : String) -> Unit {
  for c in s {
    let scalar = c.to_int()
    if scalar <= 0x7F {
      buf.write_byte(scalar.to_byte())
    } else if scalar <= 0x7FF {
      buf.write_byte(((scalar >> 6) | 0xC0).to_byte())
      buf.write_byte(((scalar & 0x3F) | 0x80).to_byte())
    } else if scalar <= 0xFFFF {
      buf.write_byte(((scalar >> 12) | 0xE0).to_byte())
      buf.write_byte((((scalar >> 6) & 0x3F) | 0x80).to_byte())
      buf.write_byte(((scalar & 0x3F) | 0x80).to_byte())
    } else {
      buf.write_byte(((scalar >> 18) | 0xF0).to_byte())
      buf.write_byte((((scalar >> 12) & 0x3F) | 0x80).to_byte())
      buf.write_byte((((scalar >> 6) & 0x3F) | 0x80).to_byte())
      buf.write_byte(((scalar & 0x3F) | 0x80).to_byte())
    }
  }
}

///|
fn bytes_less(lhs : Bytes, rhs : Bytes) -> Bool {
  if lhs.length() != rhs.length() {
    return lhs.length() < rhs.length()
  }
  let shared = lhs.length()
  for i = 0; i < shared; i = i + 1 {
    if lhs[i] != rhs[i] {
      return lhs[i] < rhs[i]
    }
  }
  false
}

///|
fn sort_map_entries(
  entries : Array[(CborValue, CborValue)],
) -> Array[(CborValue, CborValue)] {
  let sorted = []
  for i = 0; i < entries.length(); i = i + 1 {
    sorted.push(entries[i])
  }
  for i = 1; i < sorted.length(); i = i + 1 {
    let current = sorted[i]
    let current_key = encode(current.0)
    let mut j = i
    while j > 0 {
      let previous_key = encode(sorted[j - 1].0)
      if bytes_less(current_key, previous_key) {
        sorted[j] = sorted[j - 1]
        j = j - 1
      } else {
        break
      }
    }
    sorted[j] = current
  }
  sorted
}

///|
/// Encode a CBOR value into a mutable Buffer.
pub fn encode_to_buffer(value : CborValue, buf : Buffer) -> Unit {
  match value {
    Unsigned(i) => encode_type_and_val(buf, 0, i)
    Integer(i) =>
      if i >= 0L {
        encode_type_and_val(buf, 0, i.reinterpret_as_uint64())
      } else {
        encode_type_and_val(buf, 1, (-1L - i).reinterpret_as_uint64())
      }
    Bytes(b) => {
      encode_type_and_val(buf, 2, b.length().to_uint64())
      buf.write_bytes(b)
    }
    Text(s) => {
      let tmp = Buffer()
      encode_utf8(tmp, s)
      let utf8_bytes = tmp.to_bytes()
      encode_type_and_val(buf, 3, utf8_bytes.length().to_uint64())
      buf.write_bytes(utf8_bytes)
    }
    Array(arr) => {
      encode_type_and_val(buf, 4, arr.length().to_uint64())
      for i = 0; i < arr.length(); i = i + 1 {
        encode_to_buffer(arr[i], buf)
      }
    }
    Map(m) => {
      let sorted_entries = sort_map_entries(m)
      encode_type_and_val(buf, 5, sorted_entries.length().to_uint64())
      for i = 0; i < sorted_entries.length(); i = i + 1 {
        encode_to_buffer(sorted_entries[i].0, buf)
        encode_to_buffer(sorted_entries[i].1, buf)
      }
    }
    Tag(t, v) => {
      encode_type_and_val(buf, 6, t)
      encode_to_buffer(v, buf)
    }
    Simple(s) => encode_type_and_val(buf, 7, s.to_uint64())
    Float64(f) => encode_float(buf, f)
  }
}

///|
/// Encode a CBOR value to a byte array.
pub fn encode(value : CborValue) -> Bytes {
  let buf = Buffer()
  encode_to_buffer(value, buf)
  buf.to_bytes()
}