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

    #include "structural_svm_graph_labeling_problem_abstract.h"
    #include "../graph_cuts.h"
    #include "../matrix.h"
    #include "../array.h"
    #include <vector>
    #include <iterator>
    #include "structural_svm_problem_threaded.h"
    #include "../graph.h"
    #include "sparse_vector.h"
    #include <sstream>

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

    namespace dlib
    {

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

        template <
            typename graph_type
            >
        bool is_graph_labeling_problem (
            const dlib::array<graph_type>& samples,
            const std::vector<std::vector<bool> >& labels,
            std::string& reason_for_failure
        )
        {
            typedef typename graph_type::type node_vector_type;
            typedef typename graph_type::edge_type edge_vector_type;
            // The graph must use all dense vectors or all sparse vectors.  It can't mix the two types together.
            COMPILE_TIME_ASSERT( (is_matrix<node_vector_type>::value && is_matrix<edge_vector_type>::value) ||
                                (!is_matrix<node_vector_type>::value && !is_matrix<edge_vector_type>::value));

            std::ostringstream sout;
            reason_for_failure.clear();

            if (!is_learning_problem(samples, labels))
            {
                reason_for_failure = "is_learning_problem(samples, labels) returned false.";
                return false;
            }

            const bool ismat = is_matrix<typename graph_type::type>::value;

            // these are -1 until assigned with a value
            long node_dims = -1;
            long edge_dims = -1;

            for (unsigned long i = 0; i < samples.size(); ++i)
            {
                if (samples[i].number_of_nodes() != labels[i].size())
                {
                    sout << "samples["<<i<<"].number_of_nodes() doesn't match labels["<<i<<"].size().";
                    reason_for_failure = sout.str();
                    return false;
                }
                if (graph_contains_length_one_cycle(samples[i]))
                {
                    sout << "graph_contains_length_one_cycle(samples["<<i<<"]) returned true.";
                    reason_for_failure = sout.str();
                    return false;
                }

                for (unsigned long j = 0; j < samples[i].number_of_nodes(); ++j)
                {
                    if (ismat && samples[i].node(j).data.size() == 0)
                    {
                        sout << "A graph contains an empty vector at node: samples["<<i<<"].node("<<j<<").data.";
                        reason_for_failure = sout.str();
                        return false;
                    }

                    if (ismat && node_dims == -1)
                        node_dims = samples[i].node(j).data.size();
                    // all nodes must have vectors of the same size.
                    if (ismat && (long)samples[i].node(j).data.size() != node_dims)
                    {
                        sout << "Not all node vectors in samples["<<i<<"] are the same dimension.";
                        reason_for_failure = sout.str();
                        return false;
                    }

                    for (unsigned long n = 0; n < samples[i].node(j).number_of_neighbors(); ++n)
                    {
                        if (ismat && samples[i].node(j).edge(n).size() == 0)
                        {
                            sout << "A graph contains an empty vector at edge: samples["<<i<<"].node("<<j<<").edge("<<n<<").";
                            reason_for_failure = sout.str();
                            return false;
                        }
                        if (min(samples[i].node(j).edge(n)) < 0)
                        {
                            sout << "A graph contains negative values on an edge vector at: samples["<<i<<"].node("<<j<<").edge("<<n<<").";
                            reason_for_failure = sout.str();
                            return false;
                        }

                        if (ismat && edge_dims == -1)
                            edge_dims = samples[i].node(j).edge(n).size();
                        // all edges must have vectors of the same size.
                        if (ismat && (long)samples[i].node(j).edge(n).size() != edge_dims)
                        {
                            sout << "Not all edge vectors in samples["<<i<<"] are the same dimension.";
                            reason_for_failure = sout.str();
                            return false;
                        }
                    }
                }
            }

            return true;
        }

        template <
            typename graph_type
            >
        bool is_graph_labeling_problem (
            const dlib::array<graph_type>& samples,
            const std::vector<std::vector<bool> >& labels
        )
        {
            std::string reason_for_failure;
            return is_graph_labeling_problem(samples, labels, reason_for_failure);
        }

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

        template <
            typename T,
            typename U
            >
        bool sizes_match (
            const std::vector<std::vector<T> >& lhs,
            const std::vector<std::vector<U> >& rhs
        )
        {
            if (lhs.size() != rhs.size())
                return false;

            for (unsigned long i = 0; i < lhs.size(); ++i)
            {
                if (lhs[i].size() != rhs[i].size())
                    return false;
            }

            return true;
        }

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

        inline bool all_values_are_nonnegative (
            const std::vector<std::vector<double> >& x
        )
        {
            for (unsigned long i = 0; i < x.size(); ++i)
            {
                for (unsigned long j = 0; j < x[i].size(); ++j)
                {
                    if (x[i][j] < 0)
                        return false;
                }
            }
            return true;
        }

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

        namespace impl
        {
            template <
                typename T,
                typename enable = void
                >
            struct fvect
            {
                // In this case type should be some sparse vector type
                typedef typename T::type type;
            };

            template < typename T >
            struct fvect<T, typename enable_if<is_matrix<typename T::type> >::type>
            {
                // The point of this stuff is to create the proper matrix
                // type to represent the concatenation of an edge vector
                // with an node vector.
                typedef typename T::type      node_mat;
                typedef typename T::edge_type edge_mat;
                const static long NRd = node_mat::NR;
                const static long NRe = edge_mat::NR;
                const static long NR = ((NRd!=0) && (NRe!=0)) ? (NRd+NRe) : 0;
                typedef typename node_mat::value_type value_type;

                typedef matrix<value_type,NR,1, typename node_mat::mem_manager_type, typename node_mat::layout_type> type;
            };
        }

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

        template <
            typename graph_type
            >
        class structural_svm_graph_labeling_problem : noncopyable,
            public structural_svm_problem_threaded<matrix<double,0,1>,
                                                typename dlib::impl::fvect<graph_type>::type >
        {
        public:
            typedef matrix<double,0,1> matrix_type;
            typedef typename dlib::impl::fvect<graph_type>::type feature_vector_type;

            typedef graph_type sample_type;

            typedef std::vector<bool> label_type;

            structural_svm_graph_labeling_problem(
                const dlib::array<sample_type>& samples_,
                const std::vector<label_type>& labels_,
                const std::vector<std::vector<double> >& losses_,
                unsigned long num_threads = 2
            ) :
                structural_svm_problem_threaded<matrix_type,feature_vector_type>(num_threads),
                samples(samples_),
                labels(labels_),
                losses(losses_)
            {
                // make sure requires clause is not broken
    #ifdef ENABLE_ASSERTS
                std::string reason_for_failure;
                DLIB_ASSERT(is_graph_labeling_problem(samples, labels, reason_for_failure) == true ,
                        "\t structural_svm_graph_labeling_problem::structural_svm_graph_labeling_problem()"
                        << "\n\t Invalid inputs were given to this function."
                        << "\n\t reason_for_failure: " << reason_for_failure
                        << "\n\t samples.size(): " << samples.size()
                        << "\n\t labels.size():  " << labels.size()
                        << "\n\t this: " << this );
                DLIB_ASSERT((losses.size() == 0 || sizes_match(labels, losses) == true) &&
                            all_values_are_nonnegative(losses) == true,
                        "\t structural_svm_graph_labeling_problem::structural_svm_graph_labeling_problem()"
                        << "\n\t Invalid inputs were given to this function."
                        << "\n\t labels.size():  " << labels.size()
                        << "\n\t losses.size():  " << losses.size()
                        << "\n\t sizes_match(labels,losses): " << sizes_match(labels,losses)
                        << "\n\t all_values_are_nonnegative(losses): " << all_values_are_nonnegative(losses)
                        << "\n\t this: " << this );
    #endif

                loss_pos = 1.0;
                loss_neg = 1.0;

                // figure out how many dimensions are in the node and edge vectors.
                node_dims = 0;
                edge_dims = 0;
                for (unsigned long i = 0; i < samples.size(); ++i)
                {
                    for (unsigned long j = 0; j < samples[i].number_of_nodes(); ++j)
                    {
                        node_dims = std::max(node_dims,(long)max_index_plus_one(samples[i].node(j).data));
                        for (unsigned long n = 0; n < samples[i].node(j).number_of_neighbors(); ++n)
                        {
                            edge_dims = std::max(edge_dims, (long)max_index_plus_one(samples[i].node(j).edge(n)));
                        }
                    }
                }
            }

            const std::vector<std::vector<double> >& get_losses (
            ) const { return losses; }

            long get_num_edge_weights (
            ) const
            {
                return edge_dims;
            }

            void set_loss_on_positive_class (
                double loss
            )
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(loss >= 0 && get_losses().size() == 0,
                        "\t void structural_svm_graph_labeling_problem::set_loss_on_positive_class()"
                        << "\n\t Invalid inputs were given to this function."
                        << "\n\t loss: " << loss
                        << "\n\t this: " << this );

                loss_pos = loss;
            }

            void set_loss_on_negative_class (
                double loss
            )
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(loss >= 0 && get_losses().size() == 0,
                        "\t void structural_svm_graph_labeling_problem::set_loss_on_negative_class()"
                        << "\n\t Invalid inputs were given to this function."
                        << "\n\t loss: " << loss
                        << "\n\t this: " << this );

                loss_neg = loss;
            }

            double get_loss_on_negative_class (
            ) const
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(get_losses().size() == 0,
                        "\t double structural_svm_graph_labeling_problem::get_loss_on_negative_class()"
                        << "\n\t Invalid inputs were given to this function."
                        << "\n\t this: " << this );

                return loss_neg;
            }

            double get_loss_on_positive_class (
            ) const
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(get_losses().size() == 0,
                        "\t double structural_svm_graph_labeling_problem::get_loss_on_positive_class()"
                        << "\n\t Invalid inputs were given to this function."
                        << "\n\t this: " << this );

                return loss_pos;
            }

        private:
            virtual long get_num_dimensions (
            ) const
            {
                // The psi/w vector will begin with all the edge dims and then follow with the node dims.
                return edge_dims + node_dims;
            }

            virtual long get_num_samples (
            ) const
            {
                return samples.size();
            }

            template <typename psi_type>
            typename enable_if<is_matrix<psi_type> >::type get_joint_feature_vector (
                const sample_type& sample,
                const label_type& label,
                psi_type& psi
            ) const
            {
                psi.set_size(get_num_dimensions());
                psi = 0;
                for (unsigned long i = 0; i < sample.number_of_nodes(); ++i)
                {
                    // accumulate the node vectors
                    if (label[i] == true)
                        set_rowm(psi, range(edge_dims, psi.size()-1)) += sample.node(i).data;

                    for (unsigned long n = 0; n < sample.node(i).number_of_neighbors(); ++n)
                    {
                        const unsigned long j = sample.node(i).neighbor(n).index();

                        // Don't double count edges.  Also only include the vector if
                        // the labels disagree.
                        if (i < j && label[i] != label[j])
                        {
                            set_rowm(psi, range(0, edge_dims-1)) -= sample.node(i).edge(n);
                        }
                    }
                }
            }

            template <typename T>
            void add_to_sparse_vect (
                T& psi,
                const T& vect,
                unsigned long offset
            ) const
            {
                for (typename T::const_iterator i = vect.begin(); i != vect.end(); ++i)
                {
                    psi.insert(psi.end(), std::make_pair(i->first+offset, i->second));
                }
            }

            template <typename T>
            void subtract_from_sparse_vect (
                T& psi,
                const T& vect
            ) const
            {
                for (typename T::const_iterator i = vect.begin(); i != vect.end(); ++i)
                {
                    psi.insert(psi.end(), std::make_pair(i->first, -i->second));
                }
            }

            template <typename psi_type>
            typename disable_if<is_matrix<psi_type> >::type get_joint_feature_vector (
                const sample_type& sample,
                const label_type& label,
                psi_type& psi
            ) const
            {
                psi.clear();
                for (unsigned long i = 0; i < sample.number_of_nodes(); ++i)
                {
                    // accumulate the node vectors
                    if (label[i] == true)
                        add_to_sparse_vect(psi, sample.node(i).data, edge_dims);

                    for (unsigned long n = 0; n < sample.node(i).number_of_neighbors(); ++n)
                    {
                        const unsigned long j = sample.node(i).neighbor(n).index();

                        // Don't double count edges.  Also only include the vector if
                        // the labels disagree.
                        if (i < j && label[i] != label[j])
                        {
                            subtract_from_sparse_vect(psi, sample.node(i).edge(n));
                        }
                    }
                }
            }

            virtual void get_truth_joint_feature_vector (
                long idx,
                feature_vector_type& psi
            ) const
            {
                get_joint_feature_vector(samples[idx], labels[idx], psi);
            }

            virtual void separation_oracle (
                const long idx,
                const matrix_type& current_solution,
                double& loss,
                feature_vector_type& psi
            ) const
            {
                const sample_type& samp = samples[idx];

                // setup the potts graph based on samples[idx] and current_solution.
                graph<double,double>::kernel_1a g;
                copy_graph_structure(samp, g);
                for (unsigned long i = 0; i < g.number_of_nodes(); ++i)
                {
                    g.node(i).data = dot(rowm(current_solution,range(edge_dims,current_solution.size()-1)),
                                        samp.node(i).data);

                    // Include a loss augmentation so that we will get the proper loss augmented
                    // max when we use find_max_factor_graph_potts() below.
                    if (labels[idx][i])
                        g.node(i).data -= get_loss_for_sample(idx,i,!labels[idx][i]);
                    else
                        g.node(i).data += get_loss_for_sample(idx,i,!labels[idx][i]);

                    for (unsigned long n = 0; n < g.node(i).number_of_neighbors(); ++n)
                    {
                        const unsigned long j = g.node(i).neighbor(n).index();
                        // Don't compute an edge weight more than once.
                        if (i < j)
                        {
                            g.node(i).edge(n) = dot(rowm(current_solution,range(0,edge_dims-1)),
                                                    samp.node(i).edge(n));
                        }
                    }

                }

                std::vector<node_label> labeling;
                find_max_factor_graph_potts(g, labeling);

                std::vector<bool> bool_labeling;
                bool_labeling.reserve(labeling.size());
                // figure out the loss
                loss = 0;
                for (unsigned long i = 0; i < labeling.size(); ++i)
                {
                    const bool predicted_label = (labeling[i]!= 0);
                    bool_labeling.push_back(predicted_label);
                    loss += get_loss_for_sample(idx, i, predicted_label);
                }

                // compute psi
                get_joint_feature_vector(samp, bool_labeling, psi);
            }

            double get_loss_for_sample (
                long sample_idx,
                long node_idx,
                bool predicted_label
            ) const
            /*!
                requires
                    - 0 <= sample_idx < labels.size()
                    - 0 <= node_idx < labels[sample_idx].size()
                ensures
                    - returns the loss incurred for predicting that the node
                      samples[sample_idx].node(node_idx) has a label of predicted_label.
            !*/
            {
                    const bool true_label = labels[sample_idx][node_idx];
                    if (true_label != predicted_label)
                    {
                        if (losses.size() != 0)
                            return losses[sample_idx][node_idx];
                        else if (true_label == true)
                            return loss_pos;
                        else
                            return loss_neg;
                    }
                    else
                    {
                        // no loss for making the correct prediction.
                        return 0;
                    }
            }

            const dlib::array<sample_type>& samples;
            const std::vector<label_type>& labels;
            const std::vector<std::vector<double> >& losses;

            long node_dims;
            long edge_dims;
            double loss_pos;
            double loss_neg;
        };

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

    }

    #endif // DLIB_STRUCTURAL_SVM_GRAPH_LAbELING_PROBLEM_Hh_