///|
pub let max_duration_ms : Int64 = 9007199254740991L

///|
pub struct Limits {
  nodes : Int32
  edges : Int32
  steps : Int32
  results : Int32
  work : Int32
  depth : Int32
} derive(Eq, Debug)

///|
pub fn Limits::new(
  nodes~ : Int32 = 10000,
  edges~ : Int32 = 50000,
  steps~ : Int32 = 100000,
  results~ : Int32 = 10000,
  work~ : Int32 = 1000000,
  depth~ : Int32 = 256,
) -> Limits {
  { nodes, edges, steps, results, work, depth, }
}

///|
pub fn Limits::valid(self : Limits) -> Bool {
  self.nodes > 0 &&
  self.edges > 0 &&
  self.steps > 0 &&
  self.steps <= 100000 &&
  self.results > 0 &&
  self.work > 0 &&
  self.depth > 0
}

///|
pub(all) enum OccurrenceKind {
  Bpmn(@bpmn.ConnectionKind)
  SequenceMessage
  Swimlane(@swimlane.FlowDirection)
} derive(Eq, Debug)

///|
pub struct Occurrence {
  key : String
  index : Int32
  kind : OccurrenceKind
  path : ReadOnlyArray[Int32]
  source : @bpmn.SourceIdentity?
  flow : @bpmn.Flow?
  message : @sequence.Message?
  swimlane_connection : @swimlane.Connection?
} derive(Eq, Debug)

///|
pub(all) enum Control {
  Message(Int32)
  Frame(Int32)
} derive(Eq, Debug)

///|
pub struct SectionInfo {
  label : String?
  controls : ReadOnlyArray[Control]
} derive(Eq, Debug)

///|
pub struct FrameInfo {
  key : String
  path : ReadOnlyArray[Int32]
  kind : @sequence.FrameKind
  sections : ReadOnlyArray[SectionInfo]
} derive(Eq, Debug)

///|
pub(all) enum NodeKind {
  Bpmn(@bpmn.NodeKind)
  Message
  FrameEnter
  FrameExit
  Start
  End
  SwimlaneNode
  SwimlaneGroup
  SwimlaneLane
} derive(Eq, Debug)

///|
pub struct GraphNode {
  id : String
  label : String
  kind : NodeKind
  occurrence : String?
  source_node : @bpmn.Node?
  swimlane_node : @swimlane.Node?
} derive(Eq, Debug)

///|
pub(all) enum EdgeKind {
  SequenceFlow
  MessageFlow
  Order
  SwimlaneFlow
  Undirected
} derive(Eq, Debug)

///|
pub struct GraphEdge {
  kind : EdgeKind
  source : String
  target : String
  occurrence : String?
  reversed : Bool
} derive(Eq, Debug)

///|
pub struct Graph {
  view_id : String
  nodes : ReadOnlyArray[GraphNode]
  edges : ReadOnlyArray[GraphEdge]
  priv source : @render.Diagram
  priv indices : Map[String, Int32]
  priv outside : Map[String, Bool]
  priv outgoing : Map[(Int32, Int32), Array[(Int32, Int32)]]
  priv incoming : Map[(Int32, Int32), Array[(Int32, Int32)]]
}

///|
pub struct Catalog {
  view_id : String
  source : @render.Diagram
  occurrences : ReadOnlyArray[Occurrence]
  frames : ReadOnlyArray[FrameInfo]
  controls : ReadOnlyArray[Control]
  graph : Graph
  priv keys : Map[String, Int32]
  priv flows : Map[String, Int32]
}

///|
pub fn Catalog::occurrence(self : Catalog, key : String) -> Occurrence? {
  self.keys.get(key).map(i => self.occurrences[i])
}

///|
pub fn Catalog::flow(self : Catalog, id : String) -> Occurrence? {
  self.flows.get(id).map(i => self.occurrences[i])
}

///|
pub fn Catalog::connection(self : Catalog, id : String) -> Occurrence? {
  self.flow(id)
}

///|
pub struct CatalogResult {
  catalog : Catalog?
  diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
}

///|
pub(all) enum Motion {
  Step(String, Int64)
  OrientedStep(String, Int64, Bool)
  Serial(ReadOnlyArray[Motion])
  Parallel(ReadOnlyArray[Motion])
} derive(Eq, Debug)

///|
pub fn Motion::serial(children : ArrayView[Motion]) -> Motion {
  Serial(ReadOnlyArray::from_array(children))
}

///|
pub fn Motion::parallel(children : ArrayView[Motion]) -> Motion {
  Parallel(ReadOnlyArray::from_array(children))
}

///|
pub(all) enum Choice {
  Alternative(Int32)
  Repetitions(Int32)
  Optional(Bool)
} derive(Eq, Debug)

///|
pub struct Selection {
  frame_key : String
  choice : Choice
} derive(Eq, Debug)

///|
pub fn Selection::new(frame_key : String, choice : Choice) -> Selection {
  { frame_key, choice, }
}

///|
pub(all) enum PlaybackMode {
  Finite
  Illustrative
} derive(Eq, Debug)

///|
pub struct ScheduledStep {
  ordinal : Int32
  occurrence : Occurrence
  start_ms : Int64
  duration_ms : Int64
  concurrent_groups : ReadOnlyArray[Int32]
  reversed : Bool
} derive(Eq, Debug)

///|
pub struct PlaybackPlan {
  view_id : String
  mode : PlaybackMode
  duration_ms : Int64
  steps : ReadOnlyArray[ScheduledStep]
  priv source : @render.Diagram
} derive(Eq, Debug)

///|
pub fn PlaybackPlan::matches(self : PlaybackPlan, catalog : Catalog) -> Bool {
  self.view_id == catalog.view_id && self.source == catalog.source
}

///|
pub struct PlanResult {
  plan : PlaybackPlan?
  diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
}

///|
pub struct Active {
  ordinal : Int32
  key : String
  numerator : Int64
  denominator : Int64
  concurrent_groups : ReadOnlyArray[Int32]
  reversed : Bool
} derive(Eq, Debug)

///|
pub struct Sample {
  elapsed_ms : Int64
  duration_ms : Int64
  finished : Bool
  active : ReadOnlyArray[Active]
} derive(Eq, Debug)

///|
pub fn PlaybackPlan::sample_at(
  self : PlaybackPlan,
  elapsed_ms : Int64,
) -> Sample {
  let nonnegative = Int64::max(0L, elapsed_ms)
  let elapsed = if self.mode == Illustrative && self.duration_ms > 0L {
    nonnegative % self.duration_ms
  } else {
    Int64::min(nonnegative, self.duration_ms)
  }
  let active = []
  for step in self.steps {
    if elapsed >= step.start_ms && elapsed < step.start_ms + step.duration_ms {
      active.push({
        ordinal: step.ordinal,
        key: step.occurrence.key,
        numerator: elapsed - step.start_ms,
        denominator: step.duration_ms,
        concurrent_groups: step.concurrent_groups,
        reversed: step.reversed,
      })
    }
  }
  {
    elapsed_ms: elapsed,
    duration_ms: self.duration_ms,
    finished: self.mode == Finite && elapsed >= self.duration_ms,
    active: ReadOnlyArray::from_array(active),
  }
}

///|
pub(all) enum Direction {
  Forward
  Reverse
  Both
} derive(Eq, Debug)

///|
pub(all) enum LimitKind {
  Nodes
  Edges
  Results
  Work
  Steps
  Depth
} derive(Eq, Debug)

///|
pub(all) enum QueryFailure {
  InvalidEndpoint(String)
  OutsideView(String)
  InvalidLimits
  Limit(LimitKind)
} derive(Eq, Debug)

///|
pub struct Route {
  view_id : String
  direction : Direction
  nodes : ReadOnlyArray[GraphNode]
  edges : ReadOnlyArray[GraphEdge]
  reversed : ReadOnlyArray[Bool]
  priv source : @render.Diagram
} derive(Eq, Debug)

///|
pub(all) enum RouteResult {
  Found(Route)
  Unreachable
  Failed(QueryFailure)
} derive(Eq, Debug)

///|
pub(all) enum ReachabilityResult {
  Complete(ReadOnlyArray[GraphNode])
  Failed(QueryFailure)
} derive(Eq, Debug)

///|
priv struct Budget {
  limits : Limits
  diagnostics : Array[@diagrams.Diagnostic]
  mut work : Int32
  mut failed : Bool
}

///|
fn Budget::error(self : Budget, code : String, message : String) -> Unit {
  if !self.failed {
    self.diagnostics.push(@diagrams.Diagnostic::error(code, message))
  }
  self.failed = true
}

///|
fn Budget::charge(self : Budget, units : Int32) -> Bool {
  if units < 0 || units > self.limits.work - self.work {
    self.error("animation/plan-limit", "Animation work limit exceeded")
    return false
  }
  self.work += units
  true
}

///|
fn path_key(prefix : String, path : ArrayView[Int32]) -> String {
  let result = StringBuilder()
  result.write_string(prefix)
  for i, index in path {
    if i > 0 {
      result.write_char('/')
    }
    result.write_string(index.to_string())
  }
  result.to_string()
}

///|
fn flow_key(view : String, id : String) -> String {
  "bpmn:\{view.length()}:\{view}:\{id.length()}:\{id}"
}

///|
pub extend Limits with Eq::{equal, not_equal}

///|
pub extend Limits with @debug.Debug::{to_repr}

///|
pub extend OccurrenceKind with Eq::{equal, not_equal}

///|
pub extend OccurrenceKind with @debug.Debug::{to_repr}

///|
pub extend Occurrence with Eq::{equal, not_equal}

///|
pub extend Occurrence with @debug.Debug::{to_repr}

///|
pub extend Control with Eq::{equal, not_equal}

///|
pub extend Control with @debug.Debug::{to_repr}

///|
pub extend SectionInfo with Eq::{equal, not_equal}

///|
pub extend SectionInfo with @debug.Debug::{to_repr}

///|
pub extend FrameInfo with Eq::{equal, not_equal}

///|
pub extend FrameInfo with @debug.Debug::{to_repr}

///|
pub extend NodeKind with Eq::{equal, not_equal}

///|
pub extend NodeKind with @debug.Debug::{to_repr}

///|
pub extend GraphNode with Eq::{equal, not_equal}

///|
pub extend GraphNode with @debug.Debug::{to_repr}

///|
pub extend EdgeKind with Eq::{equal, not_equal}

///|
pub extend EdgeKind with @debug.Debug::{to_repr}

///|
pub extend GraphEdge with Eq::{equal, not_equal}

///|
pub extend GraphEdge with @debug.Debug::{to_repr}

///|
pub extend Motion with Eq::{equal, not_equal}

///|
pub extend Motion with @debug.Debug::{to_repr}

///|
pub extend Choice with Eq::{equal, not_equal}

///|
pub extend Choice with @debug.Debug::{to_repr}

///|
pub extend Selection with Eq::{equal, not_equal}

///|
pub extend Selection with @debug.Debug::{to_repr}

///|
pub extend PlaybackMode with Eq::{equal, not_equal}

///|
pub extend PlaybackMode with @debug.Debug::{to_repr}

///|
pub extend ScheduledStep with Eq::{equal, not_equal}

///|
pub extend ScheduledStep with @debug.Debug::{to_repr}

///|
pub extend PlaybackPlan with Eq::{equal, not_equal}

///|
pub extend PlaybackPlan with @debug.Debug::{to_repr}

///|
pub extend Active with Eq::{equal, not_equal}

///|
pub extend Active with @debug.Debug::{to_repr}

///|
pub extend Sample with Eq::{equal, not_equal}

///|
pub extend Sample with @debug.Debug::{to_repr}

///|
pub extend Direction with Eq::{equal, not_equal}

///|
pub extend Direction with @debug.Debug::{to_repr}

///|
pub extend LimitKind with Eq::{equal, not_equal}

///|
pub extend LimitKind with @debug.Debug::{to_repr}

///|
pub extend QueryFailure with Eq::{equal, not_equal}

///|
pub extend QueryFailure with @debug.Debug::{to_repr}

///|
pub extend Route with Eq::{equal, not_equal}

///|
pub extend Route with @debug.Debug::{to_repr}

///|
pub extend RouteResult with Eq::{equal, not_equal}

///|
pub extend RouteResult with @debug.Debug::{to_repr}

///|
pub extend ReachabilityResult with Eq::{equal, not_equal}

///|
pub extend ReachabilityResult with @debug.Debug::{to_repr}