// Mide las cuatro formas de guardar el diccionario, y las dos tablas con varios factores de carga.
// bench_spell <diccionario> <texto>. Se compila con -O2 y sin sanitizers.
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <random>
#include <utility>
#include <string>
#include <vector>

#include "bench.h"
#include "bytes_counter.h"
#include "dictionaries.h"
#include "load_words.h"
#include "spell_checker.h"

namespace {

constexpr int kLoadReps = 3;
constexpr int kAskReps = 5;

// Toda pregunta que revisar el texto le hace al diccionario, para medir solo las búsquedas.
std::vector<std::string> build_queries(const std::vector<std::string>& entries,
                                       const std::vector<std::string>& words) {
    const SortedDict dict(entries);
    std::vector<std::string> queries = words;
    std::vector<std::string> unknown;
    for (const std::string& word : words) {
        if (!dict.contains(word)) {
            unknown.push_back(word);
        }
    }
    std::sort(unknown.begin(), unknown.end());
    unknown.erase(std::unique(unknown.begin(), unknown.end()), unknown.end());
    for (const std::string& word : unknown) {
        for (std::string& candidate : one_edit_candidates(word)) {
            if (!candidate.empty()) {
                queries.push_back(std::move(candidate));
            }
        }
    }
    return queries;
}

template <typename Dict>
double ask_ns(const Dict& dict, const std::vector<std::string>& queries) {
    const double seconds = median_seconds(kAskReps, [&] {
        std::size_t found = 0;
        for (const std::string& query : queries) {
            found += dict.contains(query) ? 1 : 0;
        }
        keep(found);
    });
    return seconds * 1e9 / static_cast<double>(queries.size());
}

template <typename Build>
void measure(const char* name, const char* max_load, const std::vector<std::string>& entries,
             const std::vector<std::string>& words, const std::vector<std::string>& queries,
             Build build) {
    const double load = median_seconds(kLoadReps, [&] {
        auto dict = build();
        keep(dict.contains(entries.front()));
    });
    const std::size_t before = live_bytes();
    const auto dict = build();
    const std::size_t bytes = live_bytes() - before;
    const double check = median_seconds(kAskReps, [&] {
        keep(check_words(dict, words).size());
    });
    char real[16] = "-";
    if constexpr (requires { dict.load_factor(); }) {
        std::snprintf(real, sizeof real, "%.3f", dict.load_factor());
    }
    std::printf("%-16s %5s %6s %7.0f %8.1f %8.0f %7.1f\n", name, max_load, real, load * 1000,
                check * 1000, ask_ns(dict, queries), static_cast<double>(bytes) / (1024.0 * 1024.0));
}

// Mismas casillas y mismas preguntas, cambiando cuántas palabras se guardan: así cambia el
// factor de carga real y casi nada más.
template <template <typename> class Table>
void same_slots(const char* name, const std::vector<std::string>& shuffled,
                const std::vector<std::string>& queries, double max_load) {
    const std::size_t slots = std::size_t{1} << 20;
    for (double fraction : {0.55, 0.70, 0.80, 0.90, 0.95}) {
        const std::size_t n = static_cast<std::size_t>(fraction * static_cast<double>(slots));
        const std::vector<std::string> subset(shuffled.begin(), shuffled.begin() + static_cast<std::ptrdiff_t>(n));
        const TableDict<Table> dict(subset, max_load);
        std::printf("%-16s %6.3f %8.0f\n", name, dict.load_factor(), ask_ns(dict, queries));
    }
}

}  // namespace

int main(int argc, char** argv) {
    if (argc < 3) {
        std::fprintf(stderr, "uso: bench_spell <diccionario> <texto>\n");
        return 2;
    }
    const std::vector<std::string> entries = load_lines(argv[1]);
    const std::vector<std::string> words = extract_words(load_text(argv[2]));
    const std::vector<std::string> queries = build_queries(entries, words);

    std::printf("diccionario: %zu palabras. texto: %zu palabras. preguntas al revisar: %zu.\n",
                entries.size(), words.size(), queries.size());
    std::printf("cargar y revisar en ms; preguntar en ns por pregunta; MB reservados por el diccionario.\n");
    std::printf("Mediana de %d cargas y de %d revisiones o rondas de preguntas.\n\n", kLoadReps, kAskReps);
    std::printf("%-16s %5s %6s %7s %8s %8s %7s\n", "diseño", "máx", "real", "cargar", "revisar", "pregunta", "MB");

    measure("arreglo ordenado", "-", entries, words, queries, [&] { return SortedDict(entries); });
    measure("unordered_map", "1.00", entries, words, queries, [&] { return StdDict(entries, 1.0f); });
    for (const auto& [text, alpha] : {std::pair{"0.50", 0.5}, {"1.00", 1.0}, {"2.00", 2.0}, {"4.00", 4.0}}) {
        measure("encadenamiento", text, entries, words, queries,
                [&, alpha = alpha] { return TableDict<HashMap>(entries, alpha); });
    }
    for (const auto& [text, alpha] : {std::pair{"0.25", 0.25}, {"0.50", 0.5}, {"0.70", 0.7}}) {
        measure("linear probing", text, entries, words, queries,
                [&, alpha = alpha] { return TableDict<ProbingHashMap>(entries, alpha); });
    }

    // La tabla duplica su capacidad, así que el factor real no siempre es el máximo pedido.
    // Aquí se fija: 2^20 casillas y distintas cantidades de palabras, elegidas al azar con semilla fija.
    std::vector<std::string> shuffled = entries;
    std::mt19937 rng(2026);
    std::shuffle(shuffled.begin(), shuffled.end(), rng);
    std::printf("\nMismas 2^20 casillas, mismas preguntas. ns por pregunta.\n");
    std::printf("%-16s %6s %8s\n", "diseño", "real", "pregunta");
    same_slots<HashMap>("encadenamiento", shuffled, queries, 1.0);
    same_slots<ProbingHashMap>("linear probing", shuffled, queries, 0.9995);
    return 0;
}
