///|
pub fn Message::type_tags(
  self : Message,
  limits~ : Limits = Limits::default(),
) -> String raise OscError {
  limits.validate()
  let chars : Array[Char] = [',']
  collect_tags(self.arguments, chars, 1, limits, Budget::{ args: 0, nodes: 0, })
  String::from_array(chars[:])
}

///|
fn validate_signature(signature : String) -> Unit raise OscError {
  if signature.length() == 0 || signature[0] != 44 || signature.length() > 65536 {
    raise Invalid("signature must start with comma and fit bounds", 0)
  }
  let mut depth = 0
  for i in 1.. {
        depth += 1
        if depth > 64 {
          raise Limit("signature array depth")
        }
      }
      93 => {
        depth -= 1
        if depth < 0 {
          raise Invalid("signature unmatched close", i)
        }
      }
      105
      | 102
      | 115
      | 98
      | 104
      | 100
      | 116
      | 83
      | 99
      | 114
      | 109
      | 84
      | 70
      | 78
      | 73 => ()
      _ => raise Invalid("unsupported signature tag", i)
    }
  }
  if depth != 0 {
    raise Invalid("signature unclosed array", signature.length())
  }
}

///|
pub(all) struct Rejection {
  route : Route
  expected : String
  actual : String
} derive(Eq, Debug)

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

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

///|
pub(all) struct DispatchPlan {
  accepted : Array[Route]
  rejected : Array[Rejection]
} derive(Eq, Debug)

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

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

///|
priv struct Contract {
  id : Int32
  signature : String?
}

///|
pub struct Dispatcher {
  priv router : Router
  priv contracts : Array[Contract]
}

///|
pub fn Dispatcher::new(capacity~ : Int32 = 256) -> Dispatcher raise OscError {
  { router: Router::new(capacity~), contracts: [], }
}

///|
pub fn Dispatcher::add(
  self : Dispatcher,
  id : Int32,
  address : String,
  signature~ : String? = None,
) -> Unit raise OscError {
  if signature is Some(s) {
    validate_signature(s)
  }
  self.router.add(id, address)
  self.contracts.push({ id, signature, })
}

///|
pub fn Dispatcher::plan(
  self : Dispatcher,
  message : Message,
  all_or_nothing~ : Bool = true,
  work_limit~ : Int32 = 1000000,
  limits~ : Limits = Limits::default(),
) -> DispatchPlan raise OscError {
  // Validate payloads as well as tag structure before admitting any destination.
  ignore(encode_message(message, limits~))
  let actual = message.type_tags(limits~)
  let routes = self.router.resolve(message.address, work_limit~)
  let accepted : Array[Route] = []
  let rejected : Array[Rejection] = []
  for route in routes {
    let mut expected : String? = None
    for contract in self.contracts {
      if contract.id == route.id {
        expected = contract.signature
        break
      }
    }
    match expected {
      Some(s) if s != actual => rejected.push({ route, expected: s, actual, })
      _ => accepted.push(route)
    }
  }
  if all_or_nothing && !rejected.is_empty() {
    accepted.clear()
  }
  { accepted, rejected, }
}