use std::{cmp::max, hash::Hash, ops::Index};
use parity_scale_codec::{Decode, Encode};
use sp_runtime::SaturatedConversion;
use crate::{
    phron_primitives::{BlockHash, BlockNumber},
    data_io::MAX_DATA_BRANCH_LEN,
    BlockId, SessionBoundaries,
};
#[derive(Clone, Debug, Encode, Decode, Hash, PartialEq, Eq)]
pub struct UnvalidatedPhronProposal {
    pub branch: Vec<BlockHash>,
    pub number: BlockNumber,
}
#[derive(Debug, PartialEq, Eq)]
pub enum ValidationError {
    BranchEmpty,
    BranchTooLong {
        branch_size: usize,
    },
    BlockNumberOutOfBounds {
        branch_size: usize,
        block_number: BlockNumber,
    },
    BlockOutsideSessionBoundaries {
        session_start: BlockNumber,
        session_end: BlockNumber,
        top_block: BlockNumber,
        bottom_block: BlockNumber,
    },
}
impl UnvalidatedPhronProposal {
    pub(crate) fn new(branch: Vec<BlockHash>, block_number: BlockNumber) -> Self {
        UnvalidatedPhronProposal {
            branch,
            number: block_number,
        }
    }
    pub(crate) fn validate_bounds(
        &self,
        session_boundaries: &SessionBoundaries,
    ) -> Result<PhronProposal, ValidationError> {
        use ValidationError::*;
        if self.branch.len() > MAX_DATA_BRANCH_LEN {
            return Err(BranchTooLong {
                branch_size: self.branch.len(),
            });
        }
        if self.branch.is_empty() {
            return Err(BranchEmpty);
        }
        if self.number < <BlockNumber>::saturated_from(self.branch.len()) {
            return Err(BlockNumberOutOfBounds {
                branch_size: self.branch.len(),
                block_number: self.number,
            });
        }
        let bottom_block = self.number - <BlockNumber>::saturated_from(self.branch.len() - 1);
        let top_block = self.number;
        let session_start = session_boundaries.first_block();
        let session_end = session_boundaries.last_block();
        if session_start > bottom_block || top_block > session_end {
            return Err(BlockOutsideSessionBoundaries {
                session_start,
                session_end,
                top_block,
                bottom_block,
            });
        }
        Ok(PhronProposal {
            branch: self.branch.clone(),
            number: self.number,
        })
    }
}
#[derive(Clone, Debug, Encode, Decode, Hash, PartialEq, Eq)]
pub struct PhronProposal {
    branch: Vec<BlockHash>,
    number: BlockNumber,
}
impl Index<usize> for PhronProposal {
    type Output = BlockHash;
    fn index(&self, index: usize) -> &Self::Output {
        &self.branch[index]
    }
}
impl PhronProposal {
    pub fn len(&self) -> usize {
        self.branch.len()
    }
    pub fn top_block(&self) -> BlockId {
        (
            *self
                .branch
                .last()
                .expect("cannot be empty for correct data"),
            self.number_top_block(),
        )
            .into()
    }
    pub fn bottom_block(&self) -> BlockId {
        (
            *self
                .branch
                .first()
                .expect("cannot be empty for correct data"),
            self.number_bottom_block(),
        )
            .into()
    }
    pub fn number_below_branch(&self) -> BlockNumber {
        self.number - <BlockNumber>::saturated_from(self.branch.len())
    }
    pub fn number_bottom_block(&self) -> BlockNumber {
        self.number - <BlockNumber>::saturated_from(self.branch.len() - 1)
    }
    pub fn number_top_block(&self) -> BlockNumber {
        self.number
    }
    pub fn block_at_num(&self, num: BlockNumber) -> Option<BlockId> {
        if self.number_bottom_block() <= num && num <= self.number_top_block() {
            let ind: usize = (num - self.number_bottom_block()).saturated_into();
            return Some((self.branch[ind], num).into());
        }
        None
    }
    pub fn blocks_from_num(&self, num: BlockNumber) -> impl Iterator<Item = BlockId> + '_ {
        let num = max(num, self.number_bottom_block());
        self.branch
            .iter()
            .skip((num - self.number_bottom_block()).saturated_into())
            .cloned()
            .zip(0u32..)
            .map(move |(hash, index)| (hash, num + index).into())
    }
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum PendingProposalStatus {
    PendingTopBlock,
    TopBlockImportedButIncorrectBranch,
    TopBlockImportedButNotFinalizedAncestor,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum ProposalStatus {
    Finalize(Vec<BlockId>),
    Ignore,
    Pending(PendingProposalStatus),
}
#[cfg(test)]
mod tests {
    use sp_core::hash::H256;
    use super::{UnvalidatedPhronProposal, ValidationError::*};
    use crate::{
        phron_primitives::BlockNumber, data_io::MAX_DATA_BRANCH_LEN, SessionBoundaryInfo,
        SessionId, SessionPeriod,
    };
    #[test]
    fn proposal_with_empty_branch_is_invalid() {
        let session_boundaries =
            SessionBoundaryInfo::new(SessionPeriod(20)).boundaries_for_session(SessionId(1));
        let branch = vec![];
        let proposal = UnvalidatedPhronProposal::new(branch, session_boundaries.first_block());
        assert_eq!(
            proposal.validate_bounds(&session_boundaries),
            Err(BranchEmpty)
        );
    }
    #[test]
    fn too_long_proposal_is_invalid() {
        let session_boundaries =
            SessionBoundaryInfo::new(SessionPeriod(20)).boundaries_for_session(SessionId(1));
        let session_end = session_boundaries.last_block();
        let branch = vec![H256::default(); MAX_DATA_BRANCH_LEN + 1];
        let branch_size = branch.len();
        let proposal = UnvalidatedPhronProposal::new(branch, session_end);
        assert_eq!(
            proposal.validate_bounds(&session_boundaries),
            Err(BranchTooLong { branch_size })
        );
    }
    #[test]
    fn proposal_not_within_session_is_invalid() {
        let session_boundaries =
            SessionBoundaryInfo::new(SessionPeriod(20)).boundaries_for_session(SessionId(1));
        let session_start = session_boundaries.first_block();
        let session_end = session_boundaries.last_block();
        let branch = vec![H256::default(); 2];
        let proposal = UnvalidatedPhronProposal::new(branch.clone(), session_start);
        assert_eq!(
            proposal.validate_bounds(&session_boundaries),
            Err(BlockOutsideSessionBoundaries {
                session_start,
                session_end,
                bottom_block: session_start - 1,
                top_block: session_start
            })
        );
        let proposal = UnvalidatedPhronProposal::new(branch, session_end + 1);
        assert_eq!(
            proposal.validate_bounds(&session_boundaries),
            Err(BlockOutsideSessionBoundaries {
                session_start,
                session_end,
                bottom_block: session_end,
                top_block: session_end + 1
            })
        );
    }
    #[test]
    fn proposal_starting_at_zero_block_is_invalid() {
        let session_boundaries =
            SessionBoundaryInfo::new(SessionPeriod(20)).boundaries_for_session(SessionId(0));
        let branch = vec![H256::default(); 2];
        let proposal = UnvalidatedPhronProposal::new(branch, 1);
        assert_eq!(
            proposal.validate_bounds(&session_boundaries),
            Err(BlockNumberOutOfBounds {
                branch_size: 2,
                block_number: 1
            })
        );
    }
    #[test]
    fn valid_proposal_is_validated_positively() {
        let session_boundaries =
            SessionBoundaryInfo::new(SessionPeriod(20)).boundaries_for_session(SessionId(0));
        let branch = vec![H256::default(); MAX_DATA_BRANCH_LEN];
        let proposal =
            UnvalidatedPhronProposal::new(branch, (MAX_DATA_BRANCH_LEN + 1) as BlockNumber);
        assert!(proposal.validate_bounds(&session_boundaries).is_ok());
        let branch = vec![H256::default(); 1];
        let proposal =
            UnvalidatedPhronProposal::new(branch, (MAX_DATA_BRANCH_LEN + 1) as BlockNumber);
        assert!(proposal.validate_bounds(&session_boundaries).is_ok());
    }
}