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


#include <string>
#include <sstream>

#include <dlib/control.h>
#include <dlib/optimization.h>
#include "tester.h"

namespace  
{
    using namespace test;
    using namespace std;
    using namespace dlib;
  
    logger dlog("test.mpc");

    template <
        typename EXP1,
        typename EXP2,
        typename T, long NR, long NC, typename MM, typename L
        >
    unsigned long solve_qp_box_using_smo ( 
        const matrix_exp<EXP1>& _Q,
        const matrix_exp<EXP2>& _b,
        matrix<T,NR,NC,MM,L>& alpha,
        matrix<T,NR,NC,MM,L>& lower,
        matrix<T,NR,NC,MM,L>& upper,
        T eps,
        unsigned long max_iter
    )
    /*!
        ensures
            - solves: 0.5*trans(x)*Q*x + trans(b)*x where x is box constrained.
    !*/
    {
        const_temp_matrix<EXP1> Q(_Q);
        const_temp_matrix<EXP2> b(_b);
        //cout << "IN QP SOLVER" << endl;
        //cout << "max eig: " << max(real_eigenvalues(Q)) << endl;
        //cout << "min eig: " << min(real_eigenvalues(Q)) << endl;
        //cout << "Q: \n" << Q << endl;
        //cout << "b: \n" << b << endl;

        // make sure requires clause is not broken
        DLIB_ASSERT(Q.nr() == Q.nc() &&
                     alpha.size() == lower.size() &&
                     alpha.size() == upper.size() &&
                     is_col_vector(b) &&
                     is_col_vector(alpha) &&
                     is_col_vector(lower) &&
                     is_col_vector(upper) &&
                     b.size() == alpha.size() &&
                     b.size() == Q.nr() &&
                     alpha.size() > 0 &&
                     0 <= min(alpha-lower) &&
                     0 <= max(upper-alpha) &&
                     eps > 0 &&
                     max_iter > 0,
                     "\t unsigned long solve_qp_box_using_smo()"
                     << "\n\t Invalid arguments were given to this function"
                     << "\n\t Q.nr():               " << Q.nr()
                     << "\n\t Q.nc():               " << Q.nc()
                     << "\n\t is_col_vector(b):     " << is_col_vector(b)
                     << "\n\t is_col_vector(alpha): " << is_col_vector(alpha)
                     << "\n\t is_col_vector(lower): " << is_col_vector(lower)
                     << "\n\t is_col_vector(upper): " << is_col_vector(upper)
                     << "\n\t b.size():             " << b.size() 
                     << "\n\t alpha.size():         " << alpha.size() 
                     << "\n\t lower.size():         " << lower.size() 
                     << "\n\t upper.size():         " << upper.size() 
                     << "\n\t Q.nr():               " << Q.nr() 
                     << "\n\t min(alpha-lower):     " << min(alpha-lower) 
                     << "\n\t max(upper-alpha):     " << max(upper-alpha) 
                     << "\n\t eps:                  " << eps 
                     << "\n\t max_iter:             " << max_iter 
        );


        // Compute f'(alpha) (i.e. the gradient of f(alpha)) for the current alpha.  
        matrix<T,NR,NC,MM,L> df = Q*alpha + b;
        matrix<T,NR,NC,MM,L> QQ = reciprocal_max(diag(Q));


        unsigned long iter = 0;
        for (; iter < max_iter; ++iter)
        {
            T max_df = 0;
            long best_r =0;
            for (long r = 0; r < Q.nr(); ++r)
            {
                if (alpha(r) <= lower(r) && df(r) > 0)
                    ;//alpha(r) = lower(r);
                else if (alpha(r) >= upper(r) && df(r) < 0)
                    ;//alpha(r) = upper(r);
                else if (std::abs(df(r)) > max_df)
                {
                    best_r = r;
                    max_df = std::abs(df(r));
                }
            }

            //for (long r = 0; r < Q.nr(); ++r)
            long r = best_r;
            {

                const T old_alpha = alpha(r);
                alpha(r) = -(df(r)-Q(r,r)*alpha(r))*QQ(r);
                if (alpha(r) < lower(r))
                    alpha(r) = lower(r);
                else if (alpha(r) > upper(r))
                    alpha(r) = upper(r);

                const T delta = old_alpha-alpha(r);

                // Now update the gradient. We will perform the equivalent of: df = Q*alpha + b;
                for(long k = 0; k < df.nr(); ++k)
                    df(k) -= Q(r,k)*delta;
            }

            if (max_df < eps)
                break;
        }
        //cout << "df: \n" << trans(df) << endl;
        //cout << "objective value: " << 0.5*trans(alpha)*Q*alpha + trans(b)*alpha << endl;

        return iter+1;
    }

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

    namespace impl_mpc
    {
        template <long N>
        void pack(
            matrix<double,0,1>& out,
            const std::vector<matrix<double,N,1> >& item
        )
        {
            DLIB_CASSERT(item.size() != 0,"");
            out.set_size(item.size()*item[0].size());
            long j = 0;
            for (unsigned long i = 0; i < item.size(); ++i)
                for (long r = 0; r < item[i].size(); ++r)
                    out(j++) = item[i](r);
        }

        template <long N>
        void pack(
            matrix<double,0,1>& out,
            const matrix<double,N,1>& item,
            const long num
        )
        {
            out.set_size(item.size()*num);
            long j = 0;
            for (long r = 0; r < num; ++r)
                for (long i = 0; i < item.size(); ++i)
                    out(j++) = item(i);
        }

        template <long N>
        void unpack(
            std::vector<matrix<double,N,1> >& out,
            const matrix<double,0,1>& item 
        )
        {
            DLIB_CASSERT(out.size() != 0,"");
            DLIB_CASSERT((long)out.size()*out[0].size() == item.size(),"");
            long j = 0;
            for (unsigned long i = 0; i < out.size(); ++i)
                for (long r = 0; r < out[i].size(); ++r)
                    out[i](r) = item(j++);
        }
    }

    template <long S, long I>
    unsigned long solve_linear_mpc (
        const matrix<double,S,S>& A,
        const matrix<double,S,I>& B,
        const matrix<double,S,1>& C,
        const matrix<double,S,1>& Q,
        const matrix<double,I,1>& R,
        const matrix<double,I,1>& _lower,
        const matrix<double,I,1>& _upper,
        const std::vector<matrix<double,S,1> >& target, 
        const matrix<double,S,1>& initial_state,
        std::vector<matrix<double,I,1> >& controls // input and output
    )
    {
        using namespace impl_mpc;
        DLIB_CASSERT(target.size() == controls.size(),"");

        matrix<double> K(B.nr()*controls.size(), B.nc()*controls.size());
        matrix<double,0,1> M(B.nr()*controls.size());

        // compute powers of A: Apow[i] == A^i
        std::vector<matrix<double,S,S> > Apow(controls.size());
        Apow[0] = identity_matrix(A);
        for (unsigned long i = 1; i < Apow.size(); ++i)
            Apow[i] = A*Apow[i-1];

        // fill in K
        K = 0;
        for (unsigned long r = 0; r < controls.size(); ++r)
            for (unsigned long c = 0; c <= r; ++c)
                set_subm(K,r*B.nr(),c*B.nc(), B.nr(), B.nc()) = Apow[r-c]*B;

        // fill in M
        set_subm(M,0*A.nr(),0,A.nr(),1) = A*initial_state + C;
        for (unsigned long i = 1; i < controls.size(); ++i)
            set_subm(M,i*A.nr(),0,A.nr(),1) = A*subm(M,(i-1)*A.nr(),0,A.nr(),1) + C;

        //cout << "M: \n" << M << endl;
        //cout << "K: \n" << K << endl;

        matrix<double,0,1> t, v, lower, upper;
        pack(t, target);
        pack(v, controls);
        pack(lower, _lower, controls.size());
        pack(upper, _upper, controls.size());


        matrix<double> QQ(K.nr(),K.nr()), RR(K.nc(),K.nc());
        QQ = 0;
        RR = 0;
        for (unsigned long c = 0; c < controls.size(); ++c)
        {
            set_subm(QQ,c*Q.nr(),c*Q.nr(),Q.nr(),Q.nr()) = diagm(Q);
            set_subm(RR,c*R.nr(),c*R.nr(),R.nr(),R.nr()) = diagm(R);
        }

        matrix<double> m1 = trans(K)*QQ*K+RR;
        matrix<double> m2 = trans(K)*QQ*(M-t);


        // run the solver...
        unsigned long iter;
        iter = solve_qp_box_using_smo(
            m1,
            m2,
            v,
            lower,
            upper,
            0.00000001,
            100000);

        //cout << "iterations: " << iter << endl;

        unpack(controls, v);
        return iter;
    }



    class test_mpc : public tester
    {
    public:
        test_mpc (
        ) :
            tester ("test_mpc",
                    "Runs tests on the mpc object.")
        {}

        void perform_test (
        )
        {
            // a basic position + velocity model
            matrix<double,2,2> A;
            A = 1, 1,
            0, 1;
            matrix<double,2,1> B, C;
            B = 0,
            1;

            C = 0.02,0.1; // no constant bias

            matrix<double,2,1> Q;
            Q = 2, 0; // only care about getting the position right
            matrix<double,1,1> R, lower, upper;
            R = 1;

            lower = -0.2;
            upper =  0.2;

            std::vector<matrix<double,1,1> > controls(30);
            std::vector<matrix<double,2,1> > target(30);
            for (unsigned long i = 0; i < controls.size(); ++i)
            {
                controls[i] = 0;
                target[i] = 0;
            }

            mpc<2,1,30> solver(A,B,C,Q,R,lower,upper);
            solver.set_epsilon(0.00000001);
            solver.set_max_iterations(10000);
            matrix<double,2,1> initial_state;
            initial_state = 0;
            initial_state(0) = 5;
            for (int i = 0; i < 30; ++i)
            {
                print_spinner();
                matrix<double,1,1> control = solver(initial_state);

                for (unsigned long i = 1; i < controls.size(); ++i)
                    controls[i-1] = controls[i];

                // Compute the correct control via SMO and make sure it matches.
                solve_linear_mpc(A,B,C,Q,R,lower,upper, target, initial_state, controls);
                dlog << LINFO << "ERROR: " << length(control-controls[0]);
                DLIB_TEST(length(control-controls[0]) < 1e-7);

                initial_state = A*initial_state + B*control + C;
                //cout << control(0) << "\t" << trans(initial_state);
            }

            {
                // also just generally test our QP solver.
                matrix<double,20,20> Q = gaussian_randm(20,20,5);
                Q = Q*trans(Q);

                matrix<double,20,1> b = randm(20,1)-0.5;
                matrix<double,20,1> alpha, lower, upper, alpha2;
                alpha = 0;
                alpha2 = 0;
                lower = -4;
                upper = 3;

                solve_qp_box_using_smo(Q,b,alpha,lower, upper, 1e-12, 500000);
                solve_qp_box_constrained(Q,b,alpha2,lower, upper, 1e-12, 50000);
                dlog << LINFO << trans(alpha);
                dlog << LINFO << trans(alpha2);
                dlog << LINFO << "objective value:  " << 0.5*trans(alpha)*Q*alpha + trans(b)*alpha;
                dlog << LINFO << "objective value2: " << 0.5*trans(alpha2)*Q*alpha + trans(b)*alpha2;
                DLIB_TEST_MSG(max(abs(alpha-alpha2)) < 1e-7, max(abs(alpha-alpha2)));
            }
        }
    } a;

}