///|
priv struct Timing {
  action : Motion
  children : Array[Int32]
  groups : ReadOnlyArray[Int32]
  mut duration : Int64
  mut start : Int64
}

///|
/// Validate a finite, explicit scenario. Parallel children share a start and the
/// following serial item waits for the longest branch, without stage barriers.
pub fn PlaybackPlan::scenario(
  catalog : Catalog,
  motion : Motion,
  limits~ : Limits = Limits::new(),
) -> PlanResult {
  let budget = Budget::{ limits, diagnostics: [], work: 0, failed: false, }
  fn fail() -> PlanResult {
    { plan: None, diagnostics: ReadOnlyArray::from_array(budget.diagnostics), }
  }
  if !limits.valid() {
    budget.error(
      "animation/plan-limit", "Animation limits must be positive and steps at most 100000",
    )
    return fail()
  }
  let timing : Array[Timing] = []
  let stack : Array[(Motion, Int32?, Int32, ReadOnlyArray[Int32])] = [
    (motion, None, 0, []),
  ]
  let mut leaves = 0
  while stack.pop() is Some((action, parent, depth, groups)) {
    if !budget.charge(1 + groups.length()) {
      return fail()
    }
    if depth > limits.depth {
      budget.error("animation/plan-limit", "Scenario depth limit exceeded")
      return fail()
    }
    let index = timing.length()
    timing.push({ action, children: [], groups, duration: 0L, start: 0L, })
    if parent is Some(parent) {
      timing[parent].children.push(index)
    }
    let children : ReadOnlyArray[Motion] = match action {
      Step(key, duration) | OrientedStep(key, duration, _) => {
        if !budget.charge(key.length()) {
          return fail()
        }
        if catalog.occurrence(key) == None {
          budget.error(
            "animation/invalid-identity", "Scenario names an occurrence outside its catalog",
          )
          return fail()
        }
        if duration <= 0L || duration > max_duration_ms {
          budget.error(
            "animation/invalid-duration", "Step duration must be positive and exactly representable in JavaScript",
          )
          return fail()
        }
        if leaves >= limits.steps {
          budget.error("animation/plan-limit", "Scenario step limit exceeded")
          return fail()
        }
        leaves += 1
        timing[index].duration = duration
        []
      }
      Serial(children) | Parallel(children) => children
    }
    let groups = match action {
      Parallel(_) =>
        ReadOnlyArray::from_array(
          groups.iter().concat([index].iter()).to_array(),
        )
      _ => groups
    }
    if children.length() > limits.work - budget.work {
      budget.error(
        "animation/plan-limit", "Scenario size exceeds its work limit",
      )
      return fail()
    }
    for i in 0.. ()
      Serial(_) => {
        let mut total = 0L
        for child in node.children {
          if !budget.charge(1) {
            return fail()
          }
          let duration = timing[child].duration
          if duration > max_duration_ms - total {
            budget.error(
              "animation/plan-limit", "Total duration exceeds JavaScript's exact integer range",
            )
            return fail()
          }
          total += duration
        }
        node.duration = total
      }
      Parallel(_) =>
        for child in node.children {
          if !budget.charge(1) {
            return fail()
          }
          node.duration = Int64::max(node.duration, timing[child].duration)
        }
    }
  }
  let steps : Array[ScheduledStep] = []
  for node in timing {
    if !budget.charge(1) {
      return fail()
    }
    let mut cursor = node.start
    for child in node.children {
      timing[child].start = cursor
      if node.action is Serial(_) {
        cursor += timing[child].duration
      }
    }
    let leaf = match node.action {
      Step(key, duration) => Some((key, duration, false))
      OrientedStep(key, duration, reversed) => Some((key, duration, reversed))
      _ => None
    }
    if leaf is Some((key, duration, reversed)) {
      steps.push({
        ordinal: steps.length(),
        occurrence: catalog.occurrence(key).unwrap(),
        start_ms: node.start,
        duration_ms: duration,
        concurrent_groups: node.groups,
        reversed,
      })
    }
  }
  {
    plan: Some({
      view_id: catalog.view_id,
      source: catalog.source,
      mode: Finite,
      duration_ms: timing[0].duration,
      steps: ReadOnlyArray::from_array(steps),
    }),
    diagnostics: [],
  }
}

///|
/// The authored catalog is an illustration, including separate alternatives.
/// Repeating it never asserts that these connections form a feasible execution.
pub fn PlaybackPlan::illustrative(
  catalog : Catalog,
  duration_ms~ : Int64 = 1000L,
  kinds~ : ArrayView[@bpmn.ConnectionKind] = [SequenceFlow, MessageFlow],
  limits~ : Limits = Limits::new(),
) -> PlanResult {
  let budget = Budget::{ limits, diagnostics: [], work: 0, failed: false, }
  fn fail() -> PlanResult {
    { plan: None, diagnostics: ReadOnlyArray::from_array(budget.diagnostics), }
  }
  if !limits.valid() {
    budget.error("animation/plan-limit", "Invalid animation limits")
    return fail()
  }
  if duration_ms <= 0L || duration_ms > max_duration_ms {
    budget.error(
      "animation/invalid-duration", "Illustrative duration must be positive and exactly representable in JavaScript",
    )
    return fail()
  }
  let selected_kinds = []
  for kind in kinds {
    if !budget.charge(1) {
      return fail()
    }
    if !selected_kinds.contains(kind) {
      selected_kinds.push(kind)
    }
  }
  let actions = []
  for occurrence in catalog.occurrences {
    if !budget.charge(1) {
      return fail()
    }
    let selected = match occurrence.kind {
      SequenceMessage => true
      Swimlane(_) => true
      Bpmn(kind) => selected_kinds.contains(kind)
    }
    if selected {
      if actions.length() >= limits.steps {
        budget.error("animation/plan-limit", "Illustrative step limit exceeded")
        return fail()
      }
      match occurrence.kind {
        Swimlane(Backward) =>
          actions.push(Motion::OrientedStep(occurrence.key, duration_ms, true))
        _ => actions.push(Motion::Step(occurrence.key, duration_ms))
      }
    }
  }
  if actions.is_empty() {
    return {
      plan: None,
      diagnostics: [
        @diagrams.Diagnostic::error(
          "animation/no-flow", "The selected view has no selected visible connection occurrences",
        ),
      ],
    }
  }
  let result = PlaybackPlan::scenario(catalog, Motion::serial(actions), limits~)
  { ..result, plan: result.plan.map(p => { ..p, mode: Illustrative, }), }
}

///|
/// Repeat a validated nonempty plan without changing its source or timing.
pub fn PlaybackPlan::repeating(self : PlaybackPlan) -> PlanResult {
  if self.duration_ms <= 0L || self.steps.is_empty() {
    return {
      plan: None,
      diagnostics: [
        @diagrams.Diagnostic::error(
          "animation/no-flow", "A repeating plan needs a connection occurrence",
        ),
      ],
    }
  }
  { plan: Some({ ..self, mode: Illustrative, }), diagnostics: [], }
}