use Result;
use serialize::{Serialize, SerializeInto, generic_serialize_into};

use crypto::sexp::{Sexp, String_};

impl Serialize for Sexp {
    fn serialize(&self, o: &mut dyn std::io::Write) -> Result<()> {
        match self {
            Sexp::String(ref s) => s.serialize(o),
            Sexp::List(ref l) => {
                write!(o, "(")?;
                for sexp in l {
                    sexp.serialize(o)?;
                }
                write!(o, ")")?;
                Ok(())
            },
        }
    }
}

impl SerializeInto for Sexp {
    fn serialized_len(&self) -> usize {
        match self {
            Sexp::String(ref s) => s.serialized_len(),
            Sexp::List(ref l) =>
                2 + l.iter().map(|s| s.serialized_len()).sum::<usize>(),
        }
    }

    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
        generic_serialize_into(self, buf)
    }
}

impl Serialize for String_ {
    fn serialize(&self, o: &mut dyn std::io::Write) -> Result<()> {
        if let Some(display) = self.display_hint() {
            write!(o, "[{}:", display.len())?;
            o.write_all(display)?;
            write!(o, "]")?;
        }
        write!(o, "{}:", self.len())?;
        o.write_all(self)?;
        Ok(())
    }
}

/// Computes the length of the size tag for a given string length.
fn size_tag_len(len: usize) -> usize {
    // Compute log10(self.len()).
    let mut l = len;
    let mut digits = 0;
    while l > 0 {
        l /= 10;
        digits += 1;
    }

    std::cmp::max(1, digits) // 0 takes up 1 char, too.
}

impl SerializeInto for String_ {
    fn serialized_len(&self) -> usize {
        self.display_hint()
            .map(|d| size_tag_len(d.len()) + 3 + d.len()).unwrap_or(0)
            + size_tag_len(self.len()) + 1 + self.len()
    }

    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
        generic_serialize_into(self, buf)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sexp() {
        assert_eq!(
            &Sexp::List(vec![]).to_vec().unwrap(),
            b"()");
        assert_eq!(
            &Sexp::List(vec![Sexp::String(b"hi"[..].into()),
                            Sexp::String(b"ho"[..].into()),
            ]).to_vec().unwrap(),
            b"(2:hi2:ho)");
        assert_eq!(
            &Sexp::List(vec![
                Sexp::String(b"hi"[..].into()),
                Sexp::String(String_::with_display_hint(b"ho".to_vec(),
                                                        b"fancy".to_vec())),
            ]).to_vec().unwrap(),
            b"(2:hi[5:fancy]2:ho)");
        assert_eq!(
            &Sexp::List(vec![
                Sexp::String(b"hi"[..].into()),
                Sexp::List(vec![
                    Sexp::String(b"ha"[..].into()),
                    Sexp::String(b"ho"[..].into()),
                ]),
            ]).to_vec().unwrap(),
            b"(2:hi(2:ha2:ho))");
        assert_eq!(
            &Sexp::List(vec![
                Sexp::String(b"sig-val"[..].into()),
                Sexp::List(vec![
                    Sexp::String(b"rsa"[..].into()),
                    Sexp::List(vec![
                        Sexp::String(b"s"[..].into()),
                        Sexp::String(b"abc"[..].into()),
                    ]),
                ]),
            ]).to_vec().unwrap(),
            b"(7:sig-val(3:rsa(1:s3:abc)))");
    }

    #[test]
    fn string() {
        assert_eq!(&String_::new(b"hi".to_vec()).to_vec().unwrap(),
                   b"2:hi");
        assert_eq!(&String_::with_display_hint(b"hi".to_vec(),
                                               b"fancy".to_vec())
                   .to_vec().unwrap(),
                   b"[5:fancy]2:hi");
    }
}
