// WARC-Date parsing (ISO 28500:2017 clause 5.4).
//
// WARC-Date uses the W3CDTF profile of ISO 8601: a calendar date at
// one of the granularities YYYY, YYYY-MM or YYYY-MM-DD, optionally
// followed by a time of day with one to nine fractional-second digits
// and a mandatory `Z` suffix when the time part is present. Values are
// parsed digit-by-digit over raw bytes; no floating point is involved,
// so fractional seconds are kept as their exact digit string.

///|
/// A parsed WARC-Date. Optional components are absent at coarser
/// granularities; `fraction` is the exact fractional-second digit
/// string ("" when there are no fractional seconds).
pub struct WarcDate {
  year : Int
  month : Int?
  day : Int?
  hour : Int?
  minute : Int?
  second : Int?
  fraction : String
} derive(Eq, @debug.Debug)

///|
/// The days in each month (non-leap year).
fn days_in_month(year : Int, month : Int) -> Int {
  if month == 2 {
    if year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) {
      return 29
    }
    return 28
  }
  let days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
  days[month - 1]
}

///|
/// A structured invalid-date error at a byte offset.
fn bad(pos : Int, record_index : Int64, context : String) -> WarcError {
  WarcError::new(
    WarcErrorStage::Date,
    WarcErrorKind::InvalidDate,
    pos.to_int64(),
    record_index,
    context,
  )
}

///|
/// Read exactly `n` digits at `pos`; on success returns the value and
/// the position past the digits, otherwise a structured error.
fn read_digits(
  data : Bytes,
  pos : Int,
  end : Int,
  n : Int,
  record_index : Int64,
  what : String,
) -> Result[(Int, Int), WarcError] {
  if pos + n > end {
    return Err(bad(pos, record_index, "expected \{n} digits for \{what}"))
  }
  let mut value = 0
  let mut i = pos
  while i < pos + n {
    if !is_digit(data[i]) {
      return Err(bad(i, record_index, "expected \{n} digits for \{what}"))
    }
    value = value * 10 + digit_value(data[i])
    i = i + 1
  }
  Ok((value, pos + n))
}

///|
/// Parse a WARC-Date value (without the field name or line ending).
pub fn parse_warc_date(
  s : String,
  record_index : Int64,
) -> Result[WarcDate, WarcError] {
  let data = @utf8.encode(s)
  parse_warc_date_bytes(data, 0, data.length(), record_index)
}

///|
/// Parse a WARC-Date over a byte span. Shared with the streaming
/// decoder, which works on raw bytes rather than field strings.
pub fn parse_warc_date_bytes(
  data : Bytes,
  start : Int,
  end : Int,
  record_index : Int64,
) -> Result[WarcDate, WarcError] {
  let mut pos = start
  let year_read = read_digits(data, pos, end, 4, record_index, "year")
  let (year, p) = match year_read {
    Ok(x) => x
    Err(e) => return Err(e)
  }
  pos = p
  let mut month : Int? = None
  let mut day : Int? = None
  let mut hour : Int? = None
  let mut minute : Int? = None
  let mut second : Int? = None
  let mut fraction = ""
  if pos < end && data[pos] == b'-' {
    let m_read = read_digits(data, pos + 1, end, 2, record_index, "month")
    let (m, p2) = match m_read {
      Ok(x) => x
      Err(e) => return Err(e)
    }
    if m < 1 || m > 12 {
      return Err(bad(pos, record_index, "month must be between 1 and 12"))
    }
    month = Some(m)
    pos = p2
    if pos < end && data[pos] == b'-' {
      let d_read = read_digits(data, pos + 1, end, 2, record_index, "day")
      let (d, p3) = match d_read {
        Ok(x) => x
        Err(e) => return Err(e)
      }
      if d < 1 || d > days_in_month(year, m) {
        return Err(
          bad(
            pos, record_index, "day is out of range for the given year and month",
          ),
        )
      }
      day = Some(d)
      pos = p3
    }
  }
  if pos < end && data[pos] == b'T' {
    let h_read = read_digits(data, pos + 1, end, 2, record_index, "hour")
    let (h, p4) = match h_read {
      Ok(x) => x
      Err(e) => return Err(e)
    }
    if h > 23 {
      return Err(bad(pos, record_index, "hour must be between 0 and 23"))
    }
    hour = Some(h)
    pos = p4
    if pos < end && data[pos] == b':' {
      let min_read = read_digits(data, pos + 1, end, 2, record_index, "minute")
      let (mi, p5) = match min_read {
        Ok(x) => x
        Err(e) => return Err(e)
      }
      if mi > 59 {
        return Err(bad(pos, record_index, "minute must be between 0 and 59"))
      }
      minute = Some(mi)
      pos = p5
      if pos < end && data[pos] == b':' {
        let s_read = read_digits(data, pos + 1, end, 2, record_index, "second")
        let (sec, p6) = match s_read {
          Ok(x) => x
          Err(e) => return Err(e)
        }
        if sec > 59 {
          return Err(bad(pos, record_index, "second must be between 0 and 59"))
        }
        second = Some(sec)
        pos = p6
        if pos < end && data[pos] == b'.' {
          let fs = pos + 1
          let mut fp = fs
          while fp < end && is_digit(data[fp]) {
            fp = fp + 1
          }
          let flen = fp - fs
          if flen == 0 || flen > 9 {
            return Err(
              bad(
                pos, record_index, "fractional seconds must have 1 to 9 digits",
              ),
            )
          }
          // The bytes are all ASCII digits, so decoding cannot fail.
          fraction = @utf8.decode(data.view(start=fs, end=fp)) catch {
            _ => return Err(bad(fs, record_index, "invalid fractional digits"))
          }
          pos = fp
        }
      }
    }
    // The time form is UTC and must end with Z.
    if pos >= end || data[pos] != b'Z' {
      return Err(bad(pos, record_index, "date-time form must end with Z (UTC)"))
    }
    pos = pos + 1
  }
  if pos != end {
    return Err(bad(pos, record_index, "unexpected trailing characters"))
  }
  Ok(WarcDate::{ year, month, day, hour, minute, second, fraction })
}

///|
/// The four-digit year.
pub fn WarcDate::year(self : WarcDate) -> Int {
  self.year
}

///|
/// The month, present at YYYY-MM granularity and finer.
pub fn WarcDate::month(self : WarcDate) -> Int? {
  self.month
}

///|
/// The day, present at YYYY-MM-DD granularity and finer.
pub fn WarcDate::day(self : WarcDate) -> Int? {
  self.day
}

///|
/// Whether a time of day is present.
pub fn WarcDate::has_time(self : WarcDate) -> Bool {
  self.hour != None
}

///|
/// Whether this is the finest granularity: date plus time plus
/// fractional seconds.
pub fn WarcDate::has_fraction(self : WarcDate) -> Bool {
  self.fraction.length() > 0
}