// <ExpressionGrammar> -*- C++ -*-

#include <cmath>

#include "sparta/statistics/ExpressionGrammar.hpp"
#include "sparta/statistics/Expression.hpp"
#include "sparta/statistics/ExpressionNodeVariables.hpp"
#include "sparta/simulation/TreeNode.hpp"
#include "sparta/simulation/Clock.hpp"
#include "sparta/utils/SpartaException.hpp"
#include "sparta/simulation/TreeNodePrivateAttorney.hpp"
#include "sparta/trigger/ContextCounterTrigger.hpp"
#include "sparta/statistics/StatInstCalculator.hpp"
#include <boost/math/constants/constants.hpp>

namespace sparta {
    namespace statistics {
        namespace expression {
            namespace helpers {

//struct lazy_pow_
//{
//    template <typename X, typename E>
//    struct result {
//        typedef X type;
//    };
//
//    template <typename X, typename E>
//    X operator()(X x, E e) const {
//        return std::pow(x, e);
//    }
//}; // struct lazy_pow_

/*!
 * \brief Nullary functor adapter for boost phoneix to lazily evaluate function
 * accepting no arguments.
 * \note Always generates Expression instances
 */
struct lazy_nfunc_
{
    template<class> struct result;

    template<typename F, typename F1>
    struct result<F(F1)> { typedef Expression type; };

    template <typename F>
    Expression operator()(F f) const {
        return f();
    }
}; // struct lazy_nfunc_

/*!
 * \brief Unary functor adapter for boost phoneix to lazily evaluate function
 * accepting 1 argument
 */
struct lazy_ufunc_
{
    template<class> struct result;

    template<typename F, typename F1, typename A1>
    struct result<F(F1,A1)> { typedef A1& type; };

    template <typename F, typename A1>
    A1& operator()(F f, A1& a1) const {
        a1 = f(a1);
        return a1;
    }
}; // struct lazy_ufunc_

/*!
 * \brief Binary functor adapter for boost phoneix to lazily evaluate function
 * accepting 2 arguments
 */
struct lazy_bfunc_
{
    template<class> struct result;

    template<typename F, typename F1, typename A1, typename A2>
    struct result<F(F1,A1,A2)> { typedef A1& type; };

    template <typename F, typename A1, typename A2>
    A1& operator()(F f, A1& a1, A2& a2) const {
        a1 = f(a1, a2);
        return a1;
    }
}; // struct lazy_bfunc_

/*!
 * \brief Ternary functor adapter for boost phoneix to lazily evaluate function
 * accepting 3 arguments
 */
struct lazy_tfunc_
{
    template<class> struct result;

    template<typename F, typename F1, typename A1, typename A2, typename A3>
    struct result<F(F1,A1,A2,A3)> { typedef A1& type; };

    template <typename F, typename A1, typename A2, typename A3>
    A1& operator()(F f, A1& a1, A2& a2, A3& a3) const {
        a1 = f(a1, a2, a3);
        return a1;
    }
}; // struct lazy_tfunc_

/*!
 * \brief Functor class for generating Binary Functions based on C++ builtin
 * functions (e.g. pow)
 *
 * \example
 * \code
 * lazy_builtin_bfunc_ lbbf("pow", &std::pow<double>);
 * // Given: Expression a(2), b(3);
 * Expression c = lbbf(a,b);
 * assert(c.evaluate() == 8);
 * \endcode
 */
struct lazy_builtin_bfunc_
{
    typedef double(*fxn_t)(double, double);

    /*!
     * \brief Name of the function generated by an instance of this class
     */
    std::string name_;

    /*!
     * \brief Funciton pointer to binary function
     */
    const fxn_t fxn_;

    /*!
     * \brief Construct the lazy binary f unc expression generator
     * \param name Name given to the generated function node when invoked
     * \param fxn Function attached to the function node when invoked
     */
    lazy_builtin_bfunc_(const std::string& name, fxn_t fxn)
        : name_(name),
          fxn_(fxn)
    {
        sparta_assert(fxn_ != nullptr);
    }

    template<class> struct result;

    template<typename F, typename A1, typename A2>
    struct result<F(A1,A2)> { typedef A1& type; };

    /*!
     * \brief Invoke this lazy function to generate a new binary function
     * Expression
     */
    template <typename A1, typename A2>
    A1& operator()(A1& a1, A2& a2) const {
        a1 = Expression(name_, fxn_, a1, a2);
        return a1;
    }
}; // struct lazy_builtin_bfunc_

/*!
 * \brief Functor class for generating StatVariable expression items
 */
class lazy_gen_var_
{
    TreeNode* n_; //!< Context node
    std::vector<const TreeNode*>& used_; //!< Disallowed nodes

public:

    /*!
     * \brief Constructor
     * \param n Context node in which to search for children
     * \param used Children already used in a parent expression. These nodes
     * must be rejected as they would create cycles if encountered
     */
    lazy_gen_var_(TreeNode* n, std::vector<const TreeNode*>& used) :
        n_(n),
        used_(used)
    { }

    template <typename A1>
    struct result { typedef Expression type; };

    template <typename A1>
    Expression operator()(A1 a1) const
    {
        if(n_ == nullptr){
            // Must construct with non-null to actually use
            throw SpartaException("No context TreeNode specified for expression parsing. Cannot "
                                "resolve \"") << a1 << "\" to a simulation object because there is "
                                "no context";
        }

        const TreeNode* n = TreeNodePrivateAttorney::getChild(n_, a1);

        // Check for cycles
        if(std::find(used_.begin(), used_.end(), n) != used_.end()){
            SpartaException ex("Cycle detected in a sparta statistic expression: ");
            for(auto& un : used_){
                ex << un->getLocation() << " -> ";
            }
            ex << '[' << n->getLocation() << ']';
            throw ex;
        }

        std::shared_ptr<StatInstCalculator> calculator;
        if(n == nullptr) {
            calculator = trigger::ContextCounterTrigger::findRegisteredContextCounterAggregateFcn(
                n_->getRoot(), a1);
            if(calculator) {
                n = calculator->getNode();
            }
        }

        if(n == nullptr) {
            sparta_assert(calculator == nullptr);
            SpartaException ex("While parsing the expression or term of expression: '");
            ex << a1 << "', SPARTA was unable to find a tree node that matched this name.";
            throw ex;
        }

        used_.push_back(n); // Add to used list

        StatVariable * sv = nullptr;
        if(calculator == nullptr) {
            sv = new StatVariable(n, used_); // Throws if cannot convert
        } else {
            sv = new StatVariable(calculator, used_);
        }

        used_.pop_back(); // Remove the stat so it can be used higher up or by
                          // other sibling expressions in the expression

        return Expression(sv);
    }
}; // class lazy_gen_var_

} // namespace helpers

namespace functions {

/*!
 * \brief Binary function if statement. If x is nan or inf, returns y. otherwise returns x.
 */
static double ifnan(double x, double y)
{
    if(isnan(x) || isinf(x)){
        return y;
    }
    return x;
}

/*!
 * \brief Ternary function if statement. If cond is nonzero, returns opt_nonzero. Otherwise returns opt_zero
 */
static double if_function(double cond, double opt_nonzero, double opt_zero)
{
    if(cond != 0){
        return opt_nonzero;
    }
    return opt_zero;
}
            } // namespace functions

            namespace grammar {

ExpressionGrammar::constants_::constants_()
{
    // Constants
    this->add
        ("c_pi",            Expression(boost::math::constants::pi<double>()))
        ("c_root_pi",       Expression(boost::math::constants::root_pi<double>()))
        ("c_root_half_pi",  Expression(boost::math::constants::root_half_pi<double>()))
        ("c_root_two_pi",   Expression(boost::math::constants::root_two_pi<double>()))
        ("c_root_ln_four",  Expression(boost::math::constants::root_ln_four<double>()))
        ("c_e",             Expression(boost::math::constants::e<double>()))
        ("c_half",          Expression(boost::math::constants::half<double>()))
        ("c_euler",         Expression(boost::math::constants::euler<double>()))
        ("c_root_two",      Expression(boost::math::constants::root_two<double>()))
        ("c_ln_two",        Expression(boost::math::constants::ln_two<double>()))
        ("c_ln_ln_two",     Expression(boost::math::constants::ln_ln_two<double>()))
        ("c_third",         Expression(boost::math::constants::third<double>()))
        ("c_twothirds",     Expression(boost::math::constants::twothirds<double>()))
        ("c_pi_minus_three",Expression(boost::math::constants::pi_minus_three<double>()))
        ("c_four_minus_pi", Expression(boost::math::constants::four_minus_pi<double>()))
        ("c_nan",           Expression(NAN))
        ("c_inf",           Expression(INFINITY))
        ;
}

ExpressionGrammar::builtin_vars_::builtin_vars_(TreeNode* n,
                                                std::vector<const TreeNode*>& used)
{
    sparta_assert(nullptr != n,
                      "cannot construct ExpressionGrammar::builtin_vars_ with a null context");

    static auto get_clock_from_node = [](TreeNode* n) -> const Clock & {
        //If we are tied to a clock object directly, return it.
        auto clk = n->getClock();
        if (clk) {
            return *clk;
        }

        //If this node is not tied to a clock directly, throw immediately
        //(notice this method returns a Clock reference... returning null
        //is not valid by design)
        throw SpartaException("Unable to determine clock from the context: ")
            << n->getLocation();

        sparta_assert(!"Unreachable");
        static Clock clock("dummy");
        return clock;
    };

    // Global Variables
    this->add
        ("g_ticks" ,       [n, &used]() -> Expression {
            const Clock & clk = get_clock_from_node(n);
            auto scheduler = clk.getScheduler();
            sparta_assert(scheduler);
            return Expression(new StatVariable(&scheduler->getCurrentTicksROCounter(), used));
        })
        ("g_seconds",      [n, &used]() -> Expression {
            const Clock & clk = get_clock_from_node(n);
            auto scheduler = clk.getScheduler();
            sparta_assert(scheduler);
            return Expression(new StatVariable(&scheduler->getSecondsStatisticDef(), used));
        })
        ("g_milliseconds", [n, &used]() -> Expression {
            const Clock & clk = get_clock_from_node(n);
            auto scheduler = clk.getScheduler();
            sparta_assert(scheduler);
            return Expression(new StatVariable(&scheduler->getCurrentMillisecondsStatisticDef(), used));
        })
        ("g_microseconds", [n, &used]() -> Expression {
            const Clock & clk = get_clock_from_node(n);
            auto scheduler = clk.getScheduler();
            sparta_assert(scheduler);
            return Expression(new StatVariable(&scheduler->getCurrentMicrosecondsStatisticDef(), used));
        })
        ("g_nanoseconds",  [n, &used]() -> Expression {
            const Clock & clk = get_clock_from_node(n);
            auto scheduler = clk.getScheduler();
            sparta_assert(scheduler);
            return Expression(new StatVariable(&scheduler->getCurrentNanosecondsStatisticDef(), used));
        })
        ("g_picoseconds",  [n, &used]() -> Expression {
            const Clock & clk = get_clock_from_node(n);
            auto scheduler = clk.getScheduler();
            sparta_assert(scheduler);
            return Expression(new StatVariable(&scheduler->getCurrentPicosecondsROCounter(), used));
        })
        ;

    // Local Variables
    this->add
        ("cycles", [n, &used]() -> Expression {
            const Clock* clk = n->getClock();
            if(!clk){
                throw SpartaException("Unable to determine clock from the context: ")
                    << n->getLocation();
            }
            //! \todo Use this node instead of a statvariable ?
            return Expression(new StatVariable(&clk->getCyclesROCounter(), used));
        })
        ;
    this->add
        ("freq_mhz", [n]() -> Expression {
            const Clock* clk = n->getClock();
            if(!clk){
                throw SpartaException("Unable to determine clock from the context: ")
                    << n->getLocation();
            }
            return Expression(clk->getFrequencyMhz());
        })
        ;
}

ExpressionGrammar::variable_::variable_(sparta::TreeNode* n,
                                        std::vector<const TreeNode*>& used) :
    variable_::base_type(start)
{
    sparta_assert(nullptr != n,
                      "cannot construct ExpressionGrammar::variable_ with a null context");

    using qi::ascii::char_;
    using qi::_val;

    // Variable factory
    helpers::lazy_gen_var_ lgv(n, used);
    phoenix::function<helpers::lazy_gen_var_> lazy_gen_var(lgv);

    start = str [_val = lazy_gen_var(qi::_1)];

    // TreeNode names: All alphanumeric characters, and "_.[]"
    str %= +(char_("a-zA-Z0-9_\\.\\[\\]"));
}

ExpressionGrammar::ufunc_::ufunc_(std::vector<const TreeNode*>& used)
{
    // Math Utils
    this->add
        ("abs"  ,   [](Expression& a) -> Expression {return Expression("abs",     (fptr_dd_t)&std::fabs, a);}) // Behave like fabs
        ("fabs" ,   [](Expression& a) -> Expression {return Expression("fabs",    (fptr_dd_t)&std::fabs, a);})
        ("acos" ,   [](Expression& a) -> Expression {return Expression("acos",    (fptr_dd_t)&std::acos, a);})
        ("asin" ,   [](Expression& a) -> Expression {return Expression("asin",    (fptr_dd_t)&std::asin, a);})
        ("atan" ,   [](Expression& a) -> Expression {return Expression("atan",    (fptr_dd_t)&std::atan, a);})
        ("ceil" ,   [](Expression& a) -> Expression {return Expression("ceil",    (fptr_dd_t)&std::ceil, a);})
        ("trunc",   [](Expression& a) -> Expression {return Expression("trunc",   (fptr_dd_t)&std::trunc, a);})
        ("round",   [](Expression& a) -> Expression {return Expression("round",   (fptr_dd_t)&std::round, a);})
        ("cos"  ,   [](Expression& a) -> Expression {return Expression("cos",     (fptr_dd_t)&std::cos, a);})
        ("cosh" ,   [](Expression& a) -> Expression {return Expression("cosh",    (fptr_dd_t)&std::cosh, a);})
        ("exp"  ,   [](Expression& a) -> Expression {return Expression("exp",     (fptr_dd_t)&std::exp, a);})
        ("exp2" ,   [](Expression& a) -> Expression {return Expression("exp2",    (fptr_dd_t)&std::exp2, a);})
        ("floor",   [](Expression& a) -> Expression {return Expression("floor",   (fptr_dd_t)&std::floor, a);})
        ("ln"   ,   [](Expression& a) -> Expression {return Expression("ln",      (fptr_dd_t)&std::log, a);})
        ("log2" ,   [](Expression& a) -> Expression {return Expression("log2",    (fptr_dd_t)&std::log2, a);})
        ("log10",   [](Expression& a) -> Expression {return Expression("log10",   (fptr_dd_t)&std::log10, a);})
        ("sin"  ,   [](Expression& a) -> Expression {return Expression("sin",     (fptr_dd_t)&std::sin, a);})
        ("sinh" ,   [](Expression& a) -> Expression {return Expression("sinh",    (fptr_dd_t)&std::sinh, a);})
        ("sqrt" ,   [](Expression& a) -> Expression {return Expression("sqrt",    (fptr_dd_t)&std::sqrt, a);})
        ("cbrt" ,   [](Expression& a) -> Expression {return Expression("cbrt",    (fptr_dd_t)&std::cbrt, a);})
        ("tan"  ,   [](Expression& a) -> Expression {return Expression("tan",     (fptr_dd_t)&std::tan, a);})
        ("tanh" ,   [](Expression& a) -> Expression {return Expression("tanh",    (fptr_dd_t)&std::tanh, a);})
        ("isnan",   [](Expression& a) -> Expression {return Expression("isnan",   (fptr_bd_t)&std::isnan, a);})
        ("isinf",   [](Expression& a) -> Expression {return Expression("isinf",   (fptr_bd_t)&std::isinf, a);})
        ("signbit", [](Expression& a) -> Expression {return Expression("signbit", (fptr_bd_t)&std::signbit, a);})
        ("logb",    [](Expression& a) -> Expression {return Expression("logb",    (fptr_dd_t)&std::logb, a);})
        ("erf",     [](Expression& a) -> Expression {return Expression("erf",     (fptr_dd_t)&std::erf, a);})
        ("erfc",    [](Expression& a) -> Expression {return Expression("erfc",    (fptr_dd_t)&std::erfc, a);})
        ("lgamma",  [](Expression& a) -> Expression {return Expression("lgamma",  (fptr_dd_t)&std::lgamma, a);})
        ("tgamma",  [](Expression& a) -> Expression {return Expression("tgamma",  (fptr_dd_t)&std::tgamma, a);})
        ;

    // Parameterized Variables
    this->add
        ("cycles", [&used](Expression& a) -> Expression {
            const Clock* clk = a.getClock();
            if(!clk){
                throw SpartaException("Unable to determine a Clock from the expression: ") << a;
            }
            //! \todo Use this node instead of a statvariable ?
            return Expression(new StatVariable(&clk->getCyclesROCounter(), used));
        })
        ;

}

ExpressionGrammar::bfunc_::bfunc_(const std::vector<const TreeNode*>& used)
{
    (void) used;

    this->add
        ("pow"  ,
         [](Expression& a, Expression& b) -> Expression {
            return Expression("pow", (fptr_ddd_t)&std::pow, a, b);
         })
        ("atan2",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("atan2", (fptr_ddd_t)&std::atan2, a, b);
         })
        ("min",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("min", (fptr_drdrdrt)&std::min<double>, a, b);
         })
        ("max",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("max", (fptr_drdrdrt)&std::max<double>, a, b);
         })
        ("ifnan",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("ifnan", (fptr_ddd_t)&functions::ifnan, a, b);
         })
        ("fmod",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("fmod", (fptr_ddd_t)&std::fmod, a, b);
         })
        ("remainder",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("remainder", (fptr_ddd_t)&std::remainder, a, b);
         })
        ("hypot",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("hypot", (fptr_ddd_t)&std::hypot, a, b);
         })
        ("is_greater",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("is_greater", std::greater<double>(), a, b);
         })
        ("is_lesser",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("is_lesser", std::less<double>(), a, b);
         })
        ("is_equal",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("is_equal", std::equal_to<double>(), a, b);
         })
        ("is_not_equal",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("is_not_equal", std::not_equal_to<double>(), a, b);
         })
        ("is_greater_equal",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("is_greater_equal", std::greater_equal<double>(), a, b);
         })
        ("is_lesser_equal",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("is_lesser_equal", std::less_equal<double>(), a, b);
         })
        ("logical_and",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("logical_and", std::logical_and<double>(), a, b);
         })
        ("logical_or",
         [](Expression& a, Expression& b) -> Expression {
            return Expression("logical_or", std::logical_or<double>(), a, b);
         })
        ;
}

ExpressionGrammar::tfunc_::tfunc_(const std::vector<const TreeNode*>& used)
{
    (void) used;

    this->add
        ("cond",
         [](Expression& a, Expression& b, Expression& c) -> Expression {
            return Expression("cond", (fptr_dddd_t)&functions::if_function, a, b, c);
         })
        ;
}

ExpressionGrammar::ExpressionGrammar(sparta::TreeNode* root,
                                     std::vector<const TreeNode*>& used) :
    ExpressionGrammar::base_type(expression),
    builtin_vars(root, used),
    ufunc(used),
    bfunc(used),
    tfunc(used),
    var(root, used),
    root_(root)
{
    (void) root_;
    sparta_assert(nullptr != root,
                      "cannot construct ExpressionGrammar with a null context");

    namespace qi = qi;
    using qi::real_parser;
    using qi::real_policies;
    using qi::no_case;
    using qi::_val;
    using phoenix::ref;
    real_parser<double,real_policies<double> > real;

    helpers::lazy_builtin_bfunc_ lzbbf_pow("pow", &std::pow);
    boost::phoenix::function<helpers::lazy_builtin_bfunc_> lazy_pow(lzbbf_pow);
    boost::phoenix::function<helpers::lazy_nfunc_> lazy_nfunc;
    boost::phoenix::function<helpers::lazy_ufunc_> lazy_ufunc;
    boost::phoenix::function<helpers::lazy_bfunc_> lazy_bfunc;
    boost::phoenix::function<helpers::lazy_tfunc_> lazy_tfunc;

    expression =
        term                       [_val =  qi::_1]
        >> *(  ('+' >> term        [_val += qi::_1])
            |  ('-' >> term        [_val -= qi::_1])
            )
        ;

    term =
        factor                     [_val =  qi::_1]
        >> *(  ('*' >> factor      [_val *= qi::_1])
            |  ('/' >> factor      [_val /= qi::_1])
            )
        ;

    factor =
        primary                    [_val =  qi::_1]
        >> *(  ("**" >> factor     [_val = lazy_pow(_val, qi::_1)])
            )
        ;

    primary =
        real                       [_val =  qi::_1]
       |    '(' >> expression      [_val =  qi::_1] >> ')'
       |    ('-' >> primary        [_val = -qi::_1])
       |    ('+' >> primary        [_val =  qi::_1])
       |    (no_case[ufunc] >> '(' >> expression >> ')')
                                   [_val = lazy_ufunc(qi::_1, qi::_2)]
       |    (no_case[bfunc] >> '(' >> expression >> ','
                                   >> expression >> ')')
                                   [_val = lazy_bfunc(qi::_1, qi::_2, qi::_3)]
       |    (no_case[tfunc] >> '(' >> expression >> ','
                                   >> expression >> ','
                                   >> expression >> ')')
                                   [_val = lazy_tfunc(qi::_1, qi::_2, qi::_3, qi::_4)]
       |    no_case[constants]     [_val =  qi::_1]
       |    no_case[builtin_vars]  [_val = lazy_nfunc(qi::_1)]
       |    no_case[var]           [_val = qi::_1]
        ;

    expression.name("expression");
    term.name("term");
    factor.name("factor");
    primary.name("primary");

    qi::on_error<qi::fail>
    (
        expression
      , std::cout
            << phoenix::val("Error! Expecting ")
            << qi::_4                               // what failed?
            << phoenix::val(" here: \"")
            << phoenix::construct<std::string>(qi::_3, qi::_2)   // iterators to error-pos, end
            << phoenix::val("\"")
            << std::endl
    );

    // Debugging switches
    //qi::debug(expression);
    //qi::debug(term);
    //qi::debug(factor);
    //qi::debug(primary);
}

            } // namespace grammar
        } // namespace expression
    } // namespace statistics
} // namespace sparta


// Copyright (c) 2005 - 2015 Marc de Kamps
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
//
//    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
//    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation
//      and/or other materials provided with the distribution.
//    * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software
//      without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
#define BOOST_TEST_DYN_LINK
#include <boost/test/unit_test.hpp>
#include <TwoDLib.hpp>
#include <fstream>

using namespace TwoDLib;

BOOST_AUTO_TEST_CASE(MasterTest){
/*	Mesh mesh("condee2a5ff4-0087-4d69-bae3-c0a223d03693.mesh");

	vector<double> v{-64.99e-3, -64.99e-3, -65.01e-3, -65.01e-3};
	vector<double> w{      0.0,      0.01,     0.01,   0.0};
	Quadrilateral reversal_bin(v,w);
	mesh.InsertStationary(reversal_bin);

	vector<Redistribution> reversal_map;
	// 1 is crucial. If 0 is in there, the reversal bin will be wiped out
	for( unsigned int i = 1; i < mesh.NrQuadrilateralStrips();i++){
		Coordinates from(i,0);
		Coordinates to(0,0);
		Redistribution m(from,to,1.0);
		reversal_map.push_back(m);
	}

	std::ifstream ifstres("condee2a5ff4-0087-4d69-bae3-c0a223d03693.res");

	vector<Redistribution> reset_map = ReMapping(ifstres);
	Ode2DSystem sys(mesh,reversal_map, reset_map);
	sys.Initialize(0,0);
	TransitionMatrix mat("condee2a5ff4-0087-4d69-bae3-c0a223d03693.mat");

	vector<TransitionMatrix> vec_mat;
	vec_mat.push_back(mat);

	MasterParameter N = {100};
	Master master(sys,vec_mat,N);

	double rate = 1000.0;
	vector<double> vec_rates;
	vec_rates.push_back(rate);

	double t_step = mesh.TimeStep();
	int nr_steps = 400;
	std::ostringstream ost;
	ost << nr_steps;
	std::ofstream ofstmaster("master_" + ost.str() );
	for (int i = 0; i < nr_steps; i++){
		sys.Evolve();
		master.Apply(t_step,vec_rates);
		sys.RedistributeProbability();
	}
	sys.Dump(ofstmaster);*/
}


/*
 * Author: Benoit Sklenard benoit.sklenard@cea.fr
 *
 *
 * Copyright 2014 IMDEA Materials Institute, Getafe, Madrid, Spain
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <cmath>
#include <vector>

#include "io/Diagnostic.h"
#include "io/ParameterManager.h"

#include "okmc/Defect.h"
#include "kernel/ParticleType.h"
#include "okmc/MobileParticleParam.h"

#include <boost/numeric/ublas/io.hpp>

#include "kernel/Mesh.h"

#include "ParticleToNodeHandler.h"

#include <ctime>

using namespace Electrostatics;
using namespace Kernel;
using namespace boost::numeric;

ParticleToNodeHandler::ParticleToNodeHandler(Kernel::Domain *pDomain, Kernel::Mesh *pMesh) {
	LOWMSG("Loading Particle To Node handler");
	_pMesh   = pMesh;
	_pDomain = pDomain;
}

ParticleToNodeHandler::~ParticleToNodeHandler() {

}

inline double ParticleToNodeHandler::getOverlap(double u1, double u2) {
	return 15./16. * ((u2 - u1) - 2./3. * (u2*u2*u2 - u1*u1*u1) + 1./5. * (u2*u2*u2*u2*u2 - u1*u1*u1*u1*u1));
}

void ParticleToNodeHandler::remove(OKMC::Particle *pPart) {
	std::map<OKMC::Particle *, std::set<MeshNode *> >::iterator mit;

	mit = _syncOKMC.find(pPart);
	if (mit != _syncOKMC.end()) {
		for (std::set<MeshNode *>::iterator sit = mit->second.begin(); sit != mit->second.end(); ++sit)
			(*sit)->remove(pPart);
		_syncOKMC.erase(mit);
	}
}

void ParticleToNodeHandler::remove(LKMC::LatticeAtom *pLA) {
	std::map<LKMC::LatticeAtom *, std::set<MeshNode *> >::iterator mit;

	mit = _syncLKMC.find(pLA);
	if (mit != _syncLKMC.end()) {
		for (std::set<MeshNode *>::iterator sit = mit->second.begin(); sit != mit->second.end(); ++sit)
			(*sit)->remove(pLA);
		_syncLKMC.erase(mit);
	}
}

double ParticleToNodeHandler::addWeightNode(MeshNode *pNode, LKMC::LatticeAtom *pLA) {
	M_TYPE mt = pLA->getElement()->getMaterial();
	double dr = _pDomain->_pLaPar[mt]->_orbitalRadius;

	ublas::vector<double> n  = ublas::zero_vector<double>(3);
	ublas::vector<double> c1 = ublas::zero_vector<double>(3);
	ublas::vector<double> c2 = ublas::zero_vector<double>(3);
	MeshNode ***pNodes = _pMesh->getNodes();

	n(0)  = pLA->getCoordinates()._x;
	n(1)  = pLA->getCoordinates()._y;
	n(2)  = pLA->getCoordinates()._z;

	c1(0) = pNode->_xm;
	c1(1) = pNode->_ym;
	c1(2) = pNode->_zm;
	c2(0) = pNode->_xp;
	c2(1) = pNode->_yp;
	c2(2) = pNode->_zp;

	_pMesh->setPeriodicRelative(n, c1); // c1 = c1 - n
	_pMesh->setPeriodicRelative(n, c2); // c2 = c2 - n

	c1 /= dr;
	c2 /= dr;

	if (c1(0) <= -1) c1(0) = -1.;
	if (c2(0) <= -1) c2(0) = -1.;
	if (c1(0) >= 1)  c1(0) = 1.;
	if (c2(0) >= 1)  c2(0) = 1.;

	if (c1(1) <= -1) c1(1) = -1.;
	if (c2(1) <= -1) c2(1) = -1.;
	if (c1(1) >= 1)  c1(1) = 1.;
	if (c2(1) >= 1)  c2(1) = 1.;

	if (c1(2) <= -1) c1(2) = -1.;
	if (c2(2) <= -1) c2(2) = -1.;
	if (c1(2) >= 1)  c1(2) = 1.;
	if (c2(2) >= 1)  c2(2) = 1.;

	const double wx  = getOverlap(c1(0), c2(0));
	const double wy  = getOverlap(c1(1), c2(1));
	const double wz  = getOverlap(c1(2), c2(2));

	const double w   = wx * wy * wz;

	if (w > 0) {
		unsigned i = pNode->_ix;
		unsigned j = pNode->_iy;
		unsigned k = pNode->_iz;

		if (_pMesh->getPeriodicX() && pNode->_ix == (_pMesh->getnx() - 1))
			i = 0;
		if (_pMesh->getPeriodicY() && pNode->_iy == (_pMesh->getny() - 1))
			j = 0;
		if (_pMesh->getPeriodicZ() && pNode->_iz == (_pMesh->getnz() - 1))
			k = 0;

		pNodes[i][j][k].insert(pLA, w);

	    std::pair<std::map<LKMC::LatticeAtom *, std::set<MeshNode *> >::iterator, bool > r;
	    r = _syncLKMC.insert(std::pair<LKMC::LatticeAtom *, std::set<MeshNode *> >(pLA, std::set<MeshNode *>()));
	    r.first->second.insert(&pNodes[i][j][k]);
	}

	return w;
}

double ParticleToNodeHandler::addWeightNode(MeshNode *pNode, OKMC::Particle *pPart) {
	M_TYPE mt = pPart->getElement()->getMaterial();
	P_TYPE pt = pPart->getPType();

	double dr = _pDomain->_pMPPar->_orbitalRadius[mt][pt];
	// double dr = 1.; // _pDomain->_pLaPar[mt]->_orbitalRadius;

	ublas::vector<double> n  = ublas::zero_vector<double>(3);
	ublas::vector<double> c1 = ublas::zero_vector<double>(3);
	ublas::vector<double> c2 = ublas::zero_vector<double>(3);
	MeshNode ***pNodes = _pMesh->getNodes();

	n(0)  = pPart->getCoordinates()._x;
	n(1)  = pPart->getCoordinates()._y;
	n(2)  = pPart->getCoordinates()._z;

	c1(0) = pNode->_xm;
	c1(1) = pNode->_ym;
	c1(2) = pNode->_zm;
	c2(0) = pNode->_xp;
	c2(1) = pNode->_yp;
	c2(2) = pNode->_zp;

	_pMesh->setPeriodicRelative(n, c1); // c1 = c1 - n
	_pMesh->setPeriodicRelative(n, c2); // c2 = c2 - n

	c1 /= dr;
	c2 /= dr;

	if (c1(0) <= -1) c1(0) = -1.;
	if (c2(0) <= -1) c2(0) = -1.;
	if (c1(0) >= 1)  c1(0) = 1.;
	if (c2(0) >= 1)  c2(0) = 1.;

	if (c1(1) <= -1) c1(1) = -1.;
	if (c2(1) <= -1) c2(1) = -1.;
	if (c1(1) >= 1)  c1(1) = 1.;
	if (c2(1) >= 1)  c2(1) = 1.;

	if (c1(2) <= -1) c1(2) = -1.;
	if (c2(2) <= -1) c2(2) = -1.;
	if (c1(2) >= 1)  c1(2) = 1.;
	if (c2(2) >= 1)  c2(2) = 1.;

	const double wx  = getOverlap(c1(0), c2(0));
	const double wy  = getOverlap(c1(1), c2(1));
	const double wz  = getOverlap(c1(2), c2(2));

	const double w   = wx * wy * wz;

	if (w > 0) {
		unsigned i = pNode->_ix;
		unsigned j = pNode->_iy;
		unsigned k = pNode->_iz;

		if (_pMesh->getPeriodicX() && pNode->_ix == (_pMesh->getnx() - 1))
			i = 0;
		if (_pMesh->getPeriodicY() && pNode->_iy == (_pMesh->getny() - 1))
			j = 0;
		if (_pMesh->getPeriodicZ() && pNode->_iz == (_pMesh->getnz() - 1))
			k = 0;

		pNodes[i][j][k].insert(pPart, w);

	    std::pair<std::map<OKMC::Particle *, std::set<MeshNode *> >::iterator, bool > r;
	    r = _syncOKMC.insert(std::pair<OKMC::Particle *, std::set<MeshNode *> >(pPart, std::set<MeshNode *>()));
	    r.first->second.insert(&pNodes[i][j][k]);
	}

	return w;
}

void ParticleToNodeHandler::insert(LKMC::LatticeAtom *pLA) {
	std::set<MeshNode*> MN;
	MeshNode* pMN = NULL;

	M_TYPE mt = pLA->getElement()->getMaterial();

	double dr = _pDomain->_pLaPar[mt]->_orbitalRadius;

	remove(pLA);
	MeshNode ***pNodes  = _pMesh->getNodes();
	pMN = _pMesh->getFirstNodeFromElement(pLA->getElement());

	const size_t dx = ceil(dr / fabs(pNodes[0][0][0]._xm - pNodes[0][0][0]._xp)) + 1;
	const size_t dy = ceil(dr / fabs(pNodes[0][0][0]._ym - pNodes[0][0][0]._yp)) + 1;
	const size_t dz = ceil(dr / fabs(pNodes[0][0][0]._zm - pNodes[0][0][0]._zp)) + 1;

	const size_t startx = (dx <= pMN->_ix ? pMN->_ix - dx : _pMesh->getnx() - 1 - dx + pMN->_ix);
	const size_t endx   = (pMN->_ix + dx) % _pMesh->getnx();
	const size_t starty = (dy <= pMN->_iy ? pMN->_iy - dy : _pMesh->getny() - 1 - dy + pMN->_iy);
	const size_t endy   = (pMN->_iy + dy) % _pMesh->getny();
	const size_t startz = (dz <= pMN->_iz ? pMN->_iz - dz : _pMesh->getnz() - 1 - dz + pMN->_iz);
	const size_t endz   = (pMN->_iz + dz) % _pMesh->getnz();

	double w = 0.;
	for (size_t ix = startx; ix != endx; ix = (ix + 1) % _pMesh->getnx()) {
		for (size_t iy = starty; iy != endy; iy = (iy + 1) % _pMesh->getny()) {
			for (size_t iz = startz; iz != endz; iz = (iz + 1) % _pMesh->getnz()) {
				w += addWeightNode(&pNodes[ix][iy][iz], pLA);
			}
		}
	}

	if (_syncLKMC.find(pLA) ==_syncLKMC.end())
		ERRORMSG("LatticeAtom has not been inserted!");
}

void ParticleToNodeHandler::insert(OKMC::Particle *pPart) {
	std::set<MeshNode*> MN;
	MeshNode* pMN = NULL;

	M_TYPE mt = pPart->getElement()->getMaterial();
	P_TYPE pt = pPart->getPType();

	if (!_pDomain->_pMPPar->_mapToGrid[mt][pt]) {
	//	WARNINGMSG("Not inserted (OKMC) " << Domains::global()->PM()->getMaterialName(mt) << " " << Domains::global()->PM()->getParticleName(mt, pt));
		return ;
	}

	double dr = _pDomain->_pMPPar->_orbitalRadius[mt][pt];

	remove(pPart);
	MeshNode ***pNodes  = _pMesh->getNodes();
	pMN = _pMesh->getFirstNodeFromElement(pPart->getElement());

	const size_t dx = ceil(dr / fabs(pNodes[0][0][0]._xm - pNodes[0][0][0]._xp)) + 1;
	const size_t dy = ceil(dr / fabs(pNodes[0][0][0]._ym - pNodes[0][0][0]._yp)) + 1;
	const size_t dz = ceil(dr / fabs(pNodes[0][0][0]._zm - pNodes[0][0][0]._zp)) + 1;

	const size_t startx = (dx <= pMN->_ix ? pMN->_ix - dx : _pMesh->getnx() - 1 - dx + pMN->_ix);
	const size_t endx   = (pMN->_ix + dx) % _pMesh->getnx();
	const size_t starty = (dy <= pMN->_iy ? pMN->_iy - dy : _pMesh->getny() - 1 - dy + pMN->_iy);
	const size_t endy   = (pMN->_iy + dy) % _pMesh->getny();
	const size_t startz = (dz <= pMN->_iz ? pMN->_iz - dz : _pMesh->getnz() - 1 - dz + pMN->_iz);
	const size_t endz   = (pMN->_iz + dz) % _pMesh->getnz();

	double w = 0.;
	for (size_t ix = startx; ix != endx; ix = (ix + 1) % _pMesh->getnx()) {
		for (size_t iy = starty; iy != endy; iy = (iy + 1) % _pMesh->getny()) {
			for (size_t iz = startz; iz != endz; iz = (iz + 1) % _pMesh->getnz()) {
				w += addWeightNode(&pNodes[ix][iy][iz], pPart);
			}
		}
	}

	if (_syncOKMC.find(pPart) ==_syncOKMC.end())
		ERRORMSG("Particle has not been inserted!");
}

void ParticleToNodeHandler::getParticleNodes(OKMC::Particle *pPart, std::set<MeshNode *> &nodes) {
	std::map<OKMC::Particle *, std::set<MeshNode *> >::iterator mit = _syncOKMC.find(pPart);

	if (mit != _syncOKMC.end())
		for (std::set<MeshNode *>::iterator sit = mit->second.begin(); sit != mit->second.end(); ++sit)
			nodes.insert(*sit);
}

void ParticleToNodeHandler::getParticleNodes(LKMC::LatticeAtom *pLA, std::set<MeshNode *> &nodes) {
	std::map<LKMC::LatticeAtom *, std::set<MeshNode *> >::iterator mit = _syncLKMC.find(pLA);

	if (mit != _syncLKMC.end())
		for (std::set<MeshNode *>::iterator sit = mit->second.begin(); sit != mit->second.end(); ++sit)
			nodes.insert(*sit);
}

void ParticleToNodeHandler::print() {
	LOWMSG(_syncOKMC.size() << " particles in ParticleToNodeHandler:");

	if (!_syncOKMC.empty()) {
		unsigned i = 0;

		for (std::map<OKMC::Particle *, std::set<MeshNode *> >::iterator mit = _syncOKMC.begin(); mit != _syncOKMC.end(); ++mit) {
			double w = 0.;
			for (std::set<MeshNode *>::iterator sit = mit->second.begin(); sit != mit->second.end(); ++sit) {
				std::map<OKMC::Particle *, double>::iterator mit2 = (*sit)->_mPart.find(mit->first);
				if (mit2 != (*sit)->_mPart.end())
					w += mit2->second;
				else
					LOWMSG("Got a null value!");
			}
			LOWMSG("Particle " << i++ << " " <<  mit->first->getCoordinates() << " is affected to " << mit->second.size() << " nodes" << " weight= " << w);
		}
	}
}

void ParticleToNodeHandler::printLKMC() {
	LOWMSG(_syncLKMC.size() << " LKMC::LatticeAtom in ParticleToNodeHandler:");

	if (!_syncLKMC.empty()) {
		unsigned i = 0;

		for (std::map<LKMC::LatticeAtom *, std::set<MeshNode *> >::iterator mit = _syncLKMC.begin(); mit != _syncLKMC.end(); ++mit) {
			double w = 0.;
			for (std::set<MeshNode *>::iterator sit = mit->second.begin(); sit != mit->second.end(); ++sit) {
				std::map<LKMC::LatticeAtom *, double>::iterator mit2 = (*sit)->_mLA.find(mit->first);
				if (mit2 != (*sit)->_mLA.end())
					w += mit2->second;
				else
					LOWMSG("Got a null value!");
			}
				LOWMSG("LKMC::LaticeAtom " << ++i << " " <<  mit->first->getCoordinates() << " is affected to " << mit->second.size() << " nodes" << " weight= " << w);
		}
	}
}


#include <boost/multiprecision/rational_adaptor.hpp>


#include "neal8_algorithm.h"

#include <Eigen/Dense>
#include <memory>
#include <stan/math/prim/fun.hpp>

#include "algorithm_id.pb.h"
#include "algorithm_state.pb.h"
#include "hierarchy_id.pb.h"
#include "mixing_id.pb.h"
#include "neal2_algorithm.h"
#include "src/algorithms/marginal_algorithm.h"
#include "src/hierarchies/base_hierarchy.h"
#include "src/utils/distributions.h"

void Neal8Algorithm::read_params_from_proto(
    const bayesmix::AlgorithmParams &params) {
  // Set number of auxiliary blocks in addition to regular parameters
  BaseAlgorithm::read_params_from_proto(params);
  n_aux = params.neal8_n_aux();
}

void Neal8Algorithm::initialize() {
  MarginalAlgorithm::initialize();
  // Create correct amount of auxiliary blocks
  aux_unique_values.clear();
  for (size_t i = 0; i < n_aux; i++) {
    aux_unique_values.push_back(unique_values[0]->clone());
  }
}

void Neal8Algorithm::print_startup_message() const {
  std::string msg = "Running Neal8 algorithm (m=" + std::to_string(n_aux) +
                    " aux. blocks) with " +
                    bayesmix::HierarchyId_Name(unique_values[0]->get_id()) +
                    " hierarchies, " +
                    bayesmix::MixingId_Name(mixing->get_id()) + " mixing...";
  std::cout << msg << std::endl;
}

void Neal8Algorithm::sample_allocations() {
  // Initialize relevant values
  unsigned int n_data = data.rows();
  auto &rng = bayesmix::Rng::Instance().get();

  // Loop over data points
  for (size_t i = 0; i < n_data; i++) {
    bool singleton = (unique_values[allocations[i]]->get_card() <= 1);
    unsigned int c_old = allocations[i];

    if (singleton) {
      // Save unique value in the first auxiliary block
      bayesmix::AlgorithmState::ClusterState curr_val;
      unique_values[allocations[i]]->write_state_to_proto(&curr_val);
      aux_unique_values[0]->set_state_from_proto(curr_val);
      // Remove datum from cluster
      remove_singleton(c_old);
    } else {
      unique_values[c_old]->remove_datum(
          i, data.row(i), update_hierarchy_params(), hier_covariates.row(i));
    }

    unsigned int n_clust = unique_values.size();
    // Draw the unique values in the auxiliary blocks from their prior
    for (size_t j = singleton; j < n_aux; j++) {
      aux_unique_values[j]->sample_prior();
    }
    // Compute probabilities of clusters in log-space
    Eigen::VectorXd logprobas =
        get_cluster_prior_mass(i) + get_cluster_lpdf(i);
    // Draw a NEW value for datum allocation
    unsigned int c_new =
        bayesmix::categorical_rng(stan::math::softmax(logprobas), rng, 0);

    if (c_new >= n_clust) {
      // datum moves to a new cluster
      // Copy one of the auxiliary block as the new cluster
      std::shared_ptr<AbstractHierarchy> hier_new =
          aux_unique_values[c_new - n_clust]->clone();
      unique_values.push_back(hier_new);
      allocations[i] = n_clust;
      unique_values[n_clust]->add_datum(
          i, data.row(i), update_hierarchy_params(), hier_covariates.row(i));
    } else {
      allocations[i] = c_new;
      unique_values[c_new]->add_datum(
          i, data.row(i), update_hierarchy_params(), hier_covariates.row(i));
    }
  }
}

Eigen::VectorXd Neal8Algorithm::lpdf_marginal_component(
    std::shared_ptr<AbstractHierarchy> hier, const Eigen::MatrixXd &grid,
    const Eigen::RowVectorXd &covariate) const {
  unsigned int n_grid = grid.rows();
  Eigen::VectorXd lpdf_(n_grid);
  Eigen::MatrixXd lpdf_temp(n_grid, n_aux);
  for (size_t i = 0; i < n_aux; i++) {
    hier->sample_prior();
    lpdf_temp.col(i) = hier->like_lpdf_grid(grid, covariate);
  }
  for (size_t i = 0; i < n_grid; i++) {
    lpdf_(i) = stan::math::log_sum_exp(lpdf_temp.row(i));
  }
  return lpdf_.array() - log(n_aux);
}

Eigen::VectorXd Neal8Algorithm::get_cluster_prior_mass(
    const unsigned int data_idx) const {
  unsigned int n_data = data.rows();
  unsigned int n_clust = unique_values.size();
  Eigen::VectorXd logprior(n_clust + n_aux);
  for (size_t j = 0; j < n_clust; j++) {
    // Probability of being assigned to an already existing cluster
    logprior(j) = mixing->get_mass_existing_cluster(
        n_data - 1, n_clust, true, true, unique_values[j],
        mix_covariates.row(data_idx));
  }
  // Further update with marginal components
  for (size_t j = 0; j < n_aux; j++) {
    logprior(n_clust + j) = mixing->get_mass_new_cluster(
        n_data - 1, n_clust, true, true, mix_covariates.row(data_idx));
  }
  return logprior;
}

Eigen::VectorXd Neal8Algorithm::get_cluster_lpdf(
    const unsigned int data_idx) const {
  unsigned int n_data = data.rows();
  unsigned int n_clust = unique_values.size();
  Eigen::VectorXd loglpdf(n_clust + n_aux);
  for (size_t j = 0; j < n_clust; j++) {
    // Probability of being assigned to an already existing cluster
    loglpdf(j) = unique_values[j]->get_like_lpdf(
        data.row(data_idx), hier_covariates.row(data_idx));
  }
  for (size_t j = 0; j < n_aux; j++) {
    // Probability of being assigned to a newly created cluster
    loglpdf(n_clust + j) =
        aux_unique_values[j]->get_like_lpdf(data.row(data_idx),
                                            hier_covariates.row(data_idx)) -
        log(n_aux);
  }
  return loglpdf;
}


#include <ThermalAnalysis/LinearCombination.hpp>
#include <boost/program_options.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/filesystem.hpp>
#include <boost/algorithm/string.hpp>
#include <libField/HDF5.hpp>
#include <gputils/io.hpp>
#include <libArrhenius/Arrhenius.hpp>

#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>

namespace po = boost::program_options;

Field<double,1> read_profile(const std::string& filename)
{
  Field<double, 1> F;
  GP2DData data;
  ReadGPASCII2DDataFile(filename, data);

  F.reset(data.x.size());
  for(int i = 0; i < F.size(); ++i)
  {
    F.getAxis(0)[i] = data.x[i];
    F(i) = data.f[i];
  }

  return F;
}

int main(int argc, const char** argv)
{
  // define command line options
  po::options_description po_opts("Options");
  po_opts.add_options()("help,h", "print help message.")(
      "verbose,v", po::value<int>()->implicit_value(0), "verbose level.")
    ("Ea", po::value<double>()->default_value(6.28e5), "Activation energy [J/mol].")
    ("A",  po::value<double>()->default_value(3.1e99), "Frequency factor [1/s].")
    ("temperature-bias",  po::value<double>()->default_value(0.0), "A temperature offset that will be added to the temperature history to convert it to absolute temperature.")
    ("exposure",  po::value<double>()->default_value(1.0), "The exposure that gave the temperature history. I.e., if the temperature history corresponds to a 2.5 W exposure, then computed damage threshold scaling factors will be multiplied by 2.5 and the threshold values will correspond to power.")
    ("taus",  po::value<string>(), "Directly specify the exposure durations to run (as a comma separated list) instead of computing them [s].")
    ("tau-min",  po::value<double>()->default_value(10e-6), "The minimum exposure duration to compute the damage threshold for [s].")
    ("tau-max",  po::value<double>(), "The maximum exposure duration to compute the damage threshold for. The default is to use the entire exposure, but this may not be accurate if the cool down period is important [s].")
    ("tau-reduction-factor",  po::value<double>()->default_value(2), "The reduction factor between consecutive exposure duration. I.e., a reduction factor of 2 would cut the exposure duration in half each time.")
    ("dt",  po::value<double>()->default_value(10e-6), "Time resolution to use for generated temperature histories.")
    ("write-profiles", "Write damage threshold profiles.")
    ;

  // now define our arguments.
  po::options_description po_args("Arguments");
  po_args.add_options()
    ("thermal-profiles"  , po::value<std::vector<std::string>>()->composing(), "Text files containing thermal profiles. Temperature should be expressed in Kelvin.")
    ;

  // combine the options and arguments into one option list.
  // this is what we will use to parse the command line, but
  // when we output a description of the options, we will just use
  // po_opts
  po::options_description all_options("Options and Arguments");
  all_options.add(po_opts).add(po_args);

  // tell boost how to translate positional options to named options
  po::positional_options_description args;
  args.add("thermal-profiles", -1);

  po::variables_map vm;
  po::store(po::command_line_parser(argc, argv)
      .options(all_options)
      .positional(args)
      .run(),
      vm);
  po::notify(vm);

  if (argc == 1 || vm.count("help")) {
    std::cout << R"EOL(
    A small utility to quickly compute the damage threshold vs. exposure duration from a single temperature history computed from a long exposure.

    For the linear heat equation, the temperature rise resulting from a short exposure can be computed from the temperature rise resulting from a long exposure by
    linear combination. Even for the Penne's bioheat equation, the non-linear blood perfusion term has a small effect on the temeprature rise during an exposure, and
    linearity can be assumed.

    To run the tool, simply pass a file containing the temperature history (plain text file with time in the first column and temperature in the second column).
    By default, the Welch-Polhamus Arrhenius coefficients will be used, but these can be set wit the --Ea and --A options. For example, so use the Henruques
    coefficients (used fo skin), run

    > ./thermal-damage-trender --A 3.1e98 --Ea 6.27e5 ./Tvst.txt

    This will start printing out the damage threshold scaling factor as a function of exposure duration.

)EOL";

    std::cout << po_opts << std::endl;
    return 0;
  }

  if(vm["tau-reduction-factor"].as<double>() <= 1)
  {
    std::cerr << "Error: The tau reduction factor must be greater than 1." << std::endl;
    return 1;
  }

  for( auto file : vm["thermal-profiles"].as<std::vector<std::string>>() )
  {
    if( !boost::filesystem::exists(file) )
    {
      std::cerr << file << " does not appear to exists. Skipping" << std::endl;
      continue;
    }

    Field<double,1> Tvst = read_profile(file);
    if(Tvst.size() < 10)
    {
      std::cerr << "Thermal profile has less than 10 data points. Cannot make any reasonable predictions about threshold trends... Skipping." << std::endl;
      continue;
    }

    Tvst += vm["temperature-bias"].as<double>();
    auto T0 = Tvst[0];
    auto tmin = Tvst.getCoord(0);
    auto tmax = Tvst.getCoord(Tvst.size()-1);

    Tvst -= T0;

    // calculate how many
    std::vector<double> taus;

    if( vm.count("taus") > 0 )
    {
      std::vector<std::string> toks;
      boost::split(toks, vm["taus"].as<std::string>(),boost::is_any_of(","),boost::token_compress_on);
      for( auto& tok : toks )
      {
        taus.push_back( boost::lexical_cast<double>(tok));
      }

    }
    else
    {
      taus.push_back(tmax-tmin);
      if( vm.count("tau-max") > 0 )
      {
        taus[0] = vm["tau-max"].as<double>();
      }
      while( taus[taus.size()-1] > vm["tau-min"].as<double>() )
      {
        taus.push_back(taus[taus.size()-1]/vm["tau-reduction-factor"].as<double>());
      }
    }

    double dt = 10e-6;
    int N = tmax/dt;
    libArrhenius::ThresholdCalculator< libArrhenius::ArrheniusIntegral<double> > calc(vm["A"].as<double>(),vm["Ea"].as<double>());
    for( auto tau : taus)
    {
      Field<double,1> Tvst2(N);
      Tvst2.setCoordinateSystem( Uniform(tmin,tmax) );
      LinearCombination<_1D::MonotonicInterpolator<double>> tempBuilder;

      tempBuilder.add(Tvst, 0, 1.);
      tempBuilder.add(Tvst, tau, -1.);
      tempBuilder.build(Tvst2);

      Tvst2 += T0;

      double threshold = calc(Tvst2.size(), Tvst2.getAxis(0).data(),Tvst2.data() );
      std::cout << tau << " " << threshold * vm["exposure"].as<double>() << std::endl;

      if(vm.count("write-profiles"))
      {
        std::string ofile = file+"-"+boost::lexical_cast<std::string>(tau)+"-threshold";
        std::ofstream out(ofile.c_str());
        Tvst2 -= T0;
        Tvst2 *= threshold;
        Tvst2 += T0;
        out << Tvst2;
      }

    }

  }

  return 0;
}


#include <Eigen/Eigen>
#include <fmt/format.h>
#include <fstream>
#include <lmng/hierarchy.h>
#include <lmng/name.h>
#include <lmng/pose.h>
#include <lmng/transform.h>
#include <lmng/yaml_save_load.h>
#include <yaml-cpp/yaml.h>

static YAML::Node
  output_child(entt::registry const &registry, entt::entity child)
{
    YAML::Node yaml_node;

    yaml_node["Transform"] = lmng::save_component_as_yaml(
      registry, registry.get<lmng::transform>(child));
    yaml_node["Children"] = lmng::save_pose(registry, child);

    return std::move(yaml_node);
}

namespace lmng
{
void load_pose(
  entt::registry &registry,
  entt::entity entity,
  YAML::Node const &pose_yaml)
{
    for (auto const &yaml_entity : pose_yaml)
    {
        auto child = find_entity(registry, yaml_entity.first.as<std::string>());

        auto transform_any = construct_component_from_yaml(
          registry, "Transform", yaml_entity.second["Transform"]);

        auto transform = transform_any.cast<lmng::transform>();

        registry.replace<lmng::transform>(child, transform);

        load_pose(registry, child, yaml_entity.second["Children"]);
    }
}

YAML::Node save_pose(entt::registry const &registry, entt::entity entity)
{
    YAML::Node yaml;

    for (auto child : child_range{registry, entity})
    {
        yaml[get_name(registry, child)] = output_child(registry, child);
    }

    return yaml;
}
} // namespace lmng


// Copyright (C) 2014-2016 Vicente J. Botet Escriba
//
//  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)

// <experimental/applicative.hpp>
// <experimental/array.hpp>

#if defined JASEL_STD_EXPERIMENTAL_FUNDAMENTALS_V2_STD_OPTIONAL_NOT_INSTALLED
#warning JASEL_STD_EXPERIMENTAL_FUNDAMENTALS_V2_STD_OPTIONAL_NOT_INSTALLED
int main()
{
}
#else

//#define JASEL_STD_EXPERIMENTAL_FACTORIES_USE_OPTIONAL

#include <experimental/array.hpp>

#include <boost/detail/lightweight_test.hpp>

int twice(int i) {
  return 2*i;
}

int main()
{
#if __cplusplus >= 201402L
  namespace stde = std::experimental;

  static_assert(stde::is_applicative<stde::array_tc>::value, "ERROR");

  {
    int v=1;
    std::array<int, 1> x = stde::make<stde::array_tc>(v);
    std::array<int(*)(int), 1> f = stde::make<stde::array_tc>(twice);
    std::array<int, 1> y = stde::applicative::ap(f, x);
    BOOST_TEST(std::get<0>(y) == 2);
  }
  {
    int v=1;
    std::array<int, 2> x = stde::make<stde::array_tc>(v,v);
    auto f = stde::make<stde::array_tc>(twice, twice);
    std::array<int, 2> y = stde::applicative::ap(f, x);
    BOOST_TEST(std::get<0>(y) == 2);
    BOOST_TEST(std::get<1>(y) == 2);
  }
#endif

  return ::boost::report_errors();
}
#endif


#include <Eigen/Core>
#include <Eigen/Sparse>
#include <future>
#include <iostream>
#include <map>
#include <mutex>
#include <random>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>

#include <pybind11/eigen.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

#include "argcheck.hpp"

namespace irspack {

using CountVector = Eigen::Matrix<std::int64_t, Eigen::Dynamic, 1>;
struct Metrics {
  inline Metrics(size_t n_item) : n_item(n_item), item_cnt(n_item) {
    item_cnt.array() = 0;
  }

  inline void merge(const Metrics &other) {
    hit += other.hit;
    recall += other.recall;
    ndcg += other.ndcg;
    total_user += other.total_user;
    valid_user += other.valid_user;
    item_cnt += other.item_cnt;
    precision += other.precision;
    map += other.map;
  }

  std::map<std::string, double> as_dict() const {
    std::map<std::string, double> result;
    CountVector count_local(item_cnt);
    double total_item = item_cnt.sum();
    std::sort(count_local.data(), count_local.data() + item_cnt.rows());
    double apperere_item = 0;
    double entropy = 0;
    double gini = 0;

    for (int i = 0; i < item_cnt.rows(); i++) {
      int64_t cnt = count_local(i);
      if (cnt == 0) {
        continue;
      }
      double p = cnt / total_item;
      apperere_item++;
      entropy += -std::log(p) * p;
      gini += (2 * i - item_cnt.rows() + 1) * cnt;
    }
    gini /= (item_cnt.rows() * total_item);

    size_t denominator = valid_user > 0u ? valid_user : 1;
    result["total_user"] = total_user;
    result["valid_user"] = valid_user;
    result["n_items"] = n_item;
    result["hit"] = hit / denominator;
    result["ndcg"] = ndcg / denominator;
    result["recall"] = recall / denominator;
    result["map"] = map / denominator;
    result["precision"] = precision / denominator;
    result["appeared_item"] = apperere_item;
    result["entropy"] = entropy;
    result["gini_index"] = gini;
    return result;
  }

  size_t valid_user = 0;
  size_t total_user = 0;
  double hit = 0;
  double recall = 0;
  double ndcg = 0;
  double precision = 0;
  double map = 0;
  size_t n_item;
  CountVector item_cnt;
};

struct EvaluatorCore {
  using Real = double;
  using SparseMatrix = Eigen::SparseMatrix<Real, Eigen::RowMajor>;
  using DenseMatrix =
      Eigen::Matrix<Real, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>;
  using DenseVector = Eigen::Matrix<Real, Eigen::Dynamic, 1>;

  EvaluatorCore(const SparseMatrix &X)
      : X_(X), n_users(X.rows()), n_items(X.cols()) {
    X_.makeCompressed();
  }

  inline Metrics get_metrics(const Eigen::Ref<DenseMatrix> &scores,
                             size_t cutoff, size_t offset, size_t n_thread,
                             bool recall_with_cutoff = false) {
    Metrics overall(n_items);
    check_arg(n_users > offset, "got offset >= n_users");
    check_arg(cutoff > 0, "cutoff must be strictly greather than 0.");
    check_arg(cutoff <= n_items, "cutoff must not exeeed the number of items.");

    std::vector<std::vector<int>> uinds(n_thread);
    for (int u = 0; u < scores.rows(); u++) {
      uinds[u % n_thread].push_back(u);
    }
    std::vector<std::future<Metrics>> workers;
    for (size_t th = 0; th < n_thread; th++) {
      workers.emplace_back(
          std::async(std::launch::async, [th, this, &scores, &uinds, cutoff,
                                          offset, recall_with_cutoff]() {
            return this->get_metrics_local(scores, uinds[th], cutoff, offset,
                                           recall_with_cutoff);
          }));
    }
    for (auto &metric : workers) {
      overall.merge(metric.get());
    }
    return overall;
  }

  inline SparseMatrix get_ground_truth() const { return this->X_; }

private:
  inline Metrics get_metrics_local(const Eigen::Ref<DenseMatrix> &scores,
                                   const std::vector<int> &user_set,
                                   size_t cutoff, size_t offset,
                                   bool recall_with_cutoff = false) const {
    using StorageIndex = typename SparseMatrix::StorageIndex;

    Metrics metrics(n_items);

    const Real *buffer = scores.data();
    std::unordered_set<StorageIndex> hit_item;
    std::vector<StorageIndex> index(n_items);
    std::vector<StorageIndex> recommendation(cutoff);
    std::vector<StorageIndex> intersection(cutoff);
    std::vector<double> dcg_discount(cutoff);
    hit_item.reserve(cutoff);
    for (size_t i = 0; i < cutoff; i++) {
      dcg_discount[i] = 1 / std::log2(2 + i);
    }
    for (size_t _ = 0; _ < n_items; _++) {
      index[_] = _;
    }
    for (int u : user_set) {
      int u_orig = u + offset;
      metrics.total_user += 1;
      int begin_ptr = u * n_items;
      std::sort(index.begin(), index.end(),
                [&buffer, &begin_ptr](int i1, int i2) {
                  return buffer[begin_ptr + i1] > buffer[begin_ptr + i2];
                });
      for (size_t i = 0; i < cutoff; i++) {
        metrics.item_cnt(index[i]) += 1;
      }
      size_t n_gt = X_.outerIndexPtr()[u_orig + 1] - X_.outerIndexPtr()[u_orig];
      if (n_gt == 0) {
        continue;
      }
      metrics.valid_user += 1;

      hit_item.clear();
      if (recall_with_cutoff) {
        n_gt = std::min(n_gt, cutoff);
      }
      std::copy(index.begin(), index.begin() + cutoff, recommendation.begin());
      std::sort(index.begin(), index.begin() + cutoff);
      const StorageIndex *gb_begin =
          X_.innerIndexPtr() + X_.outerIndexPtr()[u_orig];
      const StorageIndex *gb_end =
          X_.innerIndexPtr() + X_.outerIndexPtr()[u_orig + 1];
      auto it_end =
          std::set_intersection(gb_begin, gb_end, index.begin(),
                                index.begin() + cutoff, intersection.begin());
      size_t n_hit = std::distance(intersection.begin(), it_end);
      for (auto iter = intersection.begin(); iter != it_end; iter++) {
        hit_item.insert(*iter);
      }
      if (n_hit > 0) {
        metrics.hit += 1;
      }
      metrics.precision += n_hit / static_cast<double>(cutoff);
      metrics.recall += n_hit / static_cast<double>(n_gt);
      double dcg = 0;
      double idcg =
          std::accumulate(dcg_discount.begin(),
                          dcg_discount.begin() + std::min(n_gt, cutoff), 0.);
      double cum_hit = 0;
      double average_precision = 0;
      for (size_t i = 0; i < cutoff; i++) {
        if (hit_item.find(recommendation[i]) != hit_item.end()) {
          dcg += dcg_discount[i];
          cum_hit += 1;
          average_precision += (cum_hit / (i + 1));
        }
      }
      metrics.ndcg += (dcg / idcg);
      metrics.map += average_precision / n_gt;
    }
    return metrics;
  }

  SparseMatrix X_;
  const size_t n_users;
  const size_t n_items;
};
} // namespace irspack

namespace py = pybind11;
using namespace irspack;

PYBIND11_MODULE(_evaluator, m) {
  py::class_<Metrics>(m, "Metrics")
      .def(py::init<size_t>())
      .def("merge",
           [](Metrics &this_, const Metrics &other) { this_.merge(other); })
      .def("as_dict", &Metrics::as_dict);

  py::class_<EvaluatorCore>(m, "EvaluatorCore")
      .def(py::init<const typename EvaluatorCore::SparseMatrix &>(),
           py::arg("grount_truth"))
      .def("get_metrics", &EvaluatorCore::get_metrics, py::arg("score_array"),
           py::arg("cutoff"), py::arg("offset"), py::arg("n_thread"),
           py::arg("recall_with_cutoff") = false)
      .def("get_ground_truth", &EvaluatorCore::get_ground_truth);
}