/* This file is part of DarkFi (https://dark.fi)
 *
 * Copyright (C) 2020-2023 Dyne.org foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

use std::fmt;

use darkfi_sdk::{
    crypto::{MerkleNode, MerkleTree},
    pasta::pallas,
};
use darkfi_serial::{serialize, SerialDecodable, SerialEncodable};

use super::{
    constants::{BLOCK_MAGIC_BYTES, BLOCK_VERSION},
    LeadInfo,
};
use crate::{impl_p2p_message, net::Message, tx::Transaction, util::time::Timestamp};

/// This struct represents a tuple of the form (version, previous, epoch, slot, timestamp, merkle_root).
#[derive(Debug, Clone, PartialEq, Eq, SerialEncodable, SerialDecodable)]
pub struct Header {
    /// Block version
    pub version: u8,
    /// Previous block hash
    pub previous: blake3::Hash,
    /// Epoch
    pub epoch: u64,
    /// Slot UID
    pub slot: u64,
    /// Block creation timestamp
    pub timestamp: Timestamp,
    /// Root of the transaction hashes merkle tree
    pub root: MerkleNode,
}

impl Header {
    pub fn new(
        previous: blake3::Hash,
        epoch: u64,
        slot: u64,
        timestamp: Timestamp,
        root: MerkleNode,
    ) -> Self {
        let version = BLOCK_VERSION;
        Self { version, previous, epoch, slot, timestamp, root }
    }

    /// Generate the genesis block.
    pub fn genesis_header(genesis_ts: Timestamp, genesis_data: blake3::Hash) -> Self {
        let tree = MerkleTree::new(100);
        let root = tree.root(0).unwrap();

        Self::new(genesis_data, 0, 0, genesis_ts, root)
    }

    /// Calculate the header hash
    pub fn headerhash(&self) -> blake3::Hash {
        blake3::hash(&serialize(self))
    }
}

impl Default for Header {
    fn default() -> Self {
        Header::new(
            blake3::hash(b""),
            0,
            0,
            Timestamp::current_time(),
            MerkleNode::from(pallas::Base::zero()),
        )
    }
}

/// This struct represents a tuple of the form (`magic`, `header`, `counter`, `txs`, `lead_info`).
/// The header and transactions are stored as hashes, serving as pointers to
/// the actual data in the sled database.
#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
pub struct Block {
    /// Block magic bytes
    pub magic: [u8; 4],
    /// Block header
    pub header: blake3::Hash,
    /// Trasaction hashes
    pub txs: Vec<blake3::Hash>,
    /// Lead Info
    pub lead_info: LeadInfo,
}

impl_p2p_message!(Block, "block");

impl Block {
    pub fn new(
        previous: blake3::Hash,
        epoch: u64,
        slot: u64,
        txs: Vec<blake3::Hash>,
        root: MerkleNode,
        lead_info: LeadInfo,
    ) -> Self {
        let magic = BLOCK_MAGIC_BYTES;
        let timestamp = Timestamp::current_time();
        let header = Header::new(previous, epoch, slot, timestamp, root);
        let header = header.headerhash();
        Self { magic, header, txs, lead_info }
    }

    /// Generate the genesis block.
    pub fn genesis_block(genesis_ts: Timestamp, genesis_data: blake3::Hash) -> Self {
        let magic = BLOCK_MAGIC_BYTES;
        let header = Header::genesis_header(genesis_ts, genesis_data);
        let header = header.headerhash();
        let lead_info = LeadInfo::default();
        Self { magic, header, txs: vec![], lead_info }
    }

    /// Calculate the block hash
    pub fn blockhash(&self) -> blake3::Hash {
        blake3::hash(&serialize(self))
    }
}

/// Auxiliary structure used for blockchain syncing.
#[derive(Debug, SerialEncodable, SerialDecodable)]
pub struct BlockOrder {
    /// Slot UID
    pub slot: u64,
    /// Block headerhash of that slot
    pub block: blake3::Hash,
}

impl_p2p_message!(BlockOrder, "blockorder");

/// Structure representing full block data.
#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
pub struct BlockInfo {
    /// BlockInfo magic bytes
    pub magic: [u8; 4],
    /// Block header data
    pub header: Header,
    /// Transactions payload
    pub txs: Vec<Transaction>,
    /// Lead Info,
    pub lead_info: LeadInfo,
}

impl Default for BlockInfo {
    fn default() -> Self {
        let magic = BLOCK_MAGIC_BYTES;
        Self { magic, header: Header::default(), txs: vec![], lead_info: LeadInfo::default() }
    }
}

impl_p2p_message!(BlockInfo, "blockinfo");

impl BlockInfo {
    pub fn new(header: Header, txs: Vec<Transaction>, lead_info: LeadInfo) -> Self {
        let magic = BLOCK_MAGIC_BYTES;
        Self { magic, header, txs, lead_info }
    }

    /// Calculate the block hash
    pub fn blockhash(&self) -> blake3::Hash {
        let block: Block = self.clone().into();
        block.blockhash()
    }
}

impl From<BlockInfo> for Block {
    fn from(block_info: BlockInfo) -> Self {
        let txs = block_info.txs.iter().map(|x| blake3::hash(&serialize(x))).collect();
        Self {
            magic: block_info.magic,
            header: block_info.header.headerhash(),
            txs,
            lead_info: block_info.lead_info,
        }
    }
}

/// Auxiliary structure used for blockchain syncing
#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
pub struct BlockResponse {
    /// Response blocks.
    pub blocks: Vec<BlockInfo>,
}

impl_p2p_message!(BlockResponse, "blockresponse");

/// This struct represents a block proposal, used for consensus.
#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
pub struct BlockProposal {
    /// Block hash
    pub hash: blake3::Hash,
    /// Block header hash
    pub header: blake3::Hash,
    /// Block data
    pub block: BlockInfo,
}

impl BlockProposal {
    #[allow(clippy::too_many_arguments)]
    pub fn new(header: Header, txs: Vec<Transaction>, lead_info: LeadInfo) -> Self {
        let block = BlockInfo::new(header, txs, lead_info);
        let hash = block.blockhash();
        let header = block.header.headerhash();
        Self { hash, header, block }
    }
}

impl PartialEq for BlockProposal {
    fn eq(&self, other: &Self) -> bool {
        self.hash == other.hash &&
            self.header == other.header &&
            self.block.header == other.block.header &&
            self.block.txs == other.block.txs
    }
}

impl fmt::Display for BlockProposal {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        formatter.write_fmt(format_args!(
            "BlockProposal {{ leader public key: {}, hash: {}, header: {}, epoch: {}, slot: {}, txs: {} }}",
            self.block.lead_info.public_key,
            self.hash,
            self.header,
            self.block.header.epoch,
            self.block.header.slot,
            self.block.txs.len()
        ))
    }
}

impl_p2p_message!(BlockProposal, "proposal");

impl From<BlockProposal> for BlockInfo {
    fn from(block: BlockProposal) -> BlockInfo {
        block.block
    }
}
