// Datetime and interval literal text (spec M4a section 4, M4d2 ยง4.1).

///|
priv suberror Malformed {
  Reason(String)
}

///|
/// The value of a run of digits.
fn digits_value(digits : String) -> Int64 raise Malformed {
  try @string.parse_int64(digits, base=10) catch {
    _ => raise Reason("not a number")
  } noraise {
    n => n
  }
}

///|
priv struct Cursor {
  chars : Array[Char]
  mut pos : Int32
}

///|
fn Cursor::Cursor(text : String) -> Cursor {
  { chars: text.to_array(), pos: 0, }
}

///|
fn Cursor::peek(self : Cursor) -> Char? {
  if self.pos < self.chars.length() {
    Some(self.chars[self.pos])
  } else {
    None
  }
}

///|
fn Cursor::eat(self : Cursor, c : Char) -> Bool {
  if self.peek() == Some(c) {
    self.pos += 1
    true
  } else {
    false
  }
}

///|
fn Cursor::expect(self : Cursor, c : Char) -> Unit raise Malformed {
  if !self.eat(c) {
    raise Reason("expected '\{c}'")
  }
}

///|
fn is_digit(c : Char) -> Bool {
  c >= '0' && c <= '9'
}

///|
/// The longest run of digits, which may be empty.
fn Cursor::run(self : Cursor) -> String {
  let buf = StringBuilder()
  while self.peek() is Some(c) && is_digit(c) {
    buf.write_char(c)
    self.pos += 1
  }
  buf.to_string()
}

///|
/// A number of at most 9 digits.
fn Cursor::number(self : Cursor) -> (Int32, Int32) raise Malformed {
  let digits = self.run()
  if digits.length() == 0 || digits.length() > 9 {
    raise Reason("expected a number of 1 to 9 digits")
  }
  (digits_value(digits).to_int(), digits.length())
}

///|
/// Exactly `n` digits.
fn Cursor::fixed(self : Cursor, n : Int32) -> Int32 raise Malformed {
  let (value, length) = self.number()
  if length != n {
    raise Reason("expected \{n} digits")
  }
  value
}

///|
fn Cursor::finish(self : Cursor) -> Unit raise Malformed {
  if self.pos != self.chars.length() {
    raise Reason("unexpected text at the end")
  }
}

///|
fn date_part(c : Cursor) -> @value.Date raise Error {
  let negative = c.eat('-')
  let (year, length) = c.number()
  if length < 4 {
    raise Reason("a year needs 4 digits")
  }
  c.expect('-')
  let month = c.fixed(2)
  c.expect('-')
  let day = c.fixed(2)
  let signed_year = if negative { -year } else { year }
  if signed_year < 1 || signed_year > 9999 {
    raise Reason("a year outside 0001-9999")
  }
  @value.Date::Date(year=signed_year, month~, day~)
}

///|
/// `HH:MM:SS[.f*][(+|-)HH:MM]`. Fraction digits beyond 9 are cut off. With an
/// explicit precision the result is cut down to it, with the nanoseconds of
/// one unit in the last place to add when the next digit rounds up (half up,
/// partiql-tests extract.ion, `TIME (2) '01:23:45.678'`).
fn time_part(
  c : Cursor,
  precision : Int32?,
  with_tz : Bool,
) -> (@value.Time, Int32) raise Error {
  let hour = c.fixed(2)
  c.expect(':')
  let minute = c.fixed(2)
  c.expect(':')
  let second = c.fixed(2)
  let fraction = if c.eat('.') {
    let digits = c.run()
    if digits.length() == 0 {
      raise Reason("a fraction needs digits")
    }
    digits
  } else {
    ""
  }
  let offset : Int32? = if c.eat('Z') {
    Some(0)
  } else if c.peek() is Some('+' | '-') {
    let negative = c.eat('-')
    if !negative {
      c.pos += 1
    }
    let hours = c.fixed(2)
    c.expect(':')
    let minutes = c.fixed(2)
    if hours > 23 || minutes > 59 {
      raise Reason("a time zone offset out of range")
    }
    let total = hours * 60 + minutes
    Some(if negative { -total } else { total })
  } else {
    None
  }
  if with_tz && offset is None {
    raise Reason("WITH TIME ZONE needs an offset")
  }
  // A literal without an explicit precision shows at least one fraction digit,
  // as in `second: 45.0` (partiql-tests time.ion, `time no fractional seconds`).
  let digits = precision.unwrap_or(fraction.length().min(9).max(1))
  let shown = fraction.view(end_offset=fraction.length().min(9)).to_owned()
  let all = digits_value(shown + String::make(9 - shown.length(), '0')).to_int()
  let mut unit = 1
  for _ in 0..<(9 - digits.min(9).max(0)) {
    unit = unit * 10
  }
  let nanos = all - all % unit
  let up = if all % unit * 2 >= unit && unit > 1 { unit } else { 0 }
  (
    @value.Time::Time(
      hour~,
      minute~,
      second~,
      nanos~,
      precision=digits,
      offset~,
    ),
    up,
  )
}

///|
fn field_rank(f : @ast.DatetimeField) -> Int32 raise Malformed {
  match f {
    Year => 0
    Month => 1
    Day => 2
    Hour => 3
    Minute => 4
    Second => 5
    TimezoneHour | TimezoneMinute =>
      raise Reason("a time zone field is not an interval field")
  }
}

///|
fn rank_field(rank : Int32) -> @ast.DatetimeField {
  match rank {
    0 => Year
    1 => Month
    2 => Day
    3 => Hour
    4 => Minute
    _ => Second
  }
}

///|
/// The text between a field and the next one.
fn separator(f : @ast.DatetimeField) -> Char {
  match f {
    Year => '-'
    Day => ' '
    _ => ':'
  }
}

///|
/// The fields from the qualifier's first to its last, in order. A
/// year-month field cannot meet a day-time field.
fn interval_fields(
  q : @ast.IntervalQualifier,
) -> Array[@ast.DatetimeField] raise Malformed {
  let first = field_rank(q.start)
  let last = match q.end {
    Some(f) => field_rank(f)
    None => first
  }
  if last < first || (first <= 1) != (last <= 1) {
    raise Reason("an interval qualifier that is not valid")
  }
  let fields = []
  for rank in first..<=last {
    fields.push(rank_field(rank))
  }
  fields
}

///|
/// The largest value of a field that is not the first.
fn field_limit(f : @ast.DatetimeField) -> Int32 {
  match f {
    Month => 11
    Hour => 23
    _ => 59
  }
}

///|
/// The magnitude of the numbers in an interval text, by field, in the order
/// of the fields, and the nanoseconds.
fn interval_values(
  text : String,
  q : @ast.IntervalQualifier,
  fields : Array[@ast.DatetimeField],
) -> (Bool, Array[Int64], Int64) raise Malformed {
  let c = Cursor(text)
  let negative = c.eat('-')
  if !negative {
    ignore(c.eat('+'))
  }
  let leading = q.start_precision.unwrap_or(2)
  let fraction_digits = q.fractional_precision.unwrap_or(6)
  if fraction_digits < 0 || fraction_digits > 9 {
    raise Reason("a fractional precision out of range")
  }
  let values = []
  let mut nanos = 0L
  for i, f in fields {
    if i > 0 {
      c.expect(separator(fields[i - 1]))
    }
    let digits = c.run()
    if digits.length() == 0 || digits.length() > 18 {
      raise Reason("expected a number")
    }
    if i == 0 && digits.length() > leading {
      raise Reason("more than \{leading} digits in the leading field")
    }
    let value = digits_value(digits)
    if i > 0 && value > field_limit(f).to_int64() {
      raise Reason("a field out of range")
    }
    values.push(value)
    if f is Second && i == fields.length() - 1 && c.eat('.') {
      let fraction = c.run()
      if fraction.length() == 0 || fraction.length() > 9 {
        raise Reason("a fraction of 1 to 9 digits")
      }
      let kept = fraction
        .view(end_offset=fraction.length().min(fraction_digits))
        .to_owned()
      let padded = kept + String::make(9 - kept.length(), '0')
      nanos = digits_value(padded)
    }
  }
  c.finish()
  (negative, values, nanos)
}

///|
fn literal_failure(what : String, text : String, reason : String) -> OpFailure {
  TypeFailure("invalid \{what} literal '\{text}': \{reason}")
}

///|
/// `YYYY-MM-DD`.
pub fn parse_date(text : String) -> @value.Date raise OpFailure {
  try {
    let c = Cursor(text)
    let d = date_part(c)
    c.finish()
    d
  } catch {
    Reason(reason) => raise literal_failure("date", text, reason)
    e => raise literal_failure("date", text, @debug.to_string(e))
  }
}

///|
/// `HH:MM:SS[.f*][(+|-)HH:MM]`. Without `with_tz` the offset is optional.
/// See `time_part` for the precision.
pub fn parse_time(
  text : String,
  precision : Int32?,
  with_tz : Bool,
) -> @value.Time raise OpFailure {
  try {
    let c = Cursor(text)
    let (t, up) = time_part(c, precision, with_tz)
    c.finish()
    if up > 0 {
      shift_time(t, up.to_int64())
    } else {
      t
    }
  } catch {
    Reason(reason) => raise literal_failure("time", text, reason)
    e => raise literal_failure("time", text, @debug.to_string(e))
  }
}

///|
/// `YYYY-MM-DD HH:MM:SS[.f*][(+|-)HH:MM]`; `T` may stand for the space.
pub fn parse_timestamp(
  text : String,
  precision : Int32?,
  with_tz : Bool,
) -> @value.Timestamp raise OpFailure {
  try {
    let c = Cursor(text)
    let d = date_part(c)
    if !c.eat(' ') {
      c.expect('T')
    }
    let (t, up) = time_part(c, precision, with_tz)
    c.finish()
    let ts = @value.Timestamp::Timestamp(d, t)
    if up > 0 {
      shift_timestamp(ts, up.to_int64())
    } else {
      ts
    }
  } catch {
    Reason(reason) => raise literal_failure("timestamp", text, reason)
    e => raise literal_failure("timestamp", text, @debug.to_string(e))
  }
}

///|
/// `'3'`, `'1-5'`, `'2 4:5:10.5'`, ... by the qualifier. `negative` is the
/// sign before the text; a sign inside the text turns it again.
pub fn parse_interval(
  text : String,
  negative~ : Bool,
  qualifier~ : @ast.IntervalQualifier,
) -> @value.Value raise OpFailure {
  try {
    // The body of the old `interval_literal` from `let fields = ...` to the
    // end of its `try` block, unchanged.
    let fields = interval_fields(qualifier)
    let (inner, values, nanos) = interval_values(text, qualifier, fields)
    let negative = negative != inner
    let mut years = 0L
    let mut months = 0L
    let mut days = 0L
    let mut hours = 0L
    let mut minutes = 0L
    let mut seconds = 0L
    for i, f in fields {
      match f {
        Year => years = values[i]
        Month => months = values[i]
        Day => days = values[i]
        Hour => hours = values[i]
        Minute => minutes = values[i]
        Second => seconds = values[i]
        _ => ()
      }
    }
    if fields[0] is (Year | Month) {
      @value.Value::from_interval_ym(
        @value.IntervalYM::from_parts(negative~, years~, months~),
      )
    } else {
      @value.Value::from_interval_dt(
        @value.IntervalDT::from_parts(
          negative~,
          days~,
          hours~,
          minutes~,
          seconds~,
          nanos~,
        ),
      )
    }
  } catch {
    Reason(reason) => raise literal_failure("interval", text, reason)
    e => raise literal_failure("interval", text, @debug.to_string(e))
  }
}

///|
/// Whether an interval qualifier names its fields in order and of one class.
pub fn interval_qualifier_valid(q : @ast.IntervalQualifier) -> Bool {
  try interval_fields(q) catch {
    _ => false
  } noraise {
    _ => true
  }
}