///|
/// A single cell value in a DataFrame.
///
/// `Null` represents a missing value. The `as_*` conversions `raise
/// `TypeMismatch(Expected(expected, got, ""))` on a wrong dtype (`got = Null` for a null
/// cell). Comparisons between cells are column-level — an expression's
/// `eq` / `lt` / `gt` verbs — and their dtype matrix lives on the comparison
/// kernel, not here.
///
/// The `Float` variant carries a 64-bit `Double`; the `Int` variant carries
/// a 64-bit `Int64`. Use `as_float` for explicit access; `Int` is also
/// accepted via numeric promotion to `Double`. The `Date` variant carries
/// whole days since the Unix epoch (`1970-01-01` = day `0`), rendering as an
/// ISO `YYYY-MM-DD` string.
pub(all) enum Scalar {
  Int(Int64)
  Float(Double)
  Bool(Bool)
  String(String)
  Date(Int64)
  Null
} derive(Eq, Debug)

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

///|
pub extend Scalar with Show::{output}

///|
pub extend Scalar with Debug::{to_repr}

///|
/// The `DataType` corresponding to this value's variant.
pub fn Scalar::dtype(self : Scalar) -> DataType {
  match self {
    Int(_) => DataType::Int
    Float(_) => DataType::Float
    Bool(_) => DataType::Bool
    String(_) => DataType::String
    Date(_) => DataType::Date
    Null => DataType::Null
  }
}

///|
/// `true` only for the `Null` (missing-value) variant.
pub fn Scalar::is_null(self : Scalar) -> Bool {
  self is Null
}

///|
/// Value-style rendering: `Int(42)` → `"42"`, `String("ab")` → `"ab"`,
/// `Date(0)` → `"1970-01-01"`, `Null` → `""`. Use the `Show` impl for the
/// variant-form rendering.
pub fn Scalar::to_string(self : Scalar) -> String {
  match self {
    Int(v) => v.to_string()
    Float(v) => v.to_string()
    Bool(v) => v.to_string()
    String(v) => v
    Date(v) => date_days_to_iso(v)
    Null => ""
  }
}

///|
/// The `TypeMismatch(Expected(want, got, ""))` a typed accessor raises when a
/// cell is not `want`: the dtype is read off `got`, and `message()` rebuilds
/// the "expected T, got X" wording (`X = Null` for a null cell, whose dtype is
/// `Null`). Generic in the accessor's return type since it only ever raises.
fn[T] expected_type(got : Scalar, want : DataType) -> T raise DataError {
  raise TypeMismatch(TypeMismatchDetail::Expected(want, got.dtype(), ""))
}

///|
/// `Int` cells return their `Int64`; other concrete variants and `Null`
/// `raise TypeMismatch(Expected(...))`.
pub fn Scalar::as_int(self : Scalar) -> Int64 raise DataError {
  match self {
    Int(v) => v
    other => expected_type(other, DataType::Int)
  }
}

///|
/// `Float` cells return their `Double`; `Int` cells are promoted to
/// `Double`; other concrete variants and `Null` `raise TypeMismatch(Expected(...))`.
pub fn Scalar::as_float(self : Scalar) -> Double raise DataError {
  match self {
    Float(v) => v
    Int(v) => v.to_double()
    other => expected_type(other, DataType::Float)
  }
}

///|
/// `Bool` cells return their value; other concrete variants and `Null`
/// `raise TypeMismatch(Expected(...))`.
pub fn Scalar::as_bool(self : Scalar) -> Bool raise DataError {
  match self {
    Bool(v) => v
    other => expected_type(other, DataType::Bool)
  }
}

///|
/// `String` cells return their value; other concrete variants and `Null`
/// `raise TypeMismatch(Expected(...))`.
pub fn Scalar::as_string(self : Scalar) -> String raise DataError {
  match self {
    String(v) => v
    other => expected_type(other, DataType::String)
  }
}

///|
/// `Show` renders the variant form (`Int(42)`, `String("ab")`, `Null`).
/// For value-style rendering use `to_string()`.
pub impl Show for Scalar with fn output(self, logger) {
  match self {
    Int(v) => logger <+ "Int(\{v})"
    Float(v) => logger <+ "Float(\{v})"
    Bool(v) => logger <+ "Bool(\{v})"
    String(v) => logger <+ "String(\"\{@text.escape_debug(v)}\")"
    Date(v) => logger <+ "Date(\{date_days_to_iso(v)})"
    Null => logger.write_string("Null")
  }
}

///|
/// Render whole days since the Unix epoch as an ISO `YYYY-MM-DD` date —
/// the one rendering a `Scalar::Date` has (value form, `Show` form, CSV /
/// JSON cell form all share it, so the spellings cannot drift). Proleptic
/// Gregorian, via the days-to-civil algorithm: negative day counts (dates
/// before 1970) included. The year is zero-padded to four digits — ISO
/// 8601's `YYYY` basic range is `0000`–`9999`, so year 800 renders
/// `0800-…` and year 0 `0000-…`; a year outside the basic range carries
/// its sign (the expanded-representation form).
fn date_days_to_iso(days : Int64) -> String {
  let z = days + 719468L
  // Floor division, so dates before the era anchor land in the previous
  // 400-year era instead of truncating toward zero.
  let era = if z >= 0 { z / 146097L } else { (z - 146096L) / 146097L }
  let doe = z - era * 146097L
  let yoe = (doe - doe / 1460L + doe / 36524L - doe / 146096L) / 365L
  let y = yoe + era * 400L
  let doy = doe - (365L * yoe + yoe / 4L - yoe / 100L)
  let mp = (5L * doy + 2L) / 153L
  let d = doy - (153L * mp + 2L) / 5L + 1L
  let m = if mp < 10L { mp + 3L } else { mp - 9L }
  let year = if m <= 2L { y + 1L } else { y }
  fn pad2(n : Int64) -> String {
    if n < 10 {
      "0\{n}"
    } else {
      n.to_string()
    }
  }

  // The year pads its magnitude to four digits and carries its own sign —
  // `Int64::MIN` cannot occur here (the algorithm's `y` is bounded by
  // `|days| / 365`), so negating is safe.
  fn pad4(n : Int64) -> String {
    if n < 10 {
      "000\{n}"
    } else if n < 100 {
      "00\{n}"
    } else if n < 1000 {
      "0\{n}"
    } else {
      n.to_string()
    }
  }
  let year_text = if year < 0 { "-" + pad4(-year) } else { pad4(year) }
  "\{year_text}-\{pad2(m)}-\{pad2(d)}"
}