use crate::addressing::Address;
use anyhow::Result;
use diesel::backend::Backend;
use diesel::deserialize::FromSql;
use diesel::sqlite::Sqlite;
use diesel::{deserialize, sql_types};
use filebuffer::FileBuffer;
use multihash::Hasher;
use serde::{ser, Serialize, Serializer};
use std::path::{Path};
use tracing::trace;

#[derive(Debug, Clone, Eq, PartialEq, FromSqlRow, Hash)]
pub struct Hash(pub Vec<u8>);

impl AsRef<[u8]> for Hash {
    fn as_ref(&self) -> &[u8] {
        self.0.as_ref()
    }
}

impl FromSql<sql_types::Binary, Sqlite> for Hash {
    fn from_sql(bytes: Option<&<Sqlite as Backend>::RawValue>) -> deserialize::Result<Self> {
        Ok(Hash(Vec::from(not_none!(bytes).read_blob())))
    }
}

impl Serialize for Hash {
    fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(
            b58_encode(
                Address::Hash(self.clone())
                    .encode()
                    .map_err(ser::Error::custom)?,
            )
            .as_str(),
        )
    }
}

pub trait Hashable {
    fn hash(&self) -> Result<Hash>;
}

impl Hashable for Path {
    fn hash(self: &Path) -> Result<Hash> {
        trace!("Hashing {:?}...", self);
        let fbuffer = FileBuffer::open(self)?;
        trace!("Finished hashing {:?}...", self);
        Ok(hash(&fbuffer))
    }
}

pub fn hash<T: AsRef<[u8]>>(input: T) -> Hash {
    let mut hasher = multihash::Sha2_256::default();
    hasher.update(input.as_ref());
    Hash(Vec::from(hasher.finalize()))
}

pub fn b58_encode<T: AsRef<[u8]>>(vec: T) -> String {
    multibase::encode(multibase::Base::Base58Btc, vec.as_ref())
}

pub fn b58_decode<T: AsRef<str>>(input: T) -> Result<Vec<u8>> {
    let input = input.as_ref();
    let (_base, data) = multibase::decode(input)?;
    Ok(data)
}

#[cfg(test)]
mod tests {
    use crate::util::hash::{b58_decode, b58_encode};

    #[test]
    fn test_encode_decode() {
        let content = "Hello, World!".as_bytes();

        let encoded = b58_encode(content);
        let decoded = b58_decode(encoded);

        assert!(decoded.is_ok());

        assert_eq!(content, decoded.unwrap());
    }
}
