use bevy_app::{App, Plugin, Startup};
use bevy_ecs::message::Message;
use bevy_ecs::prelude::Messages;
use bevy_ecs::resource::Resource;
use bevy_ecs::world::World;
use tokio_xmpp::Stanza;

use crate::component::system::spawn_stanza_workers;

pub trait StanzaMatcher: Send + Sync + 'static {
    type Message: Message;
    fn matches(&mut self, candidate: &Stanza) -> Option<Self::Message>;
}

// =============================================================================
// HList Structure
// =============================================================================

pub struct HNil;

pub struct HCons<Head, Tail> {
    pub head: Head,
    pub tail: Tail,
}

pub struct Matcher<M>(pub M);

// =============================================================================
// Dispatch Trait
// =============================================================================

pub trait StanzaDispatch: Send + Sync + 'static {
    fn dispatch(&mut self, stanza: &Stanza, world: &mut World);
    fn register(app: &mut App);
}

impl StanzaDispatch for HNil {
    fn dispatch(&mut self, _stanza: &Stanza, _world: &mut World) {}
    fn register(_app: &mut App) {}
}

impl<M, Tail> StanzaDispatch for HCons<Matcher<M>, Tail>
where
    M: StanzaMatcher,
    Tail: StanzaDispatch,
{
    fn dispatch(&mut self, stanza: &Stanza, world: &mut World) {
        if let Some(msg) = self.head.0.matches(stanza) {
            world.resource_mut::<Messages<M::Message>>().write(msg);
            return;
        }
        self.tail.dispatch(stanza, world)
    }

    fn register(app: &mut App) {
        app.add_message::<M::Message>();
        Tail::register(app);
    }
}

// =============================================================================
// Builder API
// =============================================================================

impl HNil {
    pub fn matcher<M: StanzaMatcher>(self, m: M) -> HCons<Matcher<M>, HNil> {
        HCons {
            head: Matcher(m),
            tail: HNil,
        }
    }
}

impl<Head, Tail> HCons<Head, Tail> {
    pub fn matcher<M: StanzaMatcher>(self, m: M) -> HCons<Matcher<M>, Self> {
        HCons {
            head: Matcher(m),
            tail: self,
        }
    }
}

// =============================================================================
// Resource Wrapper
// =============================================================================

#[derive(Resource)]
pub struct MatcherRegistry<D: StanzaDispatch> {
    pub dispatchers: D,
}

// =============================================================================
// Plugin
// =============================================================================

pub struct StanzaMatcherPlugin<D> {
    dispatchers: D,
}

impl StanzaMatcherPlugin<HNil> {
    pub fn new() -> Self {
        StanzaMatcherPlugin { dispatchers: HNil }
    }
}

impl Default for StanzaMatcherPlugin<HNil> {
    fn default() -> Self {
        Self::new()
    }
}

impl<D> StanzaMatcherPlugin<D> {
    pub fn matcher<M: StanzaMatcher>(self, m: M) -> StanzaMatcherPlugin<HCons<Matcher<M>, D>> {
        StanzaMatcherPlugin {
            dispatchers: HCons {
                head: Matcher(m),
                tail: self.dispatchers,
            },
        }
    }
}

impl<D: StanzaDispatch + Clone> Plugin for StanzaMatcherPlugin<D> {
    fn build(&self, app: &mut App) {
        D::register(app);
        app.insert_resource(MatcherRegistry {
            dispatchers: self.dispatchers.clone(),
        });
        app.add_systems(Startup, spawn_stanza_workers::<D>);
    }
}

// =============================================================================
// Clone Impls
// =============================================================================

impl Clone for HNil {
    fn clone(&self) -> Self {
        HNil
    }
}

impl<M: Clone> Clone for Matcher<M> {
    fn clone(&self) -> Self {
        Matcher(self.0.clone())
    }
}

impl<Head: Clone, Tail: Clone> Clone for HCons<Head, Tail> {
    fn clone(&self) -> Self {
        HCons {
            head: self.head.clone(),
            tail: self.tail.clone(),
        }
    }
}
