///|
/// The normative baseline is OSC 1.0 (26 March 2002), core tags i/f/s/b.
/// The generic codec remains the opt-in path for the optional tag table.
fn validate_osc10_tree(
  packet : Packet,
  parent : TimeTag?,
) -> Unit raise OscError {
  match packet {
    Message(message) => {
      if message.address.contains("//") {
        raise Invalid("OSC 1.0 profile excludes recursive descendant //", 0)
      }
      ignore(compile_pattern(message.address))
      for argument in message.arguments {
        match argument {
          I32(_) | F32(_) | Text(_) | Blob(_) => ()
          _ => raise Invalid("OSC 1.0 core entry requires only i/f/s/b tags", 0)
        }
      }
    }
    Bundle(time, children) => {
      // Strict comparison of on-wire timetags: no implicit child-time repair.
      if parent is Some(p) && time.bits() < p.bits() {
        raise Invalid("OSC 1.0 child timetag precedes parent", 0)
      }
      for child in children {
        validate_osc10_tree(child, Some(time))
      }
    }
  }
}

///|
pub fn decode_osc10(
  bytes : Bytes,
  limits~ : Limits = Limits::default(),
) -> Packet raise OscError {
  let packet = decode(bytes, limits~)
  validate_osc10_tree(packet, None)
  packet
}

///|
pub fn encode_osc10(
  packet : Packet,
  limits~ : Limits = Limits::default(),
) -> Bytes raise OscError {
  // Bound and snapshot user-owned trees before recursively checking the profile.
  let wire = encode(packet, limits~)
  validate_osc10_tree(decode(wire, limits~), None)
  wire
}

///|
pub(all) struct Osc10Batch {
  due : TimeTag
  messages : Array[Message]
} derive(Eq, Debug)

///|
pub extend Osc10Batch with Eq::{equal, not_equal}

///|
pub extend Osc10Batch with @debug.Debug::{to_repr}

///|
fn collect_osc10_batches(
  packet : Packet,
  inherited : TimeTag,
  out : Array[Osc10Batch],
) -> Unit {
  match packet {
    Message(message) => out.push({ due: inherited, messages: [message], })
    Bundle(time, children) => {
      let due = if time.is_immediate() { inherited } else { time }
      let direct : Array[Message] = []
      for child in children {
        if child is Message(message) {
          direct.push(message)
        }
      }
      // Nested bundles are separate atomic groups, not DFS interleaving points.
      if !direct.is_empty() {
        out.push({ due, messages: direct, })
      }
      for child in children {
        if child is Bundle(_, _) {
          collect_osc10_batches(child, due, out)
        }
      }
    }
  }
}

///|
/// Owns data and preserves each bundle's direct-message atomic group.
/// Hosts must run an entire returned group without unrelated callback processing.
pub fn plan_osc10(
  packet : Packet,
  now : TimeTag,
  limits~ : Limits = Limits::default(),
) -> Array[Osc10Batch] raise OscError {
  if now.is_immediate() {
    raise Invalid("OSC 1.0 planning requires absolute now", 0)
  }
  let owned = decode_osc10(encode(packet, limits~), limits~)
  let out : Array[Osc10Batch] = []
  collect_osc10_batches(owned, now, out)
  out
}