// Copyright (c) 2017-2025, University of Cincinnati, developed by Henry Schreiner
// under NSF AWARD 1414736 and by the respective contributors.
// All rights reserved.
//
// SPDX-License-Identifier: BSD-3-Clause

// Code inspired by discussion from https://github.com/CLIUtils/CLI11/issues/1149

#include <algorithm>
#include <iostream>
#include <numeric>
#include <string>
#include <utility>
#include <vector>

#include <CLI/CLI.hpp>

// only works with C++14 or higher

// Levenshtein distance function code generated by chatgpt/copilot
std::size_t levenshteinDistance(const std::string &s1, const std::string &s2) {
    std::size_t len1 = s1.size(), len2 = s2.size();
    if(len1 == 0 || len2 == 0) {
        return (std::max)(len1, len2);
    }
    std::vector<std::size_t> prev(len2 + 1), curr(len2 + 1);
    std::iota(prev.begin(), prev.end(), 0);  // Fill prev with {0, 1, ..., len2}

    for(std::size_t ii = 1; ii <= len1; ++ii) {
        curr[0] = ii;
        for(std::size_t jj = 1; jj <= len2; ++jj) {
            // If characters match, no substitution cost; otherwise, cost is 1.
            std::size_t cost = (s1[ii - 1] == s2[jj - 1]) ? 0 : 1;

            // Compute the minimum cost between:
            // - Deleting a character from `s1` (prev[jj] + 1)
            // - Inserting a character into `s1` (curr[jj - 1] + 1)
            // - Substituting a character (prev[jj - 1] + cost)

            curr[jj] = (std::min)({prev[jj] + 1, curr[jj - 1] + 1, prev[jj - 1] + cost});
        }
        prev = std::exchange(curr, prev);  // Swap vectors efficiently
    }
    return prev[len2];
}

// Finds the closest string from a list (modified from chat gpt code)
std::pair<std::string, std::size_t> findClosestMatch(const std::string &input,
                                                     const std::vector<std::string> &candidates) {
    std::string closest;
    std::size_t minDistance{std::string::npos};
    for(const auto &candidate : candidates) {
        std::size_t distance = levenshteinDistance(input, candidate);
        if(distance < minDistance) {
            minDistance = distance;
            closest = candidate;
        }
    }

    return {closest, minDistance};
}

void addSubcommandCloseMatchDetection(CLI::App *app, std::size_t minDistance = 3) {
    // if extras are not allowed then there will be no remaining
    app->allow_extras(true);
    // generate a list of subcommand names
    auto subs = app->get_subcommands(nullptr);
    CLI::results_t list;
    for(const auto *sub : subs) {
        if(!sub->get_name().empty()) {
            list.emplace_back(sub->get_name());
        }
        const auto &aliases = sub->get_aliases();
        if(!aliases.empty()) {
            list.insert(list.end(), aliases.begin(), aliases.end());
        }
    }
    // add a callback that runs before a final callback and loops over the remaining arguments for subcommands
    app->parse_complete_callback([app, minDistance, list = std::move(list)]() {
        for(auto &extra : app->remaining()) {
            if(!extra.empty() && extra.front() != '-') {
                auto closest = findClosestMatch(extra, list);
                if(closest.second <= minDistance) {
                    std::cout << "unmatched command \"" << extra << "\", closest match is " << closest.first << "\n";
                }
            }
        }
    });
}

/** This example demonstrates the use of close match detection to detect invalid commands that are close matches to
 * existing ones
 */
int main(int argc, const char *argv[]) {

    int value{0};
    CLI::App app{"App for testing prefix matching and close string matching"};
    // turn on prefix matching
    app.allow_subcommand_prefix_matching();
    app.add_option("-v", value, "value");

    app.add_subcommand("install", "");
    app.add_subcommand("upgrade", "");
    app.add_subcommand("remove", "");
    app.add_subcommand("test", "");
    // enable close matching for subcommands
    addSubcommandCloseMatchDetection(&app, 5);
    CLI11_PARSE(app, argc, argv);

    auto subs = app.get_subcommands();
    for(const auto &sub : subs) {
        std::cout << sub->get_name() << "\n";
    }
    return 0;
}
