use super::{Database, DatabaseValues, Fasta, P7Hmm};
use libnail::structs::{Profile, Sequence};

use rayon::iter::{
    plumbing::{bridge, Consumer, Producer, ProducerCallback, UnindexedConsumer},
    IndexedParallelIterator, IntoParallelIterator, ParallelIterator,
};

impl Fasta {
    pub fn par_iter(&'_ self) -> DatabaseParIter<'_, Sequence> {
        DatabaseParIter {
            inner: Box::new(self.clone()),
            names: self.index.inner.keys().map(|s| s.as_str()).collect(),
        }
    }
}

impl<'a> IntoParallelIterator for &'a Fasta {
    type Iter = DatabaseParIter<'a, Sequence>;

    type Item = Sequence;

    fn into_par_iter(self) -> Self::Iter {
        self.par_iter()
    }
}

impl P7Hmm {
    pub fn par_iter(&'_ self) -> DatabaseParIter<'_, Profile> {
        DatabaseParIter {
            inner: Box::new(self.clone()),
            names: self.index.inner.keys().map(|s| s.as_str()).collect(),
        }
    }
}

impl<'a> IntoParallelIterator for &'a P7Hmm {
    type Iter = DatabaseParIter<'a, Profile>;

    type Item = Profile;

    fn into_par_iter(self) -> Self::Iter {
        self.par_iter()
    }
}

/////

pub struct DatabaseParIter<'a, T> {
    pub(super) inner: Box<dyn Database<T>>,
    pub(super) names: Vec<&'a str>,
}

impl<'a, T> ParallelIterator for DatabaseParIter<'a, T>
where
    T: Send + Sync,
{
    type Item = T;

    fn drive_unindexed<C>(self, consumer: C) -> C::Result
    where
        C: UnindexedConsumer<Self::Item>,
    {
        bridge(self, consumer)
    }
}

impl<'a, T> IndexedParallelIterator for DatabaseParIter<'a, T>
where
    T: Send + Sync,
{
    fn len(&self) -> usize {
        self.inner.len()
    }

    fn drive<C: Consumer<Self::Item>>(self, consumer: C) -> C::Result {
        bridge(self, consumer)
    }

    fn with_producer<CB: ProducerCallback<Self::Item>>(self, callback: CB) -> CB::Output {
        let producer = DatabaseProducer {
            inner: self.inner,
            names: &self.names,
        };

        callback.callback(producer)
    }
}

pub struct DatabaseProducer<'a, T> {
    inner: Box<dyn Database<T>>,
    names: &'a [&'a str],
}

impl<'a, T> Producer for DatabaseProducer<'a, T> {
    type Item = T;

    type IntoIter = DatabaseValues<'a, T>;

    fn into_iter(self) -> Self::IntoIter {
        DatabaseValues {
            inner: self.inner,
            names_iter: Box::new(self.names.iter().copied()),
        }
    }

    fn split_at(self, index: usize) -> (Self, Self) {
        let (left, right) = self.names.split_at(index);
        (
            DatabaseProducer {
                inner: self.inner.clone(),
                names: left,
            },
            DatabaseProducer {
                inner: self.inner,
                names: right,
            },
        )
    }
}
