// Copyright 2026 Noverberrain.
// SPDX-License-Identifier: Apache-2.0

///|
/// A wire type, its exact metadata bytes, and the column's nullable flag.
/// This does not supply its SQL name, signedness, charset or decoded row value.
pub struct TableColumn {
  wire_type : Byte
  metadata : Bytes
  nullable : Bool
} derive(Debug, Eq)

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

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

///|
/// A structurally checked optional metadata TLV, preserved without interpretation.
pub struct TableMetadataField {
  field_type : Byte
  value : Bytes
} derive(Debug, Eq)

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

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

///|
/// An immutable MySQL 8.0 TABLE_MAP_EVENT (type 19).
pub struct TableMap {
  header : EventHeader
  table_id : UInt64
  flags : UInt16
  database : String
  table : String
  columns : ReadOnlyArray[TableColumn]
  optional_metadata : ReadOnlyArray[TableMetadataField]
} derive(Debug, Eq)

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

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

///|
/// Decode one complete, checksum-validated table map using an existing format.
/// Only the 8-byte table-map post-header is supported. Unknown wire types fail.
/// Optional metadata is preserved as TLVs, not interpreted as a SQL schema.
pub fn TableMap::parse(
  format : FormatDescription,
  data : BytesView,
  file_offset~ : UInt64,
  max_columns~ : Int32 = 1024,
  max_event_size~ : UInt32 = 16_777_216,
) -> TableMap raise DecodeError {
  guard max_columns > 0 else { raise InvalidColumnLimit(max_columns~) }
  let header = EventHeader::parse(data, file_offset~, max_event_size~)
  guard header.event_type == 19 else {
    raise UnexpectedEventType(
      file_offset~,
      expected=19,
      actual=header.event_type,
    )
  }
  let body = format.payload(data, file_offset~, max_event_size~)
  guard format.post_header_lengths.get(18) is Some(8) else {
    raise InvalidTableMap(
      file_offset~,
      body_offset=0,
      reason="unsupported table-map post-header length",
    )
  }
  let reader = MapReader::{ data: body, base: 0, file_offset, position: 0, }
  let table_id = reader.uint_le(6)
  let flags = read_u16_le(reader.take(2), 0)
  let database = reader.name()
  let table = reader.name()
  let count = reader.packed_length()
  guard count > 0 else { raise reader.invalid("table has no columns") }
  guard count <= max_columns.reinterpret_as_uint().to_uint64() else {
    raise ColumnLimitExceeded(file_offset~, count~, max_columns~)
  }
  let column_count = count.to_int()
  let types = reader.take(column_count)
  let metadata = reader.sized_field()
  let metadata_start = reader.position - metadata.length()
  let null_bytes = column_count / 8 +
    (if column_count % 8 == 0 { 0 } else { 1 })
  let nullable = reader.take(null_bytes)
  let metadata_reader = MapReader::{
    data: metadata,
    base: metadata_start,
    file_offset,
    position: 0,
  }
  let columns = []
  for i in 0..> (i % 8)) & 1) != 0,
    })
  }
  guard metadata_reader.remaining() == 0 else {
    raise metadata_reader.invalid("unused column metadata bytes")
  }
  let optional = []
  while reader.remaining() > 0 {
    guard optional.length() < 256 else {
      raise reader.invalid("optional metadata exceeds 256 TLV fields")
    }
    let field_type = reader.byte()
    let value = reader.sized_field().to_owned()
    optional.push(TableMetadataField::{ field_type, value, })
  }
  {
    header,
    table_id,
    flags,
    database,
    table,
    columns: ReadOnlyArray::from_array(columns),
    optional_metadata: ReadOnlyArray::from_array(optional),
  }
}

///|
// Layout widths follow MySQL 8.0's read_field_metadata. Internal NEWDATE,
// VAR_STRING and typed-array representations are deliberately not guessed.
fn column_metadata_length(
  wire_type : Byte,
  file_offset~ : UInt64,
  column_index~ : Int32,
) -> Int32 raise DecodeError {
  match wire_type {
    0..<=3 | 6..<=13 => 0
    4 | 5 | 17..<=19 | 245 | 249..<=252 | 255 => 1
    15 | 16 | 246..<=248 | 254 => 2
    _ => raise UnsupportedColumnType(file_offset~, column_index~, wire_type~)
  }
}