//! Macros to simplify calling and handling RPCs.
//!
//! These macros are specialized for our protocol, which:
//!
//!   - Use the Result(T) type to communicate rich errors to the
//!     client.
//!
//!     Cap'n Proto's generic variables must be pointers.  Thus to
//!     model non-pointer types like Result<()>, we need a separate
//!     type.  See this [message].
//!
//!       [message]: https://www.mail-archive.com/capnproto@googlegroups.com/msg01286.html
//!
//!   - Call RPCs synchronously.
//!
//!     Since Cap'n Proto is asynchronous, we use Tokio in the
//!     background.
//!
//! For wrapping an RPC, the [`crpc!`] macro is most convenient.  The
//! [`send_rpc!`] macro is useful when more control is needed.
//!
//! For dispatching an RPC, the [`srpc!`] macro is most convenient.


/// Wraps an RPC call.
///
/// This macro synthesizes a method, which marshals an RPC request,
/// synchronously calls the RPC, and massages the result.
///
/// `self.relay` must be a reference to a `CapnProtoRelay` executor,
/// and `self.cap` must be a capability (as wrapped by
/// `CapnProtoRelay`) to invoke the method on.
///
/// `$f` is simultaneously the method name and the name of the RPC
/// that should be invoked.  `$method_id` is its method identifier as
/// set in the capnproto IDL file.  (Unfortunately, the method ID is
/// not given a symbolic name by the capnproto IDL compiler so it
/// needs to be provided explicitly.)
///
/// `$marshal` is a closure, which is called to set the method's
/// parameters.  It is passed the root of the parameters data
/// structure, i.e., the `METHOD_params::Builder` data structure,
/// which is generated by the IDL compiler.
///
/// `$extract1` is a closure, which is called by the relay to allow
/// the caller to extract the results from the server's response.  It
/// is called directly by the relay thread.  This means that we can
/// pass a reference to the response, which avoids unnecessarily
/// copying data.  But, more importantly for the problem that this
/// module is trying to solve, executing it in the same thread as the
/// relay gets around the problem that capnproto capabilities are not
/// `Send` and `Sync`, and consequently a message that contains
/// capabilities can't be sent to another thread.  A result of this
/// is that the closure must be `Send` and `Sync`, since it is
/// executed by a different thread.  The closure is passed the relay's
/// capability table (`CapTable`), and the root of the response
/// (`METHOD_results::Builder`).  If a returned capability needs to be
/// used later, then this closure should wrap it using
/// `captable.insert`, and save the result.
///
/// Note: you can sometimes avoid the cost of copying the data by
/// massaging it directly in this closure, but be careful as this
/// function blocks the relay's main thread.
///
/// `$extract2` is a closure, which is called on the result of
/// `$extract1`.  It is executed in the context of the caller.  It can
/// be used to incorporate data from `self`.  (If data from self flows
/// into the result of `$extract1`, then `self` would have to have a
/// `'static` lifetime, which is doesn't have.)
macro_rules! crpc {
    ($(#[$outer:meta])*
     fn [$mod:path] $f:ident/$method_id:literal(&mut $self:ident $(, $v:ident: $t:ty)*)
         -> Result<$rt:ty>
     $marshal:expr;  // A close called as f(root) -> Result<()>
     $extract1:expr // A closure called as f(response) -> Result<$rt>
    ) => {
        crpc!(
            $(#[$outer])*
            fn [$mod] $f/$method_id(&mut self $(, $v: $t)*)
                -> Result<$rt> -> Result<$rt>
                $marshal;
                $extract1;
                |r| Ok(r));
    };

    ($(#[$outer:meta])*
     fn [$mod:path] $f:ident/$method_id:literal(&mut $self:ident $(, $v:ident: $t:ty)*)
         -> Result<$irt:ty> -> Result<$rt:ty>
     $marshal:expr;  // A closure called as f(root) -> Result<()>
     $extract1:expr; // A closure called as f(response) -> Result<$irt>
     $extract2:expr  // A closure called as f($irt) -> Result<$rt>
    ) => {
        $(#[$outer])*
        #[async_generic::async_generic]
        pub fn $f(&mut $self $(, $v: $t)*) -> Result<$rt> {
            log::trace!("{}::{}", stringify!($mod), stringify!($f));

            use $mod as m;
            paste::paste! {
                use m::[< $f _params >] as params;
                use m::[< $f _results >] as results;
            };

            let mut message
                = message::TypedBuilder::<params::Owned>::new_default();

            // Set the parameters.
            // e.g., keystore::key::sign_message_params::Builder
            let root: params::Builder = message.init_root();
            let result: Result<()> = $marshal(root);
            result?;

            let message = message::TypedReader::from(message);

            let extract1: Box<dyn FnOnce(
                std::sync::Arc<crate::capnp_relay::CapnProtoRelay>,
                &mut crate::CapTable, results::Reader)
                -> Result<$irt> + Send + Sync>
                = Box::new($extract1);

            let f = |relay: std::sync::Arc<crate::capnp_relay::CapnProtoRelay>,
                     cap_table: &mut crate::CapTable,
                     response: capability::Response<any_pointer::Owned>|
                    -> Result<Box<dyn Any + Send + Sync>>
            {
                // e.g., keystore::key::sign_message_results::Reader
                let response: results::Reader = response.get()?.get_as()?;

                Ok(extract1(relay, cap_table, response).map(|r| {
                    Box::new(r) as Box<dyn std::any::Any + Send + Sync>
                })?)
            };

            let handle = if _sync {
                $self.relay.send_rpc($self.cap.clone(),
                                     <m::Client as HasTypeId>::TYPE_ID,
                                     $method_id,
                                     message.into_inner(),
                                     f)?
            } else {
                $self.relay.send_rpc_async($self.cap.clone(),
                                           <m::Client as HasTypeId>::TYPE_ID,
                                           $method_id,
                                           message.into_inner(),
                                           f).await?
            };

            let response = if _sync {
                CapnProtoRelay::await_reply(handle)?
            } else {
                CapnProtoRelay::await_reply_async(handle).await?
            };

            let r: $irt = *response.downcast().unwrap();

            let extract2: Box<dyn FnOnce($irt) -> Result<$rt>>
                = Box::new($extract2);

            let r: Result<$rt> = extract2(r);

            Ok(r?)
        }
    };
}
