///|/ Copyright (c) Prusa Research 2020 - 2022 Tomáš Mészáros @tamasmeszaros
///|/
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
///|/
#ifndef BRUTEFORCEOPTIMIZER_HPP
#define BRUTEFORCEOPTIMIZER_HPP

#include <libslic3r/Optimize/Optimizer.hpp>

namespace Slic3r { namespace opt {

namespace detail {
// Implementing a bruteforce optimizer

// Return the number of iterations needed to reach a specific grid position (idx)
template<size_t N>
long num_iter(const std::array<size_t, N> &idx, size_t gridsz)
{
    long ret = 0;
    for (size_t i = 0; i < N; ++i)
        ret += idx[i] * std::pow(gridsz, i);

    return ret;
}

// Implementation of a grid search where the search interval is sampled in
// equidistant points for each dimension. Grid size determines the number of
// samples for one dimension so the number of function calls is gridsize ^ dimension.
struct AlgBurteForce {
    bool to_min;
    StopCriteria stc;
    size_t gridsz;

    AlgBurteForce(const StopCriteria &cr, size_t gs): stc{cr}, gridsz{gs} {}

    // This function is called recursively for each dimension and generates
    // the grid values for the particular dimension. If D is less than zero,
    // the object function input values are generated for each dimension and it
    // can be evaluated. The current best score is compared with the newly
    // returned score and changed appropriately.
    template<int D, size_t N, class Fn, class Cmp>
    bool run(std::array<size_t, N> &idx,
             Result<N> &result,
             const Bounds<N> &bounds,
             Fn &&fn,
             Cmp &&cmp)
    {
        if (stc.stop_condition()) return false;

        if constexpr (D < 0) { // Let's evaluate fn
            Input<N> inp;

            auto max_iter = stc.max_iterations();
            if (max_iter && num_iter(idx, gridsz) >= max_iter)
                return false;

            for (size_t d = 0; d < N; ++d) {
                const Bound &b = bounds[d];
                double step = (b.max() - b.min()) / (gridsz - 1);
                inp[d] = b.min() + idx[d] * step;
            }

            auto score = fn(inp);
            if (cmp(score, result.score)) { // Change current score to the new
                double absdiff = std::abs(score - result.score);

                result.score = score;
                result.optimum = inp;

                // Check if the required precision is reached.
                if (absdiff < stc.abs_score_diff() ||
                    absdiff < stc.rel_score_diff() * std::abs(score))
                    return false;
            }

        } else {
            for (size_t i = 0; i < gridsz; ++i) {
                idx[D] = i; // Mark the current grid position and dig down
                if (!run<D - 1>(idx, result, bounds, std::forward<Fn>(fn),
                                std::forward<Cmp>(cmp)))
                    return false;
            }
        }

        return true;
    }

    template<class Fn, size_t N>
    Result<N> optimize(Fn&& fn,
                       const Input<N> &/*initvals*/,
                       const Bounds<N>& bounds)
    {
        std::array<size_t, N> idx = {};
        Result<N> result;

        if (to_min) {
            result.score = std::numeric_limits<double>::max();
            run<int(N) - 1>(idx, result, bounds, std::forward<Fn>(fn),
                            std::less<double>{});
        }
        else {
            result.score = std::numeric_limits<double>::lowest();
            run<int(N) - 1>(idx, result, bounds, std::forward<Fn>(fn),
                            std::greater<double>{});
        }

        return result;
    }
};

} // namespace detail

using AlgBruteForce = detail::AlgBurteForce;

template<>
class Optimizer<AlgBruteForce> {
    AlgBruteForce m_alg;

public:

    Optimizer(const StopCriteria &cr = {}, size_t gridsz = 100)
        : m_alg{cr, gridsz}
    {}

    Optimizer& to_max() { m_alg.to_min = false; return *this; }
    Optimizer& to_min() { m_alg.to_min = true;  return *this; }

    template<class Func, size_t N>
    Result<N> optimize(Func&& func,
                       const Input<N> &initvals,
                       const Bounds<N>& bounds)
    {
        return m_alg.optimize(std::forward<Func>(func), initvals, bounds);
    }

    Optimizer &set_criteria(const StopCriteria &cr)
    {
        m_alg.stc = cr; return *this;
    }

    const StopCriteria &get_criteria() const { return m_alg.stc; }
};

}} // namespace Slic3r::opt

#endif // BRUTEFORCEOPTIMIZER_HPP
