//! Provides Bitmessage address types.

use std::{
    convert::TryInto,
    fmt,
    io::{self, Cursor, Read},
    str::FromStr,
};

use crate::{
    crypto::{PrivateKey, PublicKey},
    hash::{double_sha512, ripemd160_sha512, sha512},
    io::{ReadFrom, WriteTo},
    object::Tag,
    priv_util::ToHexString,
};

pub use crate::stream::StreamNumber;

/// A version number of a Bitmessage address.
pub type AddressVersion = crate::object::ObjectVersion;

/// This error indicates
/// that the construction from public keys failed.
#[derive(Clone, Debug)]
pub enum FromPublicKeysError {
    /// Indicates that the version supplied was not supported.
    /// The actual version supplied is returned as a payload of this variant.
    UnsupportedVersion(AddressVersion),
    /// Indicates that the derived hash has too few prefix zero bytes.
    /// The minimum count and the actual count of zero bytes are returned
    /// as payloads of this variant.
    TooFewZeros {
        /// The minimum count of zero bytes.
        min: usize,
        /// The actual count of zero bytes.
        count: usize,
    },
}

impl fmt::Display for FromPublicKeysError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::UnsupportedVersion(version) => write!(f, "unsupported version: {}", version),
            Self::TooFewZeros { min, count } => {
                write!(f, "count of zeros must be >={}, but {}", min, count)
            }
        }
    }
}

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

#[derive(Clone, PartialEq, Eq, Hash, Debug)]
struct Inner {
    version: AddressVersion,
    stream: StreamNumber,
    hash: Vec<u8>,
}

fn count_zeros(bytes: impl AsRef<[u8]>) -> usize {
    let mut count = 0;
    for _ in bytes.as_ref().iter().take_while(|b| **b == 0) {
        count += 1;
    }
    count
}

const NULL_HEAD_LENGTH: usize = 1;

impl Inner {
    pub fn from_public_keys(
        version: AddressVersion,
        stream: StreamNumber,
        public_signing_key: &PublicKey,
        public_encryption_key: &PublicKey,
    ) -> Result<Self, FromPublicKeysError> {
        if version.as_u64() < 2 || version.as_u64() > 4 {
            return Err(FromPublicKeysError::UnsupportedVersion(version));
        }
        let mut bytes = Vec::new();
        bytes.append(&mut public_signing_key.encode());
        bytes.append(&mut public_encryption_key.encode());
        let hash = ripemd160_sha512(bytes);
        let count = count_zeros(&hash);
        if count < NULL_HEAD_LENGTH {
            return Err(FromPublicKeysError::TooFewZeros {
                min: NULL_HEAD_LENGTH,
                count,
            });
        }
        Ok(Self {
            version,
            stream,
            hash: hash.as_ref().to_vec(),
        })
    }

    pub fn tag(&self) -> Tag {
        if self.version.as_u64() <= 3 {
            assert_eq!(self.hash.len(), 32);
            Tag::new(self.hash[..].try_into().unwrap())
        } else {
            let mut bytes = Vec::new();
            self.version.write_to(&mut bytes).unwrap();
            self.stream.write_to(&mut bytes).unwrap();
            self.hash.write_to(&mut bytes).unwrap();
            Tag::new(double_sha512(bytes)[32..].try_into().unwrap())
        }
    }

    pub fn broadcast_key(&self) -> PrivateKey {
        let mut bytes = Vec::new();
        self.version.write_to(&mut bytes).unwrap();
        self.stream.write_to(&mut bytes).unwrap();
        self.hash.write_to(&mut bytes).unwrap();
        let hash = if self.version.as_u64() <= 3 {
            sha512(bytes)[..32].try_into().unwrap()
        } else {
            double_sha512(bytes)[..32].try_into().unwrap()
        };
        PrivateKey::new(hash)
    }

    pub fn hash(&self) -> &[u8] {
        &self.hash
    }
}

impl fmt::Display for Inner {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut bytes = Vec::new();
        self.version.write_to(&mut bytes).unwrap();
        self.stream.write_to(&mut bytes).unwrap();
        let hash = &self.hash[count_zeros(&self.hash)..];
        hash.write_to(&mut bytes).unwrap();
        let checksum = &double_sha512(&bytes)[..4];
        bytes.extend_from_slice(checksum);
        write!(f, "BM-{}", bs58::encode(bytes).into_string())
    }
}

/// A Bitmessage address version 4.
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct AddressV4 {
    inner: Inner,
}

impl AddressV4 {
    /// Returns the address version.
    pub fn version(&self) -> AddressVersion {
        self.inner.version
    }

    /// Returns the stream number.
    pub fn stream(&self) -> StreamNumber {
        self.inner.stream
    }

    /// Returns the hash.
    pub fn hash(&self) -> &[u8] {
        self.inner.hash()
    }
}

impl fmt::Display for AddressV4 {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.inner.fmt(f)
    }
}

/// A Bitmessage address.
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum Address {
    /// A Bitmessage address version 4.
    V4(AddressV4),
}

impl Address {
    /// Constructs a Bitmessage address from public keys.
    pub fn from_public_keys(
        version: AddressVersion,
        stream: StreamNumber,
        public_signing_key: &PublicKey,
        public_encryption_key: &PublicKey,
    ) -> Result<Self, FromPublicKeysError> {
        match version.as_u64() {
            4 => {
                let inner = Inner::from_public_keys(
                    version,
                    stream,
                    public_signing_key,
                    public_encryption_key,
                )?;
                Ok(Address::V4(AddressV4 { inner }))
            }
            _ => Err(FromPublicKeysError::UnsupportedVersion(version)),
        }
    }

    /// Returns the tag.
    pub fn tag(&self) -> Tag {
        match self {
            Address::V4(address) => address.inner.tag(),
        }
    }

    /// Returns the private encryption key for broadcast.
    pub fn broadcast_key(&self) -> PrivateKey {
        match self {
            Address::V4(address) => address.inner.broadcast_key(),
        }
    }

    /// Returns the address version.
    pub fn version(&self) -> AddressVersion {
        match self {
            Self::V4(v4) => v4.version(),
        }
    }

    /// Returns the stream number.
    pub fn stream(&self) -> StreamNumber {
        match self {
            Self::V4(v4) => v4.stream(),
        }
    }

    /// Returns the hash.
    pub fn hash(&self) -> &[u8] {
        match self {
            Self::V4(v4) => v4.hash(),
        }
    }
}

impl fmt::Display for Address {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::V4(v4) => v4.fmt(f),
        }
    }
}

/// An error which can be returned
/// when parsing a Bitmessage address.
///
/// This error is used as the error type for the `FromStr` implementation
/// for [`Address`].
///
/// [`Address`]: enum.Address.html
#[derive(Debug)]
pub enum ParseAddressError {
    /// An error was caught when parsing a Base58 string.
    /// The actual error caught is returned
    /// as a payload of this variant.
    InvalidBase58(bs58::decode::Error),

    /// The length of the decoded bytes was too short to construct a Bitmessage address.
    /// The minimum length allowed and the actual length supplied
    /// are returned as payloads of this variant.
    TooShort {
        /// The minimum length allowed.
        min: usize,
        /// The actual length supplied.
        len: usize,
    },

    /// The checksums did not match.
    /// The expected and the actual checksums are returned
    /// as payloads of this variant.
    InvalidChecksum {
        /// The expected checksum.
        expected: [u8; 4],
        /// The actual checksum.
        actual: [u8; 4],
    },

    /// A standard I/O error was caught during parsing a Bitmessage address.
    /// The actual error caught is returned as a payload of this variant.
    IoError(io::Error),

    /// The version of the address was not implemented.
    /// The version number is returned as a payload of this variant.
    NotImplemented(AddressVersion),

    /// The version number was invalid.
    /// The version number is returned as a payload of this variant.
    InvalidVersion(AddressVersion),

    /// The length of the hash was too short to construct a Bitmessage address.
    /// The minimum length allowed and the actual length supplied
    /// are returned as payloads of this variant.
    HashTooShort {
        /// The minimum length allowed.
        min: usize,
        /// The actual length supplied.
        len: usize,
    },

    /// The length of the hash was too long to construct a Bitmessage address.
    /// The maximum length allowed and the actual length supplied
    /// are returned as payloads of this variant.
    HashTooLong {
        /// The maximum length allowed.
        max: usize,
        /// The actual length supplied.
        len: usize,
    },
}

impl fmt::Display for ParseAddressError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidBase58(err) => err.fmt(f),
            Self::InvalidChecksum { expected, actual } => write!(
                f,
                "checksum should be {}, but {}",
                expected.as_ref().to_hex_string(),
                actual.as_ref().to_hex_string()
            ),
            Self::IoError(err) => err.fmt(f),
            Self::NotImplemented(version) => write!(f, "version {} is not implemented", version),
            Self::InvalidVersion(version) => write!(f, "version {} is invalid", version),
            Self::TooShort { min, len } => write!(f, "length must be >={}, but {}", min, len),
            Self::HashTooShort { min, len } => {
                write!(f, "hash length must be >={}, but {}", min, len)
            }
            Self::HashTooLong { max, len } => {
                write!(f, "hash length must be <={}, but {}", max, len)
            }
        }
    }
}

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

impl From<bs58::decode::Error> for ParseAddressError {
    fn from(err: bs58::decode::Error) -> Self {
        Self::InvalidBase58(err)
    }
}

impl From<io::Error> for ParseAddressError {
    fn from(err: io::Error) -> Self {
        Self::IoError(err)
    }
}

const ADDRESS_VERSION: u64 = 4;

impl FromStr for Address {
    type Err = ParseAddressError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let s = if s.len() >= 3 && &s[..3] == "BM-" {
            &s[3..]
        } else {
            &s[..]
        };
        let bytes = bs58::decode(s).into_vec()?;
        if bytes.len() < 4 {
            return Err(ParseAddressError::TooShort {
                min: 4,
                len: bytes.len(),
            });
        }
        let checksum = &bytes[bytes.len() - 4..];
        let body = &bytes[..bytes.len() - 4];
        let double_hash = double_sha512(body);
        let actual_checksum = &double_hash[..4];
        if actual_checksum != checksum {
            return Err(ParseAddressError::InvalidChecksum {
                expected: checksum.try_into().unwrap(),
                actual: actual_checksum.try_into().unwrap(),
            });
        }
        let mut bytes = Cursor::new(body);
        let version = AddressVersion::read_from(&mut bytes)?;
        if version.as_u64() > ADDRESS_VERSION {
            return Err(ParseAddressError::NotImplemented(version));
        }
        if version.as_u64() < 1 {
            return Err(ParseAddressError::InvalidVersion(version));
        }
        let stream = StreamNumber::read_from(&mut bytes)?;
        if version.as_u64() == 4 {
            let mut data = Vec::new();
            bytes.read_to_end(&mut data)?;
            if data.len() < 4 {
                return Err(ParseAddressError::HashTooShort {
                    min: 4,
                    len: data.len(),
                });
            }
            if data.len() > 20 {
                return Err(ParseAddressError::HashTooLong {
                    max: 20,
                    len: data.len(),
                });
            }
            const HASH_LEN: usize = 20;
            let mut hash = [0; HASH_LEN];
            hash[HASH_LEN - data.len()..].copy_from_slice(&data);
            let hash = hash.to_vec();
            let inner = Inner {
                version,
                stream,
                hash,
            };
            Ok(Address::V4(AddressV4 { inner }))
        } else {
            // TODO implement version 3, 2, 1 Bitmessage addresses
            Err(ParseAddressError::NotImplemented(version))
        }
    }
}

#[test]
fn test_address_from_str() {
    let _address = "BM-NC1qK8oQk3yya1ZfHt368uXQq9W6U2om".parse::<Address>();
}
