use super::*;
use std::fmt::Debug;
use std::marker::PhantomData;
use std::ops::Range;
use std::rc::Rc;

pub trait GenericPolynomialStorageLayout: Debug + Copy + Clone + Eq + PartialEq {
    type PolyType: Copy + Clone;
    fn num_polys(&self) -> usize;
    fn poly_range(&self, poly_type: Self::PolyType) -> (Range<usize>, Self);
}

impl GenericPolynomialStorageLayout for usize {
    type PolyType = ();

    fn num_polys(&self) -> usize {
        *self
    }

    fn poly_range(&self, _: Self::PolyType) -> (Range<usize>, Self) {
        (0..*self, *self)
    }
}

#[derive(Clone)]
pub struct GenericStorage<P: PolyForm, L: GenericPolynomialStorageLayout> {
    pub(crate) inner: DVec<F>,
    pub(crate) layout: L,
    pub(crate) num_polys: usize,
    pub(crate) domain_size: usize,
    poly_form: PhantomData<P>,
}

impl<P: PolyForm, L: GenericPolynomialStorageLayout> AsSingleSlice for GenericStorage<P, L> {
    fn domain_size(&self) -> usize {
        self.domain_size
    }

    fn num_polys(&self) -> usize {
        self.num_polys
    }

    fn as_single_slice(&self) -> &[F] {
        &self.inner
    }

    fn as_single_slice_mut(&mut self) -> &mut [F] {
        &mut self.inner
    }
}

impl<P: PolyForm, L: GenericPolynomialStorageLayout> GenericStorage<P, L> {
    pub fn new(inner: DVec<F>, layout: L, domain_size: usize) -> Self {
        let num_polys = layout.num_polys();
        assert_eq!(inner.len(), num_polys * domain_size);
        Self {
            inner,
            layout,
            num_polys,
            domain_size,
            poly_form: PhantomData,
        }
    }

    pub fn into_poly_storage(self) -> GenericPolynomialStorage<'static, P> {
        let Self {
            inner,
            num_polys,
            domain_size,
            ..
        } = self;
        let chunks = inner.into_adjacent_chunks(domain_size);
        assert_eq!(chunks.len(), num_polys);

        let mut polynomials = vec![];
        for chunk in chunks {
            let poly = Poly::from(chunk);
            polynomials.push(poly)
        }
        GenericPolynomialStorage { polynomials }
    }

    pub fn as_polys(&self) -> Vec<Poly<P>> {
        self.inner
            .chunks(self.domain_size)
            .map(Poly::from)
            .collect()
    }

    pub fn as_polys_mut(&mut self) -> Vec<Poly<P>> {
        self.inner
            .chunks_mut(self.domain_size)
            .map(Poly::from)
            .collect()
    }

    pub fn as_complex_polys(&self) -> Vec<ComplexPoly<P>> {
        assert_eq!(self.num_polys % 2, 0);
        self.inner
            .chunks(self.domain_size)
            .map(Poly::from)
            .array_chunks::<2>()
            .map(|[c0, c1]| ComplexPoly::new(c0, c1))
            .collect()
    }

    pub fn as_complex_polys_mut(&mut self) -> Vec<ComplexPoly<P>> {
        assert_eq!(self.num_polys % 2, 0);
        self.inner
            .chunks_mut(self.domain_size)
            .map(Poly::from)
            .array_chunks::<2>()
            .map(|[c0, c1]| ComplexPoly::new(c0, c1))
            .collect()
    }

    pub fn into_complex_poly_storage(
        self,
    ) -> CudaResult<GenericComplexPolynomialStorage<'static, P>> {
        let Self {
            inner,
            num_polys,
            domain_size,
            ..
        } = self;
        let chunks = inner.into_adjacent_chunks(domain_size);
        assert_eq!(chunks.len(), num_polys);
        assert_eq!(chunks.len() % 2, 0);

        let mut polynomials = vec![];
        for [c0, c1] in chunks.into_iter().array_chunks::<2>() {
            let poly = ComplexPoly::new(Poly::from(c0), Poly::from(c1));
            polynomials.push(poly)
        }
        assert_eq!(polynomials.len(), num_polys >> 1);
        let new = GenericComplexPolynomialStorage { polynomials };

        Ok(new)
    }

    pub unsafe fn transmute<U: PolyForm>(self) -> GenericStorage<U, L> {
        GenericStorage::new(self.inner, self.layout, self.domain_size)
    }
}

impl<L: GenericPolynomialStorageLayout> GenericStorage<Undefined, L> {
    pub fn allocate(layout: L, domain_size: usize) -> Self {
        let num_polys = layout.num_polys();
        let inner = dvec!(num_polys * domain_size);
        Self::new(inner, layout, domain_size)
    }
}

impl<L: GenericPolynomialStorageLayout> GenericStorage<LagrangeBasis, L> {
    pub fn into_monomials(mut self) -> CudaResult<GenericStorage<MonomialBasis, L>> {
        let domain_size = self.domain_size;
        let num_polys = self.num_polys;
        let input = self.as_single_slice_mut();
        ntt::batch_ntt_with_epilogue(
            input,
            false,
            true,
            domain_size,
            num_polys,
            |chunk, stream| ntt::batch_bitreverse_on_stream(chunk, domain_size, stream),
        )?;
        let result = unsafe { self.transmute() };
        Ok(result)
    }

    pub fn fill_monomials(
        &self,
        mut storage: GenericStorage<Undefined, L>,
    ) -> CudaResult<GenericStorage<MonomialBasis, L>> {
        assert_eq!(storage.layout, self.layout);
        let domain_size = self.domain_size;
        assert_eq!(storage.domain_size, domain_size);
        let num_polys = self.num_polys;
        let inputs = self.as_single_slice();
        let outputs = storage.as_single_slice_mut();
        ntt::batch_ntt_with_epilogue_into(
            inputs,
            outputs,
            false,
            true,
            domain_size,
            num_polys,
            |chunk, stream| ntt::batch_bitreverse_on_stream(chunk, domain_size, stream),
        )?;
        let result = unsafe { storage.transmute() };
        Ok(result)
    }

    pub fn create_monomials(&self) -> CudaResult<GenericStorage<MonomialBasis, L>> {
        let storage = GenericStorage::allocate(self.layout, self.domain_size);
        self.fill_monomials(storage)
    }
}

impl<L: GenericPolynomialStorageLayout> GenericStorage<MonomialBasis, L> {
    pub fn into_evaluations(mut self) -> CudaResult<GenericStorage<LagrangeBasis, L>> {
        let domain_size = self.domain_size;
        let num_polys = self.num_polys;
        let input = self.as_single_slice_mut();
        ntt::batch_ntt_with_epilogue(
            input,
            false,
            false,
            domain_size,
            num_polys,
            |chunk, stream| ntt::batch_bitreverse_on_stream(chunk, domain_size, stream),
        )?;
        let evaluations = unsafe { self.transmute() };
        Ok(evaluations)
    }

    pub fn fill_evaluations(
        &self,
        mut storage: GenericStorage<Undefined, L>,
    ) -> CudaResult<GenericStorage<LagrangeBasis, L>> {
        assert_eq!(storage.layout, self.layout);
        let domain_size = self.domain_size;
        assert_eq!(storage.domain_size, domain_size);
        let num_polys = self.num_polys;
        let inputs = self.as_single_slice();
        let outputs = storage.as_single_slice_mut();
        ntt::batch_ntt_with_epilogue_into(
            inputs,
            outputs,
            false,
            false,
            domain_size,
            num_polys,
            |chunk, stream| ntt::batch_bitreverse_on_stream(chunk, domain_size, stream),
        )?;
        let result = unsafe { storage.transmute() };
        Ok(result)
    }

    pub fn create_evaluations(&self) -> CudaResult<GenericStorage<LagrangeBasis, L>> {
        let storage = GenericStorage::allocate(self.layout, self.domain_size);
        self.fill_evaluations(storage)
    }

    pub fn into_coset_evaluations(
        mut self,
        coset_idx: usize,
        lde_degree: usize,
    ) -> CudaResult<GenericStorage<CosetEvaluations, L>> {
        let domain_size = self.domain_size;
        let num_polys = self.num_polys;
        let inputs = self.as_single_slice_mut();
        ntt::batch_coset_ntt(inputs, coset_idx, domain_size, lde_degree, num_polys)?;
        let result = unsafe { self.transmute() };
        Ok(result)
    }

    pub fn fill_coset_evaluations(
        &self,
        coset_idx: usize,
        lde_degree: usize,
        mut storage: GenericStorage<Undefined, L>,
    ) -> CudaResult<GenericStorage<CosetEvaluations, L>> {
        assert_eq!(storage.layout, self.layout);
        let domain_size = self.domain_size;
        assert_eq!(storage.domain_size, domain_size);
        let num_polys = self.num_polys;
        let inputs = self.as_single_slice();
        let outputs = storage.as_single_slice_mut();
        ntt::batch_coset_ntt_into(
            inputs,
            outputs,
            coset_idx,
            domain_size,
            lde_degree,
            num_polys,
        )?;
        let result = unsafe { storage.transmute() };
        Ok(result)
    }

    pub fn create_coset_evaluations(
        &self,
        coset_idx: usize,
        lde_degree: usize,
    ) -> CudaResult<GenericStorage<CosetEvaluations, L>> {
        let storage = GenericStorage::allocate(self.layout, self.domain_size);
        self.fill_coset_evaluations(coset_idx, lde_degree, storage)
    }

    pub fn fill_coset_evaluations_subset(
        &self,
        coset_idx: usize,
        lde_degree: usize,
        subset: L::PolyType,
        mut storage: GenericStorage<Undefined, L>,
    ) -> CudaResult<GenericStorage<CosetEvaluations, L>> {
        let (range, layout) = self.layout.poly_range(subset);
        assert!(range.end <= self.num_polys);
        assert_eq!(storage.layout, layout);
        let domain_size = self.domain_size;
        assert_eq!(storage.domain_size(), domain_size);
        let num_polys = range.len();
        let inputs = &self.as_single_slice()[range.start * domain_size..range.end * domain_size];
        let outputs = storage.as_single_slice_mut();
        if num_polys != 0 {
            ntt::batch_coset_ntt_into(
                inputs,
                outputs,
                coset_idx,
                domain_size,
                lde_degree,
                num_polys,
            )?;
        }
        let result = unsafe { storage.transmute() };
        Ok(result)
    }

    pub fn create_coset_evaluations_subset(
        &self,
        coset_idx: usize,
        lde_degree: usize,
        subset: L::PolyType,
    ) -> CudaResult<GenericStorage<CosetEvaluations, L>> {
        let (_, layout) = self.layout.poly_range(subset);
        let storage = GenericStorage::allocate(layout, self.domain_size());
        self.fill_coset_evaluations_subset(coset_idx, lde_degree, subset, storage)
    }
}

impl<L: GenericPolynomialStorageLayout> GenericStorage<CosetEvaluations, L> {
    pub fn into_monomials(
        mut self,
        coset_idx: usize,
        lde_degree: usize,
    ) -> CudaResult<GenericStorage<MonomialBasis, L>> {
        let domain_size = self.domain_size;
        let num_polys = self.num_polys;
        let inputs = self.as_single_slice_mut();
        ntt::batch_inverse_coset_ntt(inputs, coset_idx, domain_size, lde_degree, num_polys)?;
        let result = unsafe { self.transmute() };
        Ok(result)
    }

    pub fn fill_monomials(
        &self,
        coset_idx: usize,
        lde_degree: usize,
        mut storage: GenericStorage<Undefined, L>,
    ) -> CudaResult<GenericStorage<MonomialBasis, L>> {
        assert_eq!(storage.layout, self.layout);
        let domain_size = self.domain_size;
        assert_eq!(storage.domain_size, domain_size);
        let num_polys = self.num_polys;
        let inputs = self.as_single_slice();
        let outputs = storage.as_single_slice_mut();
        ntt::batch_inverse_coset_ntt_into(
            inputs,
            outputs,
            coset_idx,
            domain_size,
            lde_degree,
            num_polys,
        )?;
        let result = unsafe { storage.transmute() };
        Ok(result)
    }

    pub fn create_monomials(
        &self,
        coset_idx: usize,
        lde_degree: usize,
    ) -> CudaResult<GenericStorage<MonomialBasis, L>> {
        let storage = GenericStorage::allocate(self.layout, self.domain_size);
        self.fill_monomials(coset_idx, lde_degree, storage)
    }

    pub fn build_subtree_for_coset<H: GpuTreeHasher>(
        &self,
        cap_size: usize,
        mut nodes: DVec<H::DigestElementType>,
    ) -> CudaResult<(SubTree<H>, Vec<H::Output>)> {
        let domain_size = self.domain_size();
        let leaf_sources = self.as_single_slice();
        let subtree_root =
            compute_tree_cap::<H>(leaf_sources, &mut nodes, domain_size, cap_size, 1)?;
        let subtree = SubTree::new(Rc::new(nodes), domain_size, cap_size);
        Ok((subtree, subtree_root))
    }
}

impl<P: PolyForm, L: GenericPolynomialStorageLayout> AsMut<DVec<F>> for GenericStorage<P, L> {
    fn as_mut(&mut self) -> &mut DVec<F> {
        &mut self.inner
    }
}

impl<P: PolyForm, L: GenericPolynomialStorageLayout> AsRef<DVec<F>> for GenericStorage<P, L> {
    fn as_ref(&self) -> &DVec<F> {
        &self.inner
    }
}

pub struct GenericPolynomialStorage<'a, P: PolyForm> {
    pub(crate) polynomials: Vec<Poly<'a, P>>,
}

impl<'a, P: PolyForm> GenericPolynomialStorage<'a, P> {
    pub fn allocate(num_polys: usize, domain_size: usize) -> CudaResult<Self> {
        let storage = GenericStorage::allocate(num_polys, domain_size);
        let storage = unsafe { storage.transmute() };
        Ok(storage.into_poly_storage())
    }

    pub fn num_polys(&self) -> usize {
        self.polynomials.len()
    }
}

impl<'a, P: PolyForm> AsSingleSlice for GenericPolynomialStorage<'a, P> {
    fn domain_size(&self) -> usize {
        self.polynomials[0].domain_size()
    }

    fn num_polys(&self) -> usize {
        self.polynomials.len()
    }

    fn as_single_slice(&self) -> &[F] {
        assert_adjacent_base(&self.polynomials);
        let num_polys = self.polynomials.len();
        let domain_size = self.polynomials[0].domain_size();
        let len = num_polys * domain_size;
        unsafe { std::slice::from_raw_parts(self.polynomials[0].storage.as_ref().as_ptr(), len) }
    }

    fn as_single_slice_mut(&mut self) -> &mut [F] {
        assert_adjacent_base(&self.polynomials);
        let num_polys = self.polynomials.len();
        let domain_size = self.polynomials[0].domain_size();
        let len = num_polys * domain_size;
        unsafe {
            std::slice::from_raw_parts_mut(self.polynomials[0].storage.as_mut().as_mut_ptr(), len)
        }
    }
}

pub struct GenericComplexPolynomialStorage<'a, P: PolyForm> {
    pub polynomials: Vec<ComplexPoly<'a, P>>,
}

impl<'a, P: PolyForm> AsSingleSlice for GenericComplexPolynomialStorage<'a, P> {
    fn domain_size(&self) -> usize {
        self.polynomials[0].domain_size()
    }

    fn num_polys(&self) -> usize {
        self.polynomials.len()
    }

    fn as_single_slice(&self) -> &[F] {
        assert_adjacent_ext(&self.polynomials);
        let num_polys = 2 * self.polynomials.len();
        let domain_size = self.polynomials[0].domain_size();
        let len = num_polys * domain_size;
        unsafe { std::slice::from_raw_parts(self.polynomials[0].c0.storage.as_ref().as_ptr(), len) }
    }

    fn as_single_slice_mut(&mut self) -> &mut [F] {
        assert_adjacent_ext(&self.polynomials);
        let num_polys = 2 * self.polynomials.len();
        let domain_size = self.polynomials[0].domain_size();
        let len = num_polys * domain_size;
        unsafe {
            std::slice::from_raw_parts_mut(
                self.polynomials[0].c0.storage.as_mut().as_mut_ptr(),
                len,
            )
        }
    }
}

impl<'a, P: PolyForm> GenericComplexPolynomialStorage<'a, P> {
    pub fn allocate(num_polys: usize, domain_size: usize) -> CudaResult<Self> {
        let storage = GenericStorage::allocate(2 * num_polys, domain_size);
        let storage = unsafe { storage.transmute() };
        storage.into_complex_poly_storage()
    }
}

impl<'a> GenericComplexPolynomialStorage<'a, CosetEvaluations> {
    pub fn build_subtree_for_coset<H: GpuTreeHasher>(
        &self,
        cap_size: usize,
    ) -> CudaResult<(SubTree<H>, Vec<H::Output>)> {
        let domain_size = self.polynomials[0].domain_size();
        let leaf_sources = self.as_single_slice();
        let mut subtree = dvec!(2 * H::CAPACITY * domain_size);
        tree::build_tree::<H>(leaf_sources, &mut subtree, domain_size, cap_size, 1)?;
        let subtree_root = get_tree_cap_from_nodes::<H>(&subtree, cap_size)?;
        let subtree = SubTree::new(Rc::new(subtree), domain_size, cap_size);
        Ok((subtree, subtree_root))
    }
}
