/*
parolottero
Copyright (C) 2021-2025 Salvo "LtWorf" Tomaselli

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

This program 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 Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

author Salvo "LtWorf" Tomaselli <tiposchi@tiscali.it>
*/

#include "language.h"

#include <QDebug>
#include <QRandomGenerator>


typedef struct {
    char magic[8];

    uint32_t format;
    uint32_t version;

    uint32_t words_positions_offset;
    uint32_t words_position_size;

    uint32_t letters_offset;
    uint32_t letters_size;

    uint32_t name_offset;

} header_t;

typedef struct {
    char letter[6];
    uint8_t score;
    uint8_t vowel;
} letter_t;


/**
 * @brief Language::check_languagefile
 *
 * Does a number of checks on the language file and
 * aborts if one of them fails.
 */
void Language::check_languagefile() {
    const char* errstr = nullptr;

    header_t *header = (header_t*)this->map_ptr;

    // Check for corruption in the file
    // Check that the mmap worked
    if (this->map_ptr == nullptr) {
        errstr = "mmap failed";
        goto err;
    }
    // Check that the file contains the header
    if (this->map_size < (qint64)sizeof(header_t)) {
        errstr = "Language file is too small";
        goto err;
    }
    // Check the header magic
    if (memcmp(header->magic, "PARODICT", sizeof(header->magic)) != 0) {
        errstr = "Language file is not a Parolottero dictionary";
        goto err;
    }
    // Check the sizes
    if ((qint64)(header->words_positions_offset + (header->words_position_size * sizeof(uint32_t))) > this->map_size) {
        errstr = "Index array spans outside of the file";
        goto err;
    }
    if ((qint64)(header->letters_offset + (header->letters_size * sizeof(letter_t))) > this->map_size) {
        errstr = "Letters array spans outside of the file";
        goto err;
    }
    if (header->name_offset > this->map_size) {
        errstr = "Language name spans outside of the file";
        goto err;
    }
    if (header->words_positions_offset < sizeof(header_t) || header->letters_offset < sizeof(header_t) || header->name_offset < sizeof(header_t)) {
        errstr = "Wrong offset";
        goto err;
    }
    if (header->letters_size == 0 || header->words_position_size == 0) {
        errstr = "empty array";
        goto err;
    }
    // Check that the last byte is null
    if (this->map_ptr[this->map_size - 1] != 0) {
        errstr = "file does not end with a null byte";
        goto err;
    }

    return;
err:
    qDebug() << errstr;
    abort();
}

/**
 * @brief Language::Language
 * @param langfile: File with the language definition
 * @param parent
 *
 * Class representing a language.
 */
Language::Language(QFile &langfile, QObject *parent) : QObject(parent) {

    this->file = new QFile(this);
    this->file->setFileName(langfile.fileName());
    this->file->open(QIODevice::ReadOnly);

    this->map_ptr = this->file->map(0,this->map_size = this->file->size());

    this->check_languagefile();

    header_t *header = (header_t*)this->map_ptr;

    this->name = QString::fromUtf8((char*)(this->map_ptr + header->name_offset), -1);

    letter_t* letters = (letter_t*)(this->map_ptr + header->letters_offset);
    for (uint32_t i = 0; i < header->letters_size; i++) {
        QString l = QString::fromUtf8(letters[i].letter, -1);
        this->letters.append(l);
        if (letters[i].vowel)
            this->vowels.append(l);
        this->score[l] = letters[i].score;
    }
}

/**
 * @brief Language::is_word
 * @param word
 * @return
 *
 * Check if a word is in the language.
 *
 * It must be passed lowercase
 */
bool Language::is_word(QString word) {
    header_t *header = (header_t*)this->map_ptr;

    uint32_t *indices = (uint32_t*)(this->map_ptr + header->words_positions_offset);

    uint32_t low = 0;
    uint32_t high = header->words_position_size;

    while (high != low) {
        uint32_t index = ((high - low) / 2) + low;

        int c = word.compare(this->string_at_offset(indices[index]));

        if (c == 0)
            return true;
        else if (c > 0)
            if (low != index)
                low = index;
            else
                low++;
        else if (c < 0)
            high = index;
    }

    return false;
}

/**
 * @brief Language::is_prefix
 * @param prefix
 * @return
 *
 * Check if the language contains words that start with the given prefix.
 *
 */
bool Language::is_prefix(QString prefix) {
    header_t *header = (header_t*)this->map_ptr;

    uint32_t *indices = (uint32_t*)(this->map_ptr + header->words_positions_offset);

    uint32_t low = 0;
    uint32_t high = header->words_position_size;

    while (high != low) {
        uint32_t index = ((high - low) / 2) + low;

        auto word_at_index = this->string_at_offset(indices[index]);

        if (word_at_index.startsWith(prefix)) {
            return true;
        }

        int c = prefix.compare(word_at_index);

        if (c > 0)
            if (low != index)
                low = index;
            else
                low++;
        else if (c < 0)
            high = index;
    }

    return false;
}

/**
 * @brief Language::string_at_offset
 * @param offset
 * @return The string contained at the given offset in the file.
 *
 * This does some basic checks to try and avoid out of bounds access.
 * It only checks for the beginning of the string, doesn't make sure that it's terminated.
 */
QString Language::string_at_offset(uint32_t offset) {
    // Abort to avoid a segfault
    if (offset > this->map_size) {
        qDebug() << "Word index causes out of bounds access";
        abort();
    }

    return QString::fromUtf8((char*)this->map_ptr + offset, -1);
}

/**
 * @brief Language::get_score
 * @param letter
 * @return
 *
 * Returns the score for a letter.
 *
 * -1 if the letter doesn't exist
 */
int Language::get_score(QString letter) {
    if (!this->score.contains(letter))
        return -1;
    return this->score[letter];
}

/**
 *
 * Returns 10 random long words
 *
 * Currently long words have to be at least 6 letters.
 *
 * @brief Language::get_long_words
 * @return
 */
QStringList Language::get_long_words(unsigned int* seed) {
    QStringList r;
    QRandomGenerator* rand;
    if (seed != nullptr) {
        QRandomGenerator rng(*seed);
        rand = &rng;
    } else {
        rand = QRandomGenerator::system();
    }

    header_t *header = (header_t*)this->map_ptr;
    uint32_t *indices = (uint32_t*)(this->map_ptr + header->words_positions_offset);

    QString word;

    while (r.size() < 11) {
        int wsize = 0;
        while (wsize < 6) {
            uint32_t index = rand->bounded(header->words_position_size);
            word = this->string_at_offset(indices[index]);
            wsize = word.size();
        }
        r.append(word);

    }

    return r;
}

uint32_t Language::get_version() {
    header_t *header = (header_t*)this->map_ptr;
    return header->version;
}
