use bs58::decode::Error as B58Error;
use near_crypto_hash::CryptoHash;
use std::convert::TryFrom;

#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
#[cfg_attr(feature = "serde", derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr))]
#[cfg_attr(feature = "abi", derive(borsh::BorshSchema))]
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Default, Hash)]
#[repr(transparent)]
pub struct Base58CryptoHash(CryptoHash);

impl PartialEq<CryptoHash> for Base58CryptoHash {
    fn eq(&self, other: &CryptoHash) -> bool {
        self.0.eq(other)
    }
}

impl PartialOrd<CryptoHash> for Base58CryptoHash {
    fn partial_cmp(&self, other: &CryptoHash) -> Option<std::cmp::Ordering> {
        self.0.partial_cmp(other)
    }
}

impl From<Base58CryptoHash> for CryptoHash {
    fn from(v: Base58CryptoHash) -> Self {
        v.0
    }
}

impl From<Base58CryptoHash> for Vec<u8> {
    fn from(v: Base58CryptoHash) -> Self {
        v.0.into()
    }
}

impl From<&Base58CryptoHash> for CryptoHash {
    fn from(v: &Base58CryptoHash) -> Self {
        v.0
    }
}

impl From<CryptoHash> for Base58CryptoHash {
    fn from(c: CryptoHash) -> Self {
        Base58CryptoHash(c)
    }
}

impl From<&CryptoHash> for Base58CryptoHash {
    fn from(c: &CryptoHash) -> Self {
        Base58CryptoHash(*c)
    }
}

impl AsRef<CryptoHash> for Base58CryptoHash {
    fn as_ref(&self) -> &CryptoHash {
        &self.0
    }
}

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

#[cfg(feature = "schemars-v0_8")]
impl schemars_v0_8::JsonSchema for Base58CryptoHash {
    fn is_referenceable() -> bool {
        false
    }

    fn schema_name() -> String {
        String::schema_name()
    }

    fn json_schema(
        r#gen: &mut schemars_v0_8::r#gen::SchemaGenerator,
    ) -> schemars_v0_8::schema::Schema {
        String::json_schema(r#gen)
    }
}

impl From<&Base58CryptoHash> for String {
    fn from(hash: &Base58CryptoHash) -> Self {
        bs58::encode(&hash.0).into_string()
    }
}

impl TryFrom<String> for Base58CryptoHash {
    type Error = ParseCryptoHashError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        value.parse()
    }
}

impl TryFrom<&str> for Base58CryptoHash {
    type Error = ParseCryptoHashError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        value.parse()
    }
}

impl std::str::FromStr for Base58CryptoHash {
    type Err = ParseCryptoHashError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        let mut crypto_hash: CryptoHash = Default::default();
        let size = bs58::decode(value).onto(&mut crypto_hash)?;
        if size != std::mem::size_of::<CryptoHash>() {
            return Err(ParseCryptoHashError {
                kind: ParseCryptoHashErrorKind::InvalidLength(size),
            });
        }
        Ok(Self(crypto_hash))
    }
}

impl std::fmt::Display for Base58CryptoHash {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&String::from(self))
    }
}

#[derive(Debug)]
pub struct ParseCryptoHashError {
    kind: ParseCryptoHashErrorKind,
}

#[derive(Debug)]
enum ParseCryptoHashErrorKind {
    InvalidLength(usize),
    Base58(B58Error),
}

impl std::fmt::Display for ParseCryptoHashError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.kind {
            ParseCryptoHashErrorKind::InvalidLength(l) => {
                write!(f, "invalid length of the crypto hash, expected 32 got {l}")
            }
            ParseCryptoHashErrorKind::Base58(e) => write!(f, "base58 decoding error: {e}"),
        }
    }
}

impl From<B58Error> for ParseCryptoHashError {
    fn from(e: B58Error) -> Self {
        Self { kind: ParseCryptoHashErrorKind::Base58(e) }
    }
}

impl std::error::Error for ParseCryptoHashError {}
