use candid::{CandidType, Deserialize};
use ex3_node_error::OtherError;
use ex3_serde::bincode::deserialize;
use num_bigint::BigUint;
use serde::Serialize;

use crate::chain::Chain;
use crate::{
    impl_from_uint_for_enum, AssetId, CandidAssetId, CandidWalletRegisterId, WalletRegisterId,
};

/// Token type
#[derive(CandidType, Debug, Clone, Hash, Deserialize, Serialize, PartialEq, Eq)]
pub enum TokenType {
    /// Native asset of the chain
    /// # Example
    /// * Bitcoin, BTC
    /// * Ethereum, ETH
    Native = 0,

    /// Native gas asset of the chain
    /// # Example
    /// * Neo, GAS
    /// * Dfinity, Cycles
    NativeGas = 1,

    /// EVM fungible token type
    ERC20 = 3,

    /// EVM non-fungible token type
    ERC721 = 4,

    /// Dfinity token [DFT] standard
    DFT = 5,

    /// Dfinity token [ICRC1] standard
    ICRC1 = 6,
}

/// crypto asset
#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
pub struct CryptoAsset {
    pub id: AssetId,
    pub parent_id: Option<AssetId>,
    pub chain: Chain,
    pub network: u8,
    pub r#type: TokenType,
    pub address: String,
    /// Fee to
    /// Withdrawal fee to
    pub fee_to: Option<WalletRegisterId>,
}

#[derive(CandidType, Debug, Clone, Hash, PartialEq, Eq, Deserialize)]
pub struct CandidCryptoAsset {
    pub id: CandidAssetId,
    pub parent_id: Option<CandidAssetId>,
    pub chain: Chain,
    pub network: u8,
    pub r#type: TokenType,
    pub address: String,
    /// Fee to
    /// Withdrawal fee to
    pub fee_to: Option<CandidWalletRegisterId>,
}

impl From<CandidCryptoAsset> for CryptoAsset {
    fn from(candid_crypto_asset: CandidCryptoAsset) -> Self {
        CryptoAsset {
            id: candid_crypto_asset.id.0,
            parent_id: candid_crypto_asset.parent_id.map(|id| id.0),
            chain: candid_crypto_asset.chain,
            network: candid_crypto_asset.network,
            r#type: candid_crypto_asset.r#type,
            address: candid_crypto_asset.address,
            fee_to: candid_crypto_asset.fee_to.map(|id| id.0),
        }
    }
}

impl From<CryptoAsset> for CandidCryptoAsset {
    fn from(crypto_asset: CryptoAsset) -> Self {
        CandidCryptoAsset {
            id: crypto_asset.id.into(),
            parent_id: crypto_asset.parent_id.map(|id| CandidAssetId::from(id)),
            chain: crypto_asset.chain,
            network: crypto_asset.network,
            r#type: crypto_asset.r#type,
            address: crypto_asset.address,
            fee_to: crypto_asset.fee_to.map(|id| id.into()),
        }
    }
}

impl TryFrom<&[u8]> for CryptoAsset {
    type Error = OtherError;

    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
        let asset = deserialize(value).map_err(|e| {
            OtherError::new(format!(
                "Failed to deserialize CryptoAsset from bytes: {:?}",
                e
            ))
        })?;
        Ok(asset)
    }
}

impl From<BigUint> for TokenType {
    fn from(token_type: BigUint) -> Self {
        match token_type {
            token_type if token_type == TokenType::Native.into() => TokenType::Native,
            token_type if token_type == TokenType::NativeGas.into() => TokenType::NativeGas,
            token_type if token_type == TokenType::ERC20.into() => TokenType::ERC20,
            token_type if token_type == TokenType::ERC721.into() => TokenType::ERC721,
            token_type if token_type == TokenType::DFT.into() => TokenType::DFT,
            token_type if token_type == TokenType::ICRC1.into() => TokenType::ICRC1,
            _ => panic!("Invalid token type: {}", token_type),
        }
    }
}

impl Into<BigUint> for TokenType {
    fn into(self) -> BigUint {
        BigUint::from(self as u128)
    }
}

impl_from_uint_for_enum!(TokenType, u8, u16, u32, u64, u128);

#[cfg(test)]
mod tests {
    use ex3_serde::{bincode, cbor};

    use crate::asset::TokenType;
    use crate::chain::Chain;
    use crate::{AssetId, WalletRegisterId};

    use super::CryptoAsset;

    #[test]
    fn test_serde() {
        let asset_1 = CryptoAsset {
            id: AssetId::from(1000000u32),
            parent_id: None,
            chain: Chain::Ethereum,
            network: 1,
            r#type: TokenType::ERC20,
            address: "0x1234567889123456789123456".to_string(),
            fee_to: Some(WalletRegisterId::from(100000u32)),
        };

        // bincode
        let bincode_bytes = bincode::serialize(&asset_1).unwrap();
        let bincode_asset: CryptoAsset = bincode::deserialize(&bincode_bytes).unwrap();
        assert_eq!(asset_1, bincode_asset);

        // cbor
        let cbor_bytes = cbor::serialize(&asset_1).unwrap();
        let cbor_asset: CryptoAsset = cbor::deserialize(&cbor_bytes).unwrap();
        assert_eq!(asset_1, cbor_asset);

        let asset_2 = CryptoAsset {
            id: AssetId::from(2000000u32),
            parent_id: Some(AssetId::from(1u8)),
            chain: Chain::Ethereum,
            network: 1,
            r#type: TokenType::ERC721,
            address: "0x1234567889123456789123456".to_string(),
            fee_to: None,
        };

        // bincode
        let bincode_bytes = bincode::serialize(&asset_2).unwrap();
        let bincode_asset: CryptoAsset = bincode::deserialize(&bincode_bytes).unwrap();
        assert_eq!(asset_2, bincode_asset);

        // cbor
        let cbor_bytes = cbor::serialize(&asset_2).unwrap();
        let cbor_asset: CryptoAsset = cbor::deserialize(&cbor_bytes).unwrap();
        assert_eq!(asset_2, cbor_asset);
    }

    #[test]
    fn test_serde_size() {
        let asset_1 = CryptoAsset {
            id: AssetId::from(1000001u32),
            parent_id: None,
            chain: Chain::Ethereum,
            network: 1,
            r#type: TokenType::ERC20,
            address: "0x1234567889123456789123456".to_string(),
            fee_to: Some(WalletRegisterId::from(100000u32)),
        };

        // bincode size
        let bincode_bytes = bincode::serialize(&asset_1).unwrap();

        // custom bincode size
        let custom_bincode_bytes = bincode::serialize(&(
            &asset_1.id,
            &asset_1.parent_id,
            &asset_1.chain,
            &asset_1.network,
            &asset_1.r#type,
            &asset_1.address,
            &asset_1.fee_to,
        ))
        .unwrap();

        // cbor size
        let cbor_bytes = cbor::serialize(&asset_1).unwrap();

        // custom cbor size
        let custom_cbor_bytes = cbor::serialize(&(
            &asset_1.id,
            &asset_1.parent_id,
            &asset_1.chain,
            &asset_1.network,
            &asset_1.r#type,
            &asset_1.address,
            &asset_1.fee_to,
        ))
        .unwrap();

        assert!(bincode_bytes.len() < cbor_bytes.len());
        assert!(
            custom_cbor_bytes.len() < cbor_bytes.len(),
            " custom_bincode_bytes.len(): {}, cbor_bytes.len(): {}",
            custom_cbor_bytes.len(),
            cbor_bytes.len()
        );
        assert!(
            custom_bincode_bytes.len() < cbor_bytes.len(),
            "custom_bincode_bytes.len(): {}, cbor_bytes.len(): {}",
            custom_bincode_bytes.len(),
            cbor_bytes.len()
        );
        assert_eq!(
            custom_bincode_bytes.len(),
            bincode_bytes.len(),
            "custom_bincode_bytes.len(): {}, bincode_bytes.len(): {}",
            custom_bincode_bytes.len(),
            bincode_bytes.len()
        );
    }
}
