#include <algorithm>
#include <cstring>
#include <fstream>
#include <iostream>
#include <sstream>

#include <sodium.h>

#include "Catalog.h"

Catalog::Catalog()
    : m_loaded(false)
{
    if (sodium_init() != 0)
    {
        // TODO: throw exception here
//        return 1;
    }
}

Catalog::~Catalog()
{
}

int Catalog::load(
    char* filePath,
    char* passwordBuffer,
    uint32_t passwordBufferSize,
    float* complete)
{
    const uint32_t CHUNK_SIZE{4096};
    const uint32_t saltLen{crypto_pwhash_SALTBYTES};
    const uint32_t keyLen{crypto_secretstream_xchacha20poly1305_KEYBYTES};
    int ret = 0;

    // Setup salt and key buffers
    uint8_t salt[saltLen]{};
    uint8_t key[keyLen]{};
    uint8_t inBuffer[CHUNK_SIZE + crypto_secretstream_xchacha20poly1305_ABYTES]{};
    uint8_t outBuffer[CHUNK_SIZE]{};
    uint8_t header[crypto_secretstream_xchacha20poly1305_HEADERBYTES]{};
    crypto_secretstream_xchacha20poly1305_state state;
    std::stringstream fileContents;
    std::string decryptedData;
    uint32_t decryptedDataSize = 0;

    std::string line;
    std::string valueKey;
    std::string value;
    bool firstLine = true;

    decryptedData.reserve(8 * CHUNK_SIZE);

    // Read in salt value
    FILE* ctFile = std::fopen(filePath, "rb");
    std::fread(salt, 1, saltLen, ctFile);

    // Generate key from password and salt
    if (crypto_pwhash(key, sizeof key, passwordBuffer,
                      std::strlen(passwordBuffer), salt,
                      crypto_pwhash_OPSLIMIT_INTERACTIVE,
                      crypto_pwhash_MEMLIMIT_INTERACTIVE,
                      crypto_pwhash_ALG_ARGON2ID13) != 0)
    {
        ret = ERR_PWHASH_OUT_OF_MEMORY;
        goto end_load;
    }

    // Explicitly zeroize the password buffer
    explicit_bzero(passwordBuffer, passwordBufferSize);

    // Read in header and init crypto algorithm state
    std::fread(header, 1, sizeof header, ctFile);
    if (crypto_secretstream_xchacha20poly1305_init_pull(&state, header, key) != 0)
    {
        ret = ERR_DECRYPT_INCOMPLETE_HEADER;
        goto end_load;
    }

    // Decrypt the CT file chunk by chunk
    size_t readlen;
    long long unsigned numOutBytes;
    int eof;
    uint8_t tag;
    do
    {
        readlen = fread(inBuffer, 1, sizeof inBuffer, ctFile);
        eof = feof(ctFile);

        int rc = crypto_secretstream_xchacha20poly1305_pull(&state, outBuffer,
                                                            &numOutBytes,
                                                            &tag,inBuffer,
                                                            readlen, nullptr,
                                                            0);
        if (rc != 0)
        {
            ret = ERR_DECRYPT_CORRUPTED_CHUNK;
            goto end_load;
        }

        if (tag == crypto_secretstream_xchacha20poly1305_TAG_FINAL && ! eof)
        {
            ret = ERR_DECRYPT_PREMATURE_END;
            goto end_load;
        }

        // Append the decryted buffer to the decrypted data
        decryptedData.resize(decryptedDataSize + numOutBytes);
        std::memcpy(decryptedData.data() + decryptedDataSize, outBuffer, numOutBytes);
        decryptedDataSize += numOutBytes;
    }
    while (! eof);

    // Zero out the crypto buffers that are no longer needed
    explicit_bzero(salt, saltLen);
    explicit_bzero(key, keyLen);
    explicit_bzero(inBuffer, CHUNK_SIZE);
    explicit_bzero(outBuffer, CHUNK_SIZE);

    // Create a stringstream in order to iterate line by line
    fileContents << decryptedData;

    // Iterate line by line and parse the simple file format
    while (std::getline(fileContents, line))
    {
        if (line.size() == 0)
        {
            // Debuggery
//            std::cout << "KEY: " << valueKey << std::endl;
//            std::cout << "VAL: " << value << "\n" << std::endl;

            m_catalog.push_back({valueKey, value});
            firstLine = true;
        }
        else
        {
            if (firstLine)
            {
                valueKey.clear();
                value.clear();

                valueKey.swap(line);
                firstLine = false;
            }
            else
            {
                value += line + "\n";
            }
        }
    }

    // Debuggery
//    std::cout << "KEY: " << valueKey << std::endl;
//    std::cout << "VAL: " << value << "\n" << std::endl;
    m_catalog.push_back({valueKey, value});

    sort();

    // Now we loaded
    m_loaded = true;



    // End of the line
end_load:
    std::fclose(ctFile);
    return ret;
}


void Catalog::setAllVisible()
{
    for (auto& item : m_catalog)
    {
        item.setVisibilty(true);
    }
}

void Catalog::filter(const std::string& criteria)
{
    // TODO: would be nice if there were a more efficient way to do this
    for (auto& item : m_catalog)
    {
        if (item.match(criteria))
        {
            item.setVisibilty(true);
        }
        else
        {
            item.setVisibilty(false);
        }
    }
}

void Catalog::sort()
{
    // Sort the catalog (A -> Z) for display purposes
    std::sort(
            m_catalog.begin(),
            m_catalog.end(),
            [](auto const& lhs, auto const& rhs)
            {
                return lhs.getLcKey() < rhs.getLcKey();
            });
}