use crate::impl_from_uint_for;
use candid::{CandidType, Nat};
use ex3_serde::bincode;
use ic_stable_structures::{storable::Bound, Storable};
use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::str::FromStr;

/// Registered coin types for BIP-0044
///   https://github.com/satoshilabs/slips/blob/master/slip-0044.md
#[derive(Clone, CandidType, Hash, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub enum ChainType {
    Bitcoin = 0,
    Ethereum = 60,
    Tron = 195,
    Dfinity = 223,
    Solana = 501,
    Ton = 607,
    EX3Layer2 = 87653426,
}

impl ChainType {
    pub fn is_dfinity(&self) -> bool {
        matches!(self, ChainType::Dfinity)
    }
}

impl Ord for ChainType {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        let self_int: BigUint = self.clone().into();
        let other_int: BigUint = other.clone().into();
        self_int.cmp(&other_int)
    }
}

impl PartialOrd for ChainType {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Display for ChainType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ChainType::Bitcoin => write!(f, "Bitcoin"),
            ChainType::Ethereum => write!(f, "Ethereum"),
            ChainType::Tron => write!(f, "Tron"),
            ChainType::Dfinity => write!(f, "Dfinity"),
            ChainType::Solana => write!(f, "Solana"),
            ChainType::Ton => write!(f, "Ton"),
            ChainType::EX3Layer2 => write!(f, "EX3Layer2"),
        }
    }
}

impl FromStr for ChainType {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let s = s.to_lowercase();
        match s.as_str() {
            "bitcoin" => Ok(ChainType::Bitcoin),
            "ethereum" => Ok(ChainType::Ethereum),
            "tron" => Ok(ChainType::Tron),
            "dfinity" => Ok(ChainType::Dfinity),
            "solana" => Ok(ChainType::Solana),
            "ton" => Ok(ChainType::Ton),
            "ex3layer2" => Ok(ChainType::EX3Layer2),
            _ => Err(()),
        }
    }
}

impl From<BigUint> for ChainType {
    fn from(chain_id: BigUint) -> Self {
        match chain_id {
            chain_id if chain_id == BigUint::from(ChainType::Bitcoin) => ChainType::Bitcoin,
            chain_id if chain_id == BigUint::from(ChainType::Ethereum) => ChainType::Ethereum,
            chain_id if chain_id == BigUint::from(ChainType::Tron) => ChainType::Tron,
            chain_id if chain_id == BigUint::from(ChainType::Dfinity) => ChainType::Dfinity,
            chain_id if chain_id == BigUint::from(ChainType::Solana) => ChainType::Solana,
            chain_id if chain_id == BigUint::from(ChainType::Ton) => ChainType::Ton,
            chain_id if chain_id == BigUint::from(ChainType::EX3Layer2) => ChainType::EX3Layer2,
            _ => panic!("unknown chain id"),
        }
    }
}

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

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

#[derive(Clone, Hash, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct Chain {
    pub r#type: ChainType,
    pub network: BigUint,
}

impl Display for Chain {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}-{}", self.r#type, self.network)
    }
}

#[derive(
    Clone, CandidType, Hash, Debug, Eq, PartialEq, Deserialize, Serialize, Ord, PartialOrd,
)]
pub struct CandidChain {
    pub r#type: ChainType,
    pub network: Nat,
}

impl Ord for Chain {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        let o1 = self.r#type.cmp(&other.r#type);
        if o1 != std::cmp::Ordering::Equal {
            return o1;
        }

        self.network.cmp(&other.network)
    }
}

impl PartialOrd for Chain {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Storable for Chain {
    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.as_ref()).unwrap()
    }

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

impl From<Chain> for CandidChain {
    fn from(chain: Chain) -> Self {
        Self {
            r#type: chain.r#type,
            network: chain.network.into(),
        }
    }
}

impl From<CandidChain> for Chain {
    fn from(chain: CandidChain) -> Self {
        Self {
            r#type: chain.r#type,
            network: chain.network.into(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_storable_for_chain() {
        let chain = Chain {
            r#type: ChainType::EX3Layer2,
            network: u128::MAX.into(),
        };
        let bytes = chain.to_bytes();
        assert!(bytes.len() <= 52, "bytes.len() = {}", bytes.len());
        let chain_identifier2 = Chain::from_bytes(bytes);
        assert_eq!(chain, chain_identifier2);
    }
}
