/* This software is distributed under BSD 3-clause license (see LICENSE file).
 *
 * Copyright (c) 2012-2013 Sergey Lisitsyn
 */
#pragma once

/* Tapkee includes */
#include <tapkee/defines.hpp>
#include <tapkee/utils/time.hpp>
/* End of Tapkee includes */

namespace tapkee
{
namespace tapkee_internal
{

//! Computes laplacian of neighborhood graph.
//!
//! Follows the algorithm described below:
//! <ul>
//! <li> For each vector compute gaussian exp of distances to its neighbor vectors and
//!      put it to sparse matrix \f$ L_{i,N_i(j)} = \exp\left( - \frac{d(x_i,x_{N_i(j)})^2}{w} \right) \f$.
//! <li> Symmetrize matrix \f$ L \f$ with \f$ L_{i,j} = \max (L_{i,j}, L_{j,i}) \f$ to
//!      make neighborhood relationship symmetric.
//! <li> Compute sum vector \f$ D = \sum_{i} L_{i,j} \f$.
//! <li> Modify \f$ L = D - L \f$.
//! <li> Output matrix sparse matrix \f$ L \f$ and diagonal matrix built of vector \f$ D \f$.
//! </ul>
//!
//! @param begin begin data iterator
//! @param end end data iterator
//! @param neighbors neighbors of each vector
//! @param callback distance callback
//! @param width width \f$ w \f$ of the gaussian kernel
//!
template <class RandomAccessIterator, class DistanceCallback>
Laplacian compute_laplacian(RandomAccessIterator begin, RandomAccessIterator end, const Neighbors& neighbors,
                            DistanceCallback callback, ScalarType width)
{
    SparseTriplets sparse_triplets;

    timed_context context("Laplacian computation");
    const IndexType k = neighbors[0].size();
    sparse_triplets.reserve((k + 1) * (end - begin));

    DenseVector D = DenseVector::Zero(end - begin);
    for (RandomAccessIterator iter = begin; iter != end; ++iter)
    {
        const LocalNeighbors& current_neighbors = neighbors[iter - begin];

        for (IndexType i = 0; i < k; ++i)
        {
            ScalarType distance = callback.distance(*iter, begin[current_neighbors[i]]);
            ScalarType heat = exp(-distance * distance / width);
            D(iter - begin) += heat;
            D(current_neighbors[i]) += heat;
            sparse_triplets.push_back(SparseTriplet(current_neighbors[i], (iter - begin), -heat));
            sparse_triplets.push_back(SparseTriplet((iter - begin), current_neighbors[i], -heat));
        }
    }
    for (IndexType i = 0; i < static_cast<IndexType>(end - begin); ++i)
        sparse_triplets.push_back(SparseTriplet(i, i, D(i)));

    SparseWeightMatrix weight_matrix(end - begin, end - begin);
    weight_matrix.setFromTriplets(sparse_triplets.begin(), sparse_triplets.end());

    return Laplacian(weight_matrix, DenseDiagonalMatrix(D));
}

template <class RandomAccessIterator, class FeatureVectorCallback>
DenseSymmetricMatrixPair construct_locality_preserving_eigenproblem(SparseWeightMatrix& L, DenseDiagonalMatrix& D,
                                                                    RandomAccessIterator begin,
                                                                    RandomAccessIterator end,
                                                                    FeatureVectorCallback feature_vector_callback,
                                                                    IndexType dimension)
{
    timed_context context("Constructing LPP eigenproblem");

    DenseSymmetricMatrix lhs = DenseSymmetricMatrix::Zero(dimension, dimension);
    DenseSymmetricMatrix rhs = DenseSymmetricMatrix::Zero(dimension, dimension);

    DenseVector rank_update_vector_i(dimension);
    DenseVector rank_update_vector_j(dimension);
    for (RandomAccessIterator iter = begin; iter != end; ++iter)
    {
        feature_vector_callback.vector(*iter, rank_update_vector_i);
        rhs.selfadjointView<Eigen::Upper>().rankUpdate(rank_update_vector_i, D.diagonal()(iter - begin));
    }

    for (int i = 0; i < L.outerSize(); ++i)
    {
        for (SparseWeightMatrix::InnerIterator it(L, i); it; ++it)
        {
            feature_vector_callback.vector(begin[it.row()], rank_update_vector_i);
            feature_vector_callback.vector(begin[it.col()], rank_update_vector_j);
            lhs.selfadjointView<Eigen::Upper>().rankUpdate(rank_update_vector_i, rank_update_vector_j, it.value());
        }
    }

    return DenseSymmetricMatrixPair(lhs, rhs);
}

} // namespace tapkee_internal
} // namespace tapkee
