use std::borrow::Cow;
use std::marker::PhantomData;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PubSub<T>(PhantomData<T>);

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Query<Req, Resp>(PhantomData<(Req, Resp)>);

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Slot {
    Any,
    Bound(Cow<'static, str>),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SlotArg {
    Any,
    Bound(Cow<'static, str>),
}

pub const ANY: SlotArg = SlotArg::Any;

impl From<SlotArg> for Slot {
    fn from(value: SlotArg) -> Self {
        match value {
            SlotArg::Any => Self::Any,
            SlotArg::Bound(value) => Self::Bound(value),
        }
    }
}

impl From<&str> for SlotArg {
    fn from(value: &str) -> Self {
        if value == "*" {
            Self::Any
        } else {
            Self::Bound(Cow::Owned(value.to_string()))
        }
    }
}

impl From<&String> for SlotArg {
    fn from(value: &String) -> Self {
        value.as_str().into()
    }
}

impl From<String> for SlotArg {
    fn from(value: String) -> Self {
        if value == "*" {
            Self::Any
        } else {
            Self::Bound(Cow::Owned(value))
        }
    }
}

impl From<Cow<'static, str>> for SlotArg {
    fn from(value: Cow<'static, str>) -> Self {
        if value.as_ref() == "*" {
            Self::Any
        } else {
            Self::Bound(value)
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Topic<Kind> {
    template: &'static str,
    schema: &'static str,
    version: u32,
    slots: Vec<Slot>,
    _kind: PhantomData<Kind>,
}

impl<Kind> Topic<Kind> {
    pub fn new(
        template: &'static str,
        schema: &'static str,
        version: u32,
        slots: Vec<Slot>,
    ) -> Self {
        Self {
            template,
            schema,
            version,
            slots,
            _kind: PhantomData,
        }
    }

    pub fn key(&self) -> Cow<'static, str> {
        if self.slots.is_empty() {
            return Cow::Borrowed(self.template);
        }

        let mut slots = self.slots.iter();
        let mut parts = self.template.split('*').peekable();
        let mut key = String::with_capacity(self.template.len());

        while let Some(part) = parts.next() {
            key.push_str(part);
            if parts.peek().is_some() {
                match slots.next() {
                    Some(Slot::Any) => key.push('*'),
                    Some(Slot::Bound(value)) => key.push_str(value),
                    None => key.push('*'),
                }
            }
        }

        Cow::Owned(key)
    }

    pub fn schema(&self) -> &'static str {
        self.schema
    }

    pub const fn version(&self) -> u32 {
        self.version
    }

    pub fn is_concrete(&self) -> bool {
        self.slots.iter().all(|slot| matches!(slot, Slot::Bound(_))) && !self.key().contains('*')
    }

    pub fn publish_key(&self) -> crate::bus::Result<Cow<'static, str>> {
        let key = self.key();
        if !self.is_concrete() || key.contains('*') {
            Err(crate::bus::Error::InvalidTopic(
                "topic has wildcard '*' slot(s); bind all ids before publishing".to_string(),
            ))
        } else {
            Ok(key)
        }
    }
}

#[macro_export]
macro_rules! topic_tree {
    ($visibility:vis mod $module:ident; $($items:tt)*) => {
        $visibility mod $module {
            pub fn new() -> __builders::Root {
                __builders::Root::new(::std::vec::Vec::new())
            }

            #[doc(hidden)]
            pub mod __builders {
                #[derive(Debug, Clone)]
                pub struct Root {
                    slots: ::std::vec::Vec<$crate::bus::topic::Slot>,
                }

                impl Root {
                    pub(super) fn new(
                        slots: ::std::vec::Vec<$crate::bus::topic::Slot>,
                    ) -> Self {
                        Self { slots }
                    }
                }

                impl Root {
                    $crate::topic_tree!(@methods [] [] $($items)*);
                }

                $crate::topic_tree!(@modules [] [] $($items)*);
            }
        }
    };

    (@methods [$($template:expr),*] [$($schema:expr),*]) => {};

    (
        @methods [$($template:expr),*] [$($schema:expr),*]
        pubsub r#static: $payload:ty, version = $version:literal;
        $($rest:tt)*
    ) => {
        $crate::topic_tree!(
            @pubsub_method r#static, "static", [$($template),*] [$($schema),*] $payload, $version
        );
        $crate::topic_tree!(@methods [$($template),*] [$($schema),*] $($rest)*);
    };

    (
        @methods [$($template:expr),*] [$($schema:expr),*]
        pubsub $leaf:ident: $payload:ty, version = $version:literal;
        $($rest:tt)*
    ) => {
        $crate::topic_tree!(
            @pubsub_method $leaf, stringify!($leaf), [$($template),*] [$($schema),*] $payload, $version
        );
        $crate::topic_tree!(@methods [$($template),*] [$($schema),*] $($rest)*);
    };

    (
        @methods [$($template:expr),*] [$($schema:expr),*]
        query $leaf:ident: $request:ty => $response:ty, version = $version:literal;
        $($rest:tt)*
    ) => {
        $crate::topic_tree!(
            @query_method $leaf, stringify!($leaf), [$($template),*] [$($schema),*]
            $request => $response, $version
        );
        $crate::topic_tree!(@methods [$($template),*] [$($schema),*] $($rest)*);
    };

    (
        @methods [$($template:expr),*] [$($schema:expr),*]
        $name:ident(id) { $($children:tt)* }
        $($rest:tt)*
    ) => {
        pub fn $name(
            mut self,
            id: impl ::std::convert::Into<$crate::bus::topic::SlotArg>,
        ) -> $name::Builder {
            self.slots.push($crate::bus::topic::Slot::from(id.into()));
            $name::Builder::new(self.slots)
        }

        $crate::topic_tree!(@methods [$($template),*] [$($schema),*] $($rest)*);
    };

    (
        @methods [$($template:expr),*] [$($schema:expr),*]
        $name:ident { $($children:tt)* }
        $($rest:tt)*
    ) => {
        pub fn $name(self) -> $name::Builder {
            $name::Builder::new(self.slots)
        }

        $crate::topic_tree!(@methods [$($template),*] [$($schema),*] $($rest)*);
    };

    (
        @pubsub_method $method:ident, $segment:expr,
        [$($template:expr),*] [$($schema:expr),*]
        $payload:ty, $version:literal
    ) => {
        pub fn $method(
            self,
        ) -> $crate::bus::topic::Topic<$crate::bus::topic::PubSub<$payload>> {
            $crate::bus::topic::Topic::new(
                concat!($($template, "/",)* $segment),
                concat!($($schema, "/",)* $segment),
                $version,
                self.slots,
            )
        }
    };

    (
        @query_method $method:ident, $segment:expr,
        [$($template:expr),*] [$($schema:expr),*]
        $request:ty => $response:ty, $version:literal
    ) => {
        pub fn $method(
            self,
        ) -> $crate::bus::topic::Topic<$crate::bus::topic::Query<$request, $response>> {
            $crate::bus::topic::Topic::new(
                concat!($($template, "/",)* $segment),
                concat!($($schema, "/",)* $segment),
                $version,
                self.slots,
            )
        }
    };

    (@modules [$($template:expr),*] [$($schema:expr),*]) => {};

    (
        @modules [$($template:expr),*] [$($schema:expr),*]
        pubsub r#static: $payload:ty, version = $version:literal;
        $($rest:tt)*
    ) => {
        $crate::topic_tree!(@modules [$($template),*] [$($schema),*] $($rest)*);
    };

    (
        @modules [$($template:expr),*] [$($schema:expr),*]
        pubsub $leaf:ident: $payload:ty, version = $version:literal;
        $($rest:tt)*
    ) => {
        $crate::topic_tree!(@modules [$($template),*] [$($schema),*] $($rest)*);
    };

    (
        @modules [$($template:expr),*] [$($schema:expr),*]
        query $leaf:ident: $request:ty => $response:ty, version = $version:literal;
        $($rest:tt)*
    ) => {
        $crate::topic_tree!(@modules [$($template),*] [$($schema),*] $($rest)*);
    };

    (
        @modules [$($template:expr),*] [$($schema:expr),*]
        $name:ident(id) { $($children:tt)* }
        $($rest:tt)*
    ) => {
        $crate::topic_tree!(@module $name [$($template),*] [$($schema),*] [id] { $($children)* });
        $crate::topic_tree!(@modules [$($template),*] [$($schema),*] $($rest)*);
    };

    (
        @modules [$($template:expr),*] [$($schema:expr),*]
        $name:ident { $($children:tt)* }
        $($rest:tt)*
    ) => {
        $crate::topic_tree!(@module $name [$($template),*] [$($schema),*] [] { $($children)* });
        $crate::topic_tree!(@modules [$($template),*] [$($schema),*] $($rest)*);
    };

    (@module $name:ident [$($template:expr),*] [$($schema:expr),*] [id] { $($children:tt)* }) => {
        pub mod $name {
            #[derive(Debug, Clone)]
            pub struct Builder {
                slots: ::std::vec::Vec<$crate::bus::topic::Slot>,
            }

            impl Builder {
                pub(super) fn new(
                    slots: ::std::vec::Vec<$crate::bus::topic::Slot>,
                ) -> Self {
                    Self { slots }
                }
            }

            impl Builder {
                $crate::topic_tree!(
                    @methods [$($template,)* stringify!($name), "*"] [$($schema,)* stringify!($name)]
                    $($children)*
                );
            }

            $crate::topic_tree!(
                @modules [$($template,)* stringify!($name), "*"] [$($schema,)* stringify!($name)]
                $($children)*
            );
        }
    };

    (@module $name:ident [$($template:expr),*] [$($schema:expr),*] [] { $($children:tt)* }) => {
        pub mod $name {
            #[derive(Debug, Clone)]
            pub struct Builder {
                slots: ::std::vec::Vec<$crate::bus::topic::Slot>,
            }

            impl Builder {
                pub(super) fn new(
                    slots: ::std::vec::Vec<$crate::bus::topic::Slot>,
                ) -> Self {
                    Self { slots }
                }
            }

            impl Builder {
                $crate::topic_tree!(
                    @methods [$($template,)* stringify!($name)] [$($schema,)* stringify!($name)]
                    $($children)*
                );
            }

            $crate::topic_tree!(
                @modules [$($template,)* stringify!($name)] [$($schema,)* stringify!($name)]
                $($children)*
            );
        }
    };
}

#[cfg(test)]
mod tests {
    use crate::api::v1::{
        asset,
        component::capability::{gnss, motor},
        drive, simulation, topic,
    };
    use crate::bus::topic::{ANY, PubSub, Query, Topic};

    #[test]
    fn topic_builder_keys_match_tree_paths() {
        assert_eq!(topic::new().v1().drive().target().key(), "v1/drive/target");
        assert_eq!(
            topic::new()
                .v1()
                .component("base")
                .motor("left_wheel")
                .command()
                .key(),
            "v1/component/base/motor/left_wheel/command"
        );
        assert_eq!(
            topic::new().v1().component(ANY).motor(ANY).command().key(),
            "v1/component/*/motor/*/command"
        );
        assert_eq!(
            topic::new().v1().component("*").motor("*").command().key(),
            "v1/component/*/motor/*/command"
        );
        assert_eq!(topic::new().v1().asset().get().key(), "v1/asset/get");
        assert_eq!(
            topic::new().v1().simulation().clock().key(),
            "v1/simulation/clock"
        );
        assert_eq!(
            topic::new().v1().simulation().robot("r1").pose().key(),
            "v1/simulation/robot/r1/pose"
        );
        assert_eq!(
            topic::new().v1().simulation().robot(ANY).pose().key(),
            "v1/simulation/robot/*/pose"
        );
    }

    #[test]
    fn topic_builder_schemas_elide_holes() {
        let motor = topic::new()
            .v1()
            .component("base")
            .motor("left_wheel")
            .command();
        let gnss = topic::new().v1().component("base").gnss("receiver").data();
        let pose = topic::new().v1().simulation().robot("r1").pose();
        let target = topic::new().v1().drive().target();

        assert_eq!(motor.schema(), "v1/component/motor/command");
        assert_eq!(gnss.schema(), "v1/component/gnss/data");
        assert_eq!(pose.schema(), "v1/simulation/robot/pose");
        assert_eq!(target.schema(), "v1/drive/target");
        assert_eq!(motor.version(), 1);
        assert_eq!(gnss.version(), 1);
        assert_eq!(pose.version(), 1);
        assert_eq!(target.version(), 1);
    }

    #[test]
    fn topic_publish_keys_require_bound_slots() {
        assert!(
            topic::new()
                .v1()
                .component(ANY)
                .motor(ANY)
                .command()
                .publish_key()
                .is_err()
        );
        assert_eq!(
            topic::new()
                .v1()
                .component("base")
                .motor("left_wheel")
                .command()
                .publish_key()
                .unwrap(),
            "v1/component/base/motor/left_wheel/command"
        );
    }

    #[test]
    fn rendered_wildcards_are_not_concrete() {
        let wildcard = topic::new().v1().component("*").motor("left").command();
        let manually_under_bound = Topic::<PubSub<motor::Command>>::new(
            "v1/component/*/motor/*/command",
            "v1/component/motor/command",
            1,
            Vec::new(),
        );

        assert!(!wildcard.is_concrete());
        assert!(!manually_under_bound.is_concrete());
    }

    #[test]
    fn topic_payload_types_are_carried_by_leaf_methods() {
        fn want<T>(_: Topic<PubSub<T>>) {}
        fn want_command(_: Topic<PubSub<motor::Command>>) {}
        fn want_asset_get(_: Topic<Query<asset::GetRequest, asset::GetResponse>>) {}

        want::<drive::Target>(topic::new().v1().drive().target());
        want::<drive::State>(topic::new().v1().drive().state());
        want::<gnss::Sample>(topic::new().v1().component("base").gnss("receiver").data());
        want::<simulation::clock::Clock>(topic::new().v1().simulation().clock());
        want::<simulation::pose::Pose>(topic::new().v1().simulation().robot("r1").pose());
        want_command(
            topic::new()
                .v1()
                .component("base")
                .motor("left_wheel")
                .command(),
        );
        want_asset_get(topic::new().v1().asset().get());
    }
}
