// CAST to DATE, TIME and TIMESTAMP (spec M4d2 §4.4, §5.4).

///|
/// Text as SQL reads it for CAST: leading and trailing spaces (U+0020) off.
fn trimmed(s : String) -> String {
  s.trim(chars=" ").to_owned()
}

///|
/// The SQL  that `text` spells when it starts with a keyword
/// (`DATE '2020-01-01'`, `INTERVAL '1' DAY`); `None` for a bare string. SQL
/// 6.13 reads CAST text by the rules for ; since SQL:1999 a bare
/// string under the target's fields is valid too. Text that starts with a
/// keyword but is not a datetime or interval literal is not valid.
fn keyword_literal(text : String) -> @ast.Literal? raise OpFailure {
  guard text.get_char(0) is Some(c) && (c is ('a'..<='z') || c is ('A'..<='Z')) else {
    return None
  }
  let e = @parser.parse_expression(text) catch {
    _ => raise TypeFailure("'\{text}' is not a literal")
  }
  match e.kind {
    Lit(Date(_) as l)
    | Lit(Time(_, ..) as l)
    | Lit(Timestamp(_, ..) as l)
    | Lit(Interval(_, ..) as l) => Some(l)
    _ => raise TypeFailure("'\{text}' is not a datetime or interval literal")
  }
}

///|
fn minutes_nanos(minutes : Int32) -> @bigint.BigInt {
  big64(minutes.to_int64() * 60L) * nanos_per_second
}

///|
/// `t` with its clock fields and the fraction `nanos` at `precision`, and
/// the offset `offset`.
fn clock(
  t : @value.Time,
  nanos~ : Int32,
  precision~ : Int32,
  offset~ : Int32?,
) -> @value.Time raise OpFailure {
  @value.Time::Time(
    hour=t.hour,
    minute=t.minute,
    second=t.second,
    nanos~,
    precision~,
    offset~,
  ) catch {
    _ => raise DataFailure("a time out of range")
  }
}

///|
/// The SQL zone rule (SQL92 6.10 GR 10-11): a local time gets the session
/// offset when the target has a time zone; a time with an offset becomes
/// session local time when the target has none.
fn zoned_time(
  t : @value.Time,
  with_zone : Bool,
  session : ZoneOffset,
) -> @value.Time raise OpFailure {
  match (t.offset, with_zone) {
    (Some(_), true) | (None, false) => t
    (None, true) =>
      clock(
        t,
        nanos=t.nanos,
        precision=t.precision,
        offset=Some(session.minutes),
      )
    (Some(offset), false) => {
      let moved = time_shift(t, minutes_nanos(session.minutes - offset))
      clock(moved, nanos=moved.nanos, precision=moved.precision, offset=None)
    }
  }
}

///|
fn zoned_timestamp(
  ts : @value.Timestamp,
  with_zone : Bool,
  session : ZoneOffset,
) -> @value.Timestamp raise OpFailure {
  match (ts.time.offset, with_zone) {
    (Some(_), true) | (None, false) => ts
    (None, true) =>
      @value.Timestamp::Timestamp(
        ts.date,
        clock(
          ts.time,
          nanos=ts.time.nanos,
          precision=ts.time.precision,
          offset=Some(session.minutes),
        ),
      )
    (Some(offset), false) => {
      let moved = timestamp_shift(ts, minutes_nanos(session.minutes - offset))
      @value.Timestamp::Timestamp(
        moved.date,
        clock(
          moved.time,
          nanos=moved.time.nanos,
          precision=moved.time.precision,
          offset=None,
        ),
      )
    }
  }
}

///|
/// One unit in the last place of `p` fraction digits, in nanoseconds.
fn fraction_unit(p : Int32) -> Int32 {
  let mut unit = 1
  for _ in 0..<(9 - p) {
    unit = unit * 10
  }
  unit
}

///|
/// TIME(p): the fraction rounded half up to `p` digits; a carry wraps past
/// midnight. Without `p` the precision of the value stays.
fn time_precision(
  t : @value.Time,
  precision : Int32?,
) -> @value.Time raise OpFailure {
  guard precision is Some(p) else { return t }
  let unit = fraction_unit(p)
  let rest = t.nanos % unit
  let kept = clock(t, nanos=t.nanos - rest, precision=p, offset=t.offset)
  if rest > 0 && rest * 2 >= unit {
    time_shift(kept, big64(unit.to_int64()))
  } else {
    kept
  }
}

///|
/// TIMESTAMP(p): as TIME(p), with the carry into the date.
fn timestamp_precision(
  ts : @value.Timestamp,
  precision : Int32?,
) -> @value.Timestamp raise OpFailure {
  guard precision is Some(p) else { return ts }
  let unit = fraction_unit(p)
  let rest = ts.time.nanos % unit
  let kept = @value.Timestamp::Timestamp(
    ts.date,
    clock(
      ts.time,
      nanos=ts.time.nanos - rest,
      precision=p,
      offset=ts.time.offset,
    ),
  )
  if rest > 0 && rest * 2 >= unit {
    timestamp_shift(kept, big64(unit.to_int64()))
  } else {
    kept
  }
}

///|
fn to_date(
  x : @value.Value,
  session : ZoneOffset,
) -> @value.Value raise OpFailure {
  match x {
    Date(d) => @value.Value::from_date(checked_date(d))
    Timestamp(ts) =>
      @value.Value::from_date(
        checked_date(zoned_timestamp(ts, false, session).date),
      )
    String(s) => {
      let text = trimmed(s)
      match keyword_literal(text) {
        Some(Date(body)) => @value.Value::from_date(parse_date(body))
        Some(_) => raise TypeFailure("'\{text}' is not a DATE literal")
        None => @value.Value::from_date(parse_date(text))
      }
    }
    _ => raise TypeFailure("CAST of \{kind_name(x)} to DATE")
  }
}

///|
fn to_time(
  x : @value.Value,
  precision : Int32?,
  with_zone : Bool,
  session : ZoneOffset,
) -> @value.Value raise OpFailure {
  let t = match x {
    Time(t) => t
    Timestamp(ts) => ts.time
    String(s) => {
      let text = trimmed(s)
      match keyword_literal(text) {
        Some(Time(body, precision~, with_tz~)) =>
          parse_time(body, precision, with_tz)
        Some(_) => raise TypeFailure("'\{text}' is not a TIME literal")
        None => parse_time(text, None, false)
      }
    }
    _ =>
      raise TypeFailure(
        "CAST of \{kind_name(x)} to \{target_name(Time(precision~, zone=with_zone))}",
      )
  }
  @value.Value::from_time(
    time_precision(zoned_time(t, with_zone, session), precision),
  )
}

///|
/// TIME to TIMESTAMP takes the date from CURRENT_DATE (SQL-92 6.10 GR 11).
fn to_timestamp(
  x : @value.Value,
  precision : Int32?,
  with_zone : Bool,
  session : ZoneOffset,
  today : () -> @value.Date raise OpFailure,
) -> @value.Value raise OpFailure {
  let ts = match x {
    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")
        },
      )
    String(s) => {
      let text = trimmed(s)
      match keyword_literal(text) {
        Some(Timestamp(body, precision~, with_tz~)) =>
          parse_timestamp(body, precision, with_tz)
        Some(_) => raise TypeFailure("'\{text}' is not a TIMESTAMP literal")
        None => parse_timestamp(text, None, false)
      }
    }
    // The date is CURRENT_DATE; the time and its offset are the value's
    // (SQL-92 6.10 GR 11).
    Time(t) => @value.Timestamp::Timestamp(today(), t)
    _ =>
      raise TypeFailure(
        "CAST of \{kind_name(x)} to \{target_name(Timestamp(precision~, zone=with_zone))}",
      )
  }
  ignore(checked_date(ts.date))
  @value.Value::from_timestamp(
    timestamp_precision(zoned_timestamp(ts, with_zone, session), precision),
  )
}