use candid::CandidType;
use ex3_ic_stable_structures::{storable::Bound, Storable};
use ex3_serde::bincode;
use ex3_timestamp::TimeInNs;
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;

use crate::{BlockHash, BlockHeight, CandidBlockHeight, CandidPackageId, MerkleRoot, PackageId};

/// Block head
#[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, PartialEq, Eq)]
pub struct BlockHead {
    /// Block version
    pub version: u8,

    /// Height of the block
    pub height: BlockHeight,

    /// Package id of the block
    pub p_id: PackageId,

    /// Previous block hash
    pub pre_block_hash: BlockHash,

    /// Transaction merkle root of the block accepted transactions
    pub tx_root: MerkleRoot,

    /// Balance changed merkle root of the block
    pub balance_changed_root: MerkleRoot,

    /// Data integrity merkle root of the block
    pub data_integrity_root: MerkleRoot,

    /// Rejected transaction merkle root of the block rejected transactions
    pub rejected_tx_root: MerkleRoot,
}

#[derive(CandidType, Deserialize, Clone, Debug, Ord, PartialOrd, PartialEq, Eq)]
pub struct CandidBlockHead {
    pub version: u8,
    pub height: CandidBlockHeight,
    pub p_id: CandidPackageId,
    pub pre_block_hash: BlockHash,
    pub tx_root: MerkleRoot,
    pub balance_changed_root: MerkleRoot,
    pub data_integrity_root: MerkleRoot,
    pub rejected_tx_root: MerkleRoot,
}

impl Storable for BlockHead {
    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: 225,
        is_fixed_size: false,
    };
}

impl From<CandidBlockHead> for BlockHead {
    fn from(candid_block_head: CandidBlockHead) -> Self {
        BlockHead {
            version: candid_block_head.version,
            height: candid_block_head.height.into(),
            p_id: candid_block_head.p_id.into(),
            pre_block_hash: candid_block_head.pre_block_hash,
            tx_root: candid_block_head.tx_root,
            balance_changed_root: candid_block_head.balance_changed_root,
            data_integrity_root: candid_block_head.data_integrity_root,
            rejected_tx_root: candid_block_head.rejected_tx_root,
        }
    }
}

impl From<BlockHead> for CandidBlockHead {
    fn from(block_head: BlockHead) -> Self {
        CandidBlockHead {
            version: block_head.version,
            height: block_head.height.into(),
            p_id: block_head.p_id.into(),
            pre_block_hash: block_head.pre_block_hash,
            tx_root: block_head.tx_root,
            balance_changed_root: block_head.balance_changed_root,
            data_integrity_root: block_head.data_integrity_root,
            rejected_tx_root: block_head.rejected_tx_root,
        }
    }
}

impl BlockHead {
    pub fn hash(&self) -> BlockHash {
        let block_head_bytes = bincode::serialize(&self).unwrap();
        ex3_crypto::sha256(block_head_bytes.as_slice())
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EncodedBlockHead(pub ByteBuf);

impl From<BlockHead> for EncodedBlockHead {
    fn from(head: BlockHead) -> Self {
        let bytes = bincode::serialize(&head).unwrap();
        EncodedBlockHead(ByteBuf::from(bytes))
    }
}

impl From<EncodedBlockHead> for BlockHead {
    fn from(encoded_head: EncodedBlockHead) -> Self {
        bincode::deserialize(encoded_head.0.as_slice()).expect("Decode block head should success")
    }
}

impl std::ops::Deref for EncodedBlockHead {
    type Target = ByteBuf;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct BlockHeadExt {
    pub height: BlockHeight,
    pub validate_on_chain: bool,
    pub sharding_reported: bool,
    pub has_local_body: bool,
}

/// Block head
#[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, PartialEq, Eq)]
pub struct ConsensusBlockHead {
    /// Head
    pub head: BlockHead,

    /// Timestamp of reached consensus
    pub timestamp: TimeInNs,
}

#[derive(CandidType, Deserialize, Clone, Debug, Ord, PartialOrd, PartialEq, Eq)]
pub struct CandidConsensusBlockHead {
    pub head: CandidBlockHead,
    pub timestamp: TimeInNs,
}

impl From<CandidConsensusBlockHead> for ConsensusBlockHead {
    fn from(candid_consensus_block_head: CandidConsensusBlockHead) -> Self {
        ConsensusBlockHead {
            head: candid_consensus_block_head.head.into(),
            timestamp: candid_consensus_block_head.timestamp,
        }
    }
}

impl From<ConsensusBlockHead> for CandidConsensusBlockHead {
    fn from(consensus_block_head: ConsensusBlockHead) -> Self {
        CandidConsensusBlockHead {
            head: consensus_block_head.head.into(),
            timestamp: consensus_block_head.timestamp,
        }
    }
}

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

    #[test]
    fn test_storable_for_block_head() {
        let block_head = BlockHead {
            version: 0,
            height: BlockHeight::from(u128::MAX),
            p_id: PackageId::from(u128::MAX),
            pre_block_hash: [1; 32],
            tx_root: [2; 32],
            balance_changed_root: [3; 32],
            data_integrity_root: [4; 32],
            rejected_tx_root: [5; 32],
        };

        let bytes = block_head.to_bytes();
        assert!(
            bytes.len() <= 225,
            "block head bytes length should be less than 225, but got {}",
            bytes.len()
        );
        let block_head2 = BlockHead::from_bytes(bytes);
        assert_eq!(block_head, block_head2);
    }

    #[test]
    fn test_bincode_for_bytes() {
        let bytes = vec![1; 255];
        let bytes2 = ByteBuf::from(bytes.clone());
        let s1 = bincode::serialize(&bytes).unwrap();
        let s2 = bincode::serialize(&bytes2).unwrap();
        assert_eq!(
            s1.len(),
            s2.len(),
            "bincode serialize for bytes and ByteBuf should have same length, but got {} and {}",
            s1.len(),
            s2.len()
        );
    }
}
