    // Copyright (C) 2010  Davis E. King (davis@dlib.net)
    // License: Boost Software License   See LICENSE.txt for the full license.
    #ifndef DLIB_LINEAR_MANIFOLD_ReGULARIZER_Hh_
    #define DLIB_LINEAR_MANIFOLD_ReGULARIZER_Hh_

    #include "linear_manifold_regularizer_abstract.h"
    #include <limits>
    #include <vector>
    #include "../serialize.h"
    #include "../matrix.h"

    namespace dlib
    {
        namespace impl
        {
            class undirected_adjacency_list
            {
                /*!
                    WHAT THIS OBJECT REPRESENTS
                        This object is simply a tool for turning a vector of sample_pair objects
                        into an adjacency list with floating point weights on each edge.
                !*/
            public:

                undirected_adjacency_list (
                )
                {
                    _size = 0;
                    sum_edge_weights = 0;
                }

                struct neighbor
                {
                    neighbor(unsigned long idx, double w):index(idx), weight(w) {}
                    neighbor():index(0), weight(0) {}

                    unsigned long index;
                    double weight;
                };

                typedef std::vector<neighbor>::const_iterator const_iterator;

                size_t size (
                ) const
                /*!
                    ensures
                        - returns the number of vertices in this graph
                !*/
                {
                    return _size;
                }

                const_iterator begin(
                    unsigned long idx
                ) const
                /*!
                    requires
                        - idx < size()
                    ensures
                        - returns an iterator that points to the first neighbor of
                          the idx'th vertex.
                !*/
                {
                    return blocks[idx];
                }

                const_iterator end(
                    unsigned long idx
                ) const
                /*!
                    requires
                        - idx < size()
                    ensures
                        - returns an iterator that points one past the last neighbor
                          of the idx'th vertex.
                !*/
                {
                    return blocks[idx+1];
                }

                template <typename vector_type, typename weight_function_type>
                void build (
                    const vector_type& edges,
                    const weight_function_type& weight_funct
                )
                /*!
                    requires
                        - vector_type == a type with an interface compatible with std::vector and
                          it must in turn contain objects with an interface compatible with dlib::sample_pair
                        - edges.size() > 0
                        - contains_duplicate_pairs(edges) == false
                        - weight_funct(edges[i]) must be a valid expression that evaluates to a
                          floating point number >= 0
                    ensures
                        - #size() == one greater than the max index in edges.
                        - builds the adjacency list so that it contains all the given edges.
                        - The weight in each neighbor is set to the output of the weight_funct()
                          for the associated edge.
                !*/
                {

                    // Figure out how many neighbors each sample ultimately has.  We do this so
                    // we will know how much space to allocate in the data vector.
                    std::vector<unsigned long> num_neighbors;
                    num_neighbors.reserve(edges.size());

                    for (unsigned long i = 0; i < edges.size(); ++i)
                    {
                        // make sure num_neighbors is always big enough
                        const unsigned long min_size = std::max(edges[i].index1(), edges[i].index2())+1;
                        if (num_neighbors.size() < min_size)
                            num_neighbors.resize(min_size,  0);

                        num_neighbors[edges[i].index1()] += 1;
                        num_neighbors[edges[i].index2()] += 1;
                    }

                    _size = num_neighbors.size();

                    // Now setup the iterators in blocks.  Also setup a version of blocks that holds
                    // non-const iterators so we can use it below when we populate data.
                    std::vector<std::vector<neighbor>::iterator> mutable_blocks;
                    data.resize(edges.size()*2); // each edge will show up twice
                    blocks.resize(_size + 1);
                    blocks[0] = data.begin();
                    mutable_blocks.resize(_size + 1);
                    mutable_blocks[0] = data.begin();
                    for (unsigned long i = 0; i < num_neighbors.size(); ++i)
                    {
                        blocks[i+1]         = blocks[i]         + num_neighbors[i];
                        mutable_blocks[i+1] = mutable_blocks[i] + num_neighbors[i];
                    }

                    sum_edge_weights = 0;
                    // finally, put the edges into data
                    for (unsigned long i = 0; i < edges.size(); ++i)
                    {
                        const double weight = weight_funct(edges[i]);
                        sum_edge_weights += weight;

                        // make sure requires clause is not broken
                        DLIB_ASSERT(weight >= 0,
                            "\t void linear_manifold_regularizer::build()"
                            << "\n\t You supplied a weight_funct() that generated a negative weight."
                            << "\n\t weight: " << weight
                            );

                        *mutable_blocks[edges[i].index1()]++ = neighbor(edges[i].index2(), weight);
                        *mutable_blocks[edges[i].index2()]++ = neighbor(edges[i].index1(), weight);
                    }

                }

                double sum_of_edge_weights (
                ) const
                {
                    return sum_edge_weights;
                }

            private:

                /*!
                    INITIAL VALUE
                        - _size == 0
                        - data.size() == 0
                        - blocks.size() == 0
                        - sum_edge_weights == 0

                    CONVENTION
                        - size() == _size
                        - blocks.size() == _size + 1
                        - sum_of_edge_weights() == sum_edge_weights
                        - blocks == a vector of iterators that point into data.
                          For all valid i:
                            - The iterator range [blocks[i], blocks[i+1]) contains all the edges
                              for the i'th node in the graph
                !*/

                std::vector<neighbor> data;
                std::vector<const_iterator> blocks;
                unsigned long _size;

                double sum_edge_weights;
            };

        }

    // ----------------------------------------------------------------------------------------

        template <
            typename matrix_type
            >
        class linear_manifold_regularizer
        {

        public:
            typedef typename matrix_type::mem_manager_type mem_manager_type;
            typedef typename matrix_type::type scalar_type;
            typedef typename matrix_type::layout_type layout_type;
            typedef matrix<scalar_type,0,0,mem_manager_type,layout_type> general_matrix;

            template <
                typename vector_type1,
                typename vector_type2,
                typename weight_function_type
                >
            void build (
                const vector_type1& samples,
                const vector_type2& edges,
                const weight_function_type& weight_funct
            )
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(edges.size() > 0 &&
                            contains_duplicate_pairs(edges) == false &&
                            max_index_plus_one(edges) <= samples.size(),
                    "\t void linear_manifold_regularizer::build()"
                    << "\n\t Invalid inputs were given to this function."
                    << "\n\t edges.size():                    " << edges.size()
                    << "\n\t samples.size():                  " << samples.size()
                    << "\n\t contains_duplicate_pairs(edges): " << contains_duplicate_pairs(edges)
                    << "\n\t max_index_plus_one(edges):       " << max_index_plus_one(edges)
                    );

                impl::undirected_adjacency_list graph;
                graph.build(edges, weight_funct);

                sum_edge_weights = graph.sum_of_edge_weights();

                make_mr_matrix(samples, graph);
            }

            long dimensionality (
            ) const { return reg_mat.nr(); }

            general_matrix get_transformation_matrix (
                scalar_type intrinsic_regularization_strength
            ) const
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(intrinsic_regularization_strength >= 0,
                    "\t matrix linear_manifold_regularizer::get_transformation_matrix()"
                    << "\n\t This value must not be negative"
                    << "\n\t intrinsic_regularization_strength: " << intrinsic_regularization_strength
                    );

                if (dimensionality() == 0)
                    return general_matrix();

                // This isn't how it's defined in the referenced paper but normalizing these kinds of
                // sums is typical of most machine learning algorithms.  Moreover, doing this makes
                // the argument to this function more invariant to the size of the edge set.  So it
                // should make it easier for the user.
                intrinsic_regularization_strength /= sum_edge_weights;

                return inv_lower_triangular(chol(identity_matrix<scalar_type>(reg_mat.nr()) + intrinsic_regularization_strength*reg_mat));
            }

        private:

            template <typename vector_type>
            void make_mr_matrix (
                const vector_type& samples,
                const impl::undirected_adjacency_list& graph
            )
            /*!
                requires
                    - samples.size() == graph.size()
                ensures
                    - computes trans(X)*lap(graph)*X where X is the data matrix
                      (i.e. the matrix that contains all the samples in its rows)
                      and lap(graph) is the laplacian matrix of the graph.  The
                      resulting matrix is stored in reg_mat.
            !*/
            {
                const unsigned long dims = samples[0].size();
                reg_mat.set_size(dims,dims);
                reg_mat = 0;

                typename impl::undirected_adjacency_list::const_iterator beg, end;

                // loop over the columns of the X matrix
                for (unsigned long d = 0; d < dims; ++d)
                {
                    // loop down the row of X
                    for (unsigned long i = 0; i < graph.size(); ++i)
                    {
                        beg = graph.begin(i);
                        end = graph.end(i);

                        // if this node in the graph has any neighbors
                        if (beg != end)
                        {
                            double weight_sum = 0;
                            double val = 0;
                            for (; beg != end; ++beg)
                            {
                                val -= beg->weight * samples[beg->index](d);
                                weight_sum += beg->weight;
                            }
                            val += weight_sum * samples[i](d);

                            for (unsigned long j = 0; j < dims; ++j)
                            {
                                reg_mat(d,j) += val*samples[i](j);
                            }
                        }
                    }
                }

            }

            general_matrix reg_mat;
            double sum_edge_weights;
        };

    }

    // ----------------------------------------------------------------------------------------

    #endif // DLIB_LINEAR_MANIFOLD_ReGULARIZER_Hh_