#pragma once

#include <limits>
#include <utility>
#include <vector>

#include "ioh/common/optimization_type.hpp"
#include "ioh/common/repr.hpp"

namespace ioh
{
    namespace problem
    {
        //! Solution object
        template <typename T>
        struct Solution : common::HasRepr
        {
            //! variables
            std::vector<T> x{};

            //! objective value
            double y = std::numeric_limits<double>::signaling_NaN();

            /**
             * @brief Construct a new Solution object
             *
             * @param x variables
             * @param y objective value
             */
            Solution(const std::vector<T> &x, const double y) : x(x), y(y) {}


            //! Shorthand constructor for use with unknown optimum
            Solution(const int n_variables, const common::OptimizationType optimization_type) : 
                x(std::vector<T>(n_variables, std::numeric_limits<T>::signaling_NaN())),
                y{optimization_type == common::OptimizationType::Minimization
                             ? -std::numeric_limits<double>::infinity()
                             : std::numeric_limits<double>::infinity()} {}

            Solution() = default;

            std::string repr() const override {
                return fmt::format("<Solution x: {} y: {}>", x, y); }

            //! Cast solution to double type
            [[nodiscard]] Solution<double> as_double() const {
                return {std::vector<double>(x.begin(), x.end()), y}; }
        };

        //! Box-Constraint object
        template <typename T>
        struct Constraint : common::HasRepr
        {

            //! lower bound
            std::vector<T> lb;

            //! Upper bound
            std::vector<T> ub;

            /**
             * @brief Construct a new Constraint object
             *
             * @param lower lower bound
             * @param upper upper bound
             */
            Constraint(const std::vector<T> &lower, const std::vector<T> &upper) : lb(lower), ub(upper) {}

            /**
             * @brief Construct a new Constraint object
             *
             * @param size size of the constraint
             * @param lower lower bound
             * @param upper upper bound
             */
            explicit Constraint(const int size = 1, const T lower = std::numeric_limits<T>::lowest(),
                                const T upper = std::numeric_limits<T>::max()) :
                Constraint(std::vector<T>(size, lower), std::vector<T>(size, upper))
            {
            }

            //! Initialization helper
            void check_size(const int s)
            {
                if (ub.size() == lb.size() && lb.size() == size_t{1})
                {
                    ub = std::vector<T>(s, ub.at(0));
                    lb = std::vector<T>(s, lb.at(0));
                }

                if ((ub.size() != static_cast<size_t>(s)) || (ub.size() != lb.size()))
                    std::cout << "Bound dimension is wrong" << std::endl;
            }

            //! Check if the constraints are violated
            bool check(const std::vector<T> &x)
            {
                for (size_t i = 0; i < x.size(); i++)
                    if (!(ub.at(i) >= x.at(i) && x.at(i) <= lb.at(i)))
                        return false;
                return true;
            }

            //! Return resize version of constraint
            Constraint<T> resize(const int s) const
            {
                return Constraint<T>(std::vector<T>(s, lb.at(0)), std::vector<T>(s, ub.at(0)));
            }

            std::string repr() const override { return fmt::format("<Constraint lb: [{}] ub: [{}]>", lb, ub); }
        };


        //! struct of problem meta data
        struct MetaData : common::HasRepr
        {
            // Most of fields here are `int` and not `unsigned long` (or `size_t`) because of interoperability with some
            // problem suites.

            //! Instance id
            int instance{};

            //! problem id
            int problem_id{};

            //! problem name
            std::string name;

            //! optimization type
            common::FOptimizationType optimization_type;

            //! problem dimension
            int n_variables{};

            //! Initial objective value
            double initial_objective_value{};


            /**
             * @brief Construct a new Meta Data object
             *
             * @param problem_id The id of the problem
             * @param instance The instance of the problem
             * @param name the name of the problem
             * @param n_variables the dimension of the problem
             * @param optimization_type optimization type
             */
            MetaData(const int problem_id, const int instance, std::string name, const int n_variables,
                     const common::OptimizationType optimization_type = common::OptimizationType::Minimization) :
                instance(instance),
                problem_id(problem_id), name(std::move(name)), optimization_type{optimization_type},
                n_variables(n_variables),
                initial_objective_value(optimization_type == common::OptimizationType::Minimization
                                            ? std::numeric_limits<double>::infinity()
                                            : -std::numeric_limits<double>::infinity())
            {
            }

            /**
             * @brief Construct a new Meta Data object
             *
             * @param instance The instance of the problem
             * @param name the name of the problem
             * @param n_variables the dimension of the problem
             * @param optimization_type optimization type
             */
            MetaData(const int instance, const std::string &name, const int n_variables,
                     const common::OptimizationType optimization_type = common::OptimizationType::Minimization) :
                MetaData(0, instance, name, n_variables, optimization_type)
            {
            }

            //! comparison operator
            bool operator==(const MetaData &other) const
            {
                return instance == other.instance and problem_id == other.problem_id and name == other.name and
                    optimization_type == other.optimization_type and n_variables == other.n_variables and
                    initial_objective_value == other.initial_objective_value;
            }

            //! comparison operator
            bool operator!=(const MetaData &other) const { return not(*this == other); }

            std::string repr() const override
            {
                return fmt::format("<MetaData: {} id: {} iid: {} dim: {}>", name, problem_id, instance, n_variables);
            }
        };


        //! Problem State`
        template <typename T>
        struct State : common::HasRepr
        {
        private:
            Solution<T> initial_solution;

        public:
            //! Current number of evaluations
            int evaluations = 0;

            //! Is optimum found?
            bool optimum_found = false;

            //! Current best w.o. transformations
            Solution<T> current_best_internal{};

            //! Current best w. transformations
            Solution<T> current_best{};

            //! Current w.o. transformations
            Solution<T> current_internal{};

            //! Current w. transformations
            Solution<T> current{};

            State() = default;

            /**
             * @brief Construct a new State object
             *
             * @param initial initial objective value
             */
            State(Solution<T> initial) : initial_solution(std::move(initial)) { reset(); }

            //! reset the state
            void reset()
            {
                evaluations = 0;
                current_best = initial_solution;
                current_best_internal = initial_solution;
                optimum_found = false;
            }

            //! Update the state
            void update(const MetaData &meta_data, const Solution<T> &objective)
            {
                ++evaluations;
                if (meta_data.optimization_type(current.y, current_best.y))
                {
                    current_best_internal = current_internal;
                    current_best = current;

                    if (objective.y == current.y)
                        optimum_found = true;
                }
            }

            std::string repr() const override
            {
                return fmt::format("<State evaluations: {} optimum_found: {} current_best: {}>", evaluations,
                                   optimum_found, current_best);
            }
        };
    } // namespace problem
} // namespace ioh
