// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Traits defining credential books. These are used in the deserialization path to provide
//! the credentials to try for either advertisement version.
//! See [`CredentialBookBuilder`] for batteries-included implementations.

use crate::credential::source::{
    CredentialSource, DiscoveryCredentialSource, SliceCredentialSource,
};
use crate::credential::v0::{V0DiscoveryCryptoMaterial, V0};
use crate::credential::v1::{
    SignedSectionIdentityResolutionMaterial, SignedSectionVerificationMaterial,
    UnsignedSectionIdentityResolutionMaterial, UnsignedSectionVerificationMaterial,
    V1DiscoveryCryptoMaterial, V1,
};
#[cfg(feature = "alloc")]
use crate::credential::ReferencedMatchedCredential;
use crate::credential::{
    DiscoveryCryptoMaterial, MatchableCredential, MatchedCredential, ProtocolVersion,
};
use core::borrow::Borrow;
use core::marker::PhantomData;
use crypto_provider::CryptoProvider;

#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;

/// A collection of credentials to try when attempting to deserialize
/// advertisements of either advertisement version which is
/// valid for the given lifetime.
pub trait CredentialBook<'a>
where
    Self: 'a,
{
    /// The type of the matched credentials for the credentials
    /// held within this credential-book. May lend from the credential-book.
    type Matched: MatchedCredential;

    /// The type of V0 crypto-materials yielded by this credential-book.
    type V0Crypto: V0DiscoveryCryptoMaterial;

    /// The type of V1 crypto-materials yielded by this credential-book.
    type V1Crypto: V1DiscoveryCryptoMaterial;

    /// The iterator type returned from [`CredentialBook#v0_iter()`],
    /// which yields `(crypto-material, match_data)` pairs.
    /// This is a lending iterator which may borrow things from `self`.
    type V0Iterator: Iterator<Item = (Self::V0Crypto, Self::Matched)>;

    /// The iterator type returned from [`CredentialBook#v1_iter()`],
    /// which yields `(crypto-material, match_data)` pairs.
    /// This is a lending iterator which may borrow things from `self`.
    type V1Iterator: Iterator<Item = (Self::V1Crypto, Self::Matched)>;

    /// Returns an iterator over V0 credentials in this credential book.
    fn v0_iter(&'a self) -> Self::V0Iterator;

    /// Returns an iterator over V1 credentials in this credential book.
    fn v1_iter(&'a self) -> Self::V1Iterator;
}

/// Convenient marker struct for building credential-books with
/// some given match-data type.
pub struct CredentialBookBuilder<M: MatchedCredential> {
    _marker: PhantomData<M>,
}

impl<M: MatchedCredential> CredentialBookBuilder<M> {
    /// Constructs a [`CachedSliceCredentialBook`] from the given slices of discovery credentials,
    /// populating its internal buffers of cached credential crypto-materials for up to the
    /// given number of credentials in each version (up to `N0` credentials cached in V0,
    /// and up to `N1` credentials cached in `V1`.)
    pub fn build_cached_slice_book<'a, const N0: usize, const N1: usize, P: CryptoProvider>(
        v0_creds: &'a [MatchableCredential<V0, M>],
        v1_creds: &'a [MatchableCredential<V1, M>],
    ) -> CachedSliceCredentialBook<'a, M, N0, N1> {
        let v0_source = SliceCredentialSource::new(v0_creds);
        let v0_cache = init_cache_from_source::<V0, _, N0, P>(&v0_source);
        let v0_source = CachedCredentialSource::<_, _, N0>::new(v0_source, v0_cache);

        let v1_source = SliceCredentialSource::new(v1_creds);
        let v1_cache = init_cache_from_source::<V1, _, N1, P>(&v1_source);
        let v1_source = CachedCredentialSource::<_, _, N1>::new(v1_source, v1_cache);

        CredentialBookFromSources::new(v0_source, v1_source)
    }

    #[cfg(feature = "alloc")]
    /// Constructs a new credential-book which owns all of the given credentials,
    /// and maintains pre-calculated cryptographic information about them
    /// for speedy advertisement deserialization.
    pub fn build_precalculated_owned_book<P: CryptoProvider>(
        v0_iter: impl IntoIterator<Item = MatchableCredential<V0, M>>,
        v1_iter: impl IntoIterator<Item = MatchableCredential<V1, M>>,
    ) -> PrecalculatedOwnedCredentialBook<M> {
        let v0_source = PrecalculatedOwnedCredentialSource::<V0, M>::new::<P>(v0_iter);
        let v1_source = PrecalculatedOwnedCredentialSource::<V1, M>::new::<P>(v1_iter);
        CredentialBookFromSources::new(v0_source, v1_source)
    }
}

// Now, for the implementation details. External implementors still need
// to be able to reference these types (since the returned types from
// [`CredentialBookBuilder`]'s convenience methods are just type-aliases),
// and if they want, they can use them as building-blocks to construct
// their own [`CredentialBook`]s, but they're largely just scaffolding.

/// A structure bundling both V0 and V1 credential-sources to define
/// a credential-book which owns both of these independent sources.
pub struct CredentialBookFromSources<S0, S1> {
    /// The source for V0 credentials.
    v0_source: S0,
    /// The source for V1 credentials.
    v1_source: S1,
}

impl<'a, S0, S1> CredentialBookFromSources<S0, S1>
where
    Self: 'a,
    S0: CredentialSource<'a, V0>,
    S1: CredentialSource<'a, V1, Matched = <S0 as CredentialSource<'a, V0>>::Matched>,
    <S0 as CredentialSource<'a, V0>>::Crypto: V0DiscoveryCryptoMaterial,
    <S1 as CredentialSource<'a, V1>>::Crypto: V1DiscoveryCryptoMaterial,
{
    /// Constructs a new [`CredentialBook`] out of the two given
    /// credential-sources, one for v0 and one for v1. The match-data
    /// of both credential sources must be the same.
    pub fn new(v0_source: S0, v1_source: S1) -> Self {
        Self { v0_source, v1_source }
    }
}

impl<'a, S0, S1> CredentialBook<'a> for CredentialBookFromSources<S0, S1>
where
    Self: 'a,
    S0: CredentialSource<'a, V0>,
    S1: CredentialSource<'a, V1, Matched = <S0 as CredentialSource<'a, V0>>::Matched>,
    <S0 as CredentialSource<'a, V0>>::Crypto: V0DiscoveryCryptoMaterial,
    <S1 as CredentialSource<'a, V1>>::Crypto: V1DiscoveryCryptoMaterial,
{
    type Matched = <S0 as CredentialSource<'a, V0>>::Matched;
    type V0Crypto = <S0 as CredentialSource<'a, V0>>::Crypto;
    type V1Crypto = <S1 as CredentialSource<'a, V1>>::Crypto;
    type V0Iterator = <S0 as CredentialSource<'a, V0>>::Iterator;
    type V1Iterator = <S1 as CredentialSource<'a, V1>>::Iterator;

    fn v0_iter(&'a self) -> Self::V0Iterator {
        self.v0_source.iter()
    }
    fn v1_iter(&'a self) -> Self::V1Iterator {
        self.v1_source.iter()
    }
}

/// Type-level function used internally
/// by [`CachedCredentialSource`] in order to uniformly
/// refer to the "precalculated" crypto-material variants
/// for each protocol version.
pub(crate) mod precalculated_for_version {
    use crate::credential::v0::{
        PrecalculatedV0DiscoveryCryptoMaterial, V0DiscoveryCredential, V0,
    };
    use crate::credential::v1::{
        PrecalculatedV1DiscoveryCryptoMaterial, V1DiscoveryCredential, V1,
    };
    use crate::credential::{DiscoveryCryptoMaterial, ProtocolVersion};
    use crypto_provider::CryptoProvider;

    pub trait MappingTrait<V: ProtocolVersion> {
        type Output: DiscoveryCryptoMaterial<V>;
        /// Provides pre-calculated crypto-material for the given
        /// discovery credential.
        fn precalculate<C: CryptoProvider>(
            discovery_credential: &V::DiscoveryCredential,
        ) -> Self::Output;
    }
    pub enum Marker {}
    impl MappingTrait<V0> for Marker {
        type Output = PrecalculatedV0DiscoveryCryptoMaterial;
        fn precalculate<C: CryptoProvider>(
            discovery_credential: &V0DiscoveryCredential,
        ) -> PrecalculatedV0DiscoveryCryptoMaterial {
            PrecalculatedV0DiscoveryCryptoMaterial::new::<C>(discovery_credential)
        }
    }
    impl MappingTrait<V1> for Marker {
        type Output = PrecalculatedV1DiscoveryCryptoMaterial;
        fn precalculate<C: CryptoProvider>(
            discovery_credential: &V1DiscoveryCredential,
        ) -> PrecalculatedV1DiscoveryCryptoMaterial {
            discovery_credential.to_precalculated::<C>()
        }
    }
}

type PrecalculatedCryptoForProtocolVersion<V> =
    <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<V>>::Output;

fn precalculate_crypto_material<V: ProtocolVersion, C: CryptoProvider>(
    discovery_credential: &V::DiscoveryCredential,
) -> PrecalculatedCryptoForProtocolVersion<V>
where
    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
{
    <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<V>>::precalculate::<
        C,
    >(discovery_credential)
}

/// Iterator type for [`CachedCredentialSource`].
pub struct CachedCredentialSourceIterator<
    'a,
    V: ProtocolVersion + 'a,
    S: DiscoveryCredentialSource<'a, V> + 'a,
    const N: usize,
> where
    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
{
    /// The current index of the (enumerated) elements that we're iterating over.
    /// Always counts up at every iteration, and may lie outside of the range
    /// of the `cache` slice for uncached elements.
    current_index: usize,
    /// The cache of pre-calculated crypto-materials from the original source.
    ///
    /// The [`self.source_iterator`]'s (up-to) first `N` elements
    /// must match (up-to) the first `N` elements in the cache,
    /// and they must both appear in the same order.
    cache: &'a [Option<PrecalculatedCryptoForProtocolVersion<V>>; N],
    /// The iterator over the credentials in the original source
    source_iterator: S::Iterator,
}

impl<'a, V: ProtocolVersion + 'a, S: DiscoveryCredentialSource<'a, V> + 'a, const N: usize> Iterator
    for CachedCredentialSourceIterator<'a, V, S, N>
where
    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
{
    type Item = (PossiblyCachedDiscoveryCryptoMaterial<'a, V>, S::Matched);
    fn next(&mut self) -> Option<Self::Item> {
        // Regardless of what we're going to do with the crypto-materials, we still need
        // to advance the underlying iterator, and bail if it runs out.
        let (discovery_credential, match_data) = self.source_iterator.next()?;
        // Check whether/not we have cached crypto-material for the current index
        let crypto = match self.cache.get(self.current_index) {
            Some(precalculated_crypto_entry) => {
                let precalculated_crypto = precalculated_crypto_entry
                    .as_ref()
                    .expect("iterator still going, but cache is not full?");
                PossiblyCachedDiscoveryCryptoMaterial::from_precalculated(precalculated_crypto)
            }
            None => PossiblyCachedDiscoveryCryptoMaterial::from_discovery_credential(
                discovery_credential.borrow().clone(),
            ),
        };
        // Advance the index forward to point to the next item in the cache.
        self.current_index += 1;
        Some((crypto, match_data))
    }
}

/// A [`CredentialSource`] which augments the externally-provided [`DiscoveryCredentialSource`] with
/// a cache containing up to the specified number of pre-calculated credentials.
pub struct CachedCredentialSource<V: ProtocolVersion, S, const N: usize>
where
    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
{
    wrapped: S,
    cache: [Option<PrecalculatedCryptoForProtocolVersion<V>>; N],
}

/// Helper function to construct a cache for a [`CachedCredentialSource`] from
/// a reference to some source of discovery-credentials.
///
/// This function needs to be kept separate from [`CachedCredentialSource#new`]
/// to get around otherwise-tricky lifetime issues around ephemerally-borrowing
/// from a moved object within the body of a function.
pub(crate) fn init_cache_from_source<'a, V: ProtocolVersion, S, const N: usize, P: CryptoProvider>(
    original: &'a S,
) -> [Option<PrecalculatedCryptoForProtocolVersion<V>>; N]
where
    S: DiscoveryCredentialSource<'a, V> + 'a,
    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
{
    let mut cache = [0u8; N].map(|_| None);
    for (cache_entry, source_credential) in cache.iter_mut().zip(original.iter()) {
        let (discovery_credential, _) = source_credential;
        let precalculated_crypto_material =
            precalculate_crypto_material::<V, P>(discovery_credential.borrow());
        let _ = cache_entry.insert(precalculated_crypto_material);
    }
    cache
}

impl<'a, V: ProtocolVersion, S, const N: usize> CachedCredentialSource<V, S, N>
where
    S: DiscoveryCredentialSource<'a, V> + 'a,
    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
{
    /// Constructs a [`CachedCredentialSource`] from the given [`DiscoveryCredentialSource`]
    /// and the given (initial) cache contents, as constructed via the
    /// [`init_cache_from_source`] helper function.
    pub(crate) fn new(
        wrapped: S,
        cache: [Option<PrecalculatedCryptoForProtocolVersion<V>>; N],
    ) -> Self {
        Self { wrapped, cache }
    }
}

/// Internal implementation of [`PossiblyCachedDiscoveryCryptoMaterial`] to hide
/// what crypto-material variants we're actually storing in cached
/// credential-books.
pub(crate) enum PossiblyCachedDiscoveryCryptoMaterialKind<'a, V: ProtocolVersion>
where
    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
{
    Discovery(V::DiscoveryCredential),
    Precalculated(&'a PrecalculatedCryptoForProtocolVersion<V>),
}

/// Crypto-materials that are potentially references to
/// already-cached precomputed variants, or raw discovery
/// credentials.
pub struct PossiblyCachedDiscoveryCryptoMaterial<'a, V: ProtocolVersion>
where
    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
{
    pub(crate) wrapped: PossiblyCachedDiscoveryCryptoMaterialKind<'a, V>,
}

impl<'a, V: ProtocolVersion> PossiblyCachedDiscoveryCryptoMaterial<'a, V>
where
    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
{
    fn from_discovery_credential(discovery_credential: V::DiscoveryCredential) -> Self {
        Self { wrapped: PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(discovery_credential) }
    }
    fn from_precalculated(precalculated: &'a PrecalculatedCryptoForProtocolVersion<V>) -> Self {
        Self { wrapped: PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(precalculated) }
    }
}

impl<'a, V: ProtocolVersion> DiscoveryCryptoMaterial<V>
    for PossiblyCachedDiscoveryCryptoMaterial<'a, V>
where
    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
{
    fn metadata_nonce<C: CryptoProvider>(&self) -> [u8; 12] {
        match &self.wrapped {
            PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(x) => x.metadata_nonce::<C>(),
            PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(x) => x.metadata_nonce::<C>(),
        }
    }
}

impl<'a> V0DiscoveryCryptoMaterial for PossiblyCachedDiscoveryCryptoMaterial<'a, V0> {
    fn ldt_adv_cipher<C: CryptoProvider>(&self) -> ldt_np_adv::LdtNpAdvDecrypterXtsAes128<C> {
        match &self.wrapped {
            PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(x) => x.ldt_adv_cipher::<C>(),
            PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(x) => x.ldt_adv_cipher::<C>(),
        }
    }
}

impl<'a> V1DiscoveryCryptoMaterial for PossiblyCachedDiscoveryCryptoMaterial<'a, V1> {
    fn signed_identity_resolution_material<C: CryptoProvider>(
        &self,
    ) -> SignedSectionIdentityResolutionMaterial {
        match &self.wrapped {
            PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(x) => {
                x.signed_identity_resolution_material::<C>()
            }
            PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(x) => {
                x.signed_identity_resolution_material::<C>()
            }
        }
    }

    fn unsigned_identity_resolution_material<C: CryptoProvider>(
        &self,
    ) -> UnsignedSectionIdentityResolutionMaterial {
        match &self.wrapped {
            PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(x) => {
                x.unsigned_identity_resolution_material::<C>()
            }
            PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(x) => {
                x.unsigned_identity_resolution_material::<C>()
            }
        }
    }

    fn signed_verification_material<C: CryptoProvider>(&self) -> SignedSectionVerificationMaterial {
        match &self.wrapped {
            PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(x) => {
                x.signed_verification_material::<C>()
            }
            PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(x) => {
                x.signed_verification_material::<C>()
            }
        }
    }

    fn unsigned_verification_material<C: CryptoProvider>(
        &self,
    ) -> UnsignedSectionVerificationMaterial {
        match &self.wrapped {
            PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(x) => {
                x.unsigned_verification_material::<C>()
            }
            PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(x) => {
                x.unsigned_verification_material::<C>()
            }
        }
    }
}

impl<'a, V: ProtocolVersion, S, const N: usize> CredentialSource<'a, V>
    for CachedCredentialSource<V, S, N>
where
    Self: 'a,
    S: DiscoveryCredentialSource<'a, V> + 'a,
    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
    PossiblyCachedDiscoveryCryptoMaterial<'a, V>: DiscoveryCryptoMaterial<V>,
{
    type Matched = <S as DiscoveryCredentialSource<'a, V>>::Matched;
    type Crypto = PossiblyCachedDiscoveryCryptoMaterial<'a, V>;
    type Iterator = CachedCredentialSourceIterator<'a, V, S, N>;

    fn iter(&'a self) -> Self::Iterator {
        CachedCredentialSourceIterator {
            current_index: 0,
            cache: &self.cache,
            source_iterator: self.wrapped.iter(),
        }
    }
}

/// A simple credentials source for environments which are,
/// for all practical purposes, not space-constrained, and hence
/// can store an arbitrary amount of pre-calculated crypto-materials.
///
/// Requires `alloc` as a result of internally leveraging a `Vec`.
#[cfg(feature = "alloc")]
pub struct PrecalculatedOwnedCredentialSource<V: ProtocolVersion, M: MatchedCredential>
where
    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
{
    credentials: Vec<(PrecalculatedCryptoForProtocolVersion<V>, M)>,
}

#[cfg(feature = "alloc")]
impl<'a, V: ProtocolVersion + 'a, M: MatchedCredential + 'a>
    PrecalculatedOwnedCredentialSource<V, M>
where
    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
{
    /// Pre-calculates crypto material for the given credentials, and constructs a
    /// credentials source which holds owned copies of this crypto-material.
    pub fn new<P: CryptoProvider>(
        credential_iter: impl IntoIterator<Item = MatchableCredential<V, M>>,
    ) -> Self {
        let credentials = credential_iter
            .into_iter()
            .map(|credential| {
                (
                    precalculate_crypto_material::<V, P>(&credential.discovery_credential),
                    credential.match_data,
                )
            })
            .collect();
        Self { credentials }
    }
}

#[cfg(feature = "alloc")]
fn reference_crypto_and_match_data<C, M: MatchedCredential>(
    pair_ref: &(C, M),
) -> (&C, ReferencedMatchedCredential<M>) {
    let (c, m) = pair_ref;
    (c, m.into())
}

#[cfg(feature = "alloc")]
impl<'a, V: ProtocolVersion, M: MatchedCredential> CredentialSource<'a, V>
    for PrecalculatedOwnedCredentialSource<V, M>
where
    Self: 'a,
    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
    &'a PrecalculatedCryptoForProtocolVersion<V>: DiscoveryCryptoMaterial<V>,
{
    type Matched = ReferencedMatchedCredential<'a, M>;
    type Crypto = &'a PrecalculatedCryptoForProtocolVersion<V>;
    type Iterator = core::iter::Map<
        core::slice::Iter<'a, (PrecalculatedCryptoForProtocolVersion<V>, M)>,
        fn(
            &'a (PrecalculatedCryptoForProtocolVersion<V>, M),
        )
            -> (&'a PrecalculatedCryptoForProtocolVersion<V>, ReferencedMatchedCredential<'a, M>),
    >;

    fn iter(&'a self) -> Self::Iterator {
        self.credentials.iter().map(reference_crypto_and_match_data)
    }
}

/// Type-alias for credential sources which are provided via slice credential-sources
/// with a pre-calculated credential cache layered on top.
pub type CachedSliceCredentialSource<'a, V, M, const N: usize> =
    CachedCredentialSource<V, SliceCredentialSource<'a, V, M>, N>;

/// A [`CredentialBook`] whose sources for V0 and V1 credentials come from the given slices
/// of discovery credentials, with crypto-materials for up to the given number of credentials
/// from the beginning of each slice kept in an in-memory cache.
pub type CachedSliceCredentialBook<'a, M, const N0: usize, const N1: usize> =
    CredentialBookFromSources<
        CachedSliceCredentialSource<'a, V0, M, N0>,
        CachedSliceCredentialSource<'a, V1, M, N1>,
    >;

#[cfg(feature = "alloc")]
/// A credential-book which owns all of its (non-matched) credential data,
/// and maintains pre-calculated cryptographic information about all
/// stored credentials for speedy advertisement deserialization.
///
/// Use this credential book if memory usage is not terribly tight,
/// and you're operating in an environment with an allocator.
pub type PrecalculatedOwnedCredentialBook<M> = CredentialBookFromSources<
    PrecalculatedOwnedCredentialSource<V0, M>,
    PrecalculatedOwnedCredentialSource<V1, M>,
>;
