#ifndef TBPLAS_PYTHON_PARSER_H
#define TBPLAS_PYTHON_PARSER_H

#include <cassert>
#include <cmath>
#include <fstream>
#include <iostream>
#include <regex>
#include <set>
#include <sstream>
#include <string>

#include <eigen3/Eigen/Dense>

namespace tbplas::python {

/**
 * @brief Reset the stringstream object
 *
 * @param[inout] ss stringstream object to reset
 */
inline void reset_ss(std::stringstream& ss)
{
    ss.clear();
    ss.str("");
}

/**
 * @brief Base class for parsing input and struct files
 *
 */
class InputParser {
private:
    /**
     * @brief Go to the first line of input file after parsing an item
     *
     */
    void rewind()
    {
        infile_.clear();
        infile_.seekg(0, std::ios::beg);
    }

protected:
    /// @brief file to parse
    std::ifstream infile_;

public:
    explicit InputParser(const std::string& fileName)
    {
        infile_.open(fileName, std::ios::in);
        if (!infile_.is_open()) {
            std::cerr << "ERROR: failed to open: " << fileName << "\n";
            std::exit(-1);
        }
    }

    ~InputParser()
    {
        if (infile_.is_open()) {
            infile_.close();
        }
    }

    /**
     * @brief Get the value of key by matching the pattern and put it in stringstream
     *
     * @param[in] keyPattern pattern of the key
     * @param[out] valueStream stringstream object for holding the value
     * @param[in] withKey whether to keep keyname in stringstream
     * @return whether the key is defined in input
     */
    bool get_value(
        const std::string& keyPattern,
        std::stringstream& valueStream,
        bool withKey = false)
    {
        reset_ss(valueStream);
        rewind();
        std::string buffer;
        std::string key;
        std::regex pattern(keyPattern, std::regex_constants::icase);
        bool matched = false;
        while (std::getline(infile_, buffer)) {
            if (std::regex_match(buffer, pattern)) {
                valueStream << buffer;
                if (!withKey) {
                    valueStream >> key;
                }
                matched = true;
                break;
            }
        }
        return matched;
    }

    /**
     * @brief Get the value of key by matching the keyword and put it in stringstream
     *
     * @param[in] keyPattern pattern of the key
     * @param[out] valueStream stringstream object for holding the value
     * @param[in] withKey whether to keep keyname in stringstream
     * @return whether the key is defined in input
     */
    bool get_value_kw(
        const std::string& keyWord,
        std::stringstream& valueStream,
        bool withKey = false)
    {
        std::string key_pattern = R"(^\s*)" + keyWord + R"(\s+.*$)";
        return get_value(key_pattern, valueStream, withKey);
    }

    /**
     * @brief Read one line from file from the current position and move forward
     *
     * @param[in] lineStream stringstream object for holding the line
     * @return whether the read operation succeed
     */
    bool read_line(std::stringstream& lineStream)
    {
        reset_ss(lineStream);
        std::string buffer;
        bool status = false;
        if (std::getline(infile_, buffer)) {
            lineStream << buffer;
            status = true;
        } else {
            status = false;
        }
        return status;
    }

    /**
     * @brief Read key = value pair from file.
     *
     * @tparam value_t datatype of value
     * @param[in] keyWord keyword
     * @param[out] value value
     */
    template <typename value_t>
    void read_single(const std::string& keyWord, value_t& value)
    {
        bool matched = false;
        std::stringstream value_stream;
        if (get_value_kw(keyWord, value_stream)) {
            matched = true;
            value_stream >> value;
        }
        if (matched) {
#ifndef NDEBUG
            std::cout << "TBPMConfig." << keyWord << " = " << value << std::endl;
#endif
        } else {
            std::cerr << "Failed to set " << keyWord << " in config" << std::endl;
            std::exit(-1);
        }
    }

    /**
     * @brief Read eigen matrix or vector from file.
     *
     * @tparam value_t datatype of value
     * @param[in] keyWord keyword
     * @param[in] transpose whether to transpose the matrix w.r.t file content
     * @param[out] value value
     *
     * @note The Python frontend saves vectors in columns, so use transpose = false
     *   for vectors. For matrices, typically set it to true.
     */
    template <typename value_t>
    void read_eigen(
        const std::string& keyWord,
        Eigen::MatrixBase<value_t>& value,
        const bool& transpose)
    {
        bool matched = false;
        // R"(^\s*end\s+positions\s*$)"
        std::string end = R"(^\s*end\s+)" + keyWord + R"(\s*$)";
        std::regex end_pattern(end, std::regex_constants::icase);
        // R"(^\s*begin\s+positions\s+\w+\s*$)"
        std::string begin = R"(^\s*begin\s+)" + keyWord + R"((\s+\d+){2}\s*$)";
        std::stringstream ss;

        if (get_value(begin, ss)) {
            matched = true;

            // Read and check dimension
            // value should be empty, or have the same number of columns as file content,
            // if not transposed.
            std::string ignore;
            uint64_t rows = 0;
            uint64_t cols = 0;
            ss >> ignore >> rows >> cols;
            if (transpose) {
                assert(value.size() == 0 || value.rows() == cols);
            } else {
                assert(value.size() == 0 || value.cols() == cols);
            }

            // Read matrix into buffer
            typedef typename value_t::Scalar Scalar;
            Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic> buffer(rows, cols);
            uint64_t i = 0;
            while (read_line(ss)) {
                if (std::regex_match(ss.str(), end_pattern)) {
                    break;
                }
                for (uint64_t j = 0; j < cols; ++j) {
                    Scalar element;
                    ss >> element;
                    buffer(i, j) = element;
                }
                i++;
            }

            // Assign buffer to result
            if (transpose) {
                value = buffer.transpose();
            } else {
                value = buffer;
            }
        }

        if (matched) {
#ifndef NDEBUG
            std::cout << "TBPMConfig." << keyWord << ":\n"
                      << value << std::endl;
#endif
        } else {
            std::cerr << "Failed to set " << keyWord << " in config" << std::endl;
            std::exit(-1);
        }
    }

    /**
     * @brief Read a set of integers from file
     *
     * @param[in] keyWord keyword
     * @param[out] value value
     */
    void read_set(const std::string& keyWord, std::set<uint64_t>& value)
    {
        bool matched = false;
        value = std::set<uint64_t>();
        // R"(^\s*end\s+positions\s*$)"
        std::string end = R"(^\s*end\s+)" + keyWord + R"(\s*$)";
        std::regex end_pattern(end, std::regex_constants::icase);
        // R"(^\s*begin\s+positions\s+\w+\s*$)"
        std::string begin = R"(^\s*begin\s+)" + keyWord + R"((\s+\d+){2}\s*$)";
        std::stringstream ss;
        uint64_t rows = 0;
        uint64_t cols = 0;

        if (get_value(begin, ss)) {
            matched = true;
            std::string ignore;
            ss >> ignore >> rows >> cols;
            uint64_t i = 0;
            while (read_line(ss)) {
                if (std::regex_match(ss.str(), end_pattern)) {
                    break;
                }
                // std::cout << "in else" << std::endl;
                for (uint64_t j = 0; j < cols; ++j) {
                    uint64_t element = 0;
                    ss >> element;
                    value.insert(element);
                }
                i++;
            }
        }

        if (matched) {
#ifndef NDEBUG
            std::cout << "TBPMConfig." << keyWord << std::endl;
            auto it = value.begin();
            for (; it != value.end(); ++it) {
                std::cout << (*it) << " ";
            }
            std::cout << std::endl;
#endif
        } else {
            std::cerr << "Failed to set " << keyWord << " in config" << std::endl;
            std::exit(-1);
        }
    }

    /**
     * @brief Read a vector of tuples with two doubles.
     *
     * @param[in] keyWord keyword
     * @param[out] value value
     *
     * @note This function can be replaced with read_eigen.
     */
    void read_vector_tuple(const std::string& keyWord, std::vector<std::tuple<double, double>>& value)
    {
        bool matched = false;
        value = std::vector<std::tuple<double, double>>();
        // R"(^\s*end\s+positions\s*$)"
        std::string end = R"(^\s*end\s+)" + keyWord + R"(\s*$)";
        std::regex end_pattern(end, std::regex_constants::icase);
        // R"(^\s*begin\s+positions\s+\w+\s*$)"
        std::string begin = R"(^\s*begin\s+)" + keyWord + R"((\s+\d+){2}\s*$)";
        std::stringstream ss;
        uint64_t rows = 0;
        uint64_t cols = 0;

        if (get_value(begin, ss)) {
            matched = true;
            std::string ignore;
            ss >> ignore >> rows >> cols;

            value.reserve(rows);
            uint64_t i = 0;
            while (read_line(ss)) {
                if (std::regex_match(ss.str(), end_pattern)) {
                    break;
                }
                double a = 0;
                double b = 0;
                ss >> a >> b;
                value.emplace_back(a, b);
                i++;
            }
        }

        if (matched) {
#ifndef NDEBUG
            std::cout << "TBPMConfig." << keyWord << std::endl;
            auto it = value.begin();
            for (; it != value.end(); ++it) {
                double a = -1;
                double b = -1;
                std::tie(a, b) = (*it);
                std::cout << a << "  " << b << std::endl;
            }
#endif
        } else {
            std::cerr << "Failed to set " << keyWord << " in config" << std::endl;
            std::exit(-1);
        }
    }
};

} // namespace tbplas::python
#endif