// ============================================================
// Blackboard patches and transactions
//
// Behavior trees frequently evaluate speculative actions before deciding
// whether to keep their side effects. A patch gives save-game systems,
// network replication, and audit logs a small deterministic description of
// the changes between two blackboards. A transaction uses the same diff
// machinery while keeping uncommitted writes isolated from the live state.
// ============================================================

///|
/// A single change needed to transform one blackboard into another.
pub(all) enum BlackboardChange {
  Added(String, Value)
  Updated(String, Value, Value)
  Removed(String, Value)
}

///|
/// Return the key affected by a change.
pub fn BlackboardChange::key(self : BlackboardChange) -> String {
  match self {
    BlackboardChange::Added(key, _) => key
    BlackboardChange::Updated(key, _, _) => key
    BlackboardChange::Removed(key, _) => key
  }
}

///|
/// Return the previous value, when the key existed before the change.
pub fn BlackboardChange::before(self : BlackboardChange) -> Value? {
  match self {
    BlackboardChange::Added(_, _) => None
    BlackboardChange::Updated(_, value, _) => Some(value)
    BlackboardChange::Removed(_, value) => Some(value)
  }
}

///|
/// Return the value after applying the change, when the key remains present.
pub fn BlackboardChange::after(self : BlackboardChange) -> Value? {
  match self {
    BlackboardChange::Added(_, value) => Some(value)
    BlackboardChange::Updated(_, _, value) => Some(value)
    BlackboardChange::Removed(_, _) => None
  }
}

///|
/// Return a stable change category for logs and replication metrics.
pub fn BlackboardChange::kind(self : BlackboardChange) -> String {
  match self {
    BlackboardChange::Added(_, _) => "added"
    BlackboardChange::Updated(_, _, _) => "updated"
    BlackboardChange::Removed(_, _) => "removed"
  }
}

///|
/// Format one change as a compact audit line.
pub fn BlackboardChange::to_text(self : BlackboardChange) -> String {
  match self {
    BlackboardChange::Added(key, value) => "added \{key}=\{value.to_text()}"
    BlackboardChange::Updated(key, before, after) =>
      "updated \{key}: \{before.to_text()} -> \{after.to_text()}"
    BlackboardChange::Removed(key, value) => "removed \{key}=\{value.to_text()}"
  }
}

///|
/// A deterministic collection of blackboard changes.
pub struct BlackboardPatch {
  changes : Array[BlackboardChange]
}

///|
/// Create a patch from an array of changes.
pub fn BlackboardPatch::new(
  changes : Array[BlackboardChange],
) -> BlackboardPatch {
  { changes, }
}

///|
/// Return the number of changes in the patch.
pub fn BlackboardPatch::size(self : BlackboardPatch) -> Int {
  self.changes.length()
}

///|
/// Return whether applying this patch would leave the target unchanged.
pub fn BlackboardPatch::is_empty(self : BlackboardPatch) -> Bool {
  self.changes.length() == 0
}

///|
/// Return a copy of the ordered changes for inspection or persistence.
pub fn BlackboardPatch::changes(
  self : BlackboardPatch,
) -> Array[BlackboardChange] {
  let result : Array[BlackboardChange] = []
  for change in self.changes {
    result.push(change)
  }
  result
}

///|
/// Format the patch as one change per line.
pub fn BlackboardPatch::to_text(self : BlackboardPatch) -> String {
  let output = StringBuilder::new()
  for change in self.changes {
    output.write_string(change.to_text())
    output.write_string("\n")
  }
  output.to_string()
}

///|
/// Compare two supported values without relying on representation details.
pub fn Value::same(self : Value, other : Value) -> Bool {
  match self {
    Value::Bool(left) =>
      match other {
        Value::Bool(right) => left == right
        _ => false
      }
    Value::Int(left) =>
      match other {
        Value::Int(right) => left == right
        _ => false
      }
    Value::Double(left) =>
      match other {
        Value::Double(right) => left == right
        _ => false
      }
    Value::Str(left) =>
      match other {
        Value::Str(right) => left == right
        _ => false
      }
  }
}

///|
/// Compute the ordered changes that transform `self` into `target`.
/// Existing keys keep the source insertion order; newly added keys follow the
/// target insertion order. This makes patches stable in logs and fixtures.
pub fn Blackboard::diff(
  self : Blackboard,
  target : Blackboard,
) -> BlackboardPatch {
  let changes : Array[BlackboardChange] = []
  for key in self.keys() {
    let before = self.get_value(key).unwrap()
    match target.get_value(key) {
      None => changes.push(BlackboardChange::Removed(key, before))
      Some(after) =>
        if !before.same(after) {
          changes.push(BlackboardChange::Updated(key, before, after))
        }
    }
  }
  for key in target.keys() {
    if !self.has(key) {
      changes.push(BlackboardChange::Added(key, target.get_value(key).unwrap()))
    }
  }
  BlackboardPatch::new(changes)
}

///|
/// Apply a patch and return the number of changes accepted.
pub fn BlackboardPatch::apply(self : BlackboardPatch, bb : Blackboard) -> Int {
  let mut applied = 0
  for change in self.changes {
    match change {
      BlackboardChange::Added(key, value) => {
        bb.set_value(key, value)
        applied = applied + 1
      }
      BlackboardChange::Updated(key, _, value) => {
        bb.set_value(key, value)
        applied = applied + 1
      }
      BlackboardChange::Removed(key, _) =>
        if bb.has(key) {
          bb.remove(key)
          applied = applied + 1
        }
    }
  }
  applied
}

///|
/// A live blackboard transaction with isolated writes.
pub struct BlackboardTransaction {
  target : Blackboard
  original : Blackboard
  working : Blackboard
  closed : Ref[Bool]
}

///|
/// Begin a transaction. Changes are invisible to the target until commit.
pub fn Blackboard::begin_transaction(
  self : Blackboard,
) -> BlackboardTransaction {
  {
    target: self,
    original: self.clone(),
    working: self.clone(),
    closed: Ref::new(false),
  }
}

///|
/// Return the isolated working blackboard for a transaction.
pub fn BlackboardTransaction::blackboard(
  self : BlackboardTransaction,
) -> Blackboard {
  self.working
}

///|
/// Return the pending changes since the transaction began.
pub fn BlackboardTransaction::changes(
  self : BlackboardTransaction,
) -> BlackboardPatch {
  self.original.diff(self.working)
}

///|
/// Return whether the transaction can still be committed.
pub fn BlackboardTransaction::is_open(self : BlackboardTransaction) -> Bool {
  !self.closed.get()
}

///|
/// Commit all working values atomically and return whether the commit happened.
pub fn BlackboardTransaction::commit(self : BlackboardTransaction) -> Bool {
  if self.closed.get() {
    return false
  }
  self.target.clear()
  self.target.merge(self.working)
  self.closed.set(true)
  true
}

///|
/// Discard working values. Rollback is idempotent and never changes the target.
pub fn BlackboardTransaction::rollback(self : BlackboardTransaction) -> Unit {
  self.closed.set(true)
}

///|
/// Run an isolated update and commit only when the callback accepts it.
pub fn Blackboard::transactional(
  self : Blackboard,
  update : (Blackboard) -> Bool,
) -> Bool {
  let transaction = self.begin_transaction()
  let accepted = update(transaction.blackboard())
  if accepted {
    transaction.commit()
  } else {
    transaction.rollback()
    false
  }
}

///|
/// Apply a patch only when a caller-provided validation callback accepts the
/// resulting state. This is useful for atomic action planning and save games.
pub fn Blackboard::apply_patch_if(
  self : Blackboard,
  patch : BlackboardPatch,
  validate : (Blackboard) -> Bool,
) -> Bool {
  self.transactional(fn(working) {
    let _ = patch.apply(working)
    validate(working)
  })
}