use log::info;
use std::sync::{Arc, Mutex};

use dbus::{
    channel::{MatchingReceiver, Sender},
    message::MatchRule,
    nonblock::SyncConnection,
    Path,
};
use dbus_crossroads::Crossroads;

#[derive(Clone)]
pub struct DbusRegistry {
    connection: Arc<SyncConnection>,
    crossroads: Arc<Mutex<Crossroads>>,
    message_handler_started: Arc<Mutex<bool>>,
}

impl DbusRegistry {
    pub fn new(connection: Arc<SyncConnection>) -> Self {
        let mut cr = Crossroads::new();
        // Enable async support for the crossroads instance.
        // (Currently irrelevant since dbus generates sync code)
        cr.set_async_support(Some((
            connection.clone(),
            Box::new(|x| {
                tokio::spawn(x);
            }),
        )));

        Self {
            connection,
            crossroads: Arc::new(Mutex::new(cr)),
            message_handler_started: Arc::new(Mutex::new(false)),
        }
    }

    pub fn register_object<T, F, R>(&self, path: &str, implementation: T, register_fn: F)
    where
        T: Send + Clone + 'static,
        F: Fn(&mut Crossroads) -> R,
        R: IntoIterator<Item = dbus_crossroads::IfaceToken<T>>,
    {
        let dbus_path = String::from(path);

        let mut cr = self.crossroads.lock().unwrap();
        let tokens: Vec<_> = register_fn(&mut cr).into_iter().collect();
        cr.insert(dbus_path, &tokens, implementation);

        // Start message handler if not already started
        let mut handler_started = self.message_handler_started.lock().unwrap();
        if !*handler_started {
            let crossroads_clone = self.crossroads.clone();
            self.connection.start_receive(
                MatchRule::new_method_call(),
                Box::new(move |msg, conn| {
                    let mut cr = crossroads_clone.lock().unwrap();
                    cr.handle_message(msg, conn).is_ok()
                }),
            );
            *handler_started = true;
            info!(target: "dbus", "Started D-Bus message handler");
        }

        info!(target: "dbus", "Registered object at {} with {} interfaces", path, tokens.len());
    }

    pub fn send_signal<S>(&self, path: &str, signal: S) -> Result<u32, ()>
    where
        S: dbus::message::SignalArgs + dbus::arg::AppendAll,
    {
        let message = signal.to_emit_message(&Path::new(path).unwrap());
        self.connection.send(message)
    }
}
