///|
/// Offline playback over a pure scene and a validated source-bound plan. The
/// earlier first-flow plan remains a compatible optional input.
pub struct AnimatedRenderer {
  plan : @animation.MotionPlan?
  playback : @animation.PlaybackPlan?
  svg : @svg.SvgRenderer
  limits : @animation.Limits
  query_limits : @animation.Limits
  sequence_view_id : String?
  output_units : Int32
}

///|
pub fn AnimatedRenderer::new(
  plan? : @animation.MotionPlan,
  playback? : @animation.PlaybackPlan,
  svg~ : @svg.SvgRenderer = @svg.SvgRenderer::new(),
  limits~ : @animation.Limits = @animation.Limits::new(),
  query_limits~ : @animation.Limits = @animation.Limits::new(),
  sequence_view_id? : String,
  output_units~ : Int32 = 33554432,
) -> AnimatedRenderer {
  { plan, playback, svg, limits, query_limits, sequence_view_id, output_units, }
}

///|
pub impl @render.Renderer for AnimatedRenderer with fn render(self, diagram) {
  fn failed(code : String, message : String) -> @render.Rendered {
    @render.Rendered::failed("text/html", [
      @diagrams.Diagnostic::error(code, message),
    ])
  }
  if self.plan != None && self.playback != None {
    return failed("animation/invalid-plan", "Supply one playback plan")
  }
  if !self.limits.valid() || !self.query_limits.valid() {
    return failed(
      "animation/plan-limit", "Animation and query limits must be valid",
    )
  }
  let budget = HtmlBudget::{ limit: self.output_units, used: 0, failed: false, }
  if self.output_units <= 0 ||
    !budget.reserve(player_source.length() + player_style.length() + 10000) {
    return failed("animation/render-limit", "HTML output limit is too small")
  }
  let view_id = self.sequence_view_id.unwrap_or_else(() => {
    self.playback.map(p => p.view_id).unwrap_or("sequence")
  })
  let result = self.svg.scene(
    diagram,
    sequence_view_id=view_id,
    limits=self.limits,
    output_units=self.output_units,
  )
  guard result.scene is Some(scene) else {
    return @render.Rendered::failed("text/html", result.diagnostics)
  }
  let planned = match self.playback {
    Some(plan) => {
      if !plan.matches(scene.catalog) {
        return failed(
          "animation/plan-mismatch", "Plan and scene name different source diagrams",
        )
      }
      Some(plan)
    }
    None => None
  }
  let plan = match planned {
    Some(plan) => plan
    None => {
      let planned = match self.plan {
        Some(legacy) => {
          if !(diagram is Bpmn(_)) || legacy.view_id != scene.catalog.view_id {
            return failed(
              "animation/plan-mismatch", "The legacy plan must name this BPMN view",
            )
          }
          guard scene.catalog.flow(legacy.flow_id) is Some(occurrence) else {
            return failed(
              "animation/plan-mismatch", "Legacy plan names no visible connection",
            )
          }
          let finite = @animation.PlaybackPlan::scenario(
            scene.catalog,
            Step(occurrence.key, legacy.duration_ms.to_int64()),
            limits=self.limits,
          )
          guard finite.plan is Some(finite) else {
            return @render.Rendered::failed("text/html", finite.diagnostics)
          }
          finite.repeating()
        }
        None =>
          @animation.PlaybackPlan::illustrative(
            scene.catalog,
            limits=self.limits,
          )
      }
      guard planned.plan is Some(plan) else {
        return @render.Rendered::failed("text/html", planned.diagnostics)
      }
      plan
    }
  }
  if !budget.reserve(scene.display_svg.length()) {
    return failed(
      "animation/render-limit", "Display SVG exceeds the HTML output limit",
    )
  }
  let data = playback_data(
    scene,
    plan,
    self.query_limits,
    self.limits.steps,
    budget,
  )
  if budget.failed {
    return failed(
      "animation/render-limit", "Embedded playback data exceeds the HTML output limit",
    )
  }
  let encoded = safe_script_json(data)
  let explanation = if plan.mode == Illustrative {
    "Illustrative flow. Conditions, events and gateway synchronization are not evaluated."
  } else {
    "Explicit scenario. Selected connections illustrate the supplied order; no process execution is inferred."
  }
  let html = "\nDiagram flow illustration\n
\n

\{explanation}

\n
\n\n\n\n\n\n0 / \{plan.duration_ms} ms\n\n
\n
Structural exploration\n\n\n\n\n\n\n\n\n\n\n
\n

Paused

\n
\{scene.display_svg}
\n\n\n
\n" if html.length() > self.output_units { return failed("animation/render-limit", "HTML output limit exceeded") } @render.Rendered::ok(html, "text/html", diagnostics=result.diagnostics) } ///| pub extend AnimatedRenderer with @render.Renderer::{render}