#include "../include/WordUtils.h"
#include <string>
#include <fstream>
#include <iostream>
#include <algorithm>
#include <cctype>
#include <cstring>
#include <random>
#include <vector>
#include <string>
#include <unordered_map>
#include <algorithm>
#include <iterator>

unordered_map<string, unordered_map<string, int>> WordUtils::load_data(const string& path) {
    vector<string> data = split_string(load_file(path), HTML);

    unordered_map<string, unordered_map<string, int>> word_frequencies = {};

    for (long unsigned int i = 0; i + 1 < data.size(); i++) {
        ranges::transform(data[i], data[i].begin(), [](const unsigned char c){ return tolower(c); });
        ranges::transform(data[i+1], data[i+1].begin(), [](const unsigned char c){ return tolower(c); });
        word_frequencies[data[i]][data[i+1]]++;
    }

    return word_frequencies;
}

string WordUtils::load_css(const string& path) {
    return load_file(path);
}

vector<string> WordUtils::predict_next_word(const string& input, const unordered_map<string, unordered_map<string, int>>& word_frequencies, const size_t count) {
    const auto it = word_frequencies.find(input);
    if (it == word_frequencies.end()) return {input};

    const auto& nextWords = it->second;

    const size_t limit = min(count, nextWords.size());
    vector<pair<string, int>> topWords(limit); // preallocate exact size

    const auto middle = topWords.begin() + limit;

    // partial_sort_copy into preallocated vector
    const auto end_it = partial_sort_copy(
        nextWords.begin(), nextWords.end(),
        topWords.begin(), middle,
        [](const auto& a, const auto& b) {
            return a.second > b.second; // sort descending
        }
    );

    // Convert pairs to strings
    vector<string> results;
    results.reserve(distance(topWords.begin(), end_it));
    for (auto it_ = topWords.begin(); it_ != end_it; ++it_) {
        results.emplace_back(it_->first);
    }

    return results;
}

string WordUtils::create_tag(const unordered_map<string, unordered_map<string, int>>& word_frequencies, const char& hash) {
    constexpr unsigned char predict_num = 5;

    const string start_words[] = {"the", "but", "with", "all", "over", "and", "our", "as", "a", "to"};

    string tags_1[27];

    minstd_rand generator(hash);

    uniform_int_distribution<size_t> start_dist(0, start_words->length() - 1);
    const size_t first_index = start_dist(generator);

    const string& first_word = start_words[first_index];
    vector<string> temp_words = predict_next_word(first_word, word_frequencies, predict_num);

    start_dist.param(uniform_int_distribution<size_t>::param_type(0, temp_words.size() - 1));

    tags_1[0] = first_word;
    tags_1[1] = temp_words[start_dist(generator)];

    uniform_int_distribution<size_t> next_word_dist; // re-used, param set each loop

    // Words inside the <p> tag
    for (unsigned short j = 0; j < 25; j++) {
        temp_words = predict_next_word(tags_1[j], word_frequencies, predict_num);

        next_word_dist.param(uniform_int_distribution<size_t>::param_type(0, temp_words.size() - 1));

        tags_1[j + 2] = temp_words[next_word_dist(generator)];
    }

    string temp_string = "<p>";

    for (const auto & tag : tags_1) {
        temp_string += tag;
        temp_string += " ";
    }

    temp_string += ".</p>\n";

    return temp_string;
}


string WordUtils::create_link(const unordered_map<string, unordered_map<string, int>>& word_frequencies, const unsigned long hash) {
    constexpr unsigned char predict_num = 10;

    const string start_words[9] = {"the", "but", "with", "all", "over", "and", "as", "a", "to"};

    string tags_1[9];

    minstd_rand generator(hash);

    uniform_int_distribution<unsigned short> outer_distribution(0, start_words->size() - 1);

    const string lol = start_words[outer_distribution(generator)];
    vector<string> temp_words = predict_next_word(lol, word_frequencies, predict_num);

    uniform_int_distribution<unsigned short> outer_2_distribution(0, temp_words.size() - 1);

    tags_1[0] = lol;
    tags_1[1] = temp_words[outer_2_distribution(generator)];

    // Words inside the <p> tag
    for (unsigned short j = 0; j < 7; j++) {
        temp_words = predict_next_word(tags_1[j], word_frequencies, predict_num);

        uniform_int_distribution<unsigned short> inner_distribution(0, temp_words.size() - 1);

        tags_1[j + 2] = temp_words[inner_distribution(generator)];
    }

    string temp_string_2 = "<a href=\"";
    string temp_string;

    for (const auto & tag : tags_1) {
        temp_string += tag;
    }

    temp_string_2 += temp_string;

    temp_string_2 += "\">";

    temp_string_2 += temp_string;

    temp_string_2 += ".</a>\n";

    return temp_string_2;
}

string WordUtils::create_image(const string& image) {
    string temp_1 = R"(<img loading="lazy" src="/)";
    temp_1 += image;
    temp_1 += ".avif\">";
    return temp_1;
}

vector<string> WordUtils::split_string(const string& input, const data_type type) {
    vector<string> data;

    const char* delimiters = nullptr;

    switch (type) {
        case HTML: delimiters = " .,!?;:()\n"; break;
        case CSS:  delimiters = "}";           break;
        case TEXT: delimiters = "\n";          break;
        case IP:   delimiters = ".";           break;
        default:   delimiters = " ";           break;
    }

    size_t start = 0;
    const size_t len = input.length();

    while (start < len) {
        size_t end = input.find_first_of(delimiters, start);

        if (end == string::npos) [[unlikely]] end = len;

        if (end > start) [[likely]] {
            data.emplace_back(input.begin() + start, input.begin() + end);
        }

        start = end + 1;
    }

    return data;
}

string WordUtils::load_file(const string& path) {
    ifstream file(path);
    string temp;
    string data;

    while (getline(file, temp)) {
        data += temp;
    }

    file.close();

    return data;
}

string WordUtils::extract_url(const string& input) {
    if (input.empty()) return "";

    const unsigned short first_line_end = input.find('\n');
    if (first_line_end == string::npos) [[unlikely]] return "";

    string first_line = input.substr(0, first_line_end);

    const unsigned short method_end = first_line.find(' ');
    if (method_end == string::npos) [[unlikely]] return "";

    const unsigned short path_end = first_line.find(' ', method_end + 1);
    if (path_end == string::npos) [[unlikely]] return "";

    return first_line.substr(method_end + 1, path_end - method_end - 1);
}

string WordUtils::extract_user_agent(const string& input) {
    if (input.empty()) [[unlikely]] return "";

    vector<string> lines = split_string(input, TEXT);

    const unsigned short first_line_end = lines[2].find('\n');
    if (first_line_end == string::npos) [[unlikely]] return "";

    string first_line = lines[2].substr(12, first_line_end);

    return first_line;
}

string WordUtils::extract_image_name(const string& input) {
    const unsigned short first_line_end = input.find('f');

    if (first_line_end == string::npos) [[unlikely]] return "";

    string first_line = input.substr(1, first_line_end);

    return first_line;
}

bool WordUtils::contains_image(const string& input) {
    const unsigned short first_line_end = input.find('.');

    if (first_line_end == 65535) [[unlikely]] return false;

    string type = input.substr(first_line_end + 1, 5);

    if (type == "avif") return true;

    return false;
}

unsigned int WordUtils::hash_url(const string& input) {
    // A very little modified version of simple hash.

    unsigned int hash = 0;

    for (const char c : input) {
        hash += static_cast<unsigned int>(c) * 597301;
    }

    return hash;
}

unsigned long WordUtils::djb2Hash(const string& str) {
    unsigned long hash = 5381;

    for (const char c : str) {
        hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
    }

    return hash;
}

unsigned long WordUtils::fnv1aHash(const std::string& str) {
    constexpr unsigned long prime = 16777619; // A commonly used prime
    unsigned long hash = 2166136261; // Initial prime value

    for (const char c : str) {
        hash ^= c;
        hash *= prime;
    }

    return hash;
}