///|
/// Text encodings allowed by the SQLite database header.
pub(all) enum TextEncoding {
  Utf8
  Utf16Le
  Utf16Be
  Unspecified
} derive(Eq, Debug)

///|
pub fn TextEncoding::from_header(
  value : UInt64,
) -> TextEncoding raise ParseError {
  match value {
    0UL => Unspecified
    1UL => Utf8
    2UL => Utf16Le
    3UL => Utf16Be
    _ => raise InvalidValue(56, "text encoding", value.to_string())
  }
}

///|
pub fn TextEncoding::label(self : TextEncoding) -> String {
  match self {
    Utf8 => "UTF-8"
    Utf16Le => "UTF-16LE"
    Utf16Be => "UTF-16BE"
    Unspecified => "unspecified"
  }
}

///|
/// Parsed representation of SQLite's 100-byte database header.
pub(all) struct DatabaseHeader {
  page_size : Int
  write_version : Int
  read_version : Int
  reserved_bytes : Int
  max_embedded_payload_fraction : Int
  min_embedded_payload_fraction : Int
  leaf_payload_fraction : Int
  file_change_counter : UInt64
  database_size_pages : UInt64
  first_freelist_trunk_page : UInt64
  total_freelist_pages : UInt64
  schema_cookie : UInt64
  schema_format : UInt64
  default_page_cache_size : Int64
  largest_root_btree_page : UInt64
  text_encoding : TextEncoding
  user_version : UInt64
  incremental_vacuum : UInt64
  application_id : UInt64
  version_valid_for : UInt64
  sqlite_version_number : UInt64
} derive(Eq, Debug)

///|
pub fn DatabaseHeader::usable_page_size(self : DatabaseHeader) -> Int {
  self.page_size - self.reserved_bytes
}

///|
pub fn DatabaseHeader::is_wal_mode(self : DatabaseHeader) -> Bool {
  self.write_version == 2 || self.read_version == 2
}

///|
pub fn DatabaseHeader::effective_database_pages(
  self : DatabaseHeader,
  file_length : Int,
) -> UInt64 {
  if self.database_size_pages > 0UL {
    self.database_size_pages
  } else if self.page_size > 0 {
    (file_length / self.page_size).to_uint64()
  } else {
    0UL
  }
}

///|
fn valid_page_size(size : Int) -> Bool {
  size >= 512 && size <= 65536 && (size & (size - 1)) == 0
}

///|
fn validate_format_version(
  value : Int,
  offset : Int,
  field : String,
) -> Unit raise ParseError {
  guard value == 1 || value == 2 else {
    raise InvalidValue(offset, field, value.to_string())
  }
}

///|
fn require_header_byte(
  actual : Int,
  expected : Int,
  offset : Int,
  field : String,
) -> Unit raise ParseError {
  guard actual == expected else {
    raise InvalidValue(offset, field, actual.to_string())
  }
}

///|
/// Parse and strictly validate the SQLite database header.
pub fn parse_database_header(data : Bytes) -> DatabaseHeader raise ParseError {
  let reader = BinaryReader::named(data, "SQLite database header")
  guard data.length() >= 100 else {
    raise UnexpectedEnd(0, 100, data.length(), "SQLite database header")
  }
  let magic = reader.read_bytes(16)
  let expected = b"SQLite format 3\x00"
  guard bytes_equal(magic, expected) else {
    raise InvalidMagic(bytes_hex(expected), bytes_hex(magic))
  }
  let raw_page_size = reader.read_u16_be()
  let page_size = if raw_page_size == 1 { 65536 } else { raw_page_size }
  guard valid_page_size(page_size) else {
    raise InvalidValue(16, "page size", raw_page_size.to_string())
  }
  let write_version = reader.read_u8().to_int()
  validate_format_version(write_version, 18, "file format write version")
  let read_version = reader.read_u8().to_int()
  validate_format_version(read_version, 19, "file format read version")
  let reserved_bytes = reader.read_u8().to_int()
  guard reserved_bytes < page_size else {
    raise InvalidValue(20, "reserved bytes", reserved_bytes.to_string())
  }
  let max_embedded_payload_fraction = reader.read_u8().to_int()
  require_header_byte(
    max_embedded_payload_fraction, 64, 21, "maximum embedded payload fraction",
  )
  let min_embedded_payload_fraction = reader.read_u8().to_int()
  require_header_byte(
    min_embedded_payload_fraction, 32, 22, "minimum embedded payload fraction",
  )
  let leaf_payload_fraction = reader.read_u8().to_int()
  require_header_byte(leaf_payload_fraction, 32, 23, "leaf payload fraction")
  let file_change_counter = reader.read_u32_be()
  let database_size_pages = reader.read_u32_be()
  let first_freelist_trunk_page = reader.read_u32_be()
  let total_freelist_pages = reader.read_u32_be()
  let schema_cookie = reader.read_u32_be()
  let schema_format = reader.read_u32_be()
  // Zero is valid for a database that has no schema yet. This matters when a
  // WAL contains the transaction that creates the first schema objects.
  guard schema_format <= 4UL else {
    raise InvalidValue(44, "schema format", schema_format.to_string())
  }
  let default_page_cache_size = reader.read_i32_be()
  let largest_root_btree_page = reader.read_u32_be()
  let text_encoding = TextEncoding::from_header(reader.read_u32_be())
  let user_version = reader.read_u32_be()
  let incremental_vacuum = reader.read_u32_be()
  let application_id = reader.read_u32_be()
  reader.skip(20)
  let version_valid_for = reader.read_u32_be()
  let sqlite_version_number = reader.read_u32_be()
  {
    page_size,
    write_version,
    read_version,
    reserved_bytes,
    max_embedded_payload_fraction,
    min_embedded_payload_fraction,
    leaf_payload_fraction,
    file_change_counter,
    database_size_pages,
    first_freelist_trunk_page,
    total_freelist_pages,
    schema_cookie,
    schema_format,
    default_page_cache_size,
    largest_root_btree_page,
    text_encoding,
    user_version,
    incremental_vacuum,
    application_id,
    version_valid_for,
    sqlite_version_number,
  }
}

///|
/// Return non-fatal relationships that cannot be decided from one field alone.
pub fn validate_database_header(
  header : DatabaseHeader,
  file_length : Int,
) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  if file_length % header.page_size != 0 {
    diagnostics.push(
      Diagnostic::warning(
        "HEADER_FILE_LENGTH",
        "file length is not an exact multiple of the declared page size",
        offset=16,
      ),
    )
  }
  let physical_pages = if header.page_size > 0 {
    (file_length / header.page_size).to_uint64()
  } else {
    0UL
  }
  if header.database_size_pages > physical_pages {
    diagnostics.push(
      Diagnostic::error(
        "HEADER_DATABASE_SIZE",
        "header database size exceeds pages available in the file",
        offset=28,
      ),
    )
  }
  if header.first_freelist_trunk_page == 0UL &&
    header.total_freelist_pages != 0UL {
    diagnostics.push(
      Diagnostic::warning(
        "HEADER_FREELIST_ROOT",
        "freelist count is non-zero but the first trunk page is zero",
        offset=32,
      ),
    )
  }
  if header.first_freelist_trunk_page > physical_pages &&
    header.first_freelist_trunk_page != 0UL {
    diagnostics.push(
      Diagnostic::error(
        "HEADER_FREELIST_RANGE",
        "first freelist trunk page is outside the database file",
        offset=32,
        page_number=header.first_freelist_trunk_page,
      ),
    )
  }
  if header.incremental_vacuum != 0UL && header.largest_root_btree_page == 0UL {
    diagnostics.push(
      Diagnostic::warning(
        "HEADER_INCREMENTAL_VACUUM",
        "incremental-vacuum flag is set without an auto-vacuum root page",
        offset=64,
      ),
    )
  }
  if header.version_valid_for != header.file_change_counter {
    diagnostics.push(
      Diagnostic::info(
        "HEADER_VERSION_VALIDITY",
        "database size field may be stale because version-valid-for differs from the change counter",
        offset=92,
      ),
    )
  }
  diagnostics
}