///|
pub fn DeadlinePlan::calculate(
  self : DeadlinePlan,
) -> Result[PlanResult, PlanError] {
  let stage_results = Array::new()
  let mut previous : DeadlineResult? = None
  for index, stage in self.stages {
    let start_date = match previous {
      Some(result) =>
        match stage.start_source() {
          PreviousAdjusted => result.adjusted_date()
          PreviousOriginal => result.original_date()
        }
      None => self.start_date
    }
    let deadline = Deadline::new(
      start_date~,
      rule=stage.rule(),
      start_rule=stage.start_rule(),
      calendar=stage.calendar(),
      extension=stage.extension(),
    )
    let result = match deadline.calculate() {
      Ok(result) => result
      Err(error) =>
        return Err(StageDeadlineFailure(index, stage.label(), error))
    }
    let result = match stage.suspension() {
      Some(suspension) =>
        match apply_stage_suspension(stage, result, suspension) {
          Ok(suspended) => suspended
          Err(error) =>
            return Err(StageSuspensionFailure(index, stage.label(), error))
        }
      None => result
    }
    stage_results.push({ index, label: stage.label(), result })
    previous = Some(result)
  }
  match previous {
    Some(result) =>
      Ok({
        start_date: self.start_date,
        stages: stage_results.copy(),
        final_original: result.original_date(),
        final_adjusted: result.adjusted_date(),
      })
    None =>
      Ok({
        start_date: self.start_date,
        stages: [],
        final_original: self.start_date,
        final_adjusted: self.start_date,
      })
  }
}

///|
fn apply_stage_suspension(
  stage : PlanStage,
  base : DeadlineResult,
  suspension : SuspensionSet,
) -> Result[DeadlineResult, SuspensionError] {
  let mut current_original = base.original
  let mut current_adjusted = base.adjusted
  let window_start = match suspension_window_start(stage, base.start) {
    Ok(start) => start
    Err(error) => return Err(error)
  }
  let use_business_days = suspension_uses_business_days(stage.rule())
  for ;; {
    if should_abort(suspension, window_start, current_adjusted) {
      return Err(Aborted)
    }
    let delay = if use_business_days {
      suspension_business_days_in_window(
        suspension,
        window_start,
        current_adjusted,
        stage.calendar(),
      )
    } else {
      suspension_days_in_window(suspension, window_start, current_adjusted)
    }
    let shifted_original = match
      shift_suspended_date(
        base.original,
        delay,
        use_business_days~,
        calendar=stage.calendar(),
      ) {
      Ok(date) => date
      Err(error) => return Err(error)
    }
    let extension_steps = Array::new()
    let shifted_adjusted = match
      apply_extension(
        stage.extension(),
        shifted_original,
        stage.calendar(),
        extension_steps,
      ) {
      Ok(date) => date
      Err(_) => return Err(OutOfRange)
    }
    if shifted_original == current_original &&
      shifted_adjusted == current_adjusted {
      return Ok(
        suspended_result(
          base, shifted_original, shifted_adjusted, delay, extension_steps,
        ),
      )
    }
    current_original = shifted_original
    current_adjusted = shifted_adjusted
  }
  Err(OutOfRange)
}

///|
fn suspension_window_start(
  stage : PlanStage,
  start : Date,
) -> Result[Date, SuspensionError] {
  if stage.start_rule() == Include {
    Ok(start)
  } else if suspension_uses_business_days(stage.rule()) {
    match stage.calendar().advance_business_days(start, 1) {
      Ok(date) => Ok(date)
      Err(_) => Err(OutOfRange)
    }
  } else {
    match start.try_add_days(1) {
      Ok(date) => Ok(date)
      Err(_) => Err(OutOfRange)
    }
  }
}

///|
fn suspension_uses_business_days(rule : DateRule) -> Bool {
  match rule {
    BusinessDays(_) => true
    _ => false
  }
}

///|
fn should_abort(suspension : SuspensionSet, start : Date, end : Date) -> Bool {
  match suspension.abort_on() {
    Some(date) => start <= date && date <= end
    None => false
  }
}

///|
fn shift_suspended_date(
  base : Date,
  delay : Int,
  use_business_days~ : Bool,
  calendar~ : Calendar,
) -> Result[Date, SuspensionError] {
  if use_business_days {
    match calendar.advance_business_days(base, delay) {
      Ok(date) => Ok(date)
      Err(_) => Err(OutOfRange)
    }
  } else {
    match base.try_add_days(delay) {
      Ok(date) => Ok(date)
      Err(_) => Err(OutOfRange)
    }
  }
}

///|
fn suspended_result(
  base : DeadlineResult,
  original : Date,
  adjusted : Date,
  delay : Int,
  extension_steps : Array[CalculationStep],
) -> DeadlineResult {
  let steps = base.steps.filter(fn(step) {
    step.kind != Extension && step.kind != Complete
  })
  if delay > 0 {
    steps.push({
      kind: Count,
      date: original,
      note: "suspension days: \{delay}",
    })
  }
  steps.append(extension_steps)
  if adjusted == original {
    steps.push({
      kind: Complete,
      date: adjusted,
      note: "deadline after suspension",
    })
  } else {
    steps.push({
      kind: Complete,
      date: adjusted,
      note: "adjusted deadline after suspension",
    })
  }
  {
    start: base.start,
    rule: base.rule,
    original,
    adjusted,
    adjustment_count: if adjusted == original {
      0
    } else {
      1
    },
    crossed_month: base.start.month != adjusted.month,
    crossed_year: base.start.year != adjusted.year,
    steps: steps.copy(),
    explanation: render_steps(steps),
  }
}