// ============================================================
// Incremental batch execution
//
// BatchRunner is for applications that need to execute a list of independent
// tasks under one frame budget. It deliberately executes at most one child per
// tick, making it suitable for server jobs, scripted agents, and UI workflows.
// ============================================================

///|
/// Failure behavior for a batch.
pub(all) enum BatchMode {
  ContinueOnFailure
  StopOnFailure
} derive(Eq, Debug)

///|
/// Continue after a failed task.
pub fn BatchMode::continue_on_failure() -> BatchMode {
  BatchMode::ContinueOnFailure
}

///|
/// Stop immediately after a failed task.
pub fn BatchMode::stop_on_failure() -> BatchMode {
  BatchMode::StopOnFailure
}

///|
/// A stable progress snapshot.
pub struct BatchReport {
  status : Status
  cursor : Int
  total : Int
  failures : Int
}

///|
/// Status observed for this report.
pub fn BatchReport::status(self : BatchReport) -> Status {
  self.status
}

///|
/// Index of the next task.
pub fn BatchReport::cursor(self : BatchReport) -> Int {
  self.cursor
}

///|
/// Number of tasks in the batch.
pub fn BatchReport::total(self : BatchReport) -> Int {
  self.total
}

///|
/// Number of failed tasks observed so far.
pub fn BatchReport::failures(self : BatchReport) -> Int {
  self.failures
}

///|
/// A sequential, incremental task batch.
pub struct BatchRunner {
  tasks : Array[Node]
  bb : Blackboard
  mode : BatchMode
  cursor : Ref[Int]
  failures : Ref[Int]
  stopped : Ref[Bool]
}

///|
/// Create an empty batch.
pub fn BatchRunner::new(bb : Blackboard, mode : BatchMode) -> BatchRunner {
  {
    tasks: [],
    bb,
    mode,
    cursor: Ref::new(0),
    failures: Ref::new(0),
    stopped: Ref::new(false),
  }
}

///|
/// Append a task before execution begins.
pub fn BatchRunner::add(self : BatchRunner, task : Node) -> Unit {
  self.tasks.push(task)
}

///|
/// Number of tasks in the batch.
pub fn BatchRunner::size(self : BatchRunner) -> Int {
  self.tasks.length()
}

///|
/// Whether all tasks have been processed or the batch has stopped.
pub fn BatchRunner::is_done(self : BatchRunner) -> Bool {
  self.stopped.get() || self.cursor.get() >= self.tasks.length()
}

///|
/// Execute at most one task and return a progress report.
pub fn BatchRunner::tick(self : BatchRunner) -> BatchReport {
  if self.is_done() {
    return {
      status: if self.stopped.get() {
        Status::BTFailure
      } else {
        Status::BTSuccess
      },
      cursor: self.cursor.get(),
      total: self.tasks.length(),
      failures: self.failures.get(),
    }
  }
  let index = self.cursor.get()
  let status = self.tasks[index].tick(self.bb)
  match status {
    Status::BTRunning => ()
    Status::BTSuccess => {
      self.tasks[index].reset()
      self.cursor.set(index + 1)
    }
    Status::BTFailure => {
      self.failures.set(self.failures.get() + 1)
      self.tasks[index].reset()
      self.cursor.set(index + 1)
      match self.mode {
        BatchMode::ContinueOnFailure => ()
        BatchMode::StopOnFailure => self.stopped.set(true)
      }
    }
  }
  {
    status: if self.is_done() {
      if self.stopped.get() {
        Status::BTFailure
      } else {
        Status::BTSuccess
      }
    } else {
      Status::BTRunning
    },
    cursor: self.cursor.get(),
    total: self.tasks.length(),
    failures: self.failures.get(),
  }
}

///|
/// Reset all tasks and start the batch again.
pub fn BatchRunner::reset(self : BatchRunner) -> Unit {
  for task in self.tasks {
    task.reset()
  }
  self.cursor.set(0)
  self.failures.set(0)
  self.stopped.set(false)
}

///|
/// The blackboard shared by all tasks.
pub fn BatchRunner::blackboard(self : BatchRunner) -> Blackboard {
  self.bb
}