use crate::addressing::Address;
use actix::prelude::*;
use anyhow::{anyhow, Result};
use diesel::backend::Backend;
use diesel::deserialize::FromSql;
use diesel::sqlite::Sqlite;
use diesel::{deserialize, sql_types};
use filebuffer::FileBuffer;
use serde::{ser, Serialize, Serializer};
use std::path::PathBuf;
use tiny_keccak::{Hasher, KangarooTwelve};

#[derive(Debug, Clone, PartialEq, FromSqlRow)]
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(
            encode(
                Address::Hash(self.clone())
                    .encode()
                    .map_err(ser::Error::custom)?,
            )
            .as_str(),
        )
    }
}

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

pub struct HasherWorker;

impl Actor for HasherWorker {
    type Context = SyncContext<Self>;
}

#[derive(Message)]
#[rtype(result = "Result<Hash>")]
pub struct ComputeHash {
    pub path: PathBuf,
}

impl Handler<ComputeHash> for HasherWorker {
    type Result = Result<Hash>;

    fn handle(&mut self, msg: ComputeHash, _: &mut Self::Context) -> Self::Result {
        msg.path.hash()
    }
}

impl Hashable for PathBuf {
    fn hash(self: &PathBuf) -> Result<Hash> {
        let fbuffer = FileBuffer::open(self)?;
        Ok(hash(&fbuffer))
    }
}

pub fn hash<T: AsRef<[u8]>>(input: T) -> Hash {
    let mut k12 = KangarooTwelve::new(b"");
    k12.update(input.as_ref());

    let mut result = [0u8; 256 / 8];
    k12.finalize(&mut result);
    Hash(Vec::from(result))
}

pub fn encode<T: AsRef<[u8]>>(vec: T) -> String {
    // multibase base58
    format!("z{}", bs58::encode(vec).into_string())
}

pub fn decode<T: AsRef<str>>(string: T) -> Result<Vec<u8>> {
    let string = string.as_ref();
    let (base, data) = string.split_at(1);

    // multibase base58
    if base != "z" {
        Err(anyhow!("data not base58 encoded, bailing"))
    } else {
        Ok(bs58::decode(data).into_vec()?)
    }
}

#[cfg(test)]
mod tests {
    use crate::hash::{decode, encode};

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

        let encoded = encode(content);
        let decoded = decode(encoded);

        assert!(decoded.is_ok());

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