use candid::{CandidType, Deserialize};
use ex3_node_error::OtherError;
use ex3_serde::bincode;
use ic_stable_structures::{storable::Bound, Storable};
use num_bigint::BigUint;
use serde::Serialize;

use crate::chain::{CandidChain, Chain};
use crate::{
    impl_from_uint_for, AssetId, BlockHeight, CandidAssetId, CandidBlockHeight,
    CandidWalletRegisterId, WalletRegisterId,
};

/// Token type
#[derive(
    CandidType, Debug, Clone, Hash, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord,
)]
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, Ord, PartialOrd)]
pub struct CryptoAsset {
    /// Specify the blockchain
    pub chain: Chain,
    /// Specify the type of token
    pub r#type: TokenType,
    /// Specify the parent asset ID
    pub parent_id: Option<AssetId>,
    /// Specify the address of the asset
    pub address: String,
}

#[derive(CandidType, Debug, Clone, Hash, PartialEq, Eq, Deserialize)]
pub struct CandidCryptoAsset {
    pub parent_id: Option<CandidAssetId>,
    pub chain: CandidChain,
    pub r#type: TokenType,
    pub address: String,
}

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

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

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

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

/// Registered crypto asset
#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
pub struct RegisteredCryptoAsset {
    pub id: AssetId,
    pub asset: CryptoAsset,
    /// Fee to
    /// Withdrawal fee to
    pub fee_to: Option<WalletRegisterId>,
    pub registered_height: BlockHeight,
}

#[derive(CandidType, Debug, Clone, Hash, PartialEq, Eq, Deserialize)]
pub struct CandidRegisteredCryptoAsset {
    pub id: CandidAssetId,
    pub asset: CandidCryptoAsset,
    /// Fee to
    /// Withdrawal fee to
    pub fee_to: Option<CandidWalletRegisterId>,
    pub registered_height: CandidBlockHeight,
}

impl From<CandidRegisteredCryptoAsset> for RegisteredCryptoAsset {
    fn from(candid_registered_crypto_asset: CandidRegisteredCryptoAsset) -> Self {
        RegisteredCryptoAsset {
            id: candid_registered_crypto_asset.id.into(),
            asset: candid_registered_crypto_asset.asset.into(),
            registered_height: candid_registered_crypto_asset.registered_height.into(),
            fee_to: candid_registered_crypto_asset.fee_to.map(|id| id.into()),
        }
    }
}

impl From<RegisteredCryptoAsset> for CandidRegisteredCryptoAsset {
    fn from(registered_crypto_asset: RegisteredCryptoAsset) -> Self {
        CandidRegisteredCryptoAsset {
            id: CandidAssetId::from(registered_crypto_asset.id),
            asset: registered_crypto_asset.asset.into(),
            registered_height: registered_crypto_asset.registered_height.into(),
            fee_to: registered_crypto_asset
                .fee_to
                .map(|id| CandidWalletRegisterId::from(id)),
        }
    }
}

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

impl From<TokenType> for BigUint {
    fn from(token_type: TokenType) -> Self {
        BigUint::from(token_type as u128)
    }
}

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

impl Storable for CryptoAsset {
    fn to_bytes(&self) -> std::borrow::Cow<[u8]> {
        bincode::serialize(self).unwrap().into()
    }

    fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
        bincode::deserialize(&bytes).unwrap()
    }

    const BOUND: Bound = Bound::Bounded {
        max_size: 256,
        is_fixed_size: false,
    };
}

impl Storable for RegisteredCryptoAsset {
    fn to_bytes(&self) -> std::borrow::Cow<[u8]> {
        bincode::serialize(self).unwrap().into()
    }

    fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
        bincode::deserialize(&bytes).unwrap()
    }

    const BOUND: Bound = Bound::Unbounded;
}

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

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

    use super::CryptoAsset;

    #[test]
    fn test_serde() {
        let asset_1 = CryptoAsset {
            parent_id: None,
            chain: Chain {
                r#type: ChainType::Ethereum,
                network: 1u8.into(),
            },
            r#type: TokenType::ERC20,
            address: "0x1234567889123456789123456".to_string(),
        };

        // 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 {
            parent_id: Some(AssetId::from(1u8)),
            chain: Chain {
                r#type: ChainType::Ethereum,
                network: 1u8.into(),
            },
            r#type: TokenType::ERC721,
            address: "0x1234567889123456789123456".to_string(),
        };

        // 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 {
            parent_id: None,
            chain: Chain {
                r#type: ChainType::Ethereum,
                network: 1u8.into(),
            },
            r#type: TokenType::ERC20,
            address: "0x1234567889123456789123456".to_string(),
        };

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

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

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

        // custom cbor size
        let custom_cbor_bytes = cbor::serialize(&(
            &asset_1.parent_id,
            &asset_1.chain,
            &asset_1.r#type,
            &asset_1.address,
        ))
        .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()
        );
    }
}
