///|
/// 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)
  Timestamp(Int64)
  Decimal(Int32, 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
    Timestamp(_) => DataType::Timestamp
    Decimal(scale, _) => DataType::Decimal(scale)
    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)
    Timestamp(v) => timestamp_micros_to_iso(v)
    Decimal(scale, v) => decimal_unscaled_to_string(scale, 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)})"
    Timestamp(v) => logger <+ "Timestamp(\{timestamp_micros_to_iso(v)})"
    Decimal(scale, v) =>
      logger <+ "Decimal(\{scale}, \{decimal_unscaled_to_string(scale, 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 (year, m, d) = date_days_to_civil(days)
  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)}"
}

///|
/// The ISO 8601 rendering of a microsecond count since the epoch — the one
/// rendering a `Scalar::Timestamp` has (value form, `Show` form, CSV / JSON
/// cell form all share it), composed from the date rendering above plus the
/// time-of-day the remainder names. The microsecond fraction is always six
/// digits, so the rendering round-trips (a parser can read back exactly the
/// count it came from); instants before the epoch carry the date's negative
/// year sign, the time-of-day stays in `[00:00:00, 24:00:00)`.
fn timestamp_micros_to_iso(micros : Int64) -> String {
  // Floor division by remainder first: the C-remainder `r` carries the
  // dividend's sign and never wraps, `micros - r` is a multiple of the
  // day bound no larger than the dividend, and the corrections keep both
  // the day quotient and the time-of-day small — so the trick stays
  // equivalent to floor division at every `Int64`, where subtracting the
  // bound before dividing (the direct spelling) would underflow near MIN.
  let r = micros % 86_400_000_000L
  let day = (micros - r) / 86_400_000_000L - (if r < 0L { 1L } else { 0L })
  let rem = r + (if r < 0L { 86_400_000_000L } else { 0L })
  fn pad2(n : Int64) -> String {
    if n < 10 {
      "0\{n}"
    } else {
      n.to_string()
    }
  }

  fn pad6(n : Int64) -> String {
    if n < 10 {
      "00000\{n}"
    } else if n < 100 {
      "0000\{n}"
    } else if n < 1000 {
      "000\{n}"
    } else if n < 10000 {
      "00\{n}"
    } else if n < 100000 {
      "0\{n}"
    } else {
      n.to_string()
    }
  }

  let hour = rem / 3_600_000_000L
  let minute = rem % 3_600_000_000L / 60_000_000L
  let second = rem % 60_000_000L / 1_000_000L
  let frac = rem % 1_000_000L
  "\{date_days_to_iso(day)}T\{pad2(hour)}:\{pad2(minute)}:\{pad2(second)}.\{pad6(frac)}"
}

///|
/// The plain decimal rendering of an unscaled magnitude under a scale — the
/// one rendering a `Scalar::Decimal` has (value form, `Show` form, CSV /
/// JSON cell form all share it): the integer part, a point, and the fraction
/// zero-padded to exactly `scale` digits, so the rendering round-trips. A
/// zero scale renders with no point (`Decimal(0)` is an integer spelling);
/// a negative magnitude keeps one sign, its fraction digits from the
/// magnitude (`-5` under scale 2 renders `-0.05`). The magnitude's digits
/// come straight from `Int64::to_string` — `MIN` included, its digits after
/// the sign are the true magnitude — and the point is placed by counting
/// digits, never by arithmetic on the magnitude: no negation to wrap, no
/// power of ten to overflow, no division to trap. A scale outside the
/// enforced envelope `[0, 18]` can only arrive in a hand-built `pub(all)`
/// scalar; it renders as a named non-value rather than a number.
fn decimal_unscaled_to_string(scale : Int32, unscaled : Int64) -> String {
  if scale < 0 || scale > 18 {
    return ""
  }
  if scale == 0 {
    return unscaled.to_string()
  }
  let raw = unscaled.to_string()
  let (sign, digits) = if unscaled < 0 {
    ("-", raw[1:].to_owned())
  } else {
    ("", raw)
  }
  if digits.length() <= scale {
    let mut frac = ""
    for _ in 0..<(scale - digits.length()) {
      frac = frac + "0"
    }
    "\{sign}0.\{frac}\{digits}"
  } else {
    let split = digits.length() - scale
    "\{sign}\{digits[:split].to_owned()}.\{digits[split:].to_owned()}"
  }
}

///|
/// The Gregorian calendar `(year, month, day)` a day count since the epoch
/// names, via the days-to-civil algorithm — negative counts (dates before
/// 1970) included. The one home of the calendar math: the ISO rendering
/// above composes it, and the engine's `dt_*` expression verbs read their
/// components from it, so the string and the verbs cannot disagree.
/// Total and correct over every `Int64`: the era arithmetic never forms
/// `days + 719468` (which wraps within 719468 of `Int64::MAX`), but splits
/// the count into an era quotient and a non-negative remainder first —
/// the same remainder-first floor division the timestamp rendering uses.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn date_days_to_civil(days : Int64) -> (Int64, Int64, Int64) {
  // `days = q * 146097 + s` with `s` in `[0, 146097)`: the C-remainder
  // never wraps, the quotient's correction shifts it to the floor, and
  // `s + 719468` is the original algorithm's `z` reduced into the current
  // era — always a small non-negative number.
  let s0 = days % 146097L
  let q0 = (days - s0) / 146097L
  let q = if s0 < 0L { q0 - 1L } else { q0 }
  let s = if s0 < 0L { s0 + 146097L } else { s0 }
  let z = s + 719468L
  // Floor division of `z` by the era length, both now non-negative and
  // small — the composite era is the split quotient plus z's own.
  let era = q + z / 146097L
  let doe = z % 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 }
  (year, m, d)
}