// Python `datetime.date` / `datetime.datetime` and `dateutil.relativedelta` semantics
// used by the simplifier.

///|
/// A Python `date` (when `is_datetime` is false) or `datetime`.
priv struct PyDT {
  y : Int
  m : Int
  d : Int
  is_datetime : Bool
  h : Int
  mi : Int
  s : Int
  us : Int
  /// UTC offset in seconds for aware datetimes
  tz : Int?
}

///|
fn make_date(y : Int, m : Int, d : Int) -> PyDT {
  { y, m, d, is_datetime: false, h: 0, mi: 0, s: 0, us: 0, tz: None }
}

///|
/// Days since 0001-01-01 (Python `toordinal()`).
fn days_from_civil(y : Int, m : Int, d : Int) -> Int {
  let y2 = if m <= 2 { y - 1 } else { y }
  let era = (if y2 >= 0 { y2 } else { y2 - 399 }) / 400
  let yoe = y2 - era * 400
  let mp = (m + 9) % 12
  let doy = (153 * mp + 2) / 5 + d - 1
  let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
  // 719163 = days from 0001-01-01 to 1970-01-01 plus one
  era * 146097 + doe - 719468 + 719163
}

///|
fn civil_from_days(n : Int) -> (Int, Int, Int) {
  let z = n - 719163 + 719468
  let era = (if z >= 0 { z } else { z - 146096 }) / 146097
  let doe = z - era * 146097
  let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
  let y = yoe + era * 400
  let doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
  let mp = (5 * doy + 2) / 153
  let d = doy - (153 * mp + 2) / 5 + 1
  let m = if mp < 10 { mp + 3 } else { mp - 9 }
  (if m <= 2 { y + 1 } else { y }, m, d)
}

///|
fn PyDT::ordinal(self : PyDT) -> Int {
  days_from_civil(self.y, self.m, self.d)
}

///|
/// Monday is 0.
fn PyDT::weekday(self : PyDT) -> Int {
  (self.ordinal() + 6) % 7
}

///|
fn check_year(y : Int) -> Unit raise @core.SqlglotError {
  if y < 1 || y > 9999 {
    raise @core.ValueError("year \{y} is out of range")
  }
}

///|
/// Adds days/seconds/microseconds (Python `+ timedelta(...)`).
fn PyDT::add_timedelta(
  self : PyDT,
  days : Int,
  seconds : Int64,
  microseconds : Int64,
) -> PyDT raise @core.SqlglotError {
  self.add_timedelta_big(
    big(days),
    @bigint.BigInt::from_int64(seconds),
    @bigint.BigInt::from_int64(microseconds),
  )
}

///|
/// `OverflowError` (reported as a `ValueError` whose message names the Python type).
fn overflow_error(msg : String) -> @core.SqlglotError {
  @core.ValueError("OverflowError: \{msg}")
}

///|
/// Python's `_PyLong_AsInt`: the value as a C int, or `OverflowError`.
fn c_int(v : @bigint.BigInt) -> Int raise @core.SqlglotError {
  if v < big(-2147483647 - 1) || v > big(2147483647) {
    raise overflow_error("Python int too large to convert to C int")
  }
  v.to_int()
}

///|
/// Python floor division and modulo of big integers by a positive `base`.
fn big_floor_divmod(
  v : @bigint.BigInt,
  base : @bigint.BigInt,
) -> (@bigint.BigInt, @bigint.BigInt) {
  let q = v / base
  let r = v - q * base
  if r < big(0) {
    (q - big(1), r + base)
  } else {
    (q, r)
  }
}

///|
/// `self + timedelta(days=days, seconds=seconds, microseconds=microseconds)` with
/// Python's range checks: the normalized day count must fit a C int and have magnitude
/// <= 999999999, and the result must lie in years 1..9999 (`OverflowError` otherwise).
fn PyDT::add_timedelta_big(
  self : PyDT,
  days : @bigint.BigInt,
  seconds : @bigint.BigInt,
  microseconds : @bigint.BigInt,
) -> PyDT raise @core.SqlglotError {
  // timedelta(...) normalization
  let us_per_day = @bigint.BigInt::from_int64(86400000000L)
  let delta_us = (days * big(86400) + seconds) * big(1000000) + microseconds
  let (delta_days_big, delta_rem) = big_floor_divmod(delta_us, us_per_day)
  let delta_days = c_int(delta_days_big)
  if delta_days < -999999999 || delta_days > 999999999 {
    raise overflow_error("days=\{delta_days}; must have magnitude <= 999999999")
  }
  let total_us = (
      self.h.to_int64() * 3600L + self.mi.to_int64() * 60L + self.s.to_int64()
    ) *
    1000000L +
    self.us.to_int64() +
    delta_rem.to_int64()
  let day_us = 86400000000L
  let mut extra_days = total_us / day_us
  let mut rem = total_us % day_us
  if rem < 0L {
    rem = rem + day_us
    extra_days = extra_days - 1L
  }
  // date.max.toordinal() == 3652059
  let ord = self.ordinal().to_int64() + delta_days.to_int64() + extra_days
  if ord < 1L || ord > 3652059L {
    raise overflow_error("date value out of range")
  }
  let (y, m, d) = civil_from_days(ord.to_int())
  let secs = (rem / 1000000L).to_int()
  {
    ..self,
    y,
    m,
    d,
    h: secs / 3600,
    mi: secs % 3600 / 60,
    s: secs % 60,
    us: (rem % 1000000L).to_int(),
  }
}

///|
fn PyDT::to_datetime(self : PyDT) -> PyDT {
  if self.is_datetime {
    self
  } else {
    { ..self, is_datetime: true }
  }
}

///|
fn PyDT::to_date(self : PyDT) -> PyDT {
  make_date(self.y, self.m, self.d)
}

///|
fn pad2(n : Int) -> String {
  if n < 10 {
    "0\{n}"
  } else {
    n.to_string()
  }
}

///|
fn pad_n(n : Int, width : Int) -> String {
  let s = n.to_string()
  if s.length() >= width {
    s
  } else {
    "0".repeat(width - s.length()) + s
  }
}

///|
/// Python `str(date)` / `str(datetime)`.
fn PyDT::to_py_string(self : PyDT) -> String {
  let date = "\{pad_n(self.y, 4)}-\{pad2(self.m)}-\{pad2(self.d)}"
  if !self.is_datetime {
    return date
  }
  let mut out = date + " \{pad2(self.h)}:\{pad2(self.mi)}:\{pad2(self.s)}"
  if self.us != 0 {
    out += "." + pad_n(self.us, 6)
  }
  match self.tz {
    Some(off) => {
      let sign = if off < 0 { "-" } else { "+" }
      let a = if off < 0 { -off } else { off }
      out += "\{sign}\{pad2(a / 3600)}:\{pad2(a % 3600 / 60)}"
      if a % 60 != 0 {
        out += ":\{pad2(a % 60)}"
      }
    }
    None => ()
  }
  out
}

///|
/// Microseconds since epoch-ish (for ordering), adjusted by the UTC offset.
fn PyDT::instant(self : PyDT) -> Int64 {
  let secs = self.ordinal().to_int64() * 86400L +
    self.h.to_int64() * 3600L +
    self.mi.to_int64() * 60L +
    self.s.to_int64() -
    self.tz.unwrap_or(0).to_int64()
  secs * 1000000L + self.us.to_int64()
}

///|
/// Python `a == b` for dates/datetimes.
fn pydt_eq(a : PyDT, b : PyDT) -> Bool {
  if a.is_datetime != b.is_datetime {
    return false
  }
  if a.tz is Some(_) != (b.tz is Some(_)) {
    return false
  }
  a.instant() == b.instant()
}

///|
/// Python ordering comparison; raises like Python's TypeError on mixed kinds.
fn pydt_cmp(a : PyDT, b : PyDT) -> Int raise @core.SqlglotError {
  if a.is_datetime != b.is_datetime {
    raise @core.ValueError("TypeError: can't compare datetime.datetime to datetime.date")
  }
  if a.tz is Some(_) != (b.tz is Some(_)) {
    raise @core.ValueError(
      "TypeError: can't compare offset-naive and offset-aware datetimes",
    )
  }
  a.instant().compare(b.instant())
}

///|
/// Python `datetime.fromisoformat(text)` (or `None` on ValueError).
fn parse_py_datetime(text : String) -> PyDT? {
  let chars = text.to_array()
  let (y, m, d, n) = match parse_iso_date_parts(chars) {
    Some(x) => x
    None => return None
  }
  let base = { ..make_date(y, m, d), is_datetime: true }
  if n == chars.length() {
    return Some(base)
  }
  if n + 1 >= chars.length() {
    return None
  }
  parse_iso_time_parts(chars[n + 1:].to_array()).map(t => {
    ..base,
    h: t.0,
    mi: t.1,
    s: t.2,
    us: t.3,
    tz: t.4,
  })
}

///|
/// Python 3.11+ ISO date forms; returns (year, month, day, consumed).
fn parse_iso_date_parts(chars : Array[Char]) -> (Int, Int, Int, Int)? {
  let n = match parse_iso_date_prefix(chars) {
    Some(n) => n
    None => return None
  }
  let year = digits_at(chars, 0, 4).unwrap()
  // week dates
  let (w, wd) = if chars[4] == 'W' {
    (digits_at(chars, 5, 2), if n == 8 { digits_at(chars, 7, 1) } else { Some(1) })
  } else if chars[4] == '-' && chars[5] == 'W' {
    (digits_at(chars, 6, 2), if n == 10 { digits_at(chars, 9, 1) } else { Some(1) })
  } else {
    (None, None)
  }
  match (w, wd) {
    (Some(week), Some(day)) => {
      // ISO week 1 contains Jan 4th
      let jan4 = make_date(year, 1, 4)
      let week1_monday = jan4.ordinal() - jan4.weekday()
      let (yy, mm, dd) = civil_from_days(week1_monday + (week - 1) * 7 + day - 1)
      Some((yy, mm, dd, n))
    }
    _ =>
      if chars[4] == '-' {
        Some((year, digits_at(chars, 5, 2).unwrap(), digits_at(chars, 8, 2).unwrap(), n))
      } else {
        Some((year, digits_at(chars, 4, 2).unwrap(), digits_at(chars, 6, 2).unwrap(), n))
      }
  }
}

///|
/// Parses HH[:MM[:SS[.f+]]] (or basic form) plus an optional UTC offset.
fn parse_iso_time_parts(arr : Array[Char]) -> (Int, Int, Int, Int, Int?)? {
  if !is_iso_time(arr[:]) {
    return None
  }
  let n = arr.length()
  let h = digits_at(arr, 0, 2).unwrap()
  let mut i = 2
  let extended = i < n && arr[i] == ':'
  let mut mi = 0
  let mut s = 0
  let mut us = 0
  let is_tz_start = fn(c : Char) { c == '+' || c == '-' || c == 'Z' }
  if i < n && !is_tz_start(arr[i]) {
    if extended {
      i += 1
    }
    mi = digits_at(arr, i, 2).unwrap()
    i += 2
    if i < n && !is_tz_start(arr[i]) {
      if extended {
        i += 1
      }
      s = digits_at(arr, i, 2).unwrap()
      i += 2
      if i < n && (arr[i] == '.' || arr[i] == ',') {
        i += 1
        let mut digits = 0
        let mut v = 0
        while i < n && arr[i] >= '0' && arr[i] <= '9' {
          if digits < 6 {
            v = v * 10 + (arr[i].to_int() - '0'.to_int())
            digits += 1
          }
          i += 1
        }
        while digits < 6 {
          v = v * 10
          digits += 1
        }
        us = v
      }
    }
  }
  let tz = if i >= n {
    None
  } else if arr[i] == 'Z' {
    Some(0)
  } else {
    let sign = if arr[i] == '-' { -1 } else { 1 }
    i += 1
    let th = digits_at(arr, i, 2).unwrap()
    i += 2
    let mut tm = 0
    let mut ts = 0
    if i < n {
      if arr[i] == ':' {
        i += 1
      }
      tm = digits_at(arr, i, 2).unwrap_or(0)
      i += 2
      if i < n {
        if arr[i] == ':' {
          i += 1
        }
        ts = digits_at(arr, i, 2).unwrap_or(0)
      }
    }
    Some(sign * (th * 3600 + tm * 60 + ts))
  }
  Some((h, mi, s, us, tz))
}

///|
/// A `dateutil.relativedelta` (normalized as by its `_fix`). Python ints are unbounded,
/// so the components are big integers.
priv struct RelDelta {
  years : @bigint.BigInt
  months : @bigint.BigInt
  days : @bigint.BigInt
  hours : @bigint.BigInt
  minutes : @bigint.BigInt
  seconds : @bigint.BigInt
  microseconds : @bigint.BigInt
}

///|
fn sign_divmod(
  v : @bigint.BigInt,
  base : Int,
) -> (@bigint.BigInt, @bigint.BigInt) {
  // Python: s = sign(v); div, mod = divmod(v * s, base); return (div * s, mod * s)
  let s = if v < big(0) { big(-1) } else { big(1) }
  let a = v * s
  let b = big(base)
  (a / b * s, a % b * s)
}

///|
fn RelDelta::fix(self : RelDelta) -> RelDelta {
  let mut r = self
  let out_of = (v : @bigint.BigInt, limit : Int) => {
    v > big(limit) || v < big(-limit)
  }
  if out_of(r.microseconds, 999999) {
    let (div, md) = sign_divmod(r.microseconds, 1000000)
    r = { ..r, microseconds: md, seconds: r.seconds + div }
  }
  if out_of(r.seconds, 59) {
    let (div, md) = sign_divmod(r.seconds, 60)
    r = { ..r, seconds: md, minutes: r.minutes + div }
  }
  if out_of(r.minutes, 59) {
    let (div, md) = sign_divmod(r.minutes, 60)
    r = { ..r, minutes: md, hours: r.hours + div }
  }
  if out_of(r.hours, 23) {
    let (div, md) = sign_divmod(r.hours, 24)
    r = { ..r, hours: md, days: r.days + div }
  }
  if out_of(r.months, 11) {
    let (div, md) = sign_divmod(r.months, 12)
    r = { ..r, months: md, years: r.years + div }
  }
  r
}

///|
fn RelDelta::zero() -> RelDelta {
  {
    years: big(0),
    months: big(0),
    days: big(0),
    hours: big(0),
    minutes: big(0),
    seconds: big(0),
    microseconds: big(0),
  }
}

///|
fn RelDelta::neg(self : RelDelta) -> RelDelta {
  {
    years: -self.years,
    months: -self.months,
    days: -self.days,
    hours: -self.hours,
    minutes: -self.minutes,
    seconds: -self.seconds,
    microseconds: -self.microseconds,
  }.fix()
}

///|
fn RelDelta::has_time(self : RelDelta) -> Bool {
  !self.hours.is_zero() ||
  !self.minutes.is_zero() ||
  !self.seconds.is_zero() ||
  !self.microseconds.is_zero()
}

///|
fn RelDelta::moves_months(self : RelDelta) -> Bool {
  !self.months.is_zero() || !self.years.is_zero()
}

///|
/// `calendar.monthrange(year, month)[1]` for any year (the leap pattern repeats every
/// 400 years).
fn days_in_month_big(year : @bigint.BigInt, month : Int) -> Int {
  let (_, y400) = big_floor_divmod(year, big(400))
  // 2000 + y400 has the same leap status as `year`
  days_in_month(2000 + y400.to_int(), month)
}

///|
/// `other + relativedelta`
fn add_reldelta(other : PyDT, rd : RelDelta) -> PyDT raise @core.SqlglotError {
  let other = if rd.has_time() && !other.is_datetime {
    other.to_datetime()
  } else {
    other
  }
  let mut year = big(other.y) + rd.years
  let mut month = other.m
  if !rd.months.is_zero() {
    // |months| <= 11 after `fix`
    month += rd.months.to_int()
    if month > 12 {
      year = year + big(1)
      month -= 12
    } else if month < 1 {
      year = year - big(1)
      month += 12
    }
  }
  let day = @core.min_int(days_in_month_big(year, month), other.d)
  // other.replace(year=year, ...)
  let year = c_int(year)
  check_year(year)
  let replaced = { ..other, y: year, m: month, d: day }
  replaced.add_timedelta_big(
    rd.days,
    rd.hours * big(3600) + rd.minutes * big(60) + rd.seconds,
    rd.microseconds,
  )
}

///|
/// Python `interval(unit, n)`; `None` for unsupported units.
fn interval_delta(unit : String, n? : @bigint.BigInt = big(1)) -> RelDelta? {
  let z = RelDelta::zero()
  let r = match unit {
    "year" => { ..z, years: n }
    "quarter" => { ..z, months: big(3) * n }
    "month" => { ..z, months: n }
    "week" => { ..z, days: big(7) * n }
    "day" => { ..z, days: n }
    "hour" => { ..z, hours: n }
    "minute" => { ..z, minutes: n }
    "second" => { ..z, seconds: n }
    "millisecond" => { ..z, microseconds: big(1000) * n }
    "microsecond" => { ..z, microseconds: n }
    _ => return None
  }
  Some(r.fix())
}

///|
/// Python `datetime_floor`; `None` for unsupported units.
fn datetime_floor(d : PyDT, unit : String, week_offset : Int) -> PyDT? {
  if d.is_datetime {
    match unit {
      "hour" => return Some({ ..d, mi: 0, s: 0, us: 0 })
      "minute" => return Some({ ..d, s: 0, us: 0 })
      "second" => return Some({ ..d, us: 0 })
      "millisecond" => return Some({ ..d, us: d.us / 1000 * 1000 })
      "microsecond" => return Some(d)
      _ => ()
    }
  }
  let result = match unit {
    "year" => { ..d, m: 1, d: 1 }
    "quarter" => {
      let m = if d.m <= 3 {
        1
      } else if d.m <= 6 {
        4
      } else if d.m <= 9 {
        7
      } else {
        10
      }
      { ..d, m, d: 1 }
    }
    "month" => { ..d, d: 1 }
    "week" => {
      let back = ((d.weekday() - week_offset) % 7 + 7) % 7
      let (y, m, dd) = civil_from_days(d.ordinal() - back)
      { ..d, y, m, d: dd }
    }
    "day" => d
    _ => return None
  }
  if result.is_datetime {
    Some({ ..result, h: 0, mi: 0, s: 0, us: 0 })
  } else {
    Some(result)
  }
}