use super::{Connection, Message, MessageItem, Error, Path, Interface, BusName};
use std::collections::BTreeMap;

/// Client side properties - get and set properties on a remote application.
pub struct Props<'a> {
    name: BusName<'a>,
    path: Path<'a>,
    interface: Interface<'a>,
    timeout_ms: i32,
    conn: &'a Connection,
}

impl<'a> Props<'a> {
    /// Create a new Props.
    pub fn new<N, P, I>(conn: &'a Connection, name: N, path: P, interface: I, timeout_ms: i32) -> Props<'a>
    where N: Into<BusName<'a>>, P: Into<Path<'a>>, I: Into<Interface<'a>> {
        Props {
            name: name.into(),
            path: path.into(),
            interface: interface.into(),
            timeout_ms: timeout_ms,
            conn: conn,
        }
    }

    /// Get a single property's value.
    pub fn get(&self, propname: &str) -> Result<MessageItem, Error> {
        let mut m = Message::method_call(&self.name, &self.path,
            &"org.freedesktop.DBus.Properties".into(), &"Get".into());
        m.append_items(&[self.interface.to_string().into(), propname.to_string().into()]);
        let mut r = try!(self.conn.send_with_reply_and_block(m, self.timeout_ms));
        let reply = try!(r.as_result()).get_items();
        if reply.len() == 1 {
            if let &MessageItem::Variant(ref v) = &reply[0] {
                return Ok((**v).clone())
            }
       }
       let f = format!("Invalid reply for property get {}: '{:?}'", propname, reply);
       return Err(Error::new_custom("InvalidReply", &f));
    }

    /// Set a single property's value.
    pub fn set(&self, propname: &str, value: MessageItem) -> Result<(), Error> {
        let mut m = Message::method_call(&self.name, &self.path,
            &"org.freedesktop.DBus.Properties".into(), &"Set".into());
        m.append_items(&[self.interface.to_string().into(), propname.to_string().into(), Box::new(value).into()]);
        let mut r = try!(self.conn.send_with_reply_and_block(m, self.timeout_ms));
        try!(r.as_result());
        Ok(())
    }

    /// Get a map of all the properties' names and their values.
    pub fn get_all(&self) -> Result<BTreeMap<String, MessageItem>, Error> {
        let mut m = Message::method_call(&self.name, &self.path,
            &"org.freedesktop.DBus.Properties".into(), &"GetAll".into());
        m.append_items(&[self.interface.to_string().into()]);
        let mut r = try!(self.conn.send_with_reply_and_block(m, self.timeout_ms));
        let reply = try!(r.as_result()).get_items();

        (|| {
            if reply.len() != 1 { return Err(()) };
            let mut t = BTreeMap::new();
            let a: &[MessageItem] = try!(reply[0].inner());
            for p in a.iter() {
                let (k, v) = try!(p.inner());
                let (k, v): (&String, &MessageItem) = (try!(k.inner()), try!(v.inner()));
                t.insert(k.clone(), v.clone());
            }
            Ok(t)
        })().map_err(|_| {
            let f = format!("Invalid reply for property GetAll: '{:?}'", reply);
            Error::new_custom("InvalidReply", &f)
        })
    }
}

/// Wrapper around Props that keeps a map of fetched properties.
pub struct PropHandler<'a> {
    p: Props<'a>,
    map: BTreeMap<String, MessageItem>,
}

impl<'a> PropHandler<'a> {
    /// Create a new PropHandler from a Props.
    pub fn new(p: Props) -> PropHandler {
        PropHandler { p: p, map: BTreeMap::new() }
    }

    /// Get a map of all the properties' names and their values.
    pub fn get_all(&mut self) -> Result<(), Error> {
        self.map = try!(self.p.get_all());
        Ok(())
    }

    /// Get a mutable reference to the PropHandler's fetched properties.
    pub fn map_mut(&mut self) -> &mut BTreeMap<String, MessageItem> { &mut self.map }

    /// Get a reference to the PropHandler's fetched properties.
    pub fn map(&self) -> &BTreeMap<String, MessageItem> { &self.map }

    /// Get a single property's value.
    pub fn get(&mut self, propname: &str) -> Result<&MessageItem, Error> {
        let v = try!(self.p.get(propname));
        self.map.insert(propname.to_string(), v);
        Ok(self.map.get(propname).unwrap())
    }

    /// Set a single property's value.
    pub fn set(&mut self, propname: &str, value: MessageItem) -> Result<(), Error> {
        try!(self.p.set(propname, value.clone()));
        self.map.insert(propname.to_string(), value);
        Ok(())
    }
}


/* Unfortunately org.freedesktop.DBus has no properties we can use for testing, but PolicyKit should be around on most distros. */
#[test]
fn test_get_policykit_version() {
    use super::BusType;
    let c = Connection::get_private(BusType::System).unwrap();
    let p = Props::new(&c, "org.freedesktop.PolicyKit1", "/org/freedesktop/PolicyKit1/Authority",
        "org.freedesktop.PolicyKit1.Authority", 10000);

    /* Let's use both the get and getall methods and see if we get the same result */
    let v = p.get("BackendVersion").unwrap();
    let vall = p.get_all().unwrap();
    let v2 = vall.get("BackendVersion").unwrap();

    assert_eq!(&v, &*v2);
    match v {
        MessageItem::Str(ref s) => { println!("Policykit Backend version is {}", s); }
        _ => { panic!("Invalid Get: {:?}", v); }
    };
}

