use std::ops::RangeInclusive;
use types::{Eth1Data, Hash256};

#[derive(Debug, PartialEq, Clone)]
pub enum Error {
    /// The timestamp of each block equal to or later than the block prior to it.
    InconsistentTimestamp { parent: u64, child: u64 },
    /// Some `Eth1Block` was provided with the same block number but different data. The source
    /// of eth1 data is inconsistent.
    Conflicting(u64),
    /// The given block was not one block number higher than the higest known block number.
    NonConsecutive { given: u64, expected: u64 },
    /// Some invariant was violated, there is a likely bug in the code.
    Internal(String),
}

/// A block of the eth1 chain.
///
/// Contains all information required to add a `BlockCache` entry.
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
pub struct Eth1Block {
    pub hash: Hash256,
    pub timestamp: u64,
    pub number: u64,
    pub deposit_root: Option<Hash256>,
    pub deposit_count: Option<u64>,
}

impl Eth1Block {
    pub fn eth1_data(self) -> Option<Eth1Data> {
        Some(Eth1Data {
            deposit_root: self.deposit_root?,
            deposit_count: self.deposit_count?,
            block_hash: self.hash,
        })
    }
}

/// Stores block and deposit contract information and provides queries based upon the block
/// timestamp.
#[derive(Debug, PartialEq, Clone, Default)]
pub struct BlockCache {
    blocks: Vec<Eth1Block>,
}

impl BlockCache {
    /// Returns the number of blocks stored in `self`.
    pub fn len(&self) -> usize {
        self.blocks.len()
    }

    /// True if the cache does not store any blocks.
    pub fn is_empty(&self) -> bool {
        self.blocks.is_empty()
    }

    /// Returns the highest block number stored.
    pub fn highest_block_number(&self) -> Option<u64> {
        self.blocks.last().map(|block| block.number)
    }

    /// Returns an iterator over all blocks.
    ///
    /// Blocks a guaranteed to be returned with;
    ///
    /// - Monotonically increasing block numbers.
    /// - Non-uniformly increasing block timestamps.
    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Eth1Block> + Clone {
        self.blocks.iter()
    }

    /// Shortens the cache, keeping the latest (by block number) `len` blocks while dropping the
    /// rest.
    ///
    /// If `len` is greater than the vector's current length, this has no effect.
    pub fn truncate(&mut self, len: usize) {
        if len < self.blocks.len() {
            self.blocks = self.blocks.split_off(self.blocks.len() - len);
        }
    }

    /// Returns the range of block numbers stored in the block cache. All blocks in this range can
    /// be accessed.
    fn available_block_numbers(&self) -> Option<RangeInclusive<u64>> {
        Some(self.blocks.first()?.number..=self.blocks.last()?.number)
    }

    /// Returns a block with the corresponding number, if any.
    pub fn block_by_number(&self, block_number: u64) -> Option<&Eth1Block> {
        self.blocks.get(
            self.blocks
                .as_slice()
                .binary_search_by(|block| block.number.cmp(&block_number))
                .ok()?,
        )
    }

    /// Insert an `Eth1Snapshot` into `self`, allowing future queries.
    ///
    /// Allows inserting either:
    ///
    /// - The root block (i.e., any block if there are no existing blocks), or,
    /// - An immediate child of the most recent (highest block number) block.
    ///
    /// ## Errors
    ///
    /// - If the cache is not empty and `item.block.block_number - 1` is not already in `self`.
    /// - If `item.block.block_number` is in `self`, but is not identical to the supplied
    /// `Eth1Snapshot`.
    /// - If `item.block.timestamp` is prior to the parent.
    pub fn insert_root_or_child(&mut self, block: Eth1Block) -> Result<(), Error> {
        let expected_block_number = self
            .highest_block_number()
            .map(|n| n + 1)
            .unwrap_or_else(|| block.number);

        // If there are already some cached blocks, check to see if the new block number is one of
        // them.
        //
        // If the block is already known, check to see the given block is identical to it. If not,
        // raise an inconsistency error. This is mostly likely caused by some fork on the eth1
        // chain.
        if let Some(local) = self.available_block_numbers() {
            if local.contains(&block.number) {
                let known_block = self.block_by_number(block.number).ok_or_else(|| {
                    Error::Internal("An expected block was not present".to_string())
                })?;

                if known_block == &block {
                    return Ok(());
                } else {
                    return Err(Error::Conflicting(block.number));
                };
            }
        }

        // Only permit blocks when it's either:
        //
        // - The first block inserted.
        // - Exactly one block number higher than the highest known block number.
        if block.number != expected_block_number {
            return Err(Error::NonConsecutive {
                given: block.number,
                expected: expected_block_number,
            });
        }

        // If the block is not the first block inserted, ensure that its timestamp is not higher
        // than its parents.
        if let Some(previous_block) = self.blocks.last() {
            if previous_block.timestamp > block.timestamp {
                return Err(Error::InconsistentTimestamp {
                    parent: previous_block.timestamp,
                    child: block.timestamp,
                });
            }
        }

        self.blocks.push(block);

        Ok(())
    }
}

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

    fn get_block(i: u64, interval_secs: u64) -> Eth1Block {
        Eth1Block {
            hash: Hash256::from_low_u64_be(i),
            timestamp: i * interval_secs,
            number: i,
            deposit_root: Some(Hash256::from_low_u64_be(i << 32)),
            deposit_count: Some(i),
        }
    }

    fn get_blocks(n: usize, interval_secs: u64) -> Vec<Eth1Block> {
        (0..n as u64)
            .into_iter()
            .map(|i| get_block(i, interval_secs))
            .collect()
    }

    fn insert(cache: &mut BlockCache, s: Eth1Block) -> Result<(), Error> {
        cache.insert_root_or_child(s)
    }

    #[test]
    fn truncate() {
        let n = 16;
        let blocks = get_blocks(n, 10);

        let mut cache = BlockCache::default();

        for block in blocks {
            insert(&mut cache, block.clone()).expect("should add consecutive blocks");
        }

        for len in vec![0, 1, 2, 3, 4, 8, 15, 16] {
            let mut cache = cache.clone();

            cache.truncate(len);

            assert_eq!(
                cache.blocks.len(),
                len,
                "should truncate to length: {}",
                len
            );
        }

        let mut cache_2 = cache.clone();
        cache_2.truncate(17);
        assert_eq!(
            cache_2.blocks.len(),
            n,
            "truncate to larger than n should be a no-op"
        );
    }

    #[test]
    fn inserts() {
        let n = 16;
        let blocks = get_blocks(n, 10);

        let mut cache = BlockCache::default();

        for block in blocks {
            insert(&mut cache, block.clone()).expect("should add consecutive blocks");
        }

        // No error for re-adding a block identical to one that exists.
        assert!(insert(&mut cache, get_block(n as u64 - 1, 10)).is_ok());

        // Error for re-adding a block that is different to the one that exists.
        assert!(insert(&mut cache, get_block(n as u64 - 1, 11)).is_err());

        // Error for adding non-consecutive blocks.
        assert!(insert(&mut cache, get_block(n as u64 + 1, 10)).is_err());
        assert!(insert(&mut cache, get_block(n as u64 + 2, 10)).is_err());

        // Error for adding timestamp prior to previous.
        assert!(insert(&mut cache, get_block(n as u64, 1)).is_err());
        // Double check to make sure previous test was only affected by timestamp.
        assert!(insert(&mut cache, get_block(n as u64, 10)).is_ok());
    }

    #[test]
    fn duplicate_timestamp() {
        let mut blocks = get_blocks(7, 10);

        blocks[0].timestamp = 0;
        blocks[1].timestamp = 10;
        blocks[2].timestamp = 10;
        blocks[3].timestamp = 20;
        blocks[4].timestamp = 30;
        blocks[5].timestamp = 40;
        blocks[6].timestamp = 40;

        let mut cache = BlockCache::default();

        for block in &blocks {
            insert(&mut cache, block.clone())
                .expect("should add consecutive blocks with duplicate timestamps");
        }

        assert_eq!(cache.blocks, blocks, "should have added all blocks");
    }
}