// ============================================================
// Reusable production policies
// ============================================================

///|
/// A decorator that blocks a completed child for a number of future frames.
pub fn cooldown_node(child : Node, cooldown_ticks : Int) -> Node {
  let remaining : Ref[Int] = Ref::new(0)
  let tick = fn(bb) {
    if remaining.get() > 0 {
      remaining.set(remaining.get() - 1)
      return Status::BTFailure
    }
    let status = child.tick(bb)
    match status {
      Status::BTSuccess | Status::BTFailure =>
        remaining.set(if cooldown_ticks > 0 { cooldown_ticks } else { 0 })
      Status::BTRunning => ()
    }
    status
  }
  let reset = fn() {
    remaining.set(0)
    child.reset()
  }
  Node::new(tick, reset)
}

///|
/// Convert a terminal result into Running for one frame before completing.
pub fn defer_terminal_node(child : Node) -> Node {
  let deferred : Ref[Status?] = Ref::new(None)
  let tick = fn(bb) {
    match deferred.get() {
      Some(status) => {
        deferred.set(None)
        status
      }
      None => {
        let status = child.tick(bb)
        match status {
          Status::BTSuccess | Status::BTFailure => {
            deferred.set(Some(status))
            Status::BTRunning
          }
          Status::BTRunning => Status::BTRunning
        }
      }
    }
  }
  let reset = fn() {
    deferred.set(None)
    child.reset()
  }
  Node::new(tick, reset)
}

///|
/// Return Success after `required` consecutive successful child ticks.
pub fn stable_success_node(child : Node, required : Int) -> Node {
  let count : Ref[Int] = Ref::new(0)
  let needed = if required > 0 { required } else { 1 }
  let tick = fn(bb) {
    match child.tick(bb) {
      Status::BTSuccess => {
        count.set(count.get() + 1)
        if count.get() >= needed {
          count.set(0)
          Status::BTSuccess
        } else {
          Status::BTRunning
        }
      }
      Status::BTFailure => {
        count.set(0)
        Status::BTFailure
      }
      Status::BTRunning => Status::BTRunning
    }
  }
  let reset = fn() {
    count.set(0)
    child.reset()
  }
  Node::new(tick, reset)
}

///|
/// Return Failure after `required` consecutive failed child ticks.
pub fn stable_failure_node(child : Node, required : Int) -> Node {
  let count : Ref[Int] = Ref::new(0)
  let needed = if required > 0 { required } else { 1 }
  let tick = fn(bb) {
    match child.tick(bb) {
      Status::BTFailure => {
        count.set(count.get() + 1)
        if count.get() >= needed {
          count.set(0)
          Status::BTFailure
        } else {
          Status::BTRunning
        }
      }
      Status::BTSuccess => {
        count.set(0)
        Status::BTSuccess
      }
      Status::BTRunning => Status::BTRunning
    }
  }
  let reset = fn() {
    count.set(0)
    child.reset()
  }
  Node::new(tick, reset)
}

///|
/// Retry only when a blackboard predicate says the action is recoverable.
pub fn recoverable_retry_node(
  child : Node,
  max_retries : Int,
  should_retry : (Blackboard) -> Bool,
) -> Node {
  let attempts : Ref[Int] = Ref::new(0)
  let limit = if max_retries > 0 { max_retries } else { 1 }
  let tick = fn(bb) {
    match child.tick(bb) {
      Status::BTSuccess => {
        attempts.set(0)
        Status::BTSuccess
      }
      Status::BTRunning => Status::BTRunning
      Status::BTFailure =>
        if should_retry(bb) && attempts.get() + 1 < limit {
          attempts.set(attempts.get() + 1)
          child.reset()
          Status::BTRunning
        } else {
          attempts.set(0)
          Status::BTFailure
        }
    }
  }
  let reset = fn() {
    attempts.set(0)
    child.reset()
  }
  Node::new(tick, reset)
}