///|
/// MutationQueue - Batches DOM mutations for efficient processing
///
/// Collects mutations and processes them in batches, optimizing by:
/// - Merging multiple changes to the same node
/// - Canceling out add+remove pairs
/// - Coalescing style property changes

///|
/// Queue for batching mutations
pub struct MutationQueue {
  /// Pending mutation records
  mut records : Array[MutationRecord]
  /// Set of dirty node IDs (for dedup checking)
  dirty_nodes : Map[Int, Bool]
  /// Whether we're currently flushing (to handle re-entrant mutations)
  mut is_flushing : Bool
  /// Records that arrived during flush (processed in next batch)
  mut deferred : Array[MutationRecord]
  /// Whether mutations are recorded at all. Recording exists solely to feed the
  /// incremental-layout bridge; a standalone tree (querySelector, one-shot build)
  /// has no consumer, so recording is off by default and enabled by that bridge.
  mut recording : Bool
}

///|
/// Result of flushing the mutation queue
pub struct FlushResult {
  /// Number of mutation records processed
  processed : Int
  /// Number of unique nodes affected
  nodes_affected : Int
  /// Whether any mutation affects layout (vs paint-only)
  needs_layout : Bool
} derive(Debug)

///|
pub impl Show for FlushResult with fn output(self, logger) {
  logger.write_string("{processed: \{self.processed}, ")
  logger.write_string("nodes_affected: \{self.nodes_affected}, ")
  logger.write_string("needs_layout: \{self.needs_layout}}")
}

///|
/// Create a new empty mutation queue
pub fn MutationQueue::new() -> MutationQueue {
  {
    records: [],
    dirty_nodes: {},
    is_flushing: false,
    deferred: [],
    recording: false,
  }
}

///|
/// Enable or disable mutation recording. When disabled (the default), `push` is a
/// no-op, so building a tree with no mutation consumer allocates no records.
pub fn MutationQueue::set_recording(self : MutationQueue, on : Bool) -> Unit {
  self.recording = on
}

///|
/// Add a mutation record to the queue
pub fn MutationQueue::push(
  self : MutationQueue,
  record : MutationRecord,
) -> Unit {
  if !self.recording {
    return
  }
  if self.is_flushing {
    // Queue for next batch to avoid mutation during iteration
    self.deferred.push(record)
  } else {
    self.records.push(record)
    self.dirty_nodes[record.target.to_int()] = true
  }
}

///|
/// Build and queue a child-list "added" record only when recording, so the
/// record and its single-element array allocate nothing on standalone trees.
pub fn MutationQueue::record_child_added(
  self : MutationQueue,
  target : NodeId,
  child : NodeId,
) -> Unit {
  if self.recording {
    self.push(MutationRecord::child_list(target, added=[child]))
  }
}

///|
/// Build and queue a child-list "removed" record only when recording.
pub fn MutationQueue::record_child_removed(
  self : MutationQueue,
  target : NodeId,
  child : NodeId,
) -> Unit {
  if self.recording {
    self.push(MutationRecord::child_list(target, removed=[child]))
  }
}

///|
/// Build and queue an attribute-change record only when recording.
pub fn MutationQueue::record_attribute(
  self : MutationQueue,
  target : NodeId,
  name : String,
  old_value? : String? = None,
  new_value? : String? = None,
) -> Unit {
  if self.recording {
    self.push(MutationRecord::attribute(target, name, old_value~, new_value~))
  }
}

///|
/// Build and queue a character-data record only when recording.
pub fn MutationQueue::record_character_data(
  self : MutationQueue,
  target : NodeId,
  old_value? : String? = None,
  new_value? : String? = None,
) -> Unit {
  if self.recording {
    self.push(MutationRecord::character_data(target, old_value~, new_value~))
  }
}

///|
/// Check if queue is empty
pub fn MutationQueue::is_empty(self : MutationQueue) -> Bool {
  self.records.is_empty()
}

///|
/// Get number of pending mutations
pub fn MutationQueue::length(self : MutationQueue) -> Int {
  self.records.length()
}

///|
/// Check if a node has pending mutations
pub fn MutationQueue::has_pending(self : MutationQueue, node : NodeId) -> Bool {
  self.dirty_nodes.get(node.to_int()).unwrap_or(false)
}

///|
/// Clear all pending mutations without processing
pub fn MutationQueue::clear(self : MutationQueue) -> Unit {
  self.records = []
  self.dirty_nodes.clear()
}

///|
/// Optimize the queue by merging related mutations
pub fn MutationQueue::optimize(self : MutationQueue) -> Array[MutationRecord] {
  if self.records.is_empty() {
    return []
  }

  // Group records by target node
  let by_node : Map[Int, Array[MutationRecord]] = {}
  for record in self.records {
    let key = record.target.to_int()
    match by_node.get(key) {
      Some(arr) => arr.push(record)
      None => by_node[key] = [record]
    }
  }

  // Merge records for each node
  let optimized : Array[MutationRecord] = []
  for _node_id, records in by_node {
    let merged = merge_node_records(records)
    for r in merged {
      optimized.push(r)
    }
  }
  optimized
}

///|
/// Merge multiple records for the same node
fn merge_node_records(records : Array[MutationRecord]) -> Array[MutationRecord] {
  // Separate by type
  let style_props : Array[String] = []
  let mut has_child_list = false
  let all_added : Array[NodeId] = []
  let all_removed : Array[NodeId] = []
  let mut has_char_data = false
  let mut char_old : String? = None
  let mut char_new : String? = None
  let attr_changes : Map[String, (String?, String?)] = {}
  let target = records[0].target
  for r in records {
    match r.type_ {
      StyleChange =>
        for prop in r.changed_properties {
          if !style_props.contains(prop) {
            style_props.push(prop)
          }
        }
      ChildList => {
        has_child_list = true
        for n in r.added_nodes {
          // Cancel out if also in removed
          let idx = all_removed.search(n)
          match idx {
            Some(i) => {
              let _ = all_removed.remove(i)
            }
            None => all_added.push(n)
          }
        }
        for n in r.removed_nodes {
          // Cancel out if also in added
          let idx = all_added.search(n)
          match idx {
            Some(i) => {
              let _ = all_added.remove(i)
            }
            None => all_removed.push(n)
          }
        }
      }
      CharacterData => {
        if !has_char_data {
          char_old = r.old_value
        }
        has_char_data = true
        char_new = r.new_value
      }
      Attributes =>
        match r.attribute_name {
          Some(name) => {
            let (old, _) = attr_changes
              .get(name)
              .unwrap_or((r.old_value, r.new_value))
            attr_changes[name] = (old, r.new_value)
          }
          None => ()
        }
    }
  }

  // Build merged records
  let result : Array[MutationRecord] = []
  if style_props.length() > 0 {
    result.push(MutationRecord::style_change(target, style_props))
  }
  if has_child_list && (all_added.length() > 0 || all_removed.length() > 0) {
    result.push(
      MutationRecord::child_list(target, added=all_added, removed=all_removed),
    )
  }
  if has_char_data {
    result.push(
      MutationRecord::character_data(
        target,
        old_value=char_old,
        new_value=char_new,
      ),
    )
  }
  for name, values in attr_changes {
    let (old, new) = values
    // Skip if value unchanged
    match (old, new) {
      (Some(o), Some(n)) if o == n => continue
      _ => ()
    }
    result.push(
      MutationRecord::attribute(target, name, old_value=old, new_value=new),
    )
  }
  result
}

///|
/// Flush the queue and return optimized records for processing
/// The caller is responsible for applying these to the layout tree
pub fn MutationQueue::flush(
  self : MutationQueue,
) -> (Array[MutationRecord], FlushResult) {
  if self.records.is_empty() {
    return ([], { processed: 0, nodes_affected: 0, needs_layout: false })
  }
  self.is_flushing = true
  let optimized = self.optimize()
  let nodes_affected = self.dirty_nodes.length()
  let needs_layout = optimized.iter().any(fn(r) { r.affects_layout() })
  let result : FlushResult = {
    processed: optimized.length(),
    nodes_affected,
    needs_layout,
  }

  // Clear current batch
  self.records = []
  self.dirty_nodes.clear()
  self.is_flushing = false

  // Move deferred to main queue for next batch
  if self.deferred.length() > 0 {
    self.records = self.deferred
    for r in self.deferred {
      self.dirty_nodes[r.target.to_int()] = true
    }
    self.deferred = []
  }
  (optimized, result)
}

///|
/// Process mutations with a callback for each record
/// Returns the flush result
pub fn[T] MutationQueue::flush_with(
  self : MutationQueue,
  handler : (MutationRecord) -> T,
) -> (Array[T], FlushResult) {
  let (records, result) = self.flush()
  let results : Array[T] = []
  for record in records {
    results.push(handler(record))
  }
  (results, result)
}