// Copyright 2022, FOSS-VG Developers and Contributers
//
// Author(s):
//  Jocadbz
//
// 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, version 3.
//
// 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
// version 3 along with this program.
// If not, see https://www.gnu.org/licenses/agpl-3.0.en.html

#include <bitset>
#include <iomanip>
#include <iostream>
#include <vector>
#include <tinyutf8/tinyutf8.h>
#include <zlib/zlib.h>
#include <cstring>

#include "../lib/cli.hpp"
#include "../lib/file.hpp"

#define EXIT_SUCCESS 0
#define EXIT_RUNTIME 1
#define EXIT_USAGE   2

#include <iostream>
#include <fstream>
#include <vector>
#include <zlib.h>

#define CHUNK_SIZE 16384 // Chunk size


std::vector<char> compressData(const char* data, int size) {
    z_stream zs;
    memset(&zs, 0, sizeof(zs));

    if (deflateInit(&zs, Z_DEFAULT_COMPRESSION) != Z_OK)
        throw(std::runtime_error("deflateInit failed while compressing."));

    zs.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(data));
    zs.avail_in = size;

    int ret;
    char outbuffer[CHUNK_SIZE];
    std::vector<char> compressedData;

    do {
        zs.next_out = reinterpret_cast<Bytef*>(outbuffer);
        zs.avail_out = CHUNK_SIZE;

        ret = deflate(&zs, Z_FINISH);

        if (compressedData.size() < zs.total_out)
            compressedData.insert(compressedData.end(), outbuffer, outbuffer + CHUNK_SIZE - zs.avail_out);
    } while (ret == Z_OK);

    deflateEnd(&zs);

    if (ret != Z_STREAM_END)
        throw(std::runtime_error("Error while compressing: " + std::to_string(ret)));

    return compressedData;
}


std::vector<char> decompressData(const char* data, int size) {
    z_stream zs;
    memset(&zs, 0, sizeof(zs));

    if (inflateInit(&zs) != Z_OK)
        throw(std::runtime_error("inflateInit failed while decompressing."));

    zs.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(data));
    zs.avail_in = size;

    int ret;
    char outbuffer[CHUNK_SIZE];
    std::vector<char> decompressedData;

    do {
        zs.next_out = reinterpret_cast<Bytef*>(outbuffer);
        zs.avail_out = CHUNK_SIZE;

        ret = inflate(&zs, 0);

        if (decompressedData.size() < zs.total_out)
            decompressedData.insert(decompressedData.end(), outbuffer, outbuffer + CHUNK_SIZE - zs.avail_out);
    } while (ret == Z_OK);

    inflateEnd(&zs);

    if (ret != Z_STREAM_END)
        throw(std::runtime_error("Error while decompressing: " + std::to_string(ret)));

    return decompressedData;
}

/*
Finnaly, the main file
⠀⠀⠀⠀⠀⠀⠀⢠⣤⣄⣀⣠⣤⣶⣿⣯⣿⣽⣾⣷⣶⣶⡦⣄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢀⣿⣟⣯⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣟⣦⣄⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⣿⣿⣿⣾⡷⣄⠀⠀⠀⠀⠀
⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣾⣿⣿⣿⣷⣿⣿⣿⣿⣿⣿⣳⡀⠀⠀⠀
⠀⠀⢠⣿⣿⣽⣿⢿⣿⣿⣿⣿⡿⣿⣿⣿⣿⢿⡿⣽⣿⣿⡿⣿⣿⣏⣿⣿⣿⣟⣿⣳⡄⠀⠀
⠀⠀⣾⣿⣿⣽⣾⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⢿⣿⣾⢿⣷⡿⣿⣿⣷⢿⣿⣿⣷⡿⣷⠀⠀
⠀⢸⣿⣿⣿⣿⣟⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣳⣿⣟⣿⣿⣿⣻⣽⣿⣿⡽⡇⠀
⠀⡿⣿⣿⣿⣿⣾⣿⣿⣿⣿⢿⣿⣿⣿⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣽⡀
⢸⣿⣿⣿⣿⣿⣿⣿⣏⠀⠈⠀⣿⣿⣿⣿⣿⣿⣟⣿⣿⣿⣿⣿⣿⣿⡿⣿⣿⣿⣿⣿⣿⢿⡇
⢨⣿⣿⣿⣿⣽⣿⣿⣇⣀⡤⠤⣟⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⣿⣿⣿⣿⠎⠀
⢰⣿⡿⣿⣿⣿⣿⣿⠃⠀⠀⠐⠃⠘⠙⢹⠿⡘⣿⠟⠉⢹⡂⠿⡞⣿⣿⣿⣿⣿⣿⣿⣿⠂⠀
⠀⣿⣿⣿⣿⣿⣿⡞⠅⣀⣀⣀⣀⠉⠓⠚⠀⠁⡏⠀⢛⠋⢀⣀⣀⣋⠸⣿⣿⣿⣿⣿⣿⠃⠀
⠀⣷⣟⣿⣿⣿⣿⣆⠠⡟⣿⣿⣿⡿⠦⠀⠀⠀⠀⠀⠠⢿⣿⣿⣿⠻⢇⣿⣿⣿⣿⣿⣿⠀⠀
⠀⠈⢸⣻⣿⣿⣿⢯⡃⢀⠹⣿⡿⠗⠀⠀⠀⠀⠀⠀⠀⠘⢿⣿⠏⣈⠈⣿⣿⣿⣿⣿⣿⠀⠀
⠀⠀⠈⣿⣿⣿⣿⣇⡗⠈⠉⠉⠉⠉⠀⠀⢐⡀⠀⠀⠀⠈⠉⠉⠉⠑⢸⣸⣿⡟⣿⣿⡟⠀⠀
⠀⠀⠀⢻⣿⣽⣿⡏⢿⡆⠀⠀⠀⠀⠀⠀⠸⢆⡄⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⡿⠇⠀⠀
⠀⠀⠀⠈⠿⣿⢿⡇⠀⠙⢦⡀⠀⠀⠀⠐⠲⠤⠔⠂⠀⠀⠀⢀⣴⣾⣿⣿⣿⣿⣿⡟⠀⠀⠀
⠀⠀⠀⠀⠀⠘⣿⣇⠀⠀⠀⠙⠢⣄⡀⠀⠀⠀⠀⣀⣐⣺⣵⣿⣿⣿⣿⣿⣿⣿⣿⠇⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠙⠋⠀⠀⠀⠀⠀⢹⢨⠑⠲⠤⠞⠋⠉⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡼⠈⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣀⣠⢤⡠⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠿⣿⣿⣿⣿⣇⠀⠀⠀⠀⠀
⠀⠀⣠⠔⠒⠉⠁⠀⠀⠘⠶⣲⠄⠀⠀⠈⠀⠀⠀⢠⢀⠀⠀⣀⠤⢻⣿⣿⣿⡏⠐⠲⣄⠀⠀
⠀⢰⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠓⠒⠚⢕⡀⠀⠀⣠⠖⠒⠚⠁⠀⢾⣿⣿⣿⡁⠀⠀⠨⡆⠀
⠀⢸⠀⠀⠀⠒⠒⠒⠒⠒⠂⠀⠐⠚⠑⠆⠀⠀⠒⠓⠀⠀⠀⠀⠀⣿⣿⣿⣿⡆⠀⠀⠀⣇⠀
⠀⠈⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠁⠀
*/

int main(int argc, char* argv[]) {
    std::vector<CLI::Flag> flags;
    flags.push_back(CLI::Flag('c', "compress",        "compress a file"));
    flags.push_back(CLI::Flag('d', "decompress",         "descompress a file"));
    std::vector<CLI::Option> options;
    std::vector<CLI::Argument> arguments;
    arguments.push_back(CLI::Argument("FILE", "path of the file to compress/decompress"));
    CLI::ArgumentsParser cliParser = CLI::ArgumentsParser(argc, argv, flags, options, arguments, "Compress or decompress files using zlib");

    if (cliParser.getFlag("help").value) {
        std::cout << cliParser.getUsage() << std::endl;
        return EXIT_SUCCESS;
    }

    if (cliParser.getFlag("license").value){
        std::cout
            << "Copyright 2022, FOSS-VG Developers and Contributers\n"
            << "\n"
            << "ZlibUtil is part of the FOSS-VG development tool suite.\n"
            << "\n"
            << "This program is free software: you can redistribute it and/or modify it\n"
            << "under the terms of the GNU Affero General Public License as published\n"
            << "by the Free Software Foundation, version 3.\n"
            << "\n"
            << "This program is distributed in the hope that it will be useful,\n"
            << "but WITHOUT ANY WARRANTY; without even the implied\n"
            << "warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"
            << "See the GNU Affero General Public License for more details.\n"
            << "\n"
            << "You should have received a copy of the GNU Affero General Public License\n"
            << "version 3 along with this program.\n"
            << "If not, see https://www.gnu.org/licenses/agpl-3.0.en.html"
            << std::endl;
        return EXIT_SUCCESS;
    }

    if (cliParser.wrongUsage) {
        std::cout << cliParser.getUsage() << std::endl;
        return EXIT_USAGE;
    }


    ErrorOr<File*> filePointer = File::open(cliParser.getArgument(0).value, 'r');
    if (filePointer.isError) {
        std::cout << "Failed to open file: " << cliParser.getArgument(0).value << std::endl;
        return EXIT_RUNTIME;
    }
    File* file = filePointer.value;
    File *writeFile;

    if (cliParser.getFlag("compress").value) {
        std::vector<uint8_t> bytes = file->read(file->size.value).value; // this is what you get from lib/file IIRC
        std::vector<char> differentBytes = std::vector<char>(bytes.begin(), bytes.end());

        std::vector<char> compressed = compressData(differentBytes.data(), file->size.value);
        std::vector<unsigned char> unsigneddata = std::vector<unsigned char>(compressed.begin(), compressed.end());
        writeFile = File::open(cliParser.getArgument(0).value + ".compressed", 'w').value;
        writeFile->write(unsigneddata);
        writeFile->close();
   	} else if (cliParser.getFlag("decompress").value) {
        std::vector<uint8_t> bytes = file->read(file->size.value).value; // this is what you get from lib/file IIRC
        std::vector<char> differentBytes = std::vector<char>(bytes.begin(), bytes.end());

        std::vector<char> compressed = decompressData(differentBytes.data(), file->size.value);
        std::vector<unsigned char> unsigneddata = std::vector<unsigned char>(compressed.begin(), compressed.end());

        writeFile = File::open(cliParser.getArgument(0).value + ".uncompressed", 'w').value;
        writeFile->write(unsigneddata);
        writeFile->close();
   	}
}
