///|
/// The outcome of a search: all entries and references collected plus the
/// final `SearchResultDone`.
pub struct SearchOutcome {
  entries : Array[SearchResultEntry]
  references : Array[SearchResultReference]
  mut result : LdapResult
} derive(Eq, @debug.Debug)

///|
pub fn SearchOutcome::new() -> SearchOutcome {
  { entries: [], references: [], result: LdapResult::success() }
}

///|
/// A completed search exchange: the final result plus any response controls
/// (e.g. the paged results cookie from RFC 2696).
pub struct SearchDone {
  result : LdapResult
  controls : Array[Control]?
} derive(Eq, @debug.Debug)

///|
/// Execute a search with optional request controls, streaming results to
/// callbacks. Returns the final result together with response controls.
pub async fn[T : LdapTransport] Session::search_done(
  self : Session[T],
  request : SearchRequest,
  controls? : Array[Control],
  on_entry? : (SearchResultEntry) -> Unit,
  on_reference? : (SearchResultReference) -> Unit,
) -> Result[SearchDone, LdapError] {
  let id = self.next_message_id()
  let message = match controls {
    Some(c) => LdapMessage::with_controls(id, SearchRequest(request), c)
    None => LdapMessage::new(id, SearchRequest(request))
  }
  let bytes = match encode_message(message) {
    Ok(b) => b
    Err(e) => return Err(e)
  }
  match self.transport.write(bytes) {
    Ok(_) => ()
    Err(e) => return Err(e)
  }
  while true {
    let response = match self.transport.read() {
      Ok(b) => b
      Err(e) => return Err(e)
    }
    let decoded = match decode_message(response, None) {
      Ok(m) => m
      Err(e) => return Err(e)
    }
    if decoded.message_id != id {
      return Err(LdapError::InvalidMessageId(decoded.message_id))
    }
    match decoded.op {
      SearchResultEntry(entry) =>
        match on_entry {
          Some(cb) => cb(entry)
          None => ()
        }
      SearchResultReference(refs) =>
        match on_reference {
          Some(cb) => cb(refs)
          None => ()
        }
      SearchResultDone(result) =>
        return Ok({ result, controls: decoded.controls })
      _ => return Err(LdapError::UnexpectedOp(protocol_op_tag(decoded.op)))
    }
  }
  abort("unreachable")
}

///|
/// Execute a search and stream the results to callbacks. `on_entry` and
/// `on_reference` are invoked as the corresponding messages arrive; the
/// returned `LdapResult` is the final `SearchResultDone`.
pub async fn[T : LdapTransport] Session::search(
  self : Session[T],
  request : SearchRequest,
  on_entry? : (SearchResultEntry) -> Unit,
  on_reference? : (SearchResultReference) -> Unit,
) -> Result[LdapResult, LdapError] {
  let done = match self.search_done(request, on_entry?, on_reference?) {
    Ok(d) => d
    Err(e) => return Err(e)
  }
  Ok(done.result)
}

///|
/// Execute a search and collect all entries, references and the final result.
pub async fn[T : LdapTransport] Session::search_to_array(
  self : Session[T],
  request : SearchRequest,
) -> Result[SearchOutcome, LdapError] {
  let acc = @ref.Ref(SearchOutcome::new())
  let result = match
    self.search(request, on_entry=fn(entry) { acc.val.entries.push(entry) }, on_reference=fn(
      refs,
    ) {
      acc.val.references.push(refs)
    }) {
    Ok(r) => r
    Err(e) => return Err(e)
  }
  acc.val.result = result
  Ok(acc.val)
}