#pragma once

#include <alpaqa/config/config.hpp>
#include <alpaqa/cutest-interface-export.h>
#include <alpaqa/problem/box-constr-problem.hpp>
#include <alpaqa/problem/sparsity.hpp>
#include <alpaqa/util/copyable_unique_ptr.hpp>

#include <iosfwd>
#include <memory>
#include <string>

namespace alpaqa {

/// Wrapper for CUTEst problems loaded from an external shared library.
///
/// @ingroup  grp_Problems
class CUTEST_INTERFACE_EXPORT CUTEstProblem
    : public BoxConstrProblem<alpaqa::EigenConfigd> {
  public:
    USING_ALPAQA_CONFIG(alpaqa::EigenConfigd);
    using Sparsity = alpaqa::Sparsity<config_t>;

    /// Load a CUTEst problem from the given shared library and OUTSDIF.d file.
    /// If @p so_fname points to a directory, `"PROBLEM.so"` is appended
    /// automatically. If @p outsdif_fname is `nullptr`, the same directory as
    /// @p so_fname is used.
    CUTEstProblem(const char *so_fname, const char *outsdif_fname = nullptr,
                  bool sparse = false);
    CUTEstProblem(const CUTEstProblem &);
    CUTEstProblem &operator=(const CUTEstProblem &);
    CUTEstProblem(CUTEstProblem &&) noexcept;
    CUTEstProblem &operator=(CUTEstProblem &&) noexcept;
    ~CUTEstProblem();

  public:
    /// The report generated by CUTEst.
    ///
    /// @see `man CUTEST_creport` and `man CUTEST_ureport`
    struct Report {
        /// Function call counters.
        ///
        /// @note   Note that hessian_times_vector, constraints and constraints_grad
        ///         may account for codes which allow the evaluation of a
        ///         selection of constraints only and may thus be much smaller
        ///         than the number of constraints times the number of
        ///         iterations.
        struct Calls {
            /// Number of calls to the objective function.
            unsigned objective = 0;
            /// Number of calls to the objective gradient.
            unsigned objective_grad = 0;
            /// Number of calls to the objective Hessian.
            unsigned objective_hess = 0;
            /// Number of Hessian times vector products.
            unsigned hessian_times_vector = 0;
            /// Number of calls to the constraint functions.
            unsigned constraints = 0;
            /// Number of calls to the constraint gradients.
            unsigned constraints_grad = 0;
            /// Number of calls to the constraint Hessians.
            unsigned constraints_hess = 0;
        };
        Calls calls; ///< Function call counters.

        /// CPU time (in seconds) for CUTEST_csetup.
        double time_setup = 0;
        /// CPU time (in seconds) since the end of CUTEST_csetup.
        double time = 0;
    };

    [[nodiscard]] Report get_report() const;
    std::ostream &format_report(std::ostream &os, const Report &r) const;
    std::ostream &format_report(std::ostream &os) const {
        return format_report(os, get_report());
    }

  public:
    std::string name = "<UNKNOWN>"; ///< Problem name
    vec x0;                         ///< Initial value of decision variables
    vec y0;                         ///< Initial value of Lagrange multipliers

  private:
    util::copyable_unique_ptr<class CUTEstLoader> impl;
    bool sparse       = false;
    mutable int nnz_H = -1;
    mutable int nnz_J = -1;
    struct SparseStorage {
        Eigen::VectorX<int> rows, cols;
    } mutable storage_jac_g, storage_hess_L;

  public:
    [[nodiscard]] real_t eval_f(crvec x) const;
    void eval_grad_f(crvec x, rvec grad_fx) const;
    void eval_g(crvec x, rvec gx) const;
    void eval_grad_g_prod(crvec x, crvec y, rvec grad_gxy) const;

    void eval_jac_g(crvec x, rvec J_values) const;
    [[nodiscard]] Sparsity get_jac_g_sparsity() const;
    void eval_grad_gi(crvec x, index_t i, rvec grad_gi) const;
    void eval_hess_L_prod(crvec x, crvec y, real_t scale, crvec v,
                          rvec Hv) const;
    void eval_hess_L(crvec x, crvec y, real_t scale, rvec H_values) const;
    [[nodiscard]] Sparsity get_hess_L_sparsity() const;
    void eval_hess_ψ_prod(crvec x, crvec y, crvec Σ, real_t scale, crvec v,
                          rvec Hv) const;
    [[nodiscard]] real_t eval_f_grad_f(crvec x, rvec grad_fx) const;
    [[nodiscard]] real_t eval_f_g(crvec x, rvec g) const;
    void eval_grad_L(crvec x, crvec y, rvec grad_L, rvec work_n) const;
};

} // namespace alpaqa