// DATE_ADD and DATE_DIFF (spec M4d3 §4.3, §4.4).

///|
/// Whether a datetime has the field (SQL-92 6.14 SR 3): DATE has YEAR,
/// MONTH and DAY; TIME has HOUR, MINUTE and SECOND; TIMESTAMP has all six.
fn has_field(f : @ast.DatetimeField, x : @value.Value) -> Bool {
  match (f, x) {
    (Year | Month | Day, Date(_) | Timestamp(_)) => true
    (Hour | Minute | Second, Time(_) | Timestamp(_)) => true
    _ => false
  }
}

///|
/// `x` plus `n` units of `f`. The result keeps the type, precision and
/// offset of `x`.
fn add_units(
  f : @ast.DatetimeField,
  n : @value.Value,
  x : @value.Value,
) -> @value.Value raise OpFailure {
  let count = match n {
    Int(i) => big64(i)
    BigInt(b) => b
    _ => raise TypeFailure("the quantity of DATE_ADD must be an integer")
  }
  if !has_field(f, x) {
    raise TypeFailure("DATE_ADD \{field_name(f)} to \{kind_name(x)}")
  }
  match f {
    Year | Month => {
      let months = if f is Year { count * 12N } else { count }
      if !fits_int64(months) {
        raise DataFailure("datetime field overflow in DATE_ADD")
      }
      match x {
        Date(d) =>
          @value.Value::from_date(date_add_months(d, months.to_int64()))
        Timestamp(ts) =>
          @value.Value::from_timestamp(
            @value.Timestamp(
              date_add_months(ts.date, months.to_int64()),
              ts.time,
            ),
          )
        _ => raise TypeFailure("DATE_ADD \{field_name(f)} to \{kind_name(x)}")
      }
    }
    _ => {
      let nanos = count * field_nanos(f)
      match x {
        Date(d) => @value.Value::from_date(date_shift(d, nanos))
        Timestamp(ts) =>
          @value.Value::from_timestamp(timestamp_shift(ts, nanos))
        Time(t) => @value.Value::from_time(time_shift(t, nanos))
        _ => raise TypeFailure("DATE_ADD \{field_name(f)} to \{kind_name(x)}")
      }
    }
  }
}

///|
/// `DATE_ADD(f, n, x)`.
pub fn date_add(
  field : @ast.DatetimeField,
  args : Array[@value.Value],
) -> @value.Value raise OpFailure {
  apply("DATE_ADD", [Integer, Datetime], args, xs => {
    add_units(field, xs[0], xs[1])
  })
}

///|
/// The instant of a datetime in nanoseconds; a DATE is midnight, and a value
/// without an offset is in the session zone (SQL-92 4.5.3).
fn zoned_instant(
  v : @value.Value,
  zone : ZoneOffset,
) -> @bigint.BigInt raise OpFailure {
  guard instant(v) is Some(n) else {
    raise TypeFailure("\{kind_name(v)} is not a datetime")
  }
  let no_offset = match v {
    Date(_) => true
    Timestamp(ts) => ts.time.offset is None
    Time(t) => t.offset is None
    _ => false
  }
  if no_offset {
    n - minutes_nanos(zone.minutes)
  } else {
    n
  }
}

///|
fn as_timestamp(v : @value.Value) -> @value.Timestamp raise OpFailure {
  match v {
    Timestamp(ts) => ts
    Date(d) =>
      @value.Timestamp::Timestamp(
        d,
        @value.Time::Time(hour=0, minute=0, second=0, precision=0) catch {
          _ => raise DataFailure("a time out of range")
        },
      )
    _ => raise TypeFailure("\{kind_name(v)} is not a date or timestamp")
  }
}

///|
/// The day number and the nanoseconds into the day of a timestamp seen
/// `shift` minutes later. No range check: DATE_DIFF gives an integer, not a
/// datetime, so a value from data outside 0001-9999 still compares.
fn local_day(ts : @value.Timestamp, shift : Int32) -> (Int64, @bigint.BigInt) {
  let total = big64(date_days(ts.date)) * nanos_per_day +
    day_nanos(ts.time) +
    minutes_nanos(shift)
  let (days, rest) = floor_split(total, nanos_per_day)
  (days.to_int64(), rest)
}

///|
/// A date as Java packs it to count months: proleptic month * 32 + day.
fn packed_date(days : Int64) -> Int64 {
  let (year, month, day) = civil_from_days(days)
  (year.to_int64() * 12L + (month - 1).to_int64()) * 32L + day.to_int64()
}

///|
/// Complete calendar months from `a` to `b`, exactly as Java
/// `ChronoUnit.MONTHS.between` (Kotlin v1): `b` moves to `a`'s offset; when
/// `b`'s time of day has not reached `a`'s, its date moves one day toward
/// `a` (a partial day does not count); then the months between the dates
/// count only when the day of the month is reached.
fn month_difference(
  a : @value.Timestamp,
  b : @value.Timestamp,
  zone : ZoneOffset,
) -> Int64 {
  let offset_a = a.time.offset.unwrap_or(zone.minutes)
  let offset_b = b.time.offset.unwrap_or(zone.minutes)
  let (days_a, time_a) = local_day(a, 0)
  let (end_days, time_b) = local_day(b, offset_a - offset_b)
  let days_b = if end_days > days_a && time_b.compare(time_a) < 0 {
    end_days - 1L
  } else if end_days < days_a && time_b.compare(time_a) > 0 {
    end_days + 1L
  } else {
    end_days
  }
  (packed_date(days_b) - packed_date(days_a)) / 32L
}

///|
/// `b - a` in whole units of `f`, toward zero (spec M4d3 §4.4).
fn diff_units(
  f : @ast.DatetimeField,
  a : @value.Value,
  b : @value.Value,
  zone : ZoneOffset,
) -> @value.Value raise OpFailure {
  let both_times = a is Time(_) && b is Time(_)
  if !both_times && !(is_point_in_time(a) && is_point_in_time(b)) {
    raise TypeFailure("DATE_DIFF of \{kind_name(a)} and \{kind_name(b)}")
  }
  if both_times && f is (Year | Month | Day) {
    raise TypeFailure("DATE_DIFF \{field_name(f)} of TIME values")
  }
  match f {
    Year | Month => {
      let m = month_difference(as_timestamp(a), as_timestamp(b), zone)
      @value.Value::from_int(if f is Year { m / 12L } else { m })
    }
    _ => {
      let delta = zoned_instant(b, zone) - zoned_instant(a, zone)
      @value.Value::from_int((delta / field_nanos(f)).to_int64())
    }
  }
}

///|
/// `DATE_DIFF(f, a, b)`.
pub fn date_diff(
  field : @ast.DatetimeField,
  args : Array[@value.Value],
  zone~ : ZoneOffset,
) -> @value.Value raise OpFailure {
  apply("DATE_DIFF", [Datetime, Datetime], args, xs => {
    diff_units(field, xs[0], xs[1], zone)
  })
}