pub use postcard_schema::{Schema, key::Key};

/// A marker trait denoting a single endpoint
///
/// Typically used with the [endpoint](crate::endpoint) macro.
pub trait Endpoint {
    /// The type of the Request (client to server)
    type Request: Schema;
    /// The type of the Response (server to client)
    type Response: Schema;
    /// The path associated with this Endpoint
    const PATH: &'static str;
    /// The unique [Key] identifying the Request
    const REQ_KEY: Key;
    /// The unique [Key] identifying the Response
    const RESP_KEY: Key;
}

/// A marker trait denoting a single topic
///
/// Unlike [Endpoint]s, [Topic]s are unidirectional, and can be sent
/// at any time asynchronously. Messages may be sent client to server,
/// or server to client.
///
/// Typically used with the [topic](crate::topic) macro.
pub trait Topic {
    /// The type of the Message (unidirectional)
    type Message: Schema + ?Sized;
    /// The path associated with this Topic
    const PATH: &'static str;
    /// The unique [Key] identifying the Message
    const TOPIC_KEY: Key;
}

/// ## Endpoint macro
///
/// Used to define a single Endpoint marker type that implements the
/// [Endpoint][crate::traits::Endpoint] trait.
///
/// Both request and response types may be borrowed or owned.
///
/// Borrowed types may contain lifetimes, but only as types with lifetimes, not
/// direct references. This means we can accept `Borrowed<'a>` as a type but not
/// `&'a str`. If both the request and response are borrowed, they must have
/// different named lifetimes.
///
/// ```rust
/// # use postcard_schema::Schema;
/// # use serde::{Serialize, Deserialize};
/// use ergot::endpoint;
///
/// #[derive(Debug, Serialize, Deserialize, Schema)]
/// pub struct Req1 {
///     a: u8,
///     b: u64,
/// }
///
/// #[derive(Debug, Serialize, Deserialize, Schema)]
/// pub struct Resp1 {
///     c: [u8; 4],
///     d: i32,
/// }
///
/// // We can use wrapper types for borrowed types
/// #[derive(Schema, Debug, PartialEq, Serialize, Deserialize)]
/// pub struct Stir<'a> {
///     s: &'a str,
/// }
///
/// // Or we can use type aliases for borrowed types
/// pub type Stir2<'a> = &'a str;
///
/// // both owned
/// endpoint!(Endpoint1, Req1, Resp1, "endpoint/1");
/// // request borrowed
/// endpoint!(Endpoint2, Stir<'a>, Resp1, "endpoint/2");
/// // response borrowed
/// endpoint!(Endpoint3, Req1, Stir<'a>, "endpoint/3");
/// // both request and response borrowed
/// endpoint!(Endpoint4, Stir<'a>, Stir<'b>, "endpoint/4");
/// // aliases work too
/// endpoint!(Endpoint5, Stir2<'a>, Stir2<'b>, "endpoint/5");
/// ```
///
/// If the path is omitted, the type name is used instead.
#[macro_export]
macro_rules! endpoint {
    ($tyname:ident, $req:tt $(< $req_lt:lifetime >)?, $resp:tt $(< $resp_lt:lifetime >)? $(,)?) => {
        /// An Endpoint definition type
        ///
        /// Generated by the `topic!()` macro
        pub struct $tyname < $($req_lt,)? $($resp_lt)? > {
            $(
                _req_lt: core::marker::PhantomData<& $req_lt ()>,
            )?
            $(
                _resp_lt: core::marker::PhantomData<& $resp_lt ()>,
            )?
            _priv: core::marker::PhantomData<()>,
        }

        impl < $($req_lt,)? $($resp_lt)? > $crate::traits::Endpoint for $tyname < $($req_lt,)? $($resp_lt)? > {
            type Request = $req $(< $req_lt >)?;
            type Response = $resp $(< $resp_lt >)?;
            const PATH: &'static str = stringify!($tyname);
            const REQ_KEY: $crate::traits::Key = $crate::traits::Key::for_path::<$req>(stringify!($tyname));
            const RESP_KEY: $crate::traits::Key = $crate::traits::Key::for_path::<$resp>(stringify!($tyname));
        }
    };
    ($tyname:ident, $req:tt $(< $req_lt:lifetime >)?, $resp:tt $(< $resp_lt:lifetime >)?, $path:expr $(,)?) => {
        /// An Endpoint definition type
        ///
        /// Generated by the `topic!()` macro
        pub struct $tyname < $($req_lt,)? $($resp_lt)? > {
            $(
                _req_lt: core::marker::PhantomData<& $req_lt ()>,
            )?
            $(
                _resp_lt: core::marker::PhantomData<& $resp_lt ()>,
            )?
            _priv: core::marker::PhantomData<()>,
        }

        impl < $($req_lt,)? $($resp_lt)? > $crate::traits::Endpoint for $tyname < $($req_lt,)? $($resp_lt)? > {
            type Request = $req $(< $req_lt >)?;
            type Response = $resp $(< $resp_lt >)?;
            const PATH: &'static str = $path;
            const REQ_KEY: $crate::traits::Key = $crate::traits::Key::for_path::<$req>($path);
            const RESP_KEY: $crate::traits::Key = $crate::traits::Key::for_path::<$resp>($path);
        }
    };
}

/// ## Topic macro
///
/// Used to define a single Topic marker type that implements the
/// [Topic][crate::traits::Topic] trait.
///
/// Borrowed types may contain lifetimes, but only as types with lifetimes, not
/// direct references. This means we can accept `Borrowed<'a>` as a type but not
/// `&'a str`.
///
/// ```rust
/// # use postcard_schema::Schema;
/// # use serde::{Serialize, Deserialize};
/// use ergot::topic;
///
/// #[derive(Schema, Debug, PartialEq, Serialize, Deserialize)]
/// pub struct Stir<'a> {
///     s: &'a str,
/// }
///
/// #[derive(Debug, Serialize, Deserialize, Schema)]
/// pub struct Message1 {
///     a: u8,
///     b: u64,
/// }
///
/// // owned topic
/// topic!(Topic1, Message1, "topic/1");
///
/// // borrowed topic
/// topic!(Topic2, Stir<'a>, "topic/2");
/// ```
///
/// If the path is omitted, the type name is used instead.
#[macro_export]
macro_rules! topic {
    ($tyname:ident, $msg:tt $(< $msg_lt:lifetime >)? $(,)?) => {
        /// A Topic definition type
        ///
        /// Generated by the `topic!()` macro
        pub struct $tyname $(< $msg_lt >)? {
            $(
                _plt: core::marker::PhantomData<& $msg_lt ()>,
            )?
            _priv: core::marker::PhantomData<()>,
        }

        impl $(< $msg_lt >)? $crate::traits::Topic for $tyname $(< $msg_lt >)? {
            type Message = $msg $(< $msg_lt >)?;
            const PATH: &'static str = stringify!($tyname);
            const TOPIC_KEY: $crate::traits::Key = $crate::traits::Key::for_path::<$msg>(stringify!($tyname));
        }
    };
    ($tyname:ident, $msg:tt $(< $msg_lt:lifetime >)?, $path:expr $(,)?) => {
        /// A Topic definition type
        ///
        /// Generated by the `topic!()` macro
        pub struct $tyname $(< $msg_lt >)? {
            $(
                _plt: core::marker::PhantomData<& $msg_lt ()>,
            )?
            _priv: core::marker::PhantomData<()>,
        }

        impl $(< $msg_lt >)? $crate::traits::Topic for $tyname $(< $msg_lt >)? {
            type Message = $msg $(< $msg_lt >)?;
            const PATH: &'static str = $path;
            const TOPIC_KEY: $crate::traits::Key = $crate::traits::Key::for_path::<$msg>($path);
        }
    };
}

#[cfg(test)]
mod test {
    use postcard_schema::Schema;
    use serde::{Deserialize, Serialize};

    use crate::topic;
    use crate::traits::{Endpoint, Topic};

    #[derive(Schema, Debug, PartialEq, Serialize, Deserialize)]
    pub struct Stir<'a> {
        s: &'a str,
    }

    #[test]
    fn test_topic_macro() {
        topic!(OwnedTopic1, u32, "owned1/u32/topic");
        topic!(OwnedTopic2, u32, "owned2/u32/topic",);
        topic!(OwnedTopic3, u32);
        topic!(OwnedTopic4, u32,);
        assert_eq!(OwnedTopic1::PATH, "owned1/u32/topic");
        assert_eq!(OwnedTopic2::PATH, "owned2/u32/topic");
        assert_eq!(OwnedTopic3::PATH, "OwnedTopic3");
        assert_eq!(OwnedTopic4::PATH, "OwnedTopic4");

        topic!(BorrowedTopic1, Stir<'a>, "borrowed1/str/topic");
        topic!(BorrowedTopic2, Stir<'a>, "borrowed2/str/topic",);
        topic!(BorrowedTopic3, Stir<'a>);
        topic!(BorrowedTopic4, Stir<'a>,);
        assert_eq!(BorrowedTopic1::PATH, "borrowed1/str/topic");
        assert_eq!(BorrowedTopic2::PATH, "borrowed2/str/topic",);
        assert_eq!(BorrowedTopic3::PATH, "BorrowedTopic3");
        assert_eq!(BorrowedTopic4::PATH, "BorrowedTopic4");
    }

    #[test]
    fn test_endpoint_macro() {
        // both owned
        endpoint!(OwnedEp1, u32, u32, "owned1/u32/ep");
        endpoint!(OwnedEp2, u32, u32, "owned2/u32/ep",);
        endpoint!(OwnedEp3, u32, u32);
        endpoint!(OwnedEp4, u32, u32,);
        assert_eq!(OwnedEp1::PATH, "owned1/u32/ep");
        assert_eq!(OwnedEp2::PATH, "owned2/u32/ep");
        assert_eq!(OwnedEp3::PATH, "OwnedEp3");
        assert_eq!(OwnedEp4::PATH, "OwnedEp4");

        // req bor, resp owned
        endpoint!(ReqBRespOEp1, Stir<'a>, u32, "reqbrespo1/ep");
        endpoint!(ReqBRespOEp2, Stir<'a>, u32, "reqbrespo2/ep",);
        endpoint!(ReqBRespOEp3, Stir<'a>, u32);
        endpoint!(ReqBRespOEp4, Stir<'a>, u32,);
        assert_eq!(ReqBRespOEp1::PATH, "reqbrespo1/ep");
        assert_eq!(ReqBRespOEp2::PATH, "reqbrespo2/ep");
        assert_eq!(ReqBRespOEp3::PATH, "ReqBRespOEp3");
        assert_eq!(ReqBRespOEp4::PATH, "ReqBRespOEp4");

        // req owned, resp bor
        endpoint!(ReqORespBEp1, u32, Stir<'a>, "reqorespb1/ep");
        endpoint!(ReqORespBEp2, u32, Stir<'a>, "reqorespb2/ep",);
        endpoint!(ReqORespBEp3, u32, Stir<'a>);
        endpoint!(ReqORespBEp4, u32, Stir<'a>,);
        assert_eq!(ReqORespBEp1::PATH, "reqorespb1/ep");
        assert_eq!(ReqORespBEp2::PATH, "reqorespb2/ep");
        assert_eq!(ReqORespBEp3::PATH, "ReqORespBEp3");
        assert_eq!(ReqORespBEp4::PATH, "ReqORespBEp4");

        // req bor, resp bor
        endpoint!(ReqBRespBEp1, Stir<'a>, Stir<'b>, "reqbrespb1/ep");
        endpoint!(ReqBRespBEp2, Stir<'a>, Stir<'b>, "reqbrespb2/ep",);
        endpoint!(ReqBRespBEp3, Stir<'a>, Stir<'b>);
        endpoint!(ReqBRespBEp4, Stir<'a>, Stir<'b>,);
        assert_eq!(ReqBRespBEp1::PATH, "reqbrespb1/ep");
        assert_eq!(ReqBRespBEp2::PATH, "reqbrespb2/ep");
        assert_eq!(ReqBRespBEp3::PATH, "ReqBRespBEp3");
        assert_eq!(ReqBRespBEp4::PATH, "ReqBRespBEp4");
    }
}
