///|
pub fn Deadline::calculate(
  self : Deadline,
) -> Result[DeadlineResult, DeadlineError] {
  let steps = Array::new()
  steps.push({ kind: Start, date: self.start_date, note: "start date" })
  let original = match
    evaluate_rule(
      self.rule,
      self.start_date,
      self.start_rule,
      self.calendar,
      steps,
    ) {
    Ok(date) => date
    Err(error) => return Err(error)
  }
  let adjusted = match
    apply_extension(self.extension, original, self.calendar, steps) {
    Ok(date) => date
    Err(error) => return Err(error)
  }
  if adjusted == original {
    steps.push({ kind: Complete, date: adjusted, note: "deadline" })
  } else {
    steps.push({ kind: Complete, date: adjusted, note: "adjusted deadline" })
  }
  Ok({
    start: self.start_date,
    rule: self.rule,
    original,
    adjusted,
    adjustment_count: if adjusted == original {
      0
    } else {
      1
    },
    crossed_month: self.start_date.month != adjusted.month,
    crossed_year: self.start_date.year != adjusted.year,
    steps: steps.copy(),
    explanation: render_steps(steps),
  })
}

///|
fn evaluate_rule(
  rule : DateRule,
  start : Date,
  start_rule : StartDateRule,
  calendar : Calendar,
  steps : Array[CalculationStep],
) -> Result[Date, DeadlineError] {
  match rule {
    Sequence(rules) => {
      let mut current = start
      let mut first = true
      for item in rules {
        let policy = if first { start_rule } else { Include }
        current = match evaluate_rule(item, current, policy, calendar, steps) {
          Ok(date) => date
          Err(error) => return Err(error)
        }
        first = false
      }
      Ok(current)
    }
    CalendarDays(count) => {
      if count < 0 {
        return Err(NegativeRuleCount)
      }
      let include_start = start_rule == Include
      let offset = if include_start && count > 0 { count - 1 } else { count }
      match start.try_add_days(offset) {
        Ok(result) => {
          steps.push({
            kind: Count,
            date: result,
            note: "calendar days: \{count}",
          })
          Ok(result)
        }
        Err(error) => Err(DateFailure(error))
      }
    }
    BusinessDays(count) => {
      if count < 0 {
        return Err(NegativeRuleCount)
      }
      let include_start = start_rule == Include
      match calendar.advance_business_days(start, count, include_start~) {
        Ok(result) => {
          steps.push({
            kind: Count,
            date: result,
            note: "business days: \{count}",
          })
          Ok(result)
        }
        Err(error) => Err(CalendarFailure(error))
      }
    }
    Weeks(count) => {
      if count < 0 {
        return Err(NegativeRuleCount)
      }
      evaluate_rule(CalendarDays(count * 7), start, start_rule, calendar, steps)
    }
    Months(count) => {
      if count < 0 {
        return Err(NegativeRuleCount)
      }
      let offset = if start_rule == Include && count > 0 {
        count - 1
      } else {
        count
      }
      match start.try_add_months(offset) {
        Ok(result) => {
          steps.push({
            kind: Count,
            date: result,
            note: "calendar months: \{count}",
          })
          Ok(result)
        }
        Err(error) => Err(DateFailure(error))
      }
    }
    Years(count) => {
      if count < 0 {
        return Err(NegativeRuleCount)
      }
      let offset = if start_rule == Include && count > 0 {
        count - 1
      } else {
        count
      }
      match start.try_add_years(offset) {
        Ok(result) => {
          steps.push({
            kind: Count,
            date: result,
            note: "calendar years: \{count}",
          })
          Ok(result)
        }
        Err(error) => Err(DateFailure(error))
      }
    }
  }
}

///|
fn apply_extension(
  policy : ExtensionPolicy,
  original : Date,
  calendar : Calendar,
  steps : Array[CalculationStep],
) -> Result[Date, DeadlineError] {
  match policy {
    NoExtension => Ok(original)
    Following =>
      if calendar.is_business_day(original) {
        Ok(original)
      } else {
        match calendar.next_business_day(original) {
          Ok(result) => {
            steps.push({
              kind: Extension,
              date: result,
              note: "following business day",
            })
            Ok(result)
          }
          Err(error) => Err(CalendarFailure(error))
        }
      }
    Preceding =>
      if calendar.is_business_day(original) {
        Ok(original)
      } else {
        match calendar.previous_business_day(original) {
          Ok(result) => {
            steps.push({
              kind: Extension,
              date: result,
              note: "preceding business day",
            })
            Ok(result)
          }
          Err(error) => Err(CalendarFailure(error))
        }
      }
    ModifiedFollowing =>
      if calendar.is_business_day(original) {
        Ok(original)
      } else {
        match calendar.next_business_day(original) {
          Ok(next) =>
            if next.month != original.month {
              match calendar.previous_business_day(original) {
                Ok(result) => {
                  steps.push({
                    kind: Extension,
                    date: result,
                    note: "modified following: preceding business day",
                  })
                  Ok(result)
                }
                Err(error) => Err(CalendarFailure(error))
              }
            } else {
              steps.push({
                kind: Extension,
                date: next,
                note: "modified following business day",
              })
              Ok(next)
            }
          Err(error) => Err(CalendarFailure(error))
        }
      }
  }
}

///|
fn render_steps(steps : Array[CalculationStep]) -> String {
  let mut text = ""
  for step in steps {
    text = text + step.date.to_string() + " — " + step.note + "\n"
  }
  text
}