// ============================================================
// Utility-based selection
//
// A utility selector evaluates candidates against the current blackboard and
// keeps the selected candidate active across Running frames. It is useful when
// an agent chooses among goals such as flee, attack, gather, and patrol.
// ============================================================

///|
/// One named candidate in a utility selector.
pub struct UtilityCandidate {
  name : String
  score : (Blackboard) -> Double
  node : Node
}

///|
/// Construct a utility candidate.
pub fn UtilityCandidate::new(
  name : String,
  score : (Blackboard) -> Double,
  node : Node,
) -> UtilityCandidate {
  { name, score, node }
}

///|
/// Candidate name for diagnostics.
pub fn UtilityCandidate::name(self : UtilityCandidate) -> String {
  self.name
}

///|
/// Evaluate a candidate against a blackboard.
pub fn UtilityCandidate::evaluate(
  self : UtilityCandidate,
  bb : Blackboard,
) -> Double {
  (self.score)(bb)
}

///|
/// Select the highest-scoring candidate and preserve it while Running.
pub fn utility_selector_node(
  candidates : Array[UtilityCandidate],
  fallback : Node,
) -> Node {
  let active : Ref[Int] = Ref::new(-1)
  let tick = fn(bb : Blackboard) {
    let selected = if active.get() >= 0 {
      active.get()
    } else {
      let best = Ref::new(-1)
      let best_score = Ref::new(-1.0 / 0.0)
      let mut i = 0
      while i < candidates.length() {
        let score = candidates[i].evaluate(bb)
        if best.get() < 0 || score > best_score.get() {
          best.set(i)
          best_score.set(score)
        }
        i = i + 1
      }
      best.get()
    }
    if selected < 0 {
      fallback.tick(bb)
    } else {
      active.set(selected)
      let status = candidates[selected].node.tick(bb)
      match status {
        Status::BTRunning => Status::BTRunning
        Status::BTSuccess | Status::BTFailure => {
          candidates[selected].node.reset()
          active.set(-1)
          status
        }
      }
    }
  }
  let reset = fn() {
    for candidate in candidates {
      candidate.node.reset()
    }
    fallback.reset()
    active.set(-1)
  }
  Node::new(tick, reset)
}

///|
/// Select a candidate only when its score meets a minimum threshold.
pub fn threshold_utility_node(
  candidates : Array[UtilityCandidate],
  threshold : Double,
  fallback : Node,
) -> Node {
  let filtered : Array[UtilityCandidate] = []
  for candidate in candidates {
    let _ = candidate
    filtered.push(candidate)
  }
  let node = utility_selector_node(filtered, fallback)
  let gate = fn(bb : Blackboard) {
    let mut has_eligible = false
    for candidate in candidates {
      if candidate.evaluate(bb) >= threshold {
        has_eligible = true
      }
    }
    has_eligible
  }
  require_node(gate, node)
}