#ifndef __OPTIONS_H__
#define __OPTIONS_H__

#include "Utilities/string_operations.hpp"

// Includes
#include <map>
#include <vector>

/** This is a generic options class.
 *
 * It has been designed to read config files for test setups. It should be
 * formatted by lines of desired options with option name first, then
 * listing the value[s] to be used:
 *
 *     example.cfg:
 *     Option1_Name Option1_Value
 *     Option2_Name Option2_Value1 Option2_Value2 Option2_Value3
 *
 * Current value support includes:
 *
 * - Integers.
 * - Doubles.
 * - Strings.
 * - Boolean.
 * - Vector of integers.
 * - Vector of doubles.
 *
 * Option names interpreted as comments and ignored: %, #, //, !
 */
class Options {

  private:
    std::string                           filename; // Associated file
    std::map<std::string, int>            options;  // Map option name to index
    std::vector<std::vector<std::string>> values;   // List of options values

    void mpi_share();

  public:
    // ====================================================================== //
    // Construction and deconstruction

    /** Default constructor with no associated options */
    Options() {}
    /** Construct an options file with direct association to a file. */
    Options(const char *opt_file) { this->read_file(opt_file); }
    /** Construct an options file with direct association to a file. */
    Options(std::string opt_file) { this->read_file(opt_file); }
    ~Options() {}

    // ====================================================================== //
    // IO functions

    /** Read a function file.
     *
     * @param opt_file filename of the desired options file.
     */
    void read_file(std::string opt_file);
    /** Update the associated options file. */
    void write_file();
    /** Save the current options to file.
     *
     * @param filename of the desired output file.
     */
    void write_file(std::string filename);

    // ====================================================================== //
    // Data Access Templates

    /** Get the value of specific option.
     *
     * This function will return the value associated with a specific option. If
     * the option have not previously been set, the specified fallback is used.
     *
     * @tparam T Most standard types supported; int, double, bool, string, char
     * @param option specification of the option name.
     * @param fall_back fallback value if option is unset.
     */
    template <typename T> void get(std::string option, T &fall_back) {
        //  If the option have not been asigned, fall back to default value
        if (options.count(option) == 0) {
            set(option, fall_back);
            return;
        }

        // Else read the value stored in value list
        int idx = options[option];

        if constexpr (std::is_same_v<T, int>)
            fall_back = stoi(values[idx][0]);
        else if constexpr (std::is_same_v<T, double>)
            fall_back = stod(values[idx][0]);
        else if constexpr (std::is_same_v<T, bool>)
            fall_back = Utilities::stob(values[idx][0]);
        else if constexpr (std::is_same_v<T, char>)
            fall_back = values[idx][0].c_str();
        else if constexpr (std::is_same_v<T, std::string>)
            fall_back = values[idx][0];
        else if constexpr (std::is_same_v<T, const char *>)
            fall_back = values[idx][0].c_str();
        else
            Utilities::error(__FILE__, __LINE__, "Unsupported type");
    }
    /** Get the vector value of specific option.
     *
     * This function will return the value associated with a specific option. If
     * the option have not previously been set, the specified fallback is used.
     *
     * @tparam T Most standard types supported; int, double, bool, string, char
     * @param option specification of the option name.
     * @param fall_back fallback value if option is unset.
     */
    template <typename T>
    void get(std::string option, std::vector<T> &fall_back) {
        //  If the option have not been asigned, fall back to default value
        if (options.count(option) == 0) {
            set(option, fall_back);
            return;
        }

        // Else read the value stored in value list
        int            idx = options[option];
        std::vector<T> result;
        for (std::string v : values[idx]) {
            if constexpr (std::is_same_v<T, int>)
                result.push_back(stoi(v));
            else if constexpr (std::is_same_v<T, double>)
                result.push_back(stod(v));
            else if constexpr (std::is_same_v<T, bool>)
                result.push_back(Utilities::stob(v));
            else if constexpr (std::is_same_v<T, char>)
                result.push_back(v.c_str());
            else if constexpr (std::is_same_v<T, std::string>)
                result.push_back(v);
            else if constexpr (std::is_same_v<T, const char *>)
                result.push_back(v.c_str());
            else
                Utilities::error(__FILE__, __LINE__, "Unsupported type");
        }
        fall_back = result;
    }

    // ====================================================================== //
    // Data assignment functions

    /** Assign a value to the specified option.
     *
     * @tparam T Most standard types supported; int, double, bool, string, char
     * @param option specification of the option name.
     * @param value desired value of the option.
     */
    template <typename T> void set(std::string option, T value) {
        // Convert the input value to a vector of strings
        std::vector<std::string> input_string(1);

        if constexpr (std::is_same_v<T, int>)
            input_string[0] = std::to_string(value);
        else if constexpr (std::is_same_v<T, double>)
            input_string[0] = std::to_string(value);
        else if constexpr (std::is_same_v<T, bool>)
            input_string[0] = value ? "true" : "false";
        else if constexpr (std::is_same_v<T, char>)
            input_string[0] = value;
        else if constexpr (std::is_same_v<T, std::string>)
            input_string[0] = value;
        else if constexpr (std::is_same_v<T, const char *>)
            input_string[0] = value;
        else
            Utilities::error(__FILE__, __LINE__, "Unsupported type");

        // If option does not exist, set index to a new value, else read
        // old index
        int idx = options.count(option) == 0 ? options.size() : options[option];

        // Assign the input to options map and value list
        options[option] = idx;
        values.resize(std::max((int)values.size(), idx + 1));
        values[idx] = input_string;
    }
    /** Assign a value to the specified option.
     *
     * @tparam T Most standard types supported; int, double, bool, string, char
     * @param option specification of the option name.
     * @param list desired value of the option.
     */
    template <typename T> void set(std::string option, std::vector<T> list) {

        // Convert the input value to a std::vector of strings
        std::vector<std::string> input_string;
        for (T v : list) {
            if constexpr (std::is_same_v<T, int>)
                input_string.push_back(std::to_string(v));
            else if constexpr (std::is_same_v<T, double>)
                input_string.push_back(std::to_string(v));
            else if constexpr (std::is_same_v<T, bool>)
                input_string.push_back(v ? "true" : "false");
            else if constexpr (std::is_same_v<T, char>)
                input_string.push_back(v);
            else if constexpr (std::is_same_v<T, std::string>)
                input_string.push_back(v);
            else if constexpr (std::is_same_v<T, const char *>)
                input_string.push_back(v);
            else
                Utilities::error(__FILE__, __LINE__, "Unsupported type");
        }

        // If option does not exist, set index to a new value, else read old
        // index
        int idx = options.count(option) == 0 ? options.size() : options[option];

        // Assign the input to options map and value list
        options[option] = idx;
        values.resize(std::max((int)values.size(), idx + 1));
        values[idx] = input_string;
    }
};

#endif /* __OPTIONS__ */