///|
/// The semantic domain in which a cron field is evaluated.
pub(all) enum FieldDomain {
  MinuteDomain
  HourDomain
  DayOfMonthDomain
  MonthDomain
  WeekdayDomain
} derive(Eq, Debug)

///|
/// Programmatic exact-value field. Use `Cron::from_fields` to validate it
/// against its eventual position.
pub fn Field::exact(value : Int) -> Field {
  Exact(value)
}

///|
pub fn Field::any() -> Field {
  Any
}

///|
pub fn Field::every(step : Int) -> Field {
  Every(step)
}

///|
pub fn Field::range(start : Int, end : Int) -> Field {
  Range(start, end)
}

///|
pub fn Field::range_every(start : Int, end : Int, step : Int) -> Field {
  RangeEvery(start, end, step)
}

///|
pub fn Field::list(items : Array[Field]) -> Field {
  List(items)
}

///|
pub fn FieldDomain::bounds(self : FieldDomain) -> (Int, Int) {
  match self {
    MinuteDomain => (0, 59)
    HourDomain => (0, 23)
    DayOfMonthDomain => (1, 31)
    MonthDomain => (1, 12)
    WeekdayDomain => (0, 6)
  }
}

///|
pub fn FieldDomain::name(self : FieldDomain) -> String {
  match self {
    MinuteDomain => "minute"
    HourDomain => "hour"
    DayOfMonthDomain => "day-of-month"
    MonthDomain => "month"
    WeekdayDomain => "weekday"
  }
}

///|
fn Field::matches_domain(
  self : Field,
  value : Int,
  domain : FieldDomain,
) -> Bool {
  let bounds = domain.bounds()
  if domain is WeekdayDomain {
    self.matches(value, lower=bounds.0) ||
    (value == 0 && self.matches(7, lower=bounds.0))
  } else {
    self.matches(value, lower=bounds.0)
  }
}

///|
/// Sorted unique values accepted inside the supplied domain.
pub fn Field::values(self : Field, domain : FieldDomain) -> Array[Int] {
  let bounds = domain.bounds()
  let values : Array[Int] = []
  for value in bounds.0..<=bounds.1 {
    if self.matches_domain(value, domain) {
      values.push(value)
    }
  }
  values
}

///|
pub fn Field::cardinality(self : Field, domain : FieldDomain) -> Int {
  self.values(domain).length()
}

///|
pub fn Field::is_empty_in(self : Field, domain : FieldDomain) -> Bool {
  self.cardinality(domain) == 0
}

///|
pub fn Field::is_full_in(self : Field, domain : FieldDomain) -> Bool {
  let bounds = domain.bounds()
  self.cardinality(domain) == bounds.1 - bounds.0 + 1
}

///|
pub fn Field::first(self : Field, domain : FieldDomain) -> Int? {
  let values = self.values(domain)
  if values.length() == 0 {
    None
  } else {
    Some(values[0])
  }
}

///|
pub fn Field::last(self : Field, domain : FieldDomain) -> Int? {
  let values = self.values(domain)
  if values.length() == 0 {
    None
  } else {
    Some(values[values.length() - 1])
  }
}

///|
/// First accepted value greater than or equal to `value`.
pub fn Field::next_at_or_after(
  self : Field,
  value : Int,
  domain : FieldDomain,
) -> Int? {
  for candidate in self.values(domain) {
    if candidate >= value {
      return Some(candidate)
    }
  }
  None
}

///|
/// Last accepted value less than or equal to `value`.
pub fn Field::previous_at_or_before(
  self : Field,
  value : Int,
  domain : FieldDomain,
) -> Int? {
  let values = self.values(domain)
  for index = values.length() - 1; index >= 0; index = index - 1 {
    if values[index] <= value {
      return Some(values[index])
    }
  }
  None
}

///|
fn contains_int(values : Array[Int], value : Int) -> Bool {
  for item in values {
    if item == value {
      return true
    }
  }
  false
}

///|
fn exact_field_from_values(values : Array[Int], domain : FieldDomain) -> Field {
  let bounds = domain.bounds()
  if values.length() == bounds.1 - bounds.0 + 1 {
    return Any
  }
  if values.length() == 1 {
    return Exact(values[0])
  }
  List(values.map(value => Exact(value)))
}

///|
/// Semantic intersection, independent of the original expression spelling.
pub fn Field::intersection(
  self : Field,
  other : Field,
  domain : FieldDomain,
) -> Field {
  let values : Array[Int] = []
  let other_values = other.values(domain)
  for value in self.values(domain) {
    if contains_int(other_values, value) {
      values.push(value)
    }
  }
  exact_field_from_values(values, domain)
}

///|
/// Semantic union, returned in ascending canonical order.
pub fn Field::union(self : Field, other : Field, domain : FieldDomain) -> Field {
  let values : Array[Int] = []
  let left = self.values(domain)
  let right = other.values(domain)
  let bounds = domain.bounds()
  for value in bounds.0..<=bounds.1 {
    if contains_int(left, value) || contains_int(right, value) {
      values.push(value)
    }
  }
  exact_field_from_values(values, domain)
}

///|
/// Values accepted by `self` but rejected by `other`.
pub fn Field::difference(
  self : Field,
  other : Field,
  domain : FieldDomain,
) -> Field {
  let values : Array[Int] = []
  let other_values = other.values(domain)
  for value in self.values(domain) {
    if !contains_int(other_values, value) {
      values.push(value)
    }
  }
  exact_field_from_values(values, domain)
}

///|
pub fn Field::overlaps(
  self : Field,
  other : Field,
  domain : FieldDomain,
) -> Bool {
  !self.intersection(other, domain).is_empty_in(domain)
}

///|
pub fn Field::is_subset_of(
  self : Field,
  other : Field,
  domain : FieldDomain,
) -> Bool {
  let other_values = other.values(domain)
  for value in self.values(domain) {
    if !contains_int(other_values, value) {
      return false
    }
  }
  true
}

///|
pub fn Field::equivalent_to(
  self : Field,
  other : Field,
  domain : FieldDomain,
) -> Bool {
  self.values(domain) == other.values(domain)
}

///|
/// A compact arithmetic-run representation of a semantic field.
pub fn Field::normalized(self : Field, domain : FieldDomain) -> Field {
  let values = self.values(domain)
  let bounds = domain.bounds()
  if values.length() == 0 {
    return List([])
  }
  if values.length() == bounds.1 - bounds.0 + 1 {
    return Any
  }
  if values.length() == 1 {
    return Exact(values[0])
  }
  let parts : Array[Field] = []
  let mut index = 0
  for ;; {
    if index >= values.length() {
      break
    }
    if index + 2 < values.length() {
      let step = values[index + 1] - values[index]
      let mut end_index = index + 1
      for ;; {
        if end_index + 1 >= values.length() ||
          values[end_index + 1] - values[end_index] != step {
          break
        }
        end_index += 1
      }
      if end_index - index + 1 >= 3 {
        if step == 1 {
          parts.push(Range(values[index], values[end_index]))
        } else {
          parts.push(RangeEvery(values[index], values[end_index], step))
        }
        index = end_index + 1
        continue
      }
    }
    parts.push(Exact(values[index]))
    index += 1
  }
  if parts.length() == 1 {
    parts[0]
  } else {
    List(parts)
  }
}

///|
fn Field::validate_in(
  self : Field,
  domain : FieldDomain,
) -> Result[Unit, CronError] {
  let bounds = domain.bounds()
  let fail = () => {
    Err(InvalidField(domain.name() + ": " + self.to_expression()))
  }
  match self {
    Any => Ok(())
    Exact(value) =>
      if domain is WeekdayDomain && value == 7 {
        Ok(())
      } else if value >= bounds.0 && value <= bounds.1 {
        Ok(())
      } else {
        fail()
      }
    Every(step) =>
      if step >= 1 && step <= bounds.1 - bounds.0 + 1 {
        Ok(())
      } else {
        fail()
      }
    Range(start, end) =>
      if start >= bounds.0 && end <= bounds.1 && start <= end {
        Ok(())
      } else if domain is WeekdayDomain &&
        start >= 0 &&
        end <= 7 &&
        start <= end {
        Ok(())
      } else {
        fail()
      }
    RangeEvery(start, end, step) =>
      if start >= bounds.0 &&
        end <= bounds.1 &&
        start <= end &&
        step >= 1 &&
        step <= bounds.1 - bounds.0 + 1 {
        Ok(())
      } else if domain is WeekdayDomain &&
        start >= 0 &&
        end <= 7 &&
        start <= end &&
        step >= 1 &&
        step <= 8 {
        Ok(())
      } else {
        fail()
      }
    List(items) => {
      if items.length() == 0 {
        return fail()
      }
      for item in items {
        match item.validate_in(domain) {
          Err(error) => return Err(error)
          Ok(_) => ()
        }
      }
      Ok(())
    }
  }
}

///|
/// Validate a programmatically constructed schedule.
pub fn Cron::validate(self : Cron) -> Result[Unit, CronError] {
  match
    (
      self.minute.validate_in(MinuteDomain),
      self.hour.validate_in(HourDomain),
      self.day_of_month.validate_in(DayOfMonthDomain),
      self.month.validate_in(MonthDomain),
      self.weekday.validate_in(WeekdayDomain),
    ) {
    (Ok(_), Ok(_), Ok(_), Ok(_), Ok(_)) => Ok(())
    (Err(error), _, _, _, _) => Err(error)
    (_, Err(error), _, _, _) => Err(error)
    (_, _, Err(error), _, _) => Err(error)
    (_, _, _, Err(error), _) => Err(error)
    (_, _, _, _, Err(error)) => Err(error)
  }
}

///|
/// Construct and validate a schedule from programmatic fields.
pub fn Cron::from_fields(
  minute : Field,
  hour : Field,
  day_of_month : Field,
  month : Field,
  weekday : Field,
) -> Result[Cron, CronError] {
  let cron : Cron = { minute, hour, day_of_month, month, weekday }
  match cron.validate() {
    Ok(_) => Ok(cron)
    Err(error) => Err(error)
  }
}

///|
/// Normalize every field while preserving matching semantics.
pub fn Cron::normalized(self : Cron) -> Cron {
  {
    minute: self.minute.normalized(MinuteDomain),
    hour: self.hour.normalized(HourDomain),
    day_of_month: self.day_of_month.normalized(DayOfMonthDomain),
    month: self.month.normalized(MonthDomain),
    weekday: self.weekday.normalized(WeekdayDomain),
  }
}

///|
/// Structural facts useful for diagnostics and user interfaces.
pub struct CronFieldSummary {
  minute_values : Int
  hour_values : Int
  day_of_month_values : Int
  month_values : Int
  weekday_values : Int
  unrestricted_fields : Int
} derive(Eq, Debug)

///|
pub fn Cron::field_summary(self : Cron) -> CronFieldSummary {
  let domains = [
    (self.minute, MinuteDomain),
    (self.hour, HourDomain),
    (self.day_of_month, DayOfMonthDomain),
    (self.month, MonthDomain),
    (self.weekday, WeekdayDomain),
  ]
  let mut unrestricted = 0
  for item in domains {
    if item.0.is_full_in(item.1) {
      unrestricted += 1
    }
  }
  {
    minute_values: self.minute.cardinality(MinuteDomain),
    hour_values: self.hour.cardinality(HourDomain),
    day_of_month_values: self.day_of_month.cardinality(DayOfMonthDomain),
    month_values: self.month.cardinality(MonthDomain),
    weekday_values: self.weekday.cardinality(WeekdayDomain),
    unrestricted_fields: unrestricted,
  }
}

///|
/// Coarse upper bound on runs in a 24-hour day, ignoring date fields.
pub fn Cron::maximum_daily_runs(self : Cron) -> Int {
  self.minute.cardinality(MinuteDomain) * self.hour.cardinality(HourDomain)
}

///|
/// True when two schedules can share at least one minute/hour/month and
/// calendar selector. This is a fast preflight; use occurrence queries for
/// an exact answer inside a bounded date range.
pub fn Cron::may_overlap(self : Cron, other : Cron) -> Bool {
  if !self.minute.overlaps(other.minute, MinuteDomain) ||
    !self.hour.overlaps(other.hour, HourDomain) ||
    !self.month.overlaps(other.month, MonthDomain) {
    return false
  }
  self.day_of_month.overlaps(other.day_of_month, DayOfMonthDomain) ||
  self.weekday.overlaps(other.weekday, WeekdayDomain)
}