/*
 * Copyright (c) 2019 Frank Fischer <frank-fischer@shadow-soft.de>
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see  <http://www.gnu.org/licenses/>
 */

//! Objects that can be combined linearly.

use super::Real;
use std::borrow::Borrow;

/// An aggregatable object.
pub trait Aggregatable: Default {
    /// Return a scaled version of `other`, i.e. `alpha * other`.
    fn new_scaled<A>(alpha: Real, other: A) -> Self
    where
        A: Borrow<Self>;

    /// Add a scaled version of `other` to `self`.
    ///
    /// This sets `self = self + alpha * other`.
    fn add_scaled<A>(&mut self, alpha: Real, other: A)
    where
        A: Borrow<Self>;

    /// Return $\sum\_{i=1}\^n alpha_i m_i$.
    ///
    /// If `aggregates` is empty return the default value.
    fn combine<I, A>(aggregates: I) -> Self
    where
        I: IntoIterator<Item = (Real, A)>,
        A: Borrow<Self>,
    {
        let mut it = aggregates.into_iter();
        let mut x;
        if let Some((alpha, y)) = it.next() {
            x = Self::new_scaled(alpha, y);
        } else {
            return Self::default();
        }

        for (alpha, y) in it {
            x.add_scaled(alpha, y);
        }

        x
    }
}

/// Implement for empty tuples.
impl Aggregatable for () {
    fn new_scaled<A>(_alpha: Real, _other: A) -> Self
    where
        A: Borrow<Self>,
    {
    }

    fn add_scaled<A>(&mut self, _alpha: Real, _other: A)
    where
        A: Borrow<Self>,
    {
    }
}

/// Implement for scalar values.
impl Aggregatable for Real {
    fn new_scaled<A>(alpha: Real, other: A) -> Self
    where
        A: Borrow<Self>,
    {
        alpha * other.borrow()
    }

    fn add_scaled<A>(&mut self, alpha: Real, other: A)
    where
        A: Borrow<Self>,
    {
        *self += alpha * other.borrow()
    }
}

/// Implement for vectors of aggregatable objects.
impl<T> Aggregatable for Vec<T>
where
    T: Aggregatable,
{
    fn new_scaled<A>(alpha: Real, other: A) -> Self
    where
        A: std::borrow::Borrow<Self>,
    {
        other
            .borrow()
            .iter()
            .map(|y| Aggregatable::new_scaled(alpha, y))
            .collect()
    }

    fn add_scaled<A>(&mut self, alpha: Real, other: A)
    where
        A: std::borrow::Borrow<Self>,
    {
        debug_assert_eq!(self.len(), other.borrow().len(), "Vectors must have the same size");
        for (ref mut x, y) in self.iter_mut().zip(other.borrow()) {
            x.add_scaled(alpha, y)
        }
    }
}
