use std::{borrow::Cow, collections::btree_map::BTreeMap, fmt::Write};

use zbus::zvariant;

pub fn structure_display_fmt<W: Write>(
    f: &mut W,
    structure: &zvariant::Structure<'_>,
) -> std::fmt::Result {
    let fields = structure.fields();
    let len = fields.len();

    match len {
        0 => {}
        1 => {
            value_display_fmt(f, &fields[0])?;
        }
        _ => {
            for (i, field) in fields.iter().enumerate() {
                value_display_fmt(f, field)?;
                if i + 1 < fields.len() {
                    f.write_char(' ')?;
                }
            }
        }
    }
    Ok(())
}

/// Implemented based on:
/// - https://gitlab.gnome.org/GNOME/glib/-/blob/e1d47f0b0d0893ac9171e24cc7bf635495376546/glib/gvariant.c#L2213
/// - https://github.com/dbus2/zbus/blob/22182f00da8d7d29508aa9db58fd90fd583bd56f/zvariant/src/value.rs#L479
pub(crate) fn value_display_fmt<W: Write>(
    f: &mut W,
    value: &zvariant::Value<'_>,
) -> std::fmt::Result {
    match value {
        zvariant::Value::U8(num) => {
            write!(f, "0x{num:02x}")
        }
        zvariant::Value::Bool(boolean) => {
            write!(f, "{boolean}")
        }
        zvariant::Value::I16(num) => {
            write!(f, "{num}")
        }
        zvariant::Value::U16(num) => {
            write!(f, "{num}")
        }
        zvariant::Value::I32(num) => {
            // Never annotate this type because it is the default for numbers
            write!(f, "{num}")
        }
        zvariant::Value::U32(num) => {
            write!(f, "{num}")
        }
        zvariant::Value::I64(num) => {
            write!(f, "{num}")
        }
        zvariant::Value::U64(num) => {
            write!(f, "{num}")
        }
        zvariant::Value::F64(num) => {
            if num.fract() == 0. {
                // Add a dot to make it clear that this is a float
                write!(f, "{num}.")
            } else {
                write!(f, "{num}")
            }
        }
        zvariant::Value::Str(string) => {
            write!(
                f,
                "{}",
                &shell_escape::escape(Cow::Borrowed(string.as_str()))
            )
        }
        zvariant::Value::Signature(val) => {
            write!(f, "{}", val.to_string())
        }
        zvariant::Value::ObjectPath(val) => {
            write!(f, "{}", val.as_str())
        }
        zvariant::Value::Value(child) => {
            f.write_str(&child.value_signature().to_string())?;
            f.write_char(' ')?;
            value_display_fmt(f, child)?;
            Ok(())
        }
        zvariant::Value::Array(array) => array_display_fmt(f, array),
        zvariant::Value::Dict(dict) => dict_display_fmt(f, dict),
        zvariant::Value::Structure(structure) => structure_display_fmt(f, structure),
        #[cfg(unix)]
        zvariant::Value::Fd(handle) => {
            write!(f, "{handle}")
        }
    }
}

pub(crate) fn dict_display_fmt<W: Write>(
    f: &mut W,
    dict: &zvariant::Dict<'_, '_>,
) -> std::fmt::Result {
    let mut bmap = BTreeMap::new();
    for (k, v) in dict.iter() {
        bmap.insert(k, v);
    }

    write!(f, "{len} ", len = bmap.len())?;

    if !bmap.is_empty() {
        // Annotate only the first entry as the rest will be of the same type.
        for (i, (key, value)) in bmap.iter().enumerate() {
            value_display_fmt(f, key)?;
            f.write_char(' ')?;
            value_display_fmt(f, value)?;

            if i + 1 < bmap.len() {
                f.write_char(' ')?;
            }
        }
    }

    Ok(())
}

pub(crate) fn array_display_fmt<W: Write>(
    f: &mut W,
    array: &zvariant::Array<'_>,
) -> std::fmt::Result {
    // Print as string if it is a bytestring (i.e., first nul character is the last
    // byte)
    write!(f, "{len} ", len = array.len())?;

    if let [leading @ .., zvariant::Value::U8(b'\0')] = array.as_ref() {
        if !leading.contains(&zvariant::Value::U8(b'\0')) {
            let bytes = leading
                .iter()
                .map(|v| {
                    v.downcast_ref::<u8>()
                        .expect("item must have a signature of a byte")
                })
                .collect::<Vec<_>>();

            let string = String::from_utf8_lossy(&bytes);
            write!(f, "b{:?}", string.as_ref())?;

            return Ok(());
        }
    }

    if !array.is_empty() {
        // Annotate only the first item as the rest will be of the same type.
        for (i, item) in array.iter().enumerate() {
            value_display_fmt(f, item)?;

            if i + 1 < array.len() {
                f.write_char(' ')?;
            }
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use std::borrow::Cow;

    use zvariant::{OwnedValue, Value};

    use super::*;

    #[derive(zvariant::Type, zvariant::OwnedValue)]
    struct Struct {
        field1: Vec<u8>,
        field2: String,
    }

    #[test]
    fn test_shell_escape() {
        let handle = "wayland:1zb*ehkl'MwqCXbcN-T=8EGr&7!zB'_0";
        let expected = "'wayland:1zb*ehkl'\\''MwqCXbcN-T=8EGr&7'\\!'zB'\\''_0'";
        let escaped = shell_escape::escape(Cow::Borrowed(handle));
        assert_eq!(escaped, expected);
    }

    #[test]
    fn test_struct_display_fmt() {
        let mut s = String::new();
        let expected = "2 0xda 0xff 'hello world'";

        let structure = Struct {
            field1: vec![0xda, 255],
            field2: "hello world".to_string(),
        };

        let v = OwnedValue::try_from(structure).unwrap();
        value_display_fmt(&mut s, &v).unwrap();

        assert_eq!("(ays)", v.value_signature().to_string());
        assert_eq!(s, expected);
    }

    #[test]
    fn test_dict_display_fmt() {
        #[rustfmt::skip]
        // From:
        //
        // busctl --user call org.freedesktop.secrets /org/freedesktop/secrets org.freedesktop.Secret.Service \
        // SearchItems a{ss} 1 e-source-uid c4df06dec5cc2eb0e992654d1aa42cd58cf05117
        let mut s = String::new();
        let expected = "1 e-source-uid c4df06dec5cc2eb0e992654d1aa42cd58cf05117";

        let mut dict = zvariant::Dict::new(&zvariant::Signature::Str, &zvariant::Signature::Str);
        dict.append(
            Value::Str(zvariant::Str::from_static("e-source-uid")),
            Value::Str(zvariant::Str::from_static(
                "c4df06dec5cc2eb0e992654d1aa42cd58cf05117",
            )),
        )
        .unwrap();

        let v = Value::Dict(dict);
        value_display_fmt(&mut s, &v).unwrap();

        assert_eq!("a{ss}", v.value_signature().to_string());
        assert_eq!(s, expected);
    }

    #[test]
    fn test_value_display_fmt() {
        // Examples from busctl(1) man page.
        let mut s = String::new();
        let v = zvariant::Value::Str(zvariant::Str::from_static("jawoll"));
        value_display_fmt(&mut s, &v).unwrap();

        assert_eq!("s", v.value_signature().to_string());
        assert_eq!(s, "jawoll");

        let mut s = String::new();
        let mut arr = zvariant::Array::new(&zvariant::Signature::Str);
        arr.append(zvariant::Value::Str(zvariant::Str::from_static("hello")))
            .unwrap();
        arr.append(zvariant::Value::Str(zvariant::Str::from_static("world")))
            .unwrap();
        arr.append(zvariant::Value::Str(zvariant::Str::from_static("foobar")))
            .unwrap();

        let v = zvariant::Value::Array(arr);
        value_display_fmt(&mut s, &v).unwrap();

        assert_eq!("as", v.value_signature().to_string());
        assert_eq!(s, "3 hello world foobar");

        let mut s = String::new();
        let mut dict =
            zvariant::Dict::new(&zvariant::Signature::Str, &zvariant::Signature::Variant);
        dict.append(
            Value::Str(zvariant::Str::from_static("One")),
            Value::Value(Box::new(zvariant::Value::Str(zvariant::Str::from_static(
                "Eins",
            )))),
        )
        .unwrap();
        dict.append(
            Value::Str(zvariant::Str::from_static("Two")),
            Value::Value(Box::new(zvariant::Value::U32(2))),
        )
        .unwrap();
        dict.append(
            Value::Str(zvariant::Str::from_static("Yes")),
            Value::Value(Box::new(zvariant::Value::Bool(true))),
        )
        .unwrap();

        let v = zvariant::Value::Dict(dict);
        value_display_fmt(&mut s, &v).unwrap();

        assert_eq!("a{sv}", v.value_signature().to_string());
        assert_eq!(s, "3 One s Eins Two u 2 Yes b true");
    }
}
