///|
/// One decoded value in a SQLite record body.
pub(all) enum RecordValue {
  Null
  Integer(Int64)
  Real(Double)
  Text(String)
  Blob(Bytes)
} derive(Eq, Debug)

///|
pub fn RecordValue::kind(self : RecordValue) -> String {
  match self {
    Null => "null"
    Integer(_) => "integer"
    Real(_) => "real"
    Text(_) => "text"
    Blob(_) => "blob"
  }
}

///|
pub fn RecordValue::display(self : RecordValue) -> String {
  match self {
    Null => "NULL"
    Integer(value) => value.to_string()
    Real(value) => value.to_string()
    Text(value) => value
    Blob(value) => "x'" + bytes_hex(value) + "'"
  }
}

///|
pub(all) struct RecordColumn {
  index : Int
  serial_type : UInt64
  body_offset : Int
  byte_length : Int
  value : RecordValue
} derive(Eq, Debug)

///|
pub(all) struct SqliteRecord {
  header_size : Int
  body_offset : Int
  columns : Array[RecordColumn]
  trailing_bytes : Int
} derive(Eq, Debug)

///|
pub fn SqliteRecord::column_count(self : SqliteRecord) -> Int {
  self.columns.length()
}

///|
pub fn SqliteRecord::value(self : SqliteRecord, index : Int) -> RecordValue? {
  if index < 0 || index >= self.columns.length() {
    None
  } else {
    Some(self.columns[index].value)
  }
}

///|
/// Return the number of body bytes represented by a serial type.
pub fn serial_type_length(serial_type : UInt64) -> Int raise ParseError {
  match serial_type {
    0UL | 8UL | 9UL => 0
    1UL => 1
    2UL => 2
    3UL => 3
    4UL => 4
    5UL => 6
    6UL | 7UL => 8
    10UL | 11UL =>
      raise InvalidValue(0, "record serial type", serial_type.to_string())
    value => {
      guard value <= 0x10000000UL else {
        raise IntegerOverflow("record serial type length")
      }
      if (value & 1UL) == 0UL {
        ((value - 12UL) / 2UL).to_int()
      } else {
        ((value - 13UL) / 2UL).to_int()
      }
    }
  }
}

///|
fn decode_record_text(bytes : Bytes, encoding : TextEncoding) -> String {
  match encoding {
    Utf8 | Unspecified => @utf8.decode_lossy(bytes[:])
    Utf16Le => @utf16.decode_lossy(bytes[:], endianness=@utf16.Little)
    Utf16Be => @utf16.decode_lossy(bytes[:], endianness=@utf16.Big)
  }
}

///|
fn decode_record_value(
  serial_type : UInt64,
  reader : BinaryReader,
  encoding : TextEncoding,
) -> RecordValue raise ParseError {
  match serial_type {
    0UL => Null
    1UL => Integer(Int64::from_int(reader.read_i8()))
    2UL => Integer(Int64::from_int(reader.read_i16_be()))
    3UL => Integer(Int64::from_int(reader.read_i24_be()))
    4UL => Integer(reader.read_i32_be())
    5UL => Integer(reader.read_int_be(6))
    6UL => Integer(reader.read_i64_be())
    7UL => Real(reader.read_f64_be())
    8UL => Integer(0L)
    9UL => Integer(1L)
    10UL | 11UL =>
      raise InvalidValue(
        reader.absolute_position(),
        "record serial type",
        serial_type.to_string(),
      )
    value => {
      let length = serial_type_length(value)
      let bytes = reader.read_bytes(length)
      if (value & 1UL) == 0UL {
        Blob(bytes)
      } else {
        Text(decode_record_text(bytes, encoding))
      }
    }
  }
}

///|
/// Parse a complete SQLite record payload.
pub fn parse_record(
  payload : Bytes,
  encoding : TextEncoding,
) -> SqliteRecord raise ParseError {
  let prefix = BinaryReader::named(payload, "SQLite record")
  let header_varint = prefix.read_varint()
  guard header_varint.value <= 0x7fffffffUL else {
    raise IntegerOverflow("record header size")
  }
  let header_size = header_varint.value.to_int()
  guard header_size >= header_varint.length && header_size <= payload.length() else {
    raise InvalidValue(0, "record header size", header_size.to_string())
  }
  let serial_reader = BinaryReader::range(
    payload,
    header_varint.length,
    header_size - header_varint.length,
    "SQLite record serial types",
  )
  let serial_types : Array[UInt64] = []
  while !serial_reader.is_empty() {
    serial_types.push(serial_reader.read_varint().value)
  }
  let body = BinaryReader::range(
    payload,
    header_size,
    payload.length() - header_size,
    "SQLite record body",
  )
  let columns : Array[RecordColumn] = []
  for index = 0; index < serial_types.length(); index = index + 1 {
    let serial_type = serial_types[index]
    let byte_length = serial_type_length(serial_type)
    let body_offset = body.relative_position()
    let value = decode_record_value(serial_type, body, encoding)
    columns.push({ index, serial_type, body_offset, byte_length, value })
  }
  {
    header_size,
    body_offset: header_size,
    columns,
    trailing_bytes: body.remaining(),
  }
}

///|
pub fn validate_record(record : SqliteRecord) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  if record.trailing_bytes > 0 {
    diagnostics.push(
      Diagnostic::warning(
        "RECORD_TRAILING_BYTES",
        "record body has " +
        record.trailing_bytes.to_string() +
        " unused byte(s)",
      ),
    )
  }
  for column in record.columns {
    if column.serial_type == 10UL || column.serial_type == 11UL {
      diagnostics.push(
        Diagnostic::error(
          "RECORD_RESERVED_SERIAL_TYPE",
          "record uses reserved serial type " + column.serial_type.to_string(),
          offset=record.body_offset + column.body_offset,
        ),
      )
    }
  }
  diagnostics
}