/*
 boost/numeric/odeint/stepper/detail/controlled_adams_bashforth_moulton.hpp

 [begin_description]
 Implemetation of an controlled adams bashforth moulton stepper.
 [end_description]

 Copyright 2017 Valentin Noah Hartmann

 Distributed under the Boost Software License, Version 1.0.
 (See accompanying file LICENSE_1_0.txt or
 copy at http://www.boost.org/LICENSE_1_0.txt)
*/

#ifndef BOOST_NUMERIC_ODEINT_STEPPER_CONTROLLED_ADAMS_BASHFORTH_MOULTON_HPP_INCLUDED
#define BOOST_NUMERIC_ODEINT_STEPPER_CONTROLLED_ADAMS_BASHFORTH_MOULTON_HPP_INCLUDED

#include <boost/numeric/odeint/stepper/stepper_categories.hpp>
#include <boost/numeric/odeint/stepper/controlled_step_result.hpp>

#include <boost/numeric/odeint/stepper/adaptive_adams_bashforth_moulton.hpp>
#include <boost/numeric/odeint/stepper/detail/pid_step_adjuster.hpp>

#include <boost/numeric/odeint/util/unwrap_reference.hpp>
#include <boost/numeric/odeint/util/is_resizeable.hpp>
#include <boost/numeric/odeint/util/resizer.hpp>

#include <boost/numeric/odeint/util/copy.hpp>
#include <boost/numeric/odeint/util/bind.hpp>

#include <iostream>

namespace boost {
namespace numeric {
namespace odeint {

template<
size_t MaxOrder,
class State,
class Value = double,
class Algebra = typename algebra_dispatcher< State >::algebra_type
>
class default_order_adjuster
{
public:
    typedef State state_type;
    typedef Value value_type;
    typedef state_wrapper< state_type > wrapped_state_type;

    typedef Algebra algebra_type;

    default_order_adjuster( const algebra_type &algebra = algebra_type() )
    : m_algebra( algebra )
    {};

    size_t adjust_order(size_t order, size_t init, std::array<wrapped_state_type, 4> &xerr)
    {
        using std::abs;

        value_type errc = abs(m_algebra.norm_inf(xerr[2].m_v));

        value_type errm1 = 3*errc;
        value_type errm2 = 3*errc;

        if(order > 2)
        {
            errm2 = abs(m_algebra.norm_inf(xerr[0].m_v));
        }
        if(order >= 2)
        {
            errm1 = abs(m_algebra.norm_inf(xerr[1].m_v));
        }

        size_t o_new = order;

        if(order == 2 && errm1 <= 0.5*errc)
        {
            o_new = order - 1;
        }
        else if(order > 2 && errm2 < errc && errm1 < errc)
        {
            o_new = order - 1;
        }

        if(init < order)
        {
            return order+1;
        }
        else if(o_new == order - 1)
        {
            return order-1;
        }
        else if(order <= MaxOrder)
        {
            value_type errp = abs(m_algebra.norm_inf(xerr[3].m_v));

            if(order > 1 && errm1 < errc && errp)
            {
                return order-1;
            }
            else if(order < MaxOrder && errp < (0.5-0.25*order/MaxOrder) * errc)
            {
                return order+1;
            }
        }

        return order;
    };
private:
    algebra_type m_algebra;
};

template<
class ErrorStepper,
class StepAdjuster = detail::pid_step_adjuster< typename ErrorStepper::state_type, 
    typename ErrorStepper::value_type,
    typename ErrorStepper::deriv_type,
    typename ErrorStepper::time_type,
    typename ErrorStepper::algebra_type,
    typename ErrorStepper::operations_type,
    detail::H211PI
    >,
class OrderAdjuster = default_order_adjuster< ErrorStepper::order_value,
    typename ErrorStepper::state_type,
    typename ErrorStepper::value_type,
    typename ErrorStepper::algebra_type
    >,
class Resizer = initially_resizer
>
class controlled_adams_bashforth_moulton
{
public:
    typedef ErrorStepper stepper_type;

    static const typename stepper_type::order_type order_value = stepper_type::order_value;
    
    typedef typename stepper_type::state_type state_type;
    typedef typename stepper_type::value_type value_type;
    typedef typename stepper_type::deriv_type deriv_type;
    typedef typename stepper_type::time_type time_type;

    typedef typename stepper_type::algebra_type algebra_type;
    typedef typename stepper_type::operations_type operations_type;
    typedef Resizer resizer_type;

    typedef StepAdjuster step_adjuster_type;
    typedef OrderAdjuster order_adjuster_type;
    typedef controlled_stepper_tag stepper_category;

    typedef typename stepper_type::wrapped_state_type wrapped_state_type;
    typedef typename stepper_type::wrapped_deriv_type wrapped_deriv_type;
    typedef std::array< wrapped_state_type , 4 > error_storage_type;

    typedef typename stepper_type::coeff_type coeff_type;
    typedef controlled_adams_bashforth_moulton< ErrorStepper , StepAdjuster , OrderAdjuster , Resizer > controlled_stepper_type;

    controlled_adams_bashforth_moulton(step_adjuster_type step_adjuster = step_adjuster_type())
    :m_stepper(),
    m_dxdt_resizer(), m_xerr_resizer(), m_xnew_resizer(),
    m_step_adjuster( step_adjuster ), m_order_adjuster()
    {};

    template< class ExplicitStepper, class System >
    void initialize(ExplicitStepper stepper, System system, state_type &inOut, time_type &t, time_type dt)
    {
        m_stepper.initialize(stepper, system, inOut, t, dt);
    };

    template< class System >
    void initialize(System system, state_type &inOut, time_type &t, time_type dt)
    {
        m_stepper.initialize(system, inOut, t, dt);
    };

    template< class ExplicitStepper, class System >
    void initialize_controlled(ExplicitStepper stepper, System system, state_type &inOut, time_type &t, time_type &dt)
    {
        reset();
        coeff_type &coeff = m_stepper.coeff();

        m_dxdt_resizer.adjust_size(inOut, [this](auto&& arg) { return this->resize_dxdt_impl<state_type>(std::forward<decltype(arg)>(arg)); });

        controlled_step_result res = fail;

        for( size_t i=0 ; i<order_value; ++i )
        {
            do
            {
                res = stepper.try_step( system, inOut, t, dt );
            }
            while(res != success);

            system( inOut , m_dxdt.m_v , t );
            
            coeff.predict(t-dt, dt);
            coeff.do_step(m_dxdt.m_v);
            coeff.confirm();

            if(coeff.m_eo < order_value)
            {
                ++coeff.m_eo;
            }
        }
    }

    template< class System >
    controlled_step_result try_step(System system, state_type & inOut, time_type &t, time_type &dt)
    {
        m_xnew_resizer.adjust_size(inOut, [this](auto&& arg) { return this->resize_xnew_impl<state_type>(std::forward<decltype(arg)>(arg)); });

        controlled_step_result res = try_step(system, inOut, t, m_xnew.m_v, dt);

        if(res == success)
        {
            boost::numeric::odeint::copy( m_xnew.m_v , inOut);
        }

        return res;
    };

    template< class System >
    controlled_step_result try_step(System system, const state_type & in, time_type &t, state_type & out, time_type &dt)
    {
        m_xerr_resizer.adjust_size(in, [this](auto&& arg) { return this->resize_xerr_impl<state_type>(std::forward<decltype(arg)>(arg)); });
        m_dxdt_resizer.adjust_size(in, [this](auto&& arg) { return this->resize_dxdt_impl<state_type>(std::forward<decltype(arg)>(arg)); });

        m_stepper.do_step_impl(system, in, t, out, dt, m_xerr[2].m_v);

        coeff_type &coeff = m_stepper.coeff();

        time_type dtPrev = dt;
        dt = m_step_adjuster.adjust_stepsize(coeff.m_eo, dt, m_xerr[2].m_v, out, m_stepper.dxdt() );

        if(dt / dtPrev >= step_adjuster_type::threshold())
        {
            system(out, m_dxdt.m_v, t+dtPrev);

            coeff.do_step(m_dxdt.m_v);
            coeff.confirm();

            t += dtPrev;

            size_t eo = coeff.m_eo;

            // estimate errors for next step
            double factor = 1;
            algebra_type m_algebra;

            m_algebra.for_each2(m_xerr[2].m_v, coeff.phi[1][eo].m_v, 
                typename operations_type::template scale_sum1<double>(factor*dt*(coeff.gs[eo])));

            if(eo > 1)
            {
                m_algebra.for_each2(m_xerr[1].m_v, coeff.phi[1][eo-1].m_v, 
                    typename operations_type::template scale_sum1<double>(factor*dt*(coeff.gs[eo-1])));
            }
            if(eo > 2)
            {
                m_algebra.for_each2(m_xerr[0].m_v, coeff.phi[1][eo-2].m_v, 
                    typename operations_type::template scale_sum1<double>(factor*dt*(coeff.gs[eo-2])));
            }
            if(eo < order_value && coeff.m_eo < coeff.m_steps_init-1)
            {
                m_algebra.for_each2(m_xerr[3].m_v, coeff.phi[1][eo+1].m_v, 
                    typename operations_type::template scale_sum1<double>(factor*dt*(coeff.gs[eo+1])));
            }

            // adjust order
            coeff.m_eo = m_order_adjuster.adjust_order(coeff.m_eo, coeff.m_steps_init-1, m_xerr);

            return success;
        }
        else
        {
            return fail;
        }
    };

    void reset() { m_stepper.reset(); };

private:
    template< class StateType >
    bool resize_dxdt_impl( const StateType &x )
    {
        return adjust_size_by_resizeability( m_dxdt, x, typename is_resizeable<deriv_type>::type() );
    };
    template< class StateType >
    bool resize_xerr_impl( const StateType &x )
    {
        bool resized( false );

        for(size_t i=0; i<m_xerr.size(); ++i)
        {
            resized |= adjust_size_by_resizeability( m_xerr[i], x, typename is_resizeable<state_type>::type() );
        }
        return resized;
    };
    template< class StateType >
    bool resize_xnew_impl( const StateType &x )
    {
        return adjust_size_by_resizeability( m_xnew, x, typename is_resizeable<state_type>::type() );
    };

    stepper_type m_stepper;

    wrapped_deriv_type m_dxdt;
    error_storage_type m_xerr;
    wrapped_state_type m_xnew;

    resizer_type m_dxdt_resizer;
    resizer_type m_xerr_resizer;
    resizer_type m_xnew_resizer;

    step_adjuster_type m_step_adjuster;
    order_adjuster_type m_order_adjuster;
};

} // odeint
} // numeric
} // boost

#endif
