// Datetime and interval arithmetic and ordering (spec M4a section 4).

///|
let nanos_per_second : @bigint.BigInt = 1000000000N

///|
let nanos_per_day : @bigint.BigInt = 86400000000000N

///|
fn big64(n : Int64) -> @bigint.BigInt {
  @bigint.BigInt::from_int64(n)
}

///|
fn fits_int64(n : @bigint.BigInt) -> Bool {
  n.compare_int64(9223372036854775807L) <= 0 &&
  n.compare_int64(-9223372036854775808L) > 0
}

///|
/// Days since 1970-01-01 (Howard Hinnant's days_from_civil).
fn days_from_civil(year : Int32, month : Int32, day : Int32) -> Int64 {
  let y = (if month <= 2 { year - 1 } else { year }).to_int64()
  let era = (if y >= 0L { y } else { y - 399L }) / 400L
  let yoe = y - era * 400L
  let m = month.to_int64()
  let doy = (153L * (if m > 2L { m - 3L } else { m + 9L }) + 2L) / 5L +
    day.to_int64() -
    1L
  let doe = yoe * 365L + yoe / 4L - yoe / 100L + doy
  era * 146097L + doe - 719468L
}

///|
/// The year, month and day of a day number (Hinnant's civil_from_days). The
/// day number is within ±4e11, so that the year fits in an Int.
fn civil_from_days(days : Int64) -> (Int32, Int32, Int32) {
  let z = days + 719468L
  let era = (if z >= 0L { z } else { z - 146096L }) / 146097L
  let doe = z - era * 146097L
  let yoe = (doe - doe / 1460L + doe / 36524L - doe / 146096L) / 365L
  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 y = yoe + era * 400L + (if m <= 2L { 1L } else { 0L })
  (y.to_int(), m.to_int(), d.to_int())
}

///|
fn leap_year(year : Int32) -> Bool {
  (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

///|
fn month_length(year : Int32, month : Int32) -> Int32 {
  match month {
    2 => if leap_year(year) { 29 } else { 28 }
    4 | 6 | 9 | 11 => 30
    _ => 31
  }
}

///|
/// A date that a datetime operation may give: SQL and Ion allow years
/// 0001-9999 (spec M4d3 §5). Values read from data keep the value model's
/// range; results are checked.
fn checked_date(d : @value.Date) -> @value.Date raise OpFailure {
  if d.year < 1 || d.year > 9999 {
    raise DataFailure(
      "datetime field overflow: year \{d.year} is outside 0001-9999",
    )
  }
  d
}

///|
/// Days beyond which a date is out of range anyway; it keeps the arithmetic
/// inside Int64.
let max_day_number : Int64 = 400000000000L

///|
fn date_of_days(days : Int64) -> @value.Date raise OpFailure {
  if days.abs() > max_day_number {
    raise DataFailure("a date out of range")
  }
  let (year, month, day) = civil_from_days(days)
  let d = @value.Date::Date(year~, month~, day~) catch {
    _ => raise DataFailure("a date out of range")
  }
  checked_date(d)
}

///|
fn date_days(d : @value.Date) -> Int64 {
  days_from_civil(d.year, d.month, d.day)
}

///|
/// The date `months` later. A day that the target month lacks is a datetime
/// field overflow (SQL-92 6.14 GR 4; spec M4d3 §5).
fn date_add_months(
  d : @value.Date,
  months : Int64,
) -> @value.Date raise OpFailure {
  if months.abs() > 100000000000L {
    raise DataFailure("a date out of range")
  }
  let total = d.year.to_int64() * 12L + (d.month - 1).to_int64() + months
  let mut years = total / 12L
  let mut rest = total % 12L
  if rest < 0L {
    years = years - 1L
    rest = rest + 12L
  }
  if years < 1L || years > 9999L {
    raise DataFailure(
      "datetime field overflow: year \{years} is outside 0001-9999",
    )
  }
  let year = years.to_int()
  let month = rest.to_int() + 1
  if d.day > month_length(year, month) {
    raise DataFailure(
      "datetime field overflow: \{year}-\{month} has no day \{d.day}",
    )
  }
  @value.Date::Date(year~, month~, day=d.day) catch {
    _ => raise DataFailure("a date out of range")
  }
}

///|
/// The nanoseconds of a day-time interval.
fn dt_nanos(i : @value.IntervalDT) -> @bigint.BigInt {
  big64(i.seconds) * nanos_per_second + big64(i.nanos.to_int64())
}

///|
fn dt_value(n : @bigint.BigInt) -> @value.Value raise OpFailure {
  let seconds = n / nanos_per_second
  if !fits_int64(seconds) {
    raise DataFailure("an interval out of range")
  }
  let rest = (n % nanos_per_second).to_int64()
  @value.Value::from_interval_dt(
    @value.IntervalDT(seconds.to_int64(), rest) catch {
      _ => raise DataFailure("an interval out of range")
    },
  )
}

///|
fn ym_value(months : @bigint.BigInt) -> @value.Value raise OpFailure {
  if !fits_int64(months) {
    raise DataFailure("an interval out of range")
  }
  @value.Value::from_interval_ym(
    @value.IntervalYM(months.to_int64()) catch {
      _ => raise DataFailure("an interval out of range")
    },
  )
}

///|
/// The fewest fraction digits that show `nanos`.
fn needed_precision(nanos : Int32) -> Int32 {
  let mut p = 9
  let mut rest = nanos
  while p > 0 && rest % 10 == 0 {
    rest = rest / 10
    p = p - 1
  }
  p
}

///|
/// The time of day at `rest` nanoseconds into the day.
fn time_at(
  rest : @bigint.BigInt,
  precision : Int32,
  offset : Int32?,
) -> @value.Time raise OpFailure {
  let seconds = (rest / nanos_per_second).to_int()
  let nanos = (rest % nanos_per_second).to_int()
  @value.Time::Time(
    hour=seconds / 3600,
    minute=seconds % 3600 / 60,
    second=seconds % 60,
    nanos~,
    precision=precision.max(needed_precision(nanos)),
    offset~,
  ) catch {
    _ => raise DataFailure("a time out of range")
  }
}

///|
fn day_nanos(t : @value.Time) -> @bigint.BigInt {
  big64((t.hour * 3600 + t.minute * 60 + t.second).to_int64()) *
  nanos_per_second +
  big64(t.nanos.to_int64())
}

///|
fn floor_split(
  n : @bigint.BigInt,
  unit : @bigint.BigInt,
) -> (@bigint.BigInt, @bigint.BigInt) {
  let q = n / unit
  let r = n % unit
  if r.compare_int(0) < 0 {
    (q - 1N, r + unit)
  } else {
    (q, r)
  }
}

///|
fn time_shift(
  t : @value.Time,
  n : @bigint.BigInt,
) -> @value.Time raise OpFailure {
  let (_, rest) = floor_split(day_nanos(t) + n, nanos_per_day)
  time_at(rest, t.precision, t.offset)
}

///|
fn timestamp_shift(
  ts : @value.Timestamp,
  n : @bigint.BigInt,
) -> @value.Timestamp raise OpFailure {
  let shifted = big64(date_days(ts.date)) * nanos_per_day +
    day_nanos(ts.time) +
    n
  let (days, rest) = floor_split(shifted, nanos_per_day)
  if !fits_int64(days) {
    raise DataFailure("a timestamp out of range")
  }
  @value.Timestamp::Timestamp(
    date_of_days(days.to_int64()),
    time_at(rest, ts.time.precision, ts.time.offset),
  )
}

///|
/// A date plus a day-time interval of whole days.
fn date_shift(
  d : @value.Date,
  n : @bigint.BigInt,
) -> @value.Date raise OpFailure {
  let (days, rest) = floor_split(n, nanos_per_day)
  if !rest.is_zero() {
    raise TypeFailure("a date plus an interval with a part under a day")
  }
  if !fits_int64(days) {
    raise DataFailure("a date out of range")
  }
  date_of_days(date_days(d) + days.to_int64())
}

///|
/// The instant of a date, a timestamp or a time of day, in nanoseconds; a
/// date is midnight and a value without an offset is at offset 0.
fn instant(v : @value.Value) -> @bigint.BigInt? {
  match v {
    Date(d) => Some(big64(date_days(d)) * nanos_per_day)
    Timestamp(ts) =>
      Some(
        big64(date_days(ts.date)) * nanos_per_day +
        day_nanos(ts.time) -
        big64((ts.time.offset.unwrap_or(0) * 60).to_int64()) * nanos_per_second,
      )
    Time(t) =>
      Some(
        day_nanos(t) -
        big64((t.offset.unwrap_or(0) * 60).to_int64()) * nanos_per_second,
      )
    _ => None
  }
}

///|
fn is_point_in_time(v : @value.Value) -> Bool {
  v is (Date(_) | Timestamp(_))
}

///|
/// The order of two datetimes of one kind (a date counts as a timestamp) or
/// two intervals of one kind; `None` for any other pair.
fn datetime_order(x : @value.Value, y : @value.Value) -> Int32? {
  match (x, y) {
    (IntervalYM(p), IntervalYM(q)) => Some(p.months.compare(q.months))
    (IntervalDT(p), IntervalDT(q)) => Some(dt_nanos(p).compare(dt_nanos(q)))
    (Time(_), Time(_)) =>
      match (instant(x), instant(y)) {
        (Some(p), Some(q)) => Some(p.compare(q))
        _ => None
      }
    _ =>
      if is_point_in_time(x) && is_point_in_time(y) {
        match (instant(x), instant(y)) {
          (Some(p), Some(q)) => Some(p.compare(q))
          _ => None
        }
      } else {
        None
      }
  }
}

///|
/// A float as the decimal of its exact binary value, not of its shortest text.
fn exact_decimal(f : Double) -> @ion_core.Decimal raise OpFailure {
  if f.is_nan() || f.is_inf() {
    raise DataFailure("NaN or infinity in interval arithmetic")
  }
  let bits = f.reinterpret_as_int64()
  let negative = bits < 0L
  let exponent_bits = ((bits >> 52) & 0x7FFL).to_int()
  let fraction = bits & 0xFFFFFFFFFFFFFL
  let mut mantissa = if exponent_bits == 0 {
    fraction
  } else {
    fraction | (1L << 52)
  }
  let mut exponent = if exponent_bits == 0 {
    -1074
  } else {
    exponent_bits - 1075
  }
  if mantissa == 0L {
    return @ion_core.Decimal::Decimal(0N, 0)
  }
  while mantissa % 2L == 0L {
    mantissa = mantissa / 2L
    exponent += 1
  }
  let m = if negative { -big64(mantissa) } else { big64(mantissa) }
  if exponent >= 0 {
    @ion_core.Decimal::Decimal(
      m * @bigint.BigInt::from_int(2).pow(@bigint.BigInt::from_int(exponent)),
      0,
    )
  } else {
    @ion_core.Decimal::Decimal(
      m * @bigint.BigInt::from_int(5).pow(@bigint.BigInt::from_int(-exponent)),
      exponent,
    )
  }
}

///|
/// The integer part of a decimal, toward zero.
fn truncated(d : @ion_core.Decimal) -> @bigint.BigInt raise OpFailure {
  // Int64: `-e` must not overflow for the Int minimum.
  let e = d.exponent().to_int64()
  if e >= 0L {
    if e > 60L {
      raise DataFailure("an interval out of range")
    }
    d.coefficient() * pow10(e.to_int())
  } else if -e > digit_count(d.coefficient()).to_int64() {
    0N
  } else {
    d.coefficient() / pow10((-e).to_int())
  }
}

///|
/// The value of a number as the factor of an interval. A float is taken at
/// its exact binary value.
fn factor(n : Num) -> @ion_core.Decimal raise OpFailure {
  match n {
    Whole(w) => @ion_core.Decimal::Decimal(w, 0)
    Exact(d) => d
    Approx(f) => exact_decimal(f)
  }
}

///|
fn scaled_ym(months : @bigint.BigInt) -> @value.Value raise OpFailure {
  ym_value(months)
}

///|
fn scaled_dt(nanos : @bigint.BigInt) -> @value.Value raise OpFailure {
  dt_value(nanos)
}

///|
/// Cuts a count of nanoseconds back to whole microseconds, the fraction
/// precision of an interval.
fn to_micros(nanos : @bigint.BigInt) -> @bigint.BigInt {
  nanos - nanos % 1000N
}

///|
/// An interval times a number. A year-month interval counts months, a
/// day-time one nanoseconds; a fractional factor cuts the result back to
/// microseconds.
fn scale_by(interval : @value.Value, n : Num) -> @value.Value raise OpFailure {
  let exact = n is Whole(_)
  match interval {
    IntervalYM(i) => {
      let product = decimal_mul(
        @ion_core.Decimal::Decimal(big64(i.months), 0),
        factor(n),
      )
      scaled_ym(truncated(product))
    }
    IntervalDT(i) => {
      let product = decimal_mul(
        @ion_core.Decimal::Decimal(dt_nanos(i), 0),
        factor(n),
      )
      let nanos = truncated(product)
      scaled_dt(if exact { nanos } else { to_micros(nanos) })
    }
    _ => raise TypeFailure("a scale of a value that is not an interval")
  }
}

///|
/// An interval divided by a number; a zero divisor is a `DataFailure`.
fn divide_by(interval : @value.Value, n : Num) -> @value.Value raise OpFailure {
  match interval {
    IntervalYM(i) => {
      let divisor = factor(n)
      let quotient = decimal_div(
        @ion_core.Decimal::Decimal(big64(i.months), 0),
        divisor,
      )
      scaled_ym(truncated(quotient))
    }
    IntervalDT(i) => {
      let divisor = factor(n)
      let quotient = decimal_div(
        @ion_core.Decimal::Decimal(dt_nanos(i), 0),
        divisor,
      )
      scaled_dt(to_micros(truncated(quotient)))
    }
    _ => raise TypeFailure("a division of a value that is not an interval")
  }
}

///|
fn is_interval(v : @value.Value) -> Bool {
  v is (IntervalYM(_) | IntervalDT(_))
}

///|
fn is_datetime(v : @value.Value) -> Bool {
  v is (Date(_) | Time(_) | Timestamp(_))
}

///|
/// `datetime + interval` (or `datetime - interval` for a negative `sign`).
fn shift_datetime(
  point : @value.Value,
  interval : @value.Value,
  negative~ : Bool,
) -> @value.Value raise OpFailure {
  match (point, interval) {
    (Date(d), IntervalYM(i)) =>
      @value.Value::from_date(
        date_add_months(d, if negative { -i.months } else { i.months }),
      )
    (Timestamp(ts), IntervalYM(i)) =>
      @value.Value::from_timestamp(
        @value.Timestamp(
          date_add_months(ts.date, if negative { -i.months } else { i.months }),
          ts.time,
        ),
      )
    (Date(d), IntervalDT(i)) =>
      @value.Value::from_date(
        date_shift(d, if negative { -dt_nanos(i) } else { dt_nanos(i) }),
      )
    (Timestamp(ts), IntervalDT(i)) =>
      @value.Value::from_timestamp(
        timestamp_shift(ts, if negative { -dt_nanos(i) } else { dt_nanos(i) }),
      )
    (Time(t), IntervalDT(i)) =>
      @value.Value::from_time(
        time_shift(t, if negative { -dt_nanos(i) } else { dt_nanos(i) }),
      )
    (Time(_), IntervalYM(_)) =>
      raise TypeFailure("a time and a year-month interval")
    _ => raise TypeFailure("a datetime and an interval that do not combine")
  }
}

///|
/// `x + y` for datetimes and intervals; `None` when neither side is one, so
/// that the number rules decide.
fn datetime_add(
  x : @value.Value,
  y : @value.Value,
) -> @value.Value? raise OpFailure {
  match (x, y) {
    (IntervalYM(p), IntervalYM(q)) =>
      Some(ym_value(big64(p.months) + big64(q.months)))
    (IntervalDT(p), IntervalDT(q)) => Some(dt_value(dt_nanos(p) + dt_nanos(q)))
    _ =>
      if is_datetime(x) && is_interval(y) {
        Some(shift_datetime(x, y, negative=false))
      } else if is_interval(x) && is_datetime(y) {
        Some(shift_datetime(y, x, negative=false))
      } else {
        None
      }
  }
}

///|
/// `x - y` for datetimes and intervals.
fn datetime_sub(
  x : @value.Value,
  y : @value.Value,
) -> @value.Value? raise OpFailure {
  match (x, y) {
    (IntervalYM(p), IntervalYM(q)) =>
      Some(ym_value(big64(p.months) - big64(q.months)))
    (IntervalDT(p), IntervalDT(q)) => Some(dt_value(dt_nanos(p) - dt_nanos(q)))
    _ =>
      if is_datetime(x) && is_interval(y) {
        Some(shift_datetime(x, y, negative=true))
      } else if (is_point_in_time(x) && is_point_in_time(y)) ||
        (x is Time(_) && y is Time(_)) {
        match (instant(x), instant(y)) {
          (Some(p), Some(q)) => Some(dt_value(p - q))
          _ => None
        }
      } else {
        None
      }
  }
}

///|
/// `x * y` for an interval and a number in either order.
fn datetime_mul(
  x : @value.Value,
  y : @value.Value,
) -> @value.Value? raise OpFailure {
  if is_interval(x) {
    match number(y) {
      Some(n) => Some(scale_by(x, n))
      None => None
    }
  } else if is_interval(y) {
    match number(x) {
      Some(n) => Some(scale_by(y, n))
      None => None
    }
  } else {
    None
  }
}

///|
/// `x / y` for an interval and a number.
fn datetime_div(
  x : @value.Value,
  y : @value.Value,
) -> @value.Value? raise OpFailure {
  if is_interval(x) {
    match number(y) {
      Some(n) => Some(divide_by(x, n))
      None => None
    }
  } else {
    None
  }
}

///|
/// `-x` for an interval.
fn interval_neg(v : @value.Value) -> @value.Value? raise OpFailure {
  match v {
    IntervalYM(i) => Some(ym_value(-big64(i.months)))
    IntervalDT(i) => Some(dt_value(-dt_nanos(i)))
    _ => None
  }
}

///|
/// `x = y` for two datetimes of the kinds that `datetime_order` compares.
fn datetime_equal(x : @value.Value, y : @value.Value) -> @value.Value? {
  match datetime_order(x, y) {
    Some(c) => Some(@value.Value::from_bool(c == 0))
    None => None
  }
}

///|
/// The time `nanos` later, around the day.
pub fn shift_time(
  t : @value.Time,
  nanos : Int64,
) -> @value.Time raise OpFailure {
  time_shift(t, big64(nanos))
}

///|
/// The timestamp `nanos` later.
pub fn shift_timestamp(
  ts : @value.Timestamp,
  nanos : Int64,
) -> @value.Timestamp raise OpFailure {
  timestamp_shift(ts, big64(nanos))
}