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

///|
/// Calendar components retain MySQL zero dates; timestamps retain wire seconds.
/// TIME is a signed duration, not a time of day. No timezone is inferred.
pub(all) enum TemporalValue {
  Year(Int32)
  Date(year~ : Int32, month~ : Int32, day~ : Int32)
  DateTime(
    year~ : Int32,
    month~ : Int32,
    day~ : Int32,
    hour~ : Int32,
    minute~ : Int32,
    second~ : Int32,
    microsecond~ : Int32
  )
  Time(
    negative~ : Bool,
    hour~ : Int32,
    minute~ : Int32,
    second~ : Int32,
    microsecond~ : Int32
  )
  Timestamp(seconds~ : UInt32, microsecond~ : Int32)
} derive(Debug, Eq)

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

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

///|
pub struct DecodedTemporal {
  value : TemporalValue
  fractional_precision : Int32
  bytes_read : Int32
} derive(Debug, Eq)

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

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

///|
/// Decode present, non-NULL DATE, YEAR, TIMESTAMP2, DATETIME2 or TIME2.
/// Validate component ranges and fractional precision 0..6. Preserve zero and
/// partial-zero dates and SQL-mode-dependent dates such as February 31.
/// No calendar normalization, SQL-mode check or timestamp timezone conversion.
/// `file_offset` is the field's first byte; `column_index` is zero-based.
/// The caller handles row presence/NULL bitmaps; trailing bytes are untouched.
pub fn TableColumn::decode_temporal(
  self : TableColumn,
  data : BytesView,
  file_offset~ : UInt64,
  column_index~ : Int32,
) -> DecodedTemporal raise DecodeError {
  let base = match self.wire_type {
    10 | 19 => 3
    13 => 1
    17 => 4
    18 => 5
    wire_type =>
      raise UnsupportedTemporalColumn(file_offset~, column_index~, wire_type~)
  }
  let modern = self.wire_type >= 17
  guard self.metadata.length() == (if modern { 1 } else { 0 }) else {
    raise InvalidTemporalMetadata(
      file_offset~,
      column_index~,
      reason="unexpected metadata length",
    )
  }
  let fsp = if modern { self.metadata[0].to_int() } else { 0 }
  guard fsp <= 6 else {
    raise InvalidTemporalMetadata(
      file_offset~,
      column_index~,
      reason="fractional precision must be 0..6",
    )
  }
  let fraction_width = (fsp + 1) / 2
  let bytes_read = base + fraction_width
  guard data.length() >= bytes_read else {
    raise TruncatedColumnValue(
      file_offset~,
      column_index~,
      expected=bytes_read,
      actual=data.length(),
    )
  }
  let value = match self.wire_type {
    13 => {
      let year = data[0].to_int()
      Year(if year == 0 { 0 } else { year + 1900 })
    }
    10 => {
      let raw = data[0].to_int() |
        (data[1].to_int() << 8) |
        (data[2].to_int() << 16)
      let year = raw >> 9
      let month = (raw >> 5) & 15
      let day = raw & 31
      temporal_check(
        year <= 9999 && month <= 12,
        file_offset~,
        column_index~,
        reason="date component out of range",
      )
      Date(year~, month~, day~)
    }
    18 => {
      let raw = temporal_uint_be(data[:5])
      temporal_check(
        raw >= 0x8000000000UL,
        file_offset~,
        column_index~,
        reason="negative DATETIME2 encoding",
      )
      let packed = raw - 0x8000000000UL
      let ym = (packed >> 22).to_int()
      let year = ym / 13
      let month = ym % 13
      let day = ((packed >> 17) & 31).to_int()
      let hour = ((packed >> 12) & 31).to_int()
      let minute = ((packed >> 6) & 63).to_int()
      let second = (packed & 63).to_int()
      temporal_check(
        year <= 9999 && hour <= 23 && minute <= 59 && second <= 59,
        file_offset~,
        column_index~,
        reason="datetime component out of range",
      )
      let microsecond = temporal_fraction(
        temporal_uint_be(data[5:bytes_read]).to_int(),
        fraction_width,
        fsp,
        file_offset~,
        column_index~,
      )
      DateTime(year~, month~, day~, hour~, minute~, second~, microsecond~)
    }
    17 => {
      let seconds = temporal_uint_be(data[:4]).to_uint()
      let microsecond = temporal_fraction(
        temporal_uint_be(data[4:bytes_read]).to_int(),
        fraction_width,
        fsp,
        file_offset~,
        column_index~,
      )
      Timestamp(seconds~, microsecond~)
    }
    _ => {
      // One biased fixed-point integer also handles negative subsecond values.
      let offset = 0x800000UL << (8 * fraction_width)
      let packed = temporal_uint_be(data[:bytes_read]).reinterpret_as_int64() -
        offset.reinterpret_as_int64()
      let negative = packed < 0
      let magnitude = if negative { -packed } else { packed }
      let integer = (magnitude >> (8 * fraction_width)).to_int()
      let raw_fraction = (magnitude & ((1L << (8 * fraction_width)) - 1)).to_int()
      let microsecond = temporal_fraction(
        raw_fraction,
        fraction_width,
        fsp,
        file_offset~,
        column_index~,
      )
      let hour = integer >> 12
      let minute = (integer >> 6) & 63
      let second = integer & 63
      temporal_check(
        hour <= 838 &&
        minute <= 59 &&
        second <= 59 &&
        !(hour == 838 && minute == 59 && second == 59 && microsecond != 0),
        file_offset~,
        column_index~,
        reason="time duration out of range",
      )
      Time(negative~, hour~, minute~, second~, microsecond~)
    }
  }
  { value, fractional_precision: fsp, bytes_read, }
}

///|
fn temporal_uint_be(data : BytesView) -> UInt64 {
  let mut value = 0UL
  for byte in data {
    value = (value << 8) | byte.to_uint().to_uint64()
  }
  value
}

///|
fn temporal_check(
  valid : Bool,
  file_offset~ : UInt64,
  column_index~ : Int32,
  reason~ : String,
) -> Unit raise DecodeError {
  guard valid else {
    raise InvalidTemporalValue(file_offset~, column_index~, reason~)
  }
}

///|
fn temporal_fraction(
  raw : Int32,
  width : Int32,
  fsp : Int32,
  file_offset~ : UInt64,
  column_index~ : Int32,
) -> Int32 raise DecodeError {
  let microsecond = raw *
    (if width == 1 { 10000 } else if width == 2 { 100 } else { 1 })
  let mut resolution = 1
  for _ in 0..<(6 - fsp) {
    resolution *= 10
  }
  temporal_check(
    microsecond <= 999999 && microsecond % resolution == 0,
    file_offset~,
    column_index~,
    reason="invalid fractional seconds or nonzero precision padding",
  )
  microsecond
}