///|
/// Binary KV3 numbers retain their storage type and exact integer/IEEE bits.
/// Tag numbers follow Valve KV3: int64=3, uint64=4, double=5, int32=11,
/// uint32=12, float=19, int16=20, uint16=21, int8=22, uint8=23.
pub(all) enum KV3BinaryValue {
  BNull
  BBool(Bool)
  BNumber(Int32, UInt64)
  BString(String)
  BBlob(Bytes)
  BArray(Array[KV3BinaryValue])
  BObject(Array[(String, KV3BinaryValue)])
  BFlag(String, KV3BinaryValue)
} derive(Eq, Debug)

///|
pub extend KV3BinaryValue with Eq::{equal, not_equal}

///|
pub extend KV3BinaryValue with @debug.Debug::{to_repr}

///|
pub(all) struct KV3BinaryDocument {
  version : Int32
  compression : Int32
  format_id : String
  root : KV3BinaryValue
} derive(Eq, Debug)

///|
pub extend KV3BinaryDocument with Eq::{equal, not_equal}

///|
pub extend KV3BinaryDocument with @debug.Debug::{to_repr}

///|
fn kv3_binary_width(tag : Int32) -> Int32 raise BinaryError {
  match tag {
    3 | 4 | 5 => 8
    11 | 12 | 19 => 4
    20 | 21 => 2
    22 | 23 => 1
    _ => raise InvalidBinary(0, "Unknown KV3 number storage type")
  }
}

///|
fn kv3_binary_float(n : Double) -> String {
  if n.reinterpret_as_uint64() == 0x8000000000000000UL {
    return "-0.0"
  }
  if n.is_nan() {
    "nan"
  } else if n.is_inf() {
    if n < 0.0 {
      "-inf"
    } else {
      "inf"
    }
  } else {
    let text = n.to_string()
    if text.contains(".") || text.contains("e") || text.contains("E") {
      text
    } else {
      text + ".0"
    }
  }
}

///|
fn KV3BinaryValue::semantic(
  self : KV3BinaryValue,
  depth : Int32,
  budget : Ref[Int32],
) -> KV3Value raise {
  budget.val += 1
  if depth > 128 || budget.val > 100000 {
    raise InvalidBinary(0, "Binary KV3 tree exceeds budget")
  }
  match self {
    BNull => KV3Null
    BBool(b) => KV3Bool(b)
    BNumber(tag, bits) => {
      let width = kv3_binary_width(tag)
      if width < 8 && bits >= 1UL << (width * 8) {
        raise InvalidBinary(0, "Number bits exceed storage width")
      }
      match tag {
        5 => KV3Float(kv3_binary_float(bits.reinterpret_as_double()))
        19 =>
          KV3Float(
            kv3_binary_float(
              Float::reinterpret_from_uint(bits.to_uint()).to_double(),
            ),
          )
        3 => KV3Integer(bits.reinterpret_as_int64().to_string())
        11 => KV3Integer(bits.to_uint().reinterpret_as_int().to_string())
        20 | 22 => {
          let sign = 1 << (width * 8 - 1)
          let n = bits.to_int()
          KV3Integer((if n >= sign { n - sign * 2 } else { n }).to_string())
        }
        _ => KV3Integer(bits.to_string())
      }
    }
    BString(s) => KV3String(s)
    BBlob(b) => KV3Blob(b)
    BArray(values) => KV3Array(values.map(v => v.semantic(depth + 1, budget)))
    BObject(entries) => {
      let keys : Map[String, Bool] = Map([])
      KV3Object(
        entries.map(pair => {
          let (key, value) = pair
          if keys.contains(key) {
            raise InvalidBinary(0, "Duplicate binary KV3 field")
          }
          keys[key] = true
          KV3Entry::{ key, value: value.semantic(depth + 1, budget), }
        }),
      )
    }
    BFlag(flag, value) => {
      if !kv3_flag(flag) || value is BFlag(_, _) {
        raise InvalidBinary(0, "Invalid binary KV3 flag")
      }
      KV3Flag(flag, value.semantic(depth + 1, budget))
    }
  }
}

///|
pub fn KV3BinaryDocument::document(
  self : KV3BinaryDocument,
) -> KV3Document raise {
  let header = KV3Header::{
    format_name: if self.format_id == generic_kv3_header().format_id {
      "generic"
    } else {
      "binary"
    },
    format_id: self.format_id,
  }
  if !kv3_guid(header.format_id) {
    raise InvalidBinary(0, "Invalid KV3 format GUID")
  }
  { header: Some(header), root: self.root.semantic(0, { val: 0, }), }
}

///|
fn kv3_binary_from_value(
  value : KV3Value,
  depth : Int32,
) -> KV3BinaryValue raise {
  if depth > 128 {
    raise InvalidBinary(0, "KV3 conversion exceeds depth")
  }
  match value {
    KV3Null => BNull
    KV3Bool(b) => BBool(b)
    KV3String(s) => BString(s)
    KV3Blob(b) => BBlob(b)
    KV3Integer(s) =>
      if s.has_prefix("-") {
        BNumber(3, @strconv.parse_int64(s[:]).reinterpret_as_uint64())
      } else {
        let digits = if s.has_prefix("+") { s[1:].to_owned() } else { s }
        BNumber(4, @strconv.parse_uint64(digits[:]))
      }
    KV3Float(s) => {
      let n = match s.to_lower() {
        "nan" => 0x7ff8000000000000UL.reinterpret_as_double()
        "inf" | "+inf" => 0x7ff0000000000000UL.reinterpret_as_double()
        "-inf" => 0xfff0000000000000UL.reinterpret_as_double()
        _ => @strconv.parse_double(s[:])
      }
      BNumber(5, n.reinterpret_as_uint64())
    }
    KV3Array(values) =>
      BArray(values.map(v => kv3_binary_from_value(v, depth + 1)))
    KV3Object(entries) =>
      BObject(
        entries.map(e => (e.key, kv3_binary_from_value(e.value, depth + 1))),
      )
    KV3Flag(flag, value) => BFlag(flag, kv3_binary_from_value(value, depth + 1))
  }
}

///|
pub fn kv3_binary_from_document(
  document : KV3Document,
) -> KV3BinaryDocument raise {
  stringify_kv3(document) |> ignore
  {
    version: 5,
    compression: 0,
    format_id: document.header.unwrap_or(generic_kv3_header()).format_id,
    root: kv3_binary_from_value(document.root, 0),
  }
}

///|
fn kv3_guid_bytes(id : String) -> Bytes raise BinaryError {
  if !kv3_guid(id) {
    raise InvalidBinary(0, "Invalid KV3 GUID")
  }
  let digits = id.replace_all(old="-", new="").to_array()
  let raw : Array[Byte] = []
  for i in 0..<16 {
    raw.push(
      (kv3_hex(digits[i * 2]).unwrap() * 16 +
      kv3_hex(digits[i * 2 + 1]).unwrap()).to_byte(),
    )
  }
  Bytes::from_array(
    [
      raw[3],
      raw[2],
      raw[1],
      raw[0],
      raw[5],
      raw[4],
      raw[7],
      raw[6],
      ..raw[8:].to_owned(),
    ],
  )
}

///|
fn kv3_bytes_guid(raw : Bytes) -> String raise BinaryError {
  if raw.length() != 16 {
    raise InvalidBinary(0, "Truncated GUID")
  }
  let chars = "0123456789abcdef".to_array()
  let out = StringBuilder()
  let order = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15]
  for i in 0..<16 {
    if [4, 6, 8, 10].contains(i) {
      out.write_char('-')
    }
    let n = raw[order[i]].to_int()
    out.write_char(chars[n / 16])
    out.write_char(chars[n % 16])
  }
  out.to_string()
}