#include "pack_archive.h"
#include "ni_parser.h"
#include "json.hpp"
#include "serializers.h"

#include <iostream>
#include <algorithm>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <filesystem>

PackArchive::PackArchive()
{

}

std::vector<std::string> PackArchive::GetImages()
{
    std::vector<std::string> imgList;

    for (uint32_t i = 0; i < ni_get_number_of_images(); i++)
    {
        char buffer[13];
        ni_get_image(buffer, i);
        imgList.push_back(buffer);
    }

    return imgList;
}


std::string PackArchive::GetFileName(const std::string &path)
{
    auto found = path.find_last_of("/\\");
    return path.substr(found+1);
}

std::string PackArchive::GetFileExtension(const std::string &FileName)
{
    if(FileName.find_last_of(".") != std::string::npos)
        return FileName.substr(FileName.find_last_of(".")+1);
    return "";
}

void PackArchive::ReplaceCharacter(std::string &theString, const std::string &toFind, const std::string &toReplace)
{
    std::size_t found;
    do
    {
        found = theString.find(toFind);
        if (found != std::string::npos)
        {
            theString.replace(found, 1, toReplace);
        }
    }
    while (found != std::string::npos);
}

void PackArchive::EraseString(std::string &theString, const std::string &toErase)
{
    std::size_t found;
    found = theString.find(toErase);
    if (found != std::string::npos)
    {
        theString.erase(found, toErase.size());
    }
}

std::string PackArchive::ToUpper(const std::string &input)
{
    std::string str = input;
    std::transform(str.begin(), str.end(), str.begin(), ::toupper);
    return str;
}

void PackArchive::Unzip(const std::string &filePath)
{
    std::string fileName = GetFileName(filePath);
    std::string ext = GetFileExtension(fileName);
    EraseString(fileName, "." + ext); // on retire l'extension du pack
    std::string path = ToUpper(fileName);


    if (std::filesystem::exists(path))
    {
        // 1. First delete files
        for(const std::filesystem::directory_entry& entry : std::filesystem::recursive_directory_iterator(path))
        {
            if (std::filesystem::is_regular_file(entry)) {
                std::filesystem::remove(entry.path());
            }
        }
    }
    // 2. then delete directories
    std::filesystem::remove_all(path);


    (void) Zip::Unzip(filePath, ".", "");
}

static bool endsWith(const std::string& str, const std::string& suffix)
{
    return str.size() >= suffix.size() && 0 == str.compare(str.size()-suffix.size(), suffix.size(), suffix);
}

bool StringToFile(const std::string &filePath, const std::string &data)
{
    bool success = false;
    std::ofstream outFile(filePath, std::ifstream::out);

    if (outFile.is_open())
    {
        outFile << data << std::endl;
        outFile.close();
        success = true;
    }
    return success;
}

void PackArchive::DecipherFileOnDisk(const std::string &fileName)
{
    FILE * pFile;
    long lSize = 512;
    char * buffer;
    size_t result;

    pFile = fopen ( fileName.c_str() , "rb+" );
    if (pFile==NULL) {fputs ("File error",stderr); exit (1);}

    // allocate memory to contain the whole file:
    buffer = (char*) malloc (sizeof(char)*lSize);
    if (buffer == NULL) {fprintf(stderr, "Memory error: %s", fileName.c_str()); exit (2);}

    // copy the file into the buffer:
    result = fread (buffer,1,lSize,pFile);
    if (result != lSize) {fprintf(stderr, "Reading error: %s", fileName.c_str()); exit (3);}

    /* the whole file is now loaded in the memory buffer. */

    // Decipher
    ni_decode_block512(reinterpret_cast<uint8_t *>(buffer));
    fseek (pFile , 0 , SEEK_SET); // ensure we are at the begining

    // Write back data on the disk
    fwrite (buffer , sizeof(char), lSize, pFile);

    // terminate
    fclose (pFile);
    free (buffer);
}

void WriteDataOnDisk(const std::string &fileName, const uint8_t *data, uint32_t size)
{
    FILE * pFile;
    pFile = fopen ( fileName.c_str() , "wb" );
    fwrite (data , sizeof(char), size, pFile);
    fclose (pFile);
}

void PackArchive::DecipherFiles(const std::string &directory, const std::string &suffix)
{
    for (const auto & rf : std::filesystem::directory_iterator(directory))
    {
        std::string oldFile = rf.path();
//        std::cout << oldFile << std::endl;

        DecipherFileOnDisk(oldFile);

        std::string newName = oldFile + suffix;
        rename(oldFile.c_str(), newName.c_str());
    }
}

std::vector<std::string> PackArchive::FilesToJson(const std::string &type, const uint8_t *data, uint32_t nb_elements)
{
    char res_file[13]; // 12 + \0
    std::vector<std::string> resList;

    for (int i = 0; i < nb_elements; i++)
    {
        memcpy(res_file, &data[i*12], 12);
        res_file[12] = '\0';

        std::string res_file_string(res_file);
        ReplaceCharacter(res_file_string, "\\", "/");

        resList.push_back(res_file_string);

        m_resources[type + std::to_string(i)] = res_file_string;
    }
    return resList;
}

void PackArchive::DecipherAll(const std::string &packFileName)
{
//    return;

    Unzip(packFileName);
    Load(packFileName);

    std::string path = mPackName + "/rf";
    for (const auto & entry : std::filesystem::directory_iterator(path))
    {
        std::cout << entry.path() << std::endl;
        DecipherFiles(entry.path(), ".bmp");
    }

    path = mPackName + "/sf";
    for (const auto & entry : std::filesystem::directory_iterator(path))
    {
        std::cout << entry.path() << std::endl;
        DecipherFiles(entry.path(), ".mp3");
    }

    nlohmann::json j;

    std::ofstream chip32("pack.chip32");


/*
$imageBird          DC8  "example.bmp", 8  ; data
$someConstant       DC32  12456789

; DSxx to declare a variable in RAM, followed by the number of elements
$RamData1           DV32    1 ; one 32-bit integer
$MyArray            DV8    10 ; array of 10 bytes

; label definition
.entry:   ;; comment here should work
*/


    // NI file is not ciphered
    uint8_t data[512];
    uint32_t size = ni_get_ri_block(data);
    WriteDataOnDisk(mPackName + "/ri", data, size);

    nlohmann::json jsonImages;
    {

        std::vector<std::string> lst = FilesToJson("ri", data, mNiFile.image_assets_count);

        for (auto &l : lst)
        {
            nlohmann::json obj;
            obj["file"] = l;
            obj["description"] = "";
            obj["format"] = "bmp";
            jsonImages.push_back(obj);
        }
    }
    j["images"] = jsonImages;

    size = ni_get_si_block(data);
    WriteDataOnDisk(mPackName + "/si", data, size);


    nlohmann::json jsonSounds;
    {
        nlohmann::json obj;
        std::vector<std::string> lst = FilesToJson("si", data, mNiFile.sound_assets_count);

        for (auto &l : lst)
        {
            nlohmann::json obj;
            obj["file"] = l;
            obj["description"] = "";
            obj["format"] = "mp3";
            jsonSounds.push_back(obj);
        }
    }
    j["sounds"] = jsonSounds;

    size = ni_get_li_block(data);
    WriteDataOnDisk(mPackName + "/li", data, size);


    std::vector<uint32_t> transitions;
    // each entry of the transitions array is a 32-bit integer
    for (int i = 0; i < size;)
    {
        transitions.push_back(leu32_get(&data[i]));
        i += 4;
    }

    // Transform into JSON
    node_info_t node;
    nlohmann::json jsonNodes;
    for (int i = 0; i < mNiFile.node_size; i++)
    {
        nlohmann::json jNode;
        ni_get_node_info(i, &node);

        jNode["id"] = i;
        jNode["image"] = static_cast<int>(node.current->image_asset_index_in_ri);
        jNode["sound"] = static_cast<int>(node.current->sound_asset_index_in_si);
        jNode["auto_jump"] = node.current->auto_play == 1 ? true : false;

        nlohmann::json jumpArray;

        for (int jIndex = 0; jIndex < node.current->ok_transition_number_of_options; jIndex++)
        {
            jumpArray.push_back(transitions[node.current->ok_transition_action_node_index_in_li + jIndex]);
        }

        jNode["jumps"] = jumpArray;

        // Autre cas
        if (node.current->ok_transition_number_of_options >= 5)
        {
            // For now, consider that this is a bad format
            // In the future, consider using ok_transition_selected_option_index when ok_transition_number_of_options == 10

            // 00 00 00 00   ==> ok transition à zéro
            // 0A 00 00 00   ==> nombre spécial, ou vraiment l'offset dans le fichier LI ?
            // 01 00 00 00   ==> l'index dans le fichier LI à l'offset (disons, le premier élément)


            std::cout << "!!!!!!!!!!!!!!!!!!" << std::endl;
        }

        chip32 << ".node" << std::to_string(i) << ":\r\n";
        if (node.current->image_asset_index_in_ri != 0xFFFFFFFF)
        {
            // Image index is in register R0
            chip32 << "\tmov " << "r0, " << "$ri" << std::to_string(node.current->image_asset_index_in_ri) << "\r\n";
            // print image syscall
            chip32 << "\tsyscall 1\r\n";
        }

        if (node.current->sound_asset_index_in_si != 0xFFFFFFFF)
        {
            // Image index is in register R0
            chip32 << "\tmov " << "r0, " << "$si" << std::to_string(node.current->sound_asset_index_in_si) << "\r\n";
            // print image syscall
            chip32 << "\tsyscall 2\r\n";
        }

        chip32 << "$li" << std::to_string(i) << " DC8 ";
        size = ni_get_li_block(data);
        for (int tr = 0; tr < node.current->ok_transition_number_of_options; tr++)
        {
            uint32_t val = leu32_get(&data[(node.current->ok_transition_action_node_index_in_li + tr) * 4]);
            chip32 << std::to_string(val);

            if (tr < (node.current->ok_transition_number_of_options - 1))
            {
                chip32 << ", ";
            }
        }
        chip32 << "\r\n";

        chip32 << "\tsyscall 3\r\n"; // wait select

        // TODO: tester le retour d'un wait event

        jsonNodes.push_back(jNode);
    }
    j["nodes"] = jsonNodes;
    j["code"] = mPackName;
    j["name"] = "";
    j["type"] = "lunii";

    std::ofstream o("pack.json");
    o << std::setw(4) << j << std::endl; // pretty print

    o.close();
    chip32.close();
}

bool PackArchive::Load(const std::string &filePath)
{
    bool success = false;
    mZip.Close();
    mCurrentNodeId = 0;

    std::string fileName = GetFileName(filePath);
    std::string ext = GetFileExtension(fileName);
    EraseString(fileName, "." + ext); // on retire l'extension du pack
    mPackName = ToUpper(fileName);

    std::cout << "Pack name: " << mPackName << std::endl;

    if (mZip.Open(filePath, true))
    {
        std::cout << "Number of files: " << mZip.NumberOfEntries() << std::endl;

        if (ParseNIFile(mPackName))
        {
            success = true;
            std::cout << "Parse NI file success\r\n"  << std::endl;
            ni_dump(&mNiFile);

            ni_get_node_info(mCurrentNodeId, &mCurrentNode);
        }
        else
        {
            std::cout << "Parse NI file error\r\n"  << std::endl;
        }
    }
    return success;
}

std::string PackArchive::OpenImage(const std::string &fileName)
{
    std::string f;
    mZip.GetFile(fileName, f);
    return f;
}

std::string PackArchive::GetImage(const std::string &fileName)
{
    //"C8B39950DE174EAA8E852A07FC468267/rf/000/05FB5530"
    std::string imagePath = mPackName + "/rf/" + fileName;
    ReplaceCharacter(imagePath, "\\", "/");

    std::cout << "Loading " + imagePath << std::endl;
    return OpenImage(imagePath);
}

std::string PackArchive::CurrentImage()
{
    return GetImage(std::string(mCurrentNode.ri_file));
}

std::string PackArchive::CurrentSound()
{
    //"C8B39950DE174EAA8E852A07FC468267/sf/000/05FB5530"
    std::string soundPath = mPackName + "/sf/" + std::string(mCurrentNode.si_file);
    ReplaceCharacter(soundPath, "\\", "/");

    std::cout << "Loading " + soundPath << std::endl;

    std::string f;
    if (mZip.GetFile(soundPath, f))
    {
        ni_decode_block512(reinterpret_cast<uint8_t *>(f.data()));
        return f;
    }
    else
    {
        std::cout << "Cannot load file from ZIP" << std::endl;
    }
    return "";
}

std::string PackArchive::CurrentSoundName()
{
    return std::string(mCurrentNode.si_file);
}

bool PackArchive::AutoPlay()
{
    return mCurrentNode.current->auto_play;
}

bool PackArchive::IsRoot() const
{
    return mCurrentNodeId == 0;
}

bool PackArchive::IsWheelEnabled() const
{
    return mCurrentNode.current->wheel;
}

void PackArchive::Next()
{
    // L'index de circulation dans le tableau des transitions commence à 1 (pas à zéro ...)
    uint32_t index = 1;
    if (mCurrentNode.current->ok_transition_selected_option_index < mNodeForChoice.current->ok_transition_number_of_options)
    {
        index = mCurrentNode.current->ok_transition_selected_option_index + 1;
    }
    // sinon on revient à l'index 0 (début du tableau des transitions)

    mCurrentNodeId = ni_get_node_index_in_li(mNodeForChoice.current->ok_transition_action_node_index_in_li, index - 1);
    ni_get_node_info(mCurrentNodeId, &mCurrentNode);
}

void PackArchive::Previous()
{
    // L'index de circulation dans le tableau des transitions commence à 1 (pas à zéro ...)
    uint32_t index = 1;
    if (mCurrentNode.current->ok_transition_selected_option_index > 1)
    {
        index = mCurrentNode.current->ok_transition_selected_option_index - 1;
    }
    else
    {
        index = mNodeForChoice.current->ok_transition_number_of_options;
    }

    mCurrentNodeId = ni_get_node_index_in_li(mNodeForChoice.current->ok_transition_action_node_index_in_li, index - 1);
    ni_get_node_info(mCurrentNodeId, &mCurrentNode);
}

void PackArchive::OkButton()
{
    if (mCurrentNode.current->home_transition_number_of_options > 0)
    {
        // On doit faire un choix!
        // On sauvegarde ce noeud car il va servir pour naviguer dans les choix
        mNodeIdForChoice = mCurrentNodeId;
        ni_get_node_info(mNodeIdForChoice, &mNodeForChoice);
    }
    mCurrentNodeId = ni_get_node_index_in_li(mCurrentNode.current->ok_transition_action_node_index_in_li, mCurrentNode.current->ok_transition_selected_option_index);
    ni_get_node_info(mCurrentNodeId, &mCurrentNode);
}

bool PackArchive::HasImage()
{
    return std::string(mCurrentNode.ri_file).size() > 1;
}

bool PackArchive::ParseNIFile(const std::string &root)
{
    bool success = true;
    std::string f;
    if (mZip.GetFile(root + "/li", f))
    {
        ni_set_li_block(reinterpret_cast<const uint8_t *>(f.data()), f.size());
    }
    else
    {
        success = false;
        std::cout << "[PACK_ARCHIVE] Cannot find LI file" << std::endl;
    }

    if (mZip.GetFile(root + "/ri", f))
    {
        ni_set_ri_block(reinterpret_cast<const uint8_t *>(f.data()), f.size());
    }
    else
    {
        success = false;
        std::cout << "[PACK_ARCHIVE] Cannot find RI file" << std::endl;
    }

    if (mZip.GetFile(root + "/si", f))
    {
        ni_set_si_block(reinterpret_cast<const uint8_t *>(f.data()), f.size());
    }
    else
    {
        success = false;
        std::cout << "[PACK_ARCHIVE] Cannot find SI file" << std::endl;
    }

    if (mZip.GetFile(root + "/ni", f))
    {
        success = success & ni_parser(&mNiFile, reinterpret_cast<const uint8_t *>(f.data()));
    }
    else
    {
        std::cout << "[PACK_ARCHIVE] Cannot find NI file" << std::endl;
    }
    return success;
}

std::string PackArchive::HexDump(const char *desc, const void *addr, int len)
{
    int i;
    unsigned char buff[17];
    const unsigned char *pc = static_cast<const unsigned char*>(addr);
    std::stringstream ss;

    // Output description if given.
    if (desc != nullptr)
    {
        ss << desc << ":\n";
    }

    if (len == 0) {
        ss << "  ZERO LENGTH\n";
        return ss.str();
    }
    if (len < 0) {
        ss << "  NEGATIVE LENGTH: " << len << "\n";
        return ss.str();
    }

    // Process every byte in the data.
    for (i = 0; i < len; i++) {
        // Multiple of 16 means new line (with line offset).

        if ((i % 16) == 0) {
            // Just don't print ASCII for the zeroth line.
            if (i != 0)
                ss <<  "  " << buff << "\n";

            // Output the offset.
            ss <<  " " << std::setfill('0') << std::setw(4) << std::hex  << i;
        }

        // Now the hex byte_to_hexcode for the specific character.
        ss <<  " " << std::setfill('0') << std::setw(2) << std::hex  << static_cast<int>(pc[i]) << ", ";

        // And store a printable ASCII character for later.
        if ((pc[i] < 0x20) || (pc[i] > 0x7e))
            buff[i % 16] = '.';
        else
            buff[i % 16] = pc[i];
        buff[(i % 16) + 1] = '\0';
    }

    // Pad out last line if not exactly 16 characters.
    while ((i % 16) != 0) {
        ss <<  "   ";
        i++;
    }

    // And print the final ASCII bit.
    ss << "  "<< buff << "\n";
    return ss.str();
}

