//  Copyright (C) 2020 Éloïs SANCHEZ.
//
// 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/>.

mod identities;
mod txs;
mod uds;

use crate::*;
use duniter_dbs::{databases::bc_v2::BcV2DbWritable, CurrencyParamsDbV2};

pub fn apply_block<B: Backend>(
    bc_db: &duniter_dbs::databases::bc_v2::BcV2Db<B>,
    block: &DubpBlockV10,
) -> KvResult<BlockMetaV2> {
    //log::info!("apply_block #{}", block.number().0);
    let block_meta = BlockMetaV2::from(block);

    (
        bc_db.blocks_meta_write(),
        bc_db.identities_write(),
        bc_db.txs_hashs_write(),
        bc_db.uds_write(),
        bc_db.uds_reval_write(),
        bc_db.uids_index_write(),
        bc_db.utxos_write(),
        bc_db.consumed_utxos_write(),
        bc_db.currency_params_write(),
    )
        .write(
            |(
                mut blocks_meta,
                mut identities,
                mut txs_hashs,
                mut uds,
                mut uds_reval,
                mut uids_index,
                mut utxos,
                mut consumed_utxos,
                mut currency_params,
            )| {
                if let Some(params) = block.currency_parameters() {
                    currency_params.upsert(
                        (),
                        CurrencyParamsDbV2 {
                            currency_name: block.currency_name(),
                            params,
                        },
                    );
                }
                blocks_meta.upsert(U32BE(block.number().0), block_meta);
                identities::update_identities::<B>(block, &mut identities)?;
                for idty in block.identities() {
                    let pubkey = idty.issuers()[0];
                    let username = idty.username().to_owned();
                    uids_index.upsert(username, PubKeyValV2(pubkey));
                }
                if let Some(dividend) = block.dividend() {
                    uds::create_uds::<B>(
                        block.number(),
                        dividend,
                        &mut identities,
                        &mut uds,
                        &mut uds_reval,
                    )?;
                }
                txs::apply_txs::<B>(
                    block.number(),
                    block.transactions(),
                    &mut txs_hashs,
                    &mut uds,
                    &mut utxos,
                    &mut consumed_utxos,
                )?;
                Ok(())
            },
        )?;

    if block_meta.number > ROLL_BACK_MAX {
        prune_bc_db(bc_db, BlockNumber(block_meta.number))?;
    }

    Ok(block_meta)
}

fn prune_bc_db<B: Backend>(
    bc_db: &duniter_dbs::databases::bc_v2::BcV2Db<B>,
    current_block_number: BlockNumber,
) -> KvResult<()> {
    bc_db
        .consumed_utxos_write()
        .remove(U32BE(current_block_number.0 - ROLL_BACK_MAX))?;
    Ok(())
}

pub fn revert_block<B: Backend>(
    bc_db: &duniter_dbs::databases::bc_v2::BcV2Db<B>,
    block: &DubpBlockV10,
) -> KvResult<Option<BlockMetaV2>> {
    (
        bc_db.blocks_meta_write(),
        bc_db.identities_write(),
        bc_db.txs_hashs_write(),
        bc_db.uds_write(),
        bc_db.uds_reval_write(),
        bc_db.uids_index_write(),
        bc_db.utxos_write(),
        bc_db.consumed_utxos_write(),
        bc_db.currency_params_write(),
    )
        .write(
            |(
                mut blocks_meta,
                mut identities,
                mut txs_hashs,
                mut uds,
                mut uds_reval,
                mut uids_index,
                mut utxos,
                mut consumed_utxos,
                mut currency_params,
            )| {
                if block.number() == BlockNumber(0) {
                    currency_params.remove(());
                }
                txs::revert_txs::<B>(
                    block.number(),
                    block.transactions(),
                    &mut txs_hashs,
                    &mut uds,
                    &mut utxos,
                    &mut consumed_utxos,
                )?;
                if block.dividend().is_some() {
                    uds::revert_uds::<B>(
                        block.number(),
                        &mut identities,
                        &mut uds,
                        &mut uds_reval,
                    )?;
                }
                identities::revert_identities::<B>(block, &mut identities)?;
                for idty in block.identities() {
                    let username = idty.username().to_owned();
                    uids_index.remove(username);
                }
                blocks_meta.remove(U32BE(block.number().0));
                Ok(if block.number() == BlockNumber(0) {
                    None
                } else {
                    blocks_meta.get(&U32BE(block.number().0 - 1))?
                })
            },
        )
}

#[cfg(test)]
mod tests {
    use super::*;
    use dubp::{
        crypto::keys::{ed25519::PublicKey, PublicKey as _},
        documents::transaction::TransactionDocumentV10Stringified,
        documents_parser::prelude::FromStringObject,
    };
    use duniter_dbs::{
        databases::bc_v2::*, BlockUtxosV2Db, UtxoIdDbV2, WalletScriptWithSourceAmountV1Db,
    };
    use maplit::hashmap;

    #[test]
    fn test_bc_apply_block() -> anyhow::Result<()> {
        let bc_db = BcV2Db::<Mem>::open(MemConf::default())?;

        let s1 = WalletScriptV10::single_sig(PublicKey::from_base58(
            "D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx",
        )?);
        let s2 = WalletScriptV10::single_sig(PublicKey::from_base58(
            "4fHMTFBMo5sTQEc5p1CNWz28S4mnnqdUBmECq1zt4n2m",
        )?);

        let b0 = DubpBlockV10::from_string_object(&DubpBlockV10Stringified {
            version: 10,
            median_time: 5_243,
            dividend: Some(1000),
            identities: vec!["D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx:Ydnclvw76/JHcKSmU9kl9Ie0ne5/X8NYOqPqbGnufIK3eEPRYYdEYaQh+zffuFhbtIRjv6m/DkVLH5cLy/IyAg==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:elois".to_owned()],
            joiners: vec!["D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx:FFeyrvYio9uYwY5aMcDGswZPNjGLrl8THn9l3EPKSNySD3SDSHjCljSfFEwb87sroyzJQoVzPwER0sW/cbZMDg==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:elois".to_owned()],
            inner_hash: Some("0000000A65A12DB95B3153BCD05DB4D5C30CC7F0B1292D9FFBC3DE67F72F6040".to_owned()),
            signature: "7B0hvcfajE2G8nBLp0vLVaQcQdQIyli21Gu8F2l+nimKHRe+fUNi+MWd1e/u29BYZa+RZ1yxhbHIbFzytg7fAA==".to_owned(),
            hash: Some("0000000000000000000000000000000000000000000000000000000000000000".to_owned()),
            ..Default::default()
        })?;

        apply_block(&bc_db, &b0)?;

        assert_eq!(bc_db.blocks_meta().count()?, 1);
        assert_eq!(bc_db.uds().count()?, 1);
        assert_eq!(bc_db.utxos().count()?, 0);
        assert_eq!(bc_db.consumed_utxos().count()?, 0);

        let b1 = DubpBlockV10::from_string_object(&DubpBlockV10Stringified {
            number: 1,
            version: 10,
            median_time: 5_245,
            transactions: vec![TransactionDocumentV10Stringified {
                currency: "test".to_owned(),
                blockstamp: "0-0000000000000000000000000000000000000000000000000000000000000000".to_owned(),
                locktime: 0,
                issuers: vec!["D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx".to_owned()],
                inputs: vec!["1000:0:D:D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx:0".to_owned()],
                unlocks: vec![],
                outputs: vec![
                    "600:0:SIG(4fHMTFBMo5sTQEc5p1CNWz28S4mnnqdUBmECq1zt4n2m)".to_owned(),
                    "400:0:SIG(D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx)".to_owned(),
                ],
                comment: "".to_owned(),
                signatures: vec![],
                hash: Some("0000000000000000000000000000000000000000000000000000000000000000".to_owned()),
            }],
            inner_hash: Some("0000000A65A12DB95B3153BCD05DB4D5C30CC7F0B1292D9FFBC3DE67F72F6040".to_owned()),
            signature: "7B0hvcfajE2G8nBLp0vLVaQcQdQIyli21Gu8F2l+nimKHRe+fUNi+MWd1e/u29BYZa+RZ1yxhbHIbFzytg7fAA==".to_owned(),
            hash: Some("0000000000000000000000000000000000000000000000000000000000000000".to_owned()),
            ..Default::default()
        })?;

        apply_block(&bc_db, &b1)?;

        assert_eq!(bc_db.blocks_meta().count()?, 2);
        assert_eq!(bc_db.uds().count()?, 0);
        assert_eq!(bc_db.utxos().count()?, 2);
        assert_eq!(
            bc_db
                .utxos()
                .iter(.., |it| it.collect::<KvResult<Vec<_>>>())?,
            vec![
                (
                    UtxoIdDbV2(Hash::default(), 0),
                    WalletScriptWithSourceAmountV1Db {
                        wallet_script: s2.clone(),
                        source_amount: SourceAmount::with_base0(600)
                    }
                ),
                (
                    UtxoIdDbV2(Hash::default(), 1),
                    WalletScriptWithSourceAmountV1Db {
                        wallet_script: s1.clone(),
                        source_amount: SourceAmount::with_base0(400)
                    }
                )
            ]
        );
        assert_eq!(bc_db.consumed_utxos().count()?, 0);

        let b2 = DubpBlockV10::from_string_object(&DubpBlockV10Stringified {
            number: 2,
            version: 10,
            median_time: 5_247,
            transactions: vec![TransactionDocumentV10Stringified {
                currency: "test".to_owned(),
                blockstamp: "0-0000000000000000000000000000000000000000000000000000000000000000".to_owned(),
                locktime: 0,
                issuers: vec!["D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx".to_owned()],
                inputs: vec!["400:0:T:0000000000000000000000000000000000000000000000000000000000000000:1".to_owned()],
                unlocks: vec![],
                outputs: vec![
                    "300:0:SIG(D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx)".to_owned(),
                    "100:0:SIG(4fHMTFBMo5sTQEc5p1CNWz28S4mnnqdUBmECq1zt4n2m)".to_owned(),
                ],
                comment: "".to_owned(),
                signatures: vec![],
                hash: Some("0101010101010101010101010101010101010101010101010101010101010101".to_owned()),
            }],
            inner_hash: Some("0000000A65A12DB95B3153BCD05DB4D5C30CC7F0B1292D9FFBC3DE67F72F6040".to_owned()),
            signature: "7B0hvcfajE2G8nBLp0vLVaQcQdQIyli21Gu8F2l+nimKHRe+fUNi+MWd1e/u29BYZa+RZ1yxhbHIbFzytg7fAA==".to_owned(),
            hash: Some("0000000000000000000000000000000000000000000000000000000000000000".to_owned()),
            ..Default::default()
        })?;

        apply_block(&bc_db, &b2)?;

        assert_eq!(bc_db.blocks_meta().count()?, 3);
        assert_eq!(bc_db.uds().count()?, 0);
        assert_eq!(bc_db.utxos().count()?, 3);
        assert_eq!(bc_db.consumed_utxos().count()?, 1);

        assert_eq!(
            bc_db
                .consumed_utxos()
                .iter(.., |it| it.collect::<KvResult<Vec<_>>>())?,
            vec![(
                U32BE(2),
                BlockUtxosV2Db(
                    hashmap![UtxoIdV10 { tx_hash: Hash::default(), output_index: 1 } => WalletScriptWithSourceAmountV1Db {
                        wallet_script: s1.clone(),
                        source_amount: SourceAmount::with_base0(400)
                    }]
                )
            )]
        );

        assert_eq!(
            bc_db
                .utxos()
                .iter(.., |it| it.collect::<KvResult<Vec<_>>>())?,
            vec![
                (
                    UtxoIdDbV2(Hash::default(), 0),
                    WalletScriptWithSourceAmountV1Db {
                        wallet_script: s2.clone(),
                        source_amount: SourceAmount::with_base0(600)
                    }
                ),
                (
                    UtxoIdDbV2(Hash([1; 32]), 0),
                    WalletScriptWithSourceAmountV1Db {
                        wallet_script: s1,
                        source_amount: SourceAmount::with_base0(300)
                    }
                ),
                (
                    UtxoIdDbV2(Hash([1; 32]), 1),
                    WalletScriptWithSourceAmountV1Db {
                        wallet_script: s2,
                        source_amount: SourceAmount::with_base0(100)
                    }
                )
            ]
        );

        Ok(())
    }
}