#pragma once

#include <alpaqa/casadi-loader-export.hpp>
#include <alpaqa/config/config.hpp>
#include <alpaqa/problem/problem.hpp>

#include <memory>
#include <string>
#include <type_traits>

namespace alpaqa {

namespace casadi_loader {
template <Config>
struct CasADiFunctionsWithParam;
} // namespace casadi_loader

/// Problem definition for a CasADi problem, loaded from a DLL.
/// @ingroup grp_Problems
template <Config Conf = DefaultConfig>
class CasADiProblem : public Problem<Conf> {
  public:
    USING_ALPAQA_CONFIG(Conf);
    using Problem<Conf>::param;
    using Problem<Conf>::D;
    using Problem<Conf>::m;

    /// Load a problem generated by CasADi (with parameters).
    ///
    /// @param  filename
    ///         Filename of the shared library to load the functions from.
    /// @param  n
    ///         Number of decision variables (@f$ x \in \R^n @f$).
    /// @param  m
    ///         Number of general constraints (@f$ g(x) \in \R^m @f$).
    /// @param  p
    ///         The number of parameters of the problem (second argument to all
    ///         CasADi functions).
    /// @param  second_order
    ///         Load the additional functions required for second-order PANOC.
    ///
    /// The file should contain functions with the names `f`, `grad_f`, `g` and
    /// `grad_g`. These functions evaluate the objective function, its gradient,
    /// the constraints, and the constraint gradient times a vector respecitvely.
    /// If @p second_order is true, additional functions `hess_L` and
    /// `hess_L_prod` should be provided to evaluate the Hessian of the
    /// Lagrangian and Hessian-vector products.
    ///
    /// If any of the dimensions are less than or equal to zero, they are
    /// determined from the `g` function in the given file.
    ///
    /// @throws std::invalid_argument
    ///         The dimensions of the loaded functions do not match.
    CasADiProblem(const std::string &filename, length_t n = 0, length_t m = 0,
                  length_t p = 0, bool second_order = false);
    ~CasADiProblem();

    CasADiProblem(CasADiProblem &&);
    CasADiProblem &operator=(CasADiProblem &&);

    real_t eval_f(crvec x) const override;
    void eval_g(crvec x, rvec g) const override;
    void eval_grad_ψ(crvec x, crvec y, crvec Σ, rvec grad_ψ, rvec work_n,
                     rvec work_m) const override;
    real_t eval_ψ_grad_ψ(crvec x, crvec y, crvec Σ, rvec grad_ψ, rvec work_n,
                         rvec work_m) const override;
    void eval_grad_L(crvec x, crvec y, rvec grad_L, rvec work_n) const override;
    real_t eval_ψ_ŷ(crvec x, crvec y, crvec Σ, rvec ŷ) const override;
    void eval_grad_ψ_from_ŷ(crvec x, crvec ŷ, rvec grad_ψ,
                            rvec work_n) const override;

  private:
    std::unique_ptr<casadi_loader::CasADiFunctionsWithParam<Conf>> impl;
};

CASADI_LOADER_EXPORT_EXTERN_TEMPLATE(class, CasADiProblem, EigenConfigd);
CASADI_LOADER_EXPORT_EXTERN_TEMPLATE(class, CasADiProblem, DefaultConfig);

} // namespace alpaqa