// Dispatch paths as a stack of render continuations.

///|
pub fn DispatchPath::new(items? : Array[DispatchStep]) -> DispatchPath {
  { frames: [{ base: Path::new(), items: items.unwrap_or([]), binds: [], }], }
}

///|
pub fn DispatchPath::of_steps(steps : Array[Step]) -> DispatchPath {
  DispatchPath::new(items=steps.map(s => Plain(step=s, origin=None)))
}

///|
pub fn DispatchPath::concat(
  self : DispatchPath,
  steps : Array[Step],
) -> DispatchPath {
  guard self.frames is [.. init, last] else {
    return DispatchPath::of_steps(steps)
  }
  let items = last.items.copy()
  for s in steps {
    items.push(Plain(step=s, origin=None))
  }
  { frames: [..init, { ..last, items, }], }
}

///|
pub fn DispatchPath::push_item(
  self : DispatchPath,
  item : DispatchStep,
) -> DispatchPath {
  guard self.frames is [.. init, last] else {
    return DispatchPath::new(items=[item])
  }
  let items = last.items.copy()
  items.push(item)
  { frames: [..init, { ..last, items, }], }
}

///|
pub fn DispatchPath::push_bind(
  self : DispatchPath,
  bind : FrameBind,
) -> DispatchPath {
  guard self.frames is [.. init, last] else {
    return DispatchPath::new().push_bind(bind)
  }
  let binds = last.binds.copy()
  // The index is how many items came BEFORE this bind, which is what lets a
  // rebuild interleave the two without the bind having to be an item.
  binds.push((last.items.length(), bind))
  { frames: [..init, { ..last, binds, }], }
}

///|
pub fn DispatchPath::push_frame(
  self : DispatchPath,
  path : Path,
) -> DispatchPath {
  let frames = self.frames.copy()
  frames.push({ base: path, items: [], binds: [], })
  { frames, }
}

///|
pub fn DispatchPath::can_pop(self : DispatchPath) -> Bool {
  match self.frames {
    [.. init, last] => !last.items.is_empty() || !init.is_empty()
    [] => false
  }
}

///|
/// Drop the binds a popped item carried into scope.
///
/// A bind is placed by how many items preceded it, so the ones that came after
/// the item now gone are exactly those whose index exceeds the shortened list.
fn keep_binds(
  binds : Array[(Int, FrameBind)],
  items_len : Int,
) -> Array[(Int, FrameBind)] {
  binds.filter(b => b.0 <= items_len)
}

///|
pub fn DispatchPath::is_root(self : DispatchPath) -> Bool {
  self.to_transaction_path().steps.is_empty()
}

///|
pub fn DispatchPath::pop_step(self : DispatchPath) -> DispatchPath {
  guard self.frames is [.. init, last] else { return self }
  if !last.items.is_empty() {
    let items = last.items[0:last.items.length() - 1].to_owned()
    {
      frames: [
        ..init,
        { ..last, items, binds: keep_binds(last.binds, items.length()), },
      ],
    }
  } else if !init.is_empty() {
    { frames: init.to_owned(), }
  } else {
    self
  }
}

///|
/// The addressing projection: every step in its abstract form.
///
/// It no longer DROPS anything — every step addresses something now — so what
/// is left is `EachRenderItStep` abstracting to a plain `SeqStep`.
pub fn DispatchPath::compact(self : DispatchPath) -> DispatchPath {
  let frames : Array[DispatchFrame] = []
  for frame in self.frames {
    let items : Array[DispatchStep] = []
    for item in frame.items {
      match item {
        Plain(step~, origin~) =>
          items.push(Plain(step=step_to_abstract(step), origin~))
      }
    }
    frames.push({ ..frame, items, })
  }
  { frames, }
}

///|
pub fn DispatchPath::to_transaction_path(self : DispatchPath) -> Path {
  guard self.frames is [.., frame] else { return Path::new() }
  let out = frame.base.steps.copy()
  for item in frame.items {
    match item {
      Plain(step~, ..) => out.push(step)
    }
  }
  { steps: out, }
}