/*
  Copyright 2015 Statoil ASA.

  This file is part of the Open Porous Media project (OPM).

  OPM is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  OPM is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with OPM.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <opm/input/eclipse/Generator/KeywordGenerator.hpp>

#include <opm/input/eclipse/Generator/KeywordLoader.hpp>

#include <opm/json/JsonObject.hpp>

#include <opm/input/eclipse/Parser/ParserKeyword.hpp>

#include <cctype>
#include <filesystem>
#include <fstream>
#include <map>
#include <sstream>
#include <string>

#include <fmt/format.h>

namespace {

    void updateFile(const std::string& newContent, const std::string& filename)
    {
        Opm::KeywordGenerator::ensurePath(filename);

        std::ofstream { filename } << newContent;
    }

    void write_file(const std::string& content,
                    const std::string& file,
                    const bool         verbose,
                    const std::string& desc)
    {
        updateFile(content, file);

        if (verbose) {
            fmt::print("Updated {} file written to {}\n", desc, file);
        }
    }

    void write_file(const std::stringstream& stream,
                    const std::string&       file,
                    const bool               verbose,
                    const std::string&       desc)
    {
        write_file(stream.str(), file, verbose, desc);
    }

    const std::string sourceHeader = R"(// Generated code.  Please do not edit this file directly.

#include <opm/input/eclipse/Deck/UDAValue.hpp>
#include <opm/input/eclipse/Parser/ParserItem.hpp>
#include <opm/input/eclipse/Parser/ParserRecord.hpp>
#include <opm/input/eclipse/Parser/Parser.hpp>

)";

} // Anonymous namespace

namespace Opm {

    KeywordGenerator::KeywordGenerator(bool verbose)
        : m_verbose(verbose)
    {}

    std::string KeywordGenerator::headerHeader(const std::string& suffix)
    {
        return fmt::format("#ifndef PARSER_KEYWORDS_{0}_HPP\n"
                           "#define PARSER_KEYWORDS_{0}_HPP\n\n"
                           "// Generated code.  Please do not edit this file directly.\n\n"
                           "#include <opm/input/eclipse/Parser/ParserKeyword.hpp>\n"
                           "namespace Opm::ParserKeywords {{\n\n", suffix);
    }

    void KeywordGenerator::ensurePath(const std::string& file_name)
    {
        std::filesystem::path file(file_name);

        if (!std::filesystem::is_directory(file.parent_path())) {
            std::filesystem::create_directories(file.parent_path());
        }
    }

    void KeywordGenerator::updateFile(const std::stringstream& newContent,
                                      const std::string& filename)
    {
        ::updateFile(newContent.str(), filename);
    }

    void KeywordGenerator::updateBuiltInHeader(const KeywordLoader& loader,
                                               const std::string& headerBuildPath,
                                               const std::string& headerPath,
                                               const std::string& sourcePath) const
    {
        std::stringstream newHeader;
        std::map<char, std::stringstream> newSources;

        newHeader << R"(#ifndef PARSER_KEYWORDS_BUILTIN_HPP
#define PARSER_KEYWORDS_BUILTIN_HPP

// Generated code.  Please do not edit this file directly.

#include <opm/input/eclipse/Parser/ParserKeyword.hpp>
#include <unordered_map>
#include <fmt/format.h>
)";

        for (const auto& kw_pair : loader) {
            const auto& first_char = kw_pair.first;
            newSources[first_char] << "// Generated code.  Please do not edit this file directly.\n\n"
                                   << fmt::format("#include <{}/Builtin.hpp>\n", headerPath)
                                   << fmt::format("#include <opm/input/eclipse/Parser/ParserKeywords/{}.hpp>\n\n", first_char)
                                   << "namespace Opm::ParserKeywords {\n";
        }

        newHeader << R"(
namespace Opm::ParserKeywords {
struct Builtin
{
)";

        for (const auto& [first_char, keywords] : loader) {
            auto& source = newSources[first_char];
            for (const auto& kw : keywords) {
                newHeader << fmt::format("    ::Opm::ParserKeyword get_{0}();\n", kw.className());
                source << fmt::format("::Opm::ParserKeyword Builtin::get_{0}() {{ return {0}{{}}; }}\n", kw.className());
            }
        }

        newHeader << R"(
    const ::Opm::ParserKeyword& operator[](const std::string& keyword) const
    {
        if (this->keywords.empty()) {
)";
        std::stringstream declareEmplace;

        for (const auto& [first_char, keywords] : loader) {
            newHeader << fmt::format("            this->emplace{}();\n", first_char);

            auto& source = newSources[first_char];
            source << fmt::format(R"(
void Builtin::emplace{}() const
{{
)",
                                  first_char);

            declareEmplace << fmt::format(R"(    void emplace{}() const;)", first_char) << '\n';

            for (const auto& kw : keywords) {
                source << fmt::format("    this->keywords.emplace(\"{0}\", {0}{{}});\n", kw.className());
            }

            source << "}\n\n} // namespace Opm::ParserKeywords\n";
        }

        newHeader << R"(        }

        const auto kw_iter = this->keywords.find(keyword);
        if (kw_iter == this->keywords.end()) {
            throw std::invalid_argument(fmt::format("No builtin keyword: {}", keyword));
        }

        return kw_iter->second;
    }

    const ::Opm::ParserKeyword& getKeyword(const std::string& keyword) const
    { return (*this)[keyword]; }

)";

        newHeader << "private:\n" << declareEmplace.str() << R"(
    mutable std::unordered_map<std::string, ::Opm::ParserKeyword> keywords{};
};

} // namespace Opm::ParserKeywords

#endif // PARSER_KEYWORDS_BUILTIN_HPP
)";

        const auto final_path = headerBuildPath + headerPath + "/Builtin.hpp";
        write_file(newHeader, final_path, this->m_verbose, "header");

        for (auto&& [first_char, source] : newSources) {
            const auto sourceFile = std::filesystem::path(sourcePath) /
                fmt::format("Builtin{}.cpp", first_char);

            write_file(source, sourceFile, m_verbose, fmt::format("builtin source for {}", first_char));
        }
    }

    void KeywordGenerator::updateInitSource(const KeywordLoader& loader,
                                            const std::string& sourceFile,
                                            const std::string& sourcePath) const
    {
        std::filesystem::path parserInitSource(sourceFile);
        std::stringstream newSource;
        newSource << R"(// Generated code.  Please do not edit this file directly.

#include <opm/input/eclipse/Parser/Parser.hpp>
#include <opm/input/eclipse/Parser/ParserKeywords/Builtin.hpp>
)";

        for (const auto& [first_char, keywords] : loader) {
            const auto header = fmt::format(R"(#ifndef OPM_PARSER_INIT_{0}_HPP
#define OPM_PARSER_INIT_{0}_HPP

// Generated code.  Please do not edit this file directly.

namespace Opm {{ class Parser; }}

namespace Opm::ParserKeywords {{
    void addDefaultKeywords{0}(Parser& p);
}} // namespace Opm::ParserKeywords

#endif // OPM_PARSER_INIT_{0}_HPP
)",
                                                   first_char);

            auto charHeaderFile = parserInitSource;
            charHeaderFile.replace_filename(
                fmt::format("include/opm/input/eclipse/Parser/ParserKeywords/ParserInit{}.hpp", first_char));

            write_file(header, charHeaderFile, m_verbose, fmt::format("init header for {}", first_char));

            std::stringstream sourceStr;
            sourceStr << fmt::format(R"(// Generated code.  Please do not edit this file directly.

#include <opm/input/eclipse/Parser/ParserKeywords/ParserInit{0}.hpp>

#include <opm/input/eclipse/Parser/Parser.hpp>
#include <opm/input/eclipse/Parser/ParserKeywords/{0}.hpp>

void Opm::ParserKeywords::addDefaultKeywords{0}([[maybe_unused]] Parser& p)
{{
    // Built-in '{0}' keywords.
)",
                                     first_char);

            for (const auto& kw : keywords) {
                sourceStr << fmt::format("    p.addParserKeyword({}{{}});", kw.className()) << '\n';
            }

            // End of Opm::ParserKeywords::addDefaultKeywords{0}()
            sourceStr << "}\n";

            const auto charSourceFile = std::filesystem::path(sourcePath) / fmt::format("ParserInit{}.cpp", first_char);
            write_file(sourceStr, charSourceFile, m_verbose, fmt::format("init source for {}", first_char));

            newSource << fmt::format("#include <opm/input/eclipse/Parser/ParserKeywords/ParserInit{}.hpp>\n",
                                     first_char);
        }

        newSource << R"(
void Opm::Parser::addDefaultKeywords()
{
)";

        for (const auto& kw_pair : loader) {
            newSource << fmt::format("    ParserKeywords::addDefaultKeywords{}(*this);", kw_pair.first) << '\n';
        }

        // End of Opm::Parser::addDefaultKeywords()
        newSource << "}\n";

        write_file(newSource, sourceFile, this->m_verbose, "init");
    }

    void KeywordGenerator::updatePybindSource(const KeywordLoader& loader,
                                              const std::string& sourceFile) const
    {
        std::stringstream newSource;

        newSource << R"(#include <string>
#include <exception>

#include <opm/json/JsonObject.hpp>
#include <opm/input/eclipse/Parser/Parser.hpp>
#include <opm/input/eclipse/Parser/ParserKeyword.hpp>
#include <opm/input/eclipse/Parser/ParserKeywords/Builtin.hpp>
#include <opm/input/eclipse/Deck/Deck.hpp>
#include <opm/input/eclipse/Parser/ErrorGuard.hpp>

#include "export.hpp"

void python::common::export_ParserKeywords(py::module& module)
{
    py::class_<ParserKeywords::Builtin>(module, "Builtin")
        .def(py::init<>())
)";

        for (const auto& kw_pair : loader) {
            const auto& keywords = kw_pair.second;
            for (const auto& kw: keywords) {
                newSource << fmt::format("        .def_property_readonly(\"{0}\", &ParserKeywords::Builtin::get_{0})\n", kw.className());
            }
        }

        newSource << R"(        .def("__getitem__", &ParserKeywords::Builtin::operator[], ref_internal);
}
)";

        fmt::print("Writing file: {}\n", sourceFile);
        write_file(newSource, sourceFile, m_verbose, "source");
    }

    void KeywordGenerator::updateKeywordSource(const KeywordLoader& loader,
                                               const std::string& sourcePath) const
    {
        for (const auto& [first_char, keywords] : loader) {
            std::stringstream newSource;

            newSource << sourceHeader
                      << "#include <opm/input/eclipse/Parser/ParserKeywords/" << first_char << ".hpp>\n\n"
                      << "namespace Opm::ParserKeywords {\n";

            for (const auto& kw : keywords) {
                newSource << kw.createCode() << '\n';
            }

            newSource << "} // namespace Opm::ParserKeywords\n";

            write_file(newSource, sourcePath + "/" + first_char + ".cpp",
                       this->m_verbose, "source");
        }
    }

    void KeywordGenerator::updateHeader(const KeywordLoader& loader,
                                        const std::string& headerBuildPath,
                                        const std::string& headerPath) const
    {
        for (const auto& [first_char, keywords] : loader) {
            std::stringstream stream;

            stream << headerHeader(std::string(1, std::toupper(first_char)));

            for (const auto& kw : keywords) {
                stream << kw.createDeclaration("   ") << '\n';
            }

            stream << "} // namespace Opm::ParserKeywords\n\n" << "#endif\n";

            const auto final_path = headerBuildPath + headerPath + '/' + std::string(1, first_char) + ".hpp";
            write_file(stream, final_path, this->m_verbose, "header");
        }
    }

    std::string KeywordGenerator::startTest(const std::string& keyword_name)
    {
        return std::string("BOOST_AUTO_TEST_CASE(TEST") + keyword_name + std::string("Keyword) {\n");
    }

    std::string KeywordGenerator::endTest()
    {
        return "}\n\n";
    }

    void KeywordGenerator::updateTest(const KeywordLoader& loader,
                                      const std::string& testFile) const
    {
        std::stringstream stream;

        stream << R"(#define BOOST_TEST_MODULE GeneratedKeywordTest
#include <boost/test/unit_test.hpp>

#include <opm/json/JsonObject.hpp>

#include <opm/input/eclipse/Units/UnitSystem.hpp>

#include <opm/input/eclipse/Parser/ParserKeyword.hpp>
#include <opm/input/eclipse/Parser/ParserItem.hpp>
#include <opm/input/eclipse/Parser/ParserRecord.hpp>

)";

        for (const auto& kw_pair : loader) {
            stream << "#include <opm/input/eclipse/Parser/ParserKeywords/" << kw_pair.first << ".hpp>\n";
        }

        stream << R"(
#include <cstddef>
#include <filesystem>
#include <string>

namespace {

void test_keyword(const Opm::ParserKeyword& inline_keyword, const std::string& json_file)
{
    const std::filesystem::path jsonPath(json_file);
    const Json::JsonObject jsonConfig(jsonPath);
    const Opm::ParserKeyword json_keyword(jsonConfig);

    BOOST_CHECK_EQUAL(json_keyword, inline_keyword);

    if (json_keyword.hasDimension()) {
        auto unitSystem = Opm::UnitSystem::newMETRIC();

        const auto& parserRecord = json_keyword.getRecord(0);
        for (std::size_t i = 0; i < parserRecord.size(); ++i) {
            const auto& item = parserRecord.get(i);
            for (const auto& dim : item.dimensions()) {
                BOOST_CHECK_NO_THROW(unitSystem.getNewDimension(dim));
            }
        }
    }
}

} // Anonymous namespace
)";

        for (const auto& [first_char, keywords] : loader) {
            stream << "\nBOOST_AUTO_TEST_CASE(TestKeywords" << first_char << ") {\n";

            for (const auto& kw: keywords) {
                stream << "    test_keyword(Opm::ParserKeywords::" << kw.getName() << "{}, \""
                       << loader.getJsonFile(kw.getName()) << "\");\n";
            }

            stream << "}\n";
        }

        updateFile(stream, testFile);
    }

} // namespace Opm
