/**
 * src/options/cli_handler.cc
 *
 * Copyright (c) 2021-2023 Bartek Kryza <bkryza@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "cli_handler.h"

#include "class_diagram/generators/plantuml/class_diagram_generator.h"
#include "include_diagram/generators/plantuml/include_diagram_generator.h"
#include "package_diagram/generators/plantuml/package_diagram_generator.h"
#include "sequence_diagram/generators/plantuml/sequence_diagram_generator.h"
#include "util/util.h"
#include "version.h"

#include <clang/Basic/Version.h>
#include <clang/Config/config.h>

namespace clanguml::cli {
cli_handler::cli_handler(
    std::ostream &ostr, std::shared_ptr<spdlog::logger> logger)
    : ostr_{ostr}
    , logger_{std::move(logger)}
{
}

void cli_handler::setup_logging()
{
    spdlog::drop("clanguml-logger");
    spdlog::register_logger(logger_);

    logger_->set_pattern("[%^%l%^] [tid %t] %v");

    if (verbose == 0) {
        logger_->set_level(spdlog::level::err);
    }
    else if (verbose == 1) {
        logger_->set_level(spdlog::level::info);
    }
    else if (verbose == 2) {
        logger_->set_level(spdlog::level::debug);
    }
    else {
        logger_->set_level(spdlog::level::trace);
    }
}

cli_flow_t cli_handler::parse(int argc, const char **argv)
{
    static const std::map<std::string, clanguml::common::generator_type_t>
        generator_type_names{
            {"plantuml", clanguml::common::generator_type_t::plantuml},
            {"json", clanguml::common::generator_type_t::json}};

    app.add_option("-c,--config", config_path,
        "Location of configuration file, when '-' read from stdin");
    app.add_option("-d,--compile-database", compilation_database_dir,
        "Location of compilation database directory");
    app.add_option("-n,--diagram-name", diagram_names,
        "List of diagram names to generate");
    app.add_option("-g,--generator", generators,
           "Name of the generator (default: plantuml)")
        ->transform(CLI::CheckedTransformer(generator_type_names));
    app.add_option("-o,--output-directory", output_directory,
        "Override output directory specified in config file");
    app.add_option("-t,--thread-count", thread_count,
        "Thread pool size (0 = hardware concurrency)");
    app.add_flag("-V,--version", show_version, "Print version and exit");
    app.add_flag("-v,--verbose", verbose,
        "Verbose logging (use multiple times to increase - e.g. -vvv)");
    app.add_flag("-q,--quiet", quiet, "Minimal logging");
    app.add_flag("-l,--list-diagrams", list_diagrams,
        "Print list of diagrams defined in the config file");
    app.add_flag("--init", initialize, "Initialize example config file");
    app.add_option("--add-compile-flag", add_compile_flag,
        "Add a compilation flag to each entry in the compilation database");
    app.add_option("--remove-compile-flag", remove_compile_flag,
        "Remove a compilation flag from each entry in the compilation "
        "database");
#if !defined(_WIN32)
    app.add_option("--query-driver", query_driver,
        "Query the specific compiler driver to extract system paths and add to "
        "compile commands (e.g. arm-none-eabi-g++)");
#endif
    app.add_option(
        "--add-class-diagram", add_class_diagram, "Add class diagram config");
    app.add_option("--add-sequence-diagram", add_sequence_diagram,
        "Add sequence diagram config");
    app.add_option("--add-package-diagram", add_package_diagram,
        "Add package diagram config");
    app.add_option("--add-include-diagram", add_include_diagram,
        "Add include diagram config");
    app.add_option("--add-diagram-from-template", add_diagram_from_template,
        "Add diagram config based on diagram template");
    app.add_option("--template-var", template_variables,
        "Specify a value for a template variable");
    app.add_flag("--list-templates", list_templates,
        "List all available diagram templates");
    app.add_option("--show-template", show_template,
        "Show specific diagram template definition");
    app.add_flag(
        "--dump-config", dump_config, "Print effective config to stdout");
    app.add_flag("--paths-relative-to-pwd", paths_relative_to_pwd,
        "If true, all paths in configuration files are relative to the $PWD "
        "instead of actual location of `.clang-uml` file.");
    app.add_flag("--no-metadata", no_metadata,
        "Skip metadata (e.g. clang-uml version) from diagrams");

    try {
        app.parse(argc, argv);
    }
    catch (const CLI::CallForHelp &e) {
        exit(app.exit(e)); // NOLINT(concurrency-mt-unsafe)
    }
    catch (const CLI::Success &e) {
        return cli_flow_t::kExit;
    }
    catch (const CLI::ParseError &e) {
        exit(app.exit(e)); // NOLINT(concurrency-mt-unsafe)
    }

    if (quiet || dump_config)
        verbose = 0;
    else
        verbose++;

    return cli_flow_t::kContinue;
}

cli_flow_t cli_handler::handle_options(int argc, const char **argv)
{
    auto res = parse(argc, argv);

    if (res != cli_flow_t::kContinue)
        return res;

    setup_logging();

    res = handle_pre_config_options();

    if (res != cli_flow_t::kContinue)
        return res;

    res = load_config();

    if (res != cli_flow_t::kContinue)
        return res;

    res = handle_post_config_options();

    return res;
}

cli_flow_t cli_handler::handle_pre_config_options()
{
    if (show_version) {
        return print_version();
    }

    if ((config_path == "-") &&
        (initialize || add_diagram_from_template ||
            add_class_diagram.has_value() || add_sequence_diagram.has_value() ||
            add_package_diagram.has_value() ||
            add_include_diagram.has_value())) {

        LOG_ERROR(
            "ERROR: Cannot add a diagram config to configuration from stdin");

        return cli_flow_t::kError;
    }

    if (initialize) {
        return create_config_file();
    }

    if (config_path != "-") {
        if (add_class_diagram) {
            return add_config_diagram(
                clanguml::common::model::diagram_t::kClass, config_path,
                *add_class_diagram);
        }

        if (add_sequence_diagram) {
            return add_config_diagram(
                clanguml::common::model::diagram_t::kSequence, config_path,
                *add_sequence_diagram);
        }

        if (add_package_diagram) {
            return add_config_diagram(
                clanguml::common::model::diagram_t::kPackage, config_path,
                *add_package_diagram);
        }

        if (add_include_diagram) {
            return add_config_diagram(
                clanguml::common::model::diagram_t::kInclude, config_path,
                *add_include_diagram);
        }
    }

    return cli_flow_t::kContinue;
}

cli_flow_t cli_handler::load_config()
{
    try {
        config = clanguml::config::load(
            config_path, paths_relative_to_pwd, no_metadata);
        return cli_flow_t::kContinue;
    }
    catch (std::runtime_error &e) {
        LOG_ERROR(e.what());
    }

    return cli_flow_t::kError;
}

cli_flow_t cli_handler::handle_post_config_options()
{
    if (dump_config) {
        return print_config();
    }

    if (list_diagrams) {
        return print_diagrams_list();
    }

    if (list_templates) {
        return print_diagram_templates();
    }

    if (show_template) {
        return print_diagram_template(show_template.value());
    }

    if (config_path != "-" && add_diagram_from_template) {
        return add_config_diagram_from_template(
            config_path, add_diagram_from_template.value(), template_variables);
    }

    LOG_INFO("Loaded clang-uml config from {}", config_path);

    //
    // Override selected config options from command line
    //
    if (compilation_database_dir) {
        config.compilation_database_dir.set(
            util::ensure_path_is_absolute(compilation_database_dir.value())
                .string());
    }

    effective_output_directory = config.output_directory();

    // Override the output directory from the config
    // with the value from the command line if any
    if (output_directory)
        effective_output_directory = output_directory.value();

    if (output_directory) {
        config.output_directory.set(
            util::ensure_path_is_absolute(output_directory.value()).string());
    }

    LOG_INFO("Loading compilation database from {} directory",
        config.compilation_database_dir());

    if (!ensure_output_directory_exists(effective_output_directory))
        return cli_flow_t::kError;

    //
    // Append add_compile_flags and remove_compile_flags to the config
    //
    if (add_compile_flag) {
        std::copy(add_compile_flag->begin(), add_compile_flag->end(),
            std::back_inserter(config.add_compile_flags.value));
        config.add_compile_flags.has_value = true;
    }

    if (remove_compile_flag) {
        std::copy(remove_compile_flag->begin(), remove_compile_flag->end(),
            std::back_inserter(config.remove_compile_flags.value));
        config.remove_compile_flags.has_value = true;
    }

#if !defined(_WIN32)
    if (query_driver) {
        config.query_driver.set(*query_driver);
    }
#endif

    return cli_flow_t::kContinue;
}

cli_flow_t cli_handler::print_version()
{
    ostr_ << "clang-uml " << clanguml::version::CLANG_UML_VERSION << '\n';
    ostr_ << "Copyright (C) 2021-2023 Bartek Kryza <bkryza@gmail.com>" << '\n';
    ostr_ << "Built against LLVM/Clang libraries version: "
          << LLVM_VERSION_STRING << std::endl;
    ostr_ << "Using LLVM/Clang libraries version: "
          << clang::getClangFullVersion() << std::endl;

    return cli_flow_t::kExit;
}

bool cli_handler::ensure_output_directory_exists(const std::string &dir)
{
    namespace fs = std::filesystem;
    using std::cout;

    fs::path output_dir{dir};

    if (fs::exists(output_dir) && !fs::is_directory(output_dir)) {
        cout << "ERROR: " << dir << " is not a directory...\n";
        return false;
    }

    if (!fs::exists(output_dir)) {
        return fs::create_directories(output_dir);
    }

    return true;
}

cli_flow_t cli_handler::print_diagrams_list()
{
    using std::cout;

    ostr_ << "The following diagrams are defined in the config file:\n";
    for (const auto &[name, diagram] : config.diagrams) {
        ostr_ << "  - " << name << " [" << to_string(diagram->type()) << "]";
        ostr_ << '\n';
    }

    return cli_flow_t::kExit;
}

cli_flow_t cli_handler::print_diagram_templates()
{
    using std::cout;

    if (!config.diagram_templates) {
        ostr_ << "No diagram templates are defined in the config file\n";
        return cli_flow_t::kExit;
    }

    ostr_ << "The following diagram templates are available:\n";
    for (const auto &[name, diagram_template] : config.diagram_templates()) {
        ostr_ << "  - " << name << " [" << to_string(diagram_template.type)
              << "]";
        if (!diagram_template.description.empty())
            ostr_ << ": " << diagram_template.description;
        ostr_ << '\n';
    }

    return cli_flow_t::kExit;
}

cli_flow_t cli_handler::print_diagram_template(const std::string &template_name)
{
    if (!config.diagram_templates ||
        config.diagram_templates().count(template_name) == 0) {
        ostr_ << "No such diagram template: " << template_name << "\n";
        return cli_flow_t::kError;
    }

    for (const auto &[name, diagram_template] : config.diagram_templates()) {
        if (template_name == name) {
            ostr_ << diagram_template.jinja_template << "\n";
            return cli_flow_t::kExit;
        }
    }

    return cli_flow_t::kError;
}

cli_flow_t cli_handler::create_config_file()
{
    namespace fs = std::filesystem;

    fs::path config_file{"./.clang-uml"};

    if (fs::exists(config_file)) {
        ostr_ << "ERROR: .clang-uml file already exists\n";
        return cli_flow_t::kError;
    }

    YAML::Emitter out;
    out.SetIndent(2);
    out << YAML::BeginMap;
    out << YAML::Comment("Change to directory where compile_commands.json is");
    out << YAML::Key << "compilation_database_dir" << YAML::Value << ".";
    out << YAML::Newline
        << YAML::Comment("Change to directory where diagram should be written");
    out << YAML::Key << "output_directory" << YAML::Value << "docs/diagrams";
    out << YAML::Key << "diagrams" << YAML::Value;
    out << YAML::BeginMap;
    out << YAML::Key << "example_class_diagram" << YAML::Value;
    out << YAML::BeginMap;
    out << YAML::Key << "type" << YAML::Value << "class";
    out << YAML::Key << "glob" << YAML::Value;
    out << YAML::BeginSeq << "src/*.cpp" << YAML::EndSeq;
    out << YAML::Key << "using_namespace" << YAML::Value;
    out << YAML::BeginSeq << "myproject" << YAML::EndSeq;
    out << YAML::Key << "include";
    out << YAML::BeginMap;
    out << YAML::Key << "namespaces";
    out << YAML::BeginSeq << "myproject" << YAML::EndSeq;
    out << YAML::EndMap;
    out << YAML::Key << "exclude";
    out << YAML::BeginMap;
    out << YAML::Key << "namespaces";
    out << YAML::BeginSeq << "myproject::detail" << YAML::EndSeq;
    out << YAML::EndMap;
    out << YAML::EndMap;
    out << YAML::EndMap;
    out << YAML::EndMap;
    out << YAML::Newline;

    std::ofstream ofs(config_file);
    ofs << out.c_str();
    ofs.close();

    return cli_flow_t::kExit;
}

cli_flow_t cli_handler::add_config_diagram(
    clanguml::common::model::diagram_t type,
    const std::string &config_file_path, const std::string &name)
{
    namespace fs = std::filesystem;

    fs::path config_file{config_file_path};

    if (!fs::exists(config_file)) {
        std::cerr << "ERROR: " << config_file_path << " file doesn't exists\n";
        return cli_flow_t::kError;
    }

    YAML::Node doc = YAML::LoadFile(config_file.string());

    for (YAML::const_iterator it = doc["diagrams"].begin();
         it != doc["diagrams"].end(); ++it) {
        if (it->first.as<std::string>() == name) {
            std::cerr << "ERROR: " << config_file_path
                      << " file already contains '" << name << "' diagram";
            return cli_flow_t::kError;
        }
    }

    if (type == clanguml::common::model::diagram_t::kClass) {
        doc["diagrams"][name]["type"] = "class";
        doc["diagrams"][name]["glob"] = std::vector<std::string>{{"src/*.cpp"}};
        doc["diagrams"][name]["using_namespace"] =
            std::vector<std::string>{{"myproject"}};
        doc["diagrams"][name]["include"]["namespaces"] =
            std::vector<std::string>{{"myproject"}};
        doc["diagrams"][name]["exclude"]["namespaces"] =
            std::vector<std::string>{{"myproject::detail"}};
    }
    else if (type == clanguml::common::model::diagram_t::kSequence) {
        doc["diagrams"][name]["type"] = "sequence";
        doc["diagrams"][name]["glob"] = std::vector<std::string>{{"src/*.cpp"}};
        doc["diagrams"][name]["combine_free_functions_into_file_participants"] =
            true;
        doc["diagrams"][name]["using_namespace"] =
            std::vector<std::string>{{"myproject"}};
        doc["diagrams"][name]["include"]["paths"] =
            std::vector<std::string>{{"src"}};
        doc["diagrams"][name]["exclude"]["namespaces"] =
            std::vector<std::string>{{"myproject::detail"}};
        doc["diagrams"][name]["start_from"] =
            std::vector<std::map<std::string, std::string>>{
                {{"function", "main(int,const char **)"}}};
    }
    else if (type == clanguml::common::model::diagram_t::kPackage) {
        doc["diagrams"][name]["type"] = "package";
        doc["diagrams"][name]["glob"] = std::vector<std::string>{{"src/*.cpp"}};
        doc["diagrams"][name]["using_namespace"] =
            std::vector<std::string>{{"myproject"}};
        doc["diagrams"][name]["include"]["namespaces"] =
            std::vector<std::string>{{"myproject"}};
        doc["diagrams"][name]["exclude"]["namespaces"] =
            std::vector<std::string>{{"myproject::detail"}};
    }
    else if (type == clanguml::common::model::diagram_t::kInclude) {
        doc["diagrams"][name]["type"] = "include";
        doc["diagrams"][name]["glob"] = std::vector<std::string>{{"src/*.cpp"}};
        doc["diagrams"][name]["relative_to"] = ".";
        doc["diagrams"][name]["include"]["paths"] =
            std::vector<std::string>{{"src"}};
    }

    YAML::Emitter out;
    out.SetIndent(2);

    out << doc;
    out << YAML::Newline;

    std::ofstream ofs(config_file);
    ofs << out.c_str();
    ofs.close();

    return cli_flow_t::kExit;
}

cli_flow_t cli_handler::add_config_diagram_from_template(
    const std::string &config_file_path, const std::string &template_name,
    const std::vector<std::string> &template_variables)
{
    if (!config.diagram_templates ||
        !(config.diagram_templates().find(template_name) !=
            config.diagram_templates().end())) {
        std::cerr << "ERROR: No such diagram template: " << template_name
                  << "\n";
        return cli_flow_t::kError;
    }

    // First, try to render the template using inja and create a YAML node from
    // it
    inja::json ctx;
    for (const auto &tv : template_variables) {
        const auto var = util::split(tv, "=");
        if (var.size() != 2) {
            std::cerr << "ERROR: Invalid template variable " << tv << "\n";
            return cli_flow_t::kError;
        }

        ctx[var.at(0)] = var.at(1);
    }

    auto diagram_template_str =
        config.diagram_templates().at(template_name).jinja_template;

    YAML::Node diagram_node;

    try {
        auto diagram_str = inja::render(diagram_template_str, ctx);
        diagram_node = YAML::Load(diagram_str);
    }
    catch (inja::InjaError &e) {
        std::cerr << "ERROR: Failed to generate diagram template '"
                  << template_name << "': " << e.what() << "\n";
        return cli_flow_t::kError;
    }
    catch (YAML::Exception &e) {
        std::cerr << "ERROR: Rendering diagram template '" << template_name
                  << "' resulted in invalid YAML: " << e.what() << "\n";
        return cli_flow_t::kError;
    }

    namespace fs = std::filesystem;

    fs::path config_file{config_file_path};

    if (!fs::exists(config_file)) {
        std::cerr << "ERROR: " << config_file_path << " file doesn't exists\n";
        return cli_flow_t::kError;
    }

    YAML::Node doc = YAML::LoadFile(config_file.string());

    const auto diagram_name = diagram_node.begin()->first.as<std::string>();
    doc["diagrams"][diagram_name] = diagram_node.begin()->second;

    YAML::Emitter out;
    out.SetIndent(2);

    out << doc;
    out << YAML::Newline;

    std::ofstream ofs(config_file);
    ofs << out.c_str();
    ofs.close();

    return cli_flow_t::kExit;
}

cli_flow_t cli_handler::print_config()
{
    YAML::Emitter out;
    out.SetIndent(2);

    out << config;
    out << YAML::Newline;

    ostr_ << out.c_str();

    return cli_flow_t::kExit;
}
} // namespace clanguml::cli