use crate::metrics::Metrics;
use crate::rndcache::RndCache;
use anyhow::Result;
use tokio::sync::Mutex;
use tokio::sync::RwLock;

use crate::chaindef::BlockHash;
use crate::chaindef::Transaction;
use bitcoincash::consensus::encode::deserialize;
use bitcoincash::hash_types::Txid;
use futures::Future;

pub struct BlockTxIDsCache {
    map: Mutex<RndCache<BlockHash, Vec<Txid>>>,
}

impl BlockTxIDsCache {
    pub fn new(bytes_capacity: u64, metrics: &Metrics) -> BlockTxIDsCache {
        let lookups = metrics.counter_int_vec(
            prometheus::Opts::new(
                "rostrum_cache_blocktxids_lookups",
                "# of cache lookups in the blocktxids cache",
            ),
            &["type"],
        );
        let churn = metrics.counter_int_vec(
            prometheus::Opts::new(
                "rostrum_cache_blocktxids_churn",
                "# of insertions and evictions from the blocktxids cache",
            ),
            &["type"],
        );
        let size = metrics.gauge_int(prometheus::Opts::new(
            "rostrum_cache_blocktxids_size",
            "Size of the blockstxid cache [bytes]",
        ));
        let entries = metrics.gauge_int(prometheus::Opts::new(
            "rostrum_cache_blocktxids_entries",
            "# of entries in the blockstxid cache",
        ));
        BlockTxIDsCache {
            map: Mutex::new(RndCache::new(bytes_capacity, lookups, churn, size, entries)),
        }
    }

    pub async fn get_or_else<Fut>(&self, blockhash: &BlockHash, fut: Fut) -> Result<Vec<Txid>>
    where
        Fut: Future<Output = Result<Vec<Txid>>>,
    {
        if let Some(txids) = self.map.lock().await.get(blockhash) {
            return Ok(txids.clone());
        }

        let txids = fut.await?;
        let mut cache_copy = txids.clone();
        cache_copy.shrink_to_fit();
        let size = cache_copy.capacity();
        self.map
            .lock()
            .await
            .put(*blockhash, cache_copy, size as u64);

        Ok(txids)
    }
}

pub struct TransactionCache {
    // Store serialized transaction (should use less RAM).
    map: RwLock<RndCache<Txid, Vec<u8>>>,
}

impl TransactionCache {
    pub fn new(bytes_capacity: u64, metrics: &Metrics) -> TransactionCache {
        let lookups = metrics.counter_int_vec(
            prometheus::Opts::new(
                "rostrum_cache_tx_lookups",
                "# of cache lookups in the transaction cache",
            ),
            &["type"],
        );
        let churn = metrics.counter_int_vec(
            prometheus::Opts::new(
                "rostrum_cache_tx_churn",
                "# of insertions and evictions from the transaction cache",
            ),
            &["type"],
        );
        let size = metrics.gauge_int(prometheus::Opts::new(
            "rostrum_cache_tx_size",
            "Size of the transaction cache [bytes]",
        ));
        let entries = metrics.gauge_int(prometheus::Opts::new(
            "rostrum_cache_tx_entries",
            "# of entries in the transaction cache",
        ));
        TransactionCache {
            map: RwLock::new(RndCache::new(bytes_capacity, lookups, churn, size, entries)),
        }
    }

    pub async fn get(&self, txid: &Txid) -> Option<Transaction> {
        if let Some(serialized_txn) = self.map.read().await.get(txid) {
            if let Ok(tx) = deserialize(serialized_txn) {
                return Some(tx);
            } else {
                trace!("failed to parse a cached tx");
            }
        }
        None
    }

    pub async fn put(&self, txid: Txid, mut serialized_tx: Vec<u8>) {
        serialized_tx.shrink_to_fit();
        let size = serialized_tx.capacity();
        self.map.write().await.put(txid, serialized_tx, size as u64);
    }
}
