/*
 * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG
 *  Copyright (C) 2020 Rosalie Wanders <rosalie@mailbox.org>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 3.
 *  You should have received a copy of the GNU General Public License
 *  along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
#include "Rom.hpp"
#include "Directories.hpp"
#include "Error.hpp"
#include "MediaLoader.hpp"
#include "m64p/Api.hpp"
#include "RomSettings.hpp"
#include "Cheats.hpp"
#include "osal/osal_files.hpp"

// 7-Zip includes
#include "../3rdParty/7-Zip/C/7zTypes.h"
#include "../3rdParty/7-Zip/C/7z.h"
#include "../3rdParty/7-Zip/C/7zAlloc.h"
#include "../3rdParty/7-Zip/C/7zBuf.h"
#include "../3rdParty/7-Zip/C/7zCrc.h"
#include "../3rdParty/7-Zip/C/7zFile.h"
#include "../3rdParty/7-Zip/C/7zVersion.h"

#include <string>
#include <unzip.h>
#include <fstream>
#include <cstdlib>
#include <cstring>
#include <algorithm>

//
// Local Defines
//

#define UNZIP_READ_SIZE 67108860 /* 64 MiB */

//
// Local Variables
//

static bool l_HasRomOpen       = false;
static bool l_HasDisk          = false;
static bool l_HasExtractedDisk = false;
static std::filesystem::path l_ExtractedDiskPath;

//
// Local Functions
//

static std::string to_lower_str(std::string str)
{
    std::string resultString = str;

    std::transform(resultString.begin(), resultString.end(), resultString.begin(), 
        [](unsigned char c)
        { 
            return std::tolower(c); 
        }
    );

    return resultString;
}

static voidpf zlib_filefunc_open(voidpf opaque, const void* filename, int mode)
{
    std::filesystem::path path = *(std::filesystem::path*)filename;

    // we need a static filestream
    static std::ifstream fileStream;

    // attempt to open file
    fileStream.open(path, std::ios::binary);
    if (!fileStream.is_open())
    {
        return nullptr;
    }

    return (voidpf)&fileStream;
}

static uLong zlib_filefunc_read(voidpf opaque, voidpf stream, void* buf, uLong size)
{
    std::ifstream* fileStream = (std::ifstream*)stream;
    fileStream->read((char*)buf, size);
    return fileStream->fail() ? 0 : size;
}

static ZPOS64_T zlib_filefunc_tell(voidpf opaque, voidpf stream)
{
    std::ifstream* fileStream = (std::ifstream*)stream;
    return fileStream->tellg();
}

static long zlib_filefunc_seek(voidpf opaque, voidpf stream, ZPOS64_T offset, int origin)
{
    std::ifstream* fileStream = (std::ifstream*)stream;
    std::ios_base::seekdir seekOrigin;

    switch (origin)
    {
    default:
        return 0;
    case ZLIB_FILEFUNC_SEEK_CUR:
        seekOrigin = std::ios::cur;
        break;
    case ZLIB_FILEFUNC_SEEK_END:
        seekOrigin = std::ios::end;
        break;
    case ZLIB_FILEFUNC_SEEK_SET:
        seekOrigin = std::ios::beg;
        break;
    }

    fileStream->seekg(offset, seekOrigin);
    return fileStream->fail() ? -1 : 0;
}

static int zlib_filefunc_close(voidpf opaque, voidpf stream)
{
    std::ifstream* fileStream = (std::ifstream*)stream;
    fileStream->close();
    return fileStream->fail() ? -1 : 0;
}

static int zlib_filefunc_testerror(voidpf opaque, voidpf stream)
{
    return errno;
}

static bool read_zip_file(std::filesystem::path file, std::filesystem::path* extractedFileName, bool* isDisk, char** buf, int* size)
{
    std::string  error;
    std::fstream fileStream;
    int          fileStreamLen;
    char*        fileStreamBuf;

    unzFile           zipFile;
    unz_global_info64 zipInfo;

    zlib_filefunc64_def filefuncs;
    filefuncs.zopen64_file = zlib_filefunc_open;
    filefuncs.zread_file   = zlib_filefunc_read;
    filefuncs.zwrite_file  = nullptr;
    filefuncs.ztell64_file = zlib_filefunc_tell;
    filefuncs.zseek64_file = zlib_filefunc_seek;
    filefuncs.zclose_file  = zlib_filefunc_close;
    filefuncs.zerror_file  = zlib_filefunc_testerror;
    filefuncs.opaque       = nullptr;

    zipFile = unzOpen2_64((const void*)&file, &filefuncs);
    if (zipFile == nullptr)
    {
        if (errno == 0)
        {
            error = "read_zip_file: unzOpen Failed!";
        }
        else
        {
            error = "read_zip_file: unzOpen Failed: ";
            error += "failed to open file: ";
            error += strerror(errno);
            error += " (";
            error += std::to_string(errno);
            error += ")";
        }
        CoreSetError(error);
        return false;
    }

    if (unzGetGlobalInfo64(zipFile, &zipInfo) != UNZ_OK)
    {
        error = "read_zip_file: unzGetGlobalInfo Failed!";
        CoreSetError(error);
        return false;
    }

    for (int i = 0; i < zipInfo.number_entry; i++)
    {
        unz_file_info fileInfo;
        char          fileName[PATH_MAX];

        // if we can't retrieve file info,
        // skip the file
        if (unzGetCurrentFileInfo(zipFile, &fileInfo, fileName, PATH_MAX, nullptr, 0, nullptr, 0) != UNZ_OK)
        {
            continue;
        }

        // make sure file has supported file format,
        // if it does, read it in memory
        std::filesystem::path fileNamePath(fileName);
        std::string fileExtension = fileNamePath.has_extension() ? fileNamePath.extension().string() : "";
        fileExtension = to_lower_str(fileExtension);
        if (fileExtension == ".z64" ||
            fileExtension == ".v64" ||
            fileExtension == ".n64" ||
            fileExtension == ".ndd" ||
            fileExtension == ".d64")
        {
            char* buffer;
            char* outBuffer;
            int   dataSize = UNZIP_READ_SIZE;
            int   total_bytes_read = 0;
            int   bytes_read = 0;

            buffer = (char*)malloc(UNZIP_READ_SIZE);
            if (buffer == nullptr)
            {
                error = "read_zip_file Failed: malloc Failed!";
                CoreSetError(error);
                return false;
            }

            outBuffer = (char*)malloc(UNZIP_READ_SIZE);
            if (outBuffer == nullptr)
            {
                free(buffer);
                error = "read_zip_file Failed: malloc Failed!";
                CoreSetError(error);
                return false;
            }

            if (unzOpenCurrentFile(zipFile) != UNZ_OK)
            {
                free(buffer);
                free(outBuffer);
                error = "read_zip_file Failed: unzOpenCurrentFile Failed!";
                CoreSetError(error);
                return false;
            }

            do
            {
                bytes_read = unzReadCurrentFile(zipFile, buffer, UNZIP_READ_SIZE);
                if (bytes_read < 0)
                {
                    unzCloseCurrentFile(zipFile);
                    unzClose(zipFile);
                    free(buffer);
                    free(outBuffer);
                    error = "read_zip_file Failed: unzReadCurrentFile Failed: ";
                    error += std::to_string(bytes_read);
                    CoreSetError(error);
                    return false;
                }

                if (bytes_read > 0)
                {
                    if (total_bytes_read + bytes_read > dataSize)
                    {
                        outBuffer = (char*)realloc(outBuffer, total_bytes_read + bytes_read);
                        dataSize += bytes_read;
                        if (outBuffer == nullptr)
                        {
                            unzCloseCurrentFile(zipFile);
                            unzClose(zipFile);
                            free(buffer);
                            free(outBuffer);
                            error = "read_zip_file Failed: realloc Failed!";
                            CoreSetError(error);
                            return false;
                        }
                    }

                    memcpy((outBuffer + total_bytes_read), buffer, bytes_read);
                    total_bytes_read += bytes_read;
                }
            } while (bytes_read > 0);

            *size              = total_bytes_read;
            *buf               = outBuffer;
            *extractedFileName = fileNamePath;
            *isDisk            = (fileExtension == ".ndd" || fileExtension == ".d64");
            unzCloseCurrentFile(zipFile);
            unzClose(zipFile);
            free(buffer);
            return true;
        }

        // break when we've iterated over all entries
        if ((i + 1) >= zipInfo.number_entry)
        {
            break;
        }

        // move to next file
        if (unzGoToNextFile(zipFile) != UNZ_OK)
        {
            unzClose(zipFile);
            error = "read_zip_file Failed: unzGoToNextFile Failed!";
            CoreSetError(error);
            return false;
        }
    }

    error = "read_zip_file Failed: no valid ROMs found in zip!";
    CoreSetError(error);
    unzClose(zipFile);
    return false;
}

static bool read_7zip_file(std::filesystem::path file, std::filesystem::path* extractedFileName, bool* isDisk, char** buf, int* size)
{
    std::string  error;

    const ISzAlloc alloc = { SzAlloc, SzFree };
    const size_t bufSize = ((size_t)1 << 18);

    ISzAlloc allocImp;
    ISzAlloc allocTempImp;

    CFileInStream archiveStream;
    CLookToRead2 lookStream;
    CSzArEx db;
    SRes res;

    // initialize allocator 
    allocImp     = alloc;
    allocTempImp = alloc;

    // try to open file
#ifdef _WIN32
    WRes wres = InFile_OpenW(&archiveStream.file, file.wstring().c_str());
#else
    WRes wres = InFile_Open(&archiveStream.file, file.string().c_str());
#endif // _WIN32
    if (wres != 0)
    {
        error = "read_7zip_file Failed: InFile_Open Failed: ";
        error += std::to_string(wres);
        CoreSetError(error);
        return false;
    }

    // create vtables for streams
    FileInStream_CreateVTable(&archiveStream);
    archiveStream.wres = 0;
    LookToRead2_CreateVTable(&lookStream, 0);
    lookStream.buf = nullptr;

    // allocate memory look reader
    lookStream.buf = (Byte*)ISzAlloc_Alloc(&allocImp, bufSize);
    if (lookStream.buf == nullptr)
    {
        error = "read_7zip_file Failed: ISzAlloc_Alloc Failed!";
        CoreSetError(error);
        return false;
    }

    // initialize look reader
    lookStream.bufSize = bufSize;
    lookStream.realStream = &archiveStream.vt;
    LookToRead2_Init(&lookStream);

    // initialize CRC table
    CrcGenerateTable();

    // initialize archive
    SzArEx_Init(&db);

    // try to open file
    res = SzArEx_Open(&db, &lookStream.vt, &allocImp, &allocTempImp);
    if (res != SZ_OK)
    {
        error = "read_7zip_file Failed: SzArEx_Open Failed: ";
        error += std::to_string(res);
        CoreSetError(error);
        SzArEx_Free(&db, &allocImp);
        ISzAlloc_Free(&allocImp, lookStream.buf);
        return false;
    }

    for (int i = 0; i < db.NumFiles; i++)
    {
        size_t filename_size = 0;
        uint16_t fileName[PATH_MAX];

        // skip directories
        if (SzArEx_IsDir(&db, i))
        {
            continue;
        }

        // skip when filename size exceeds our buffer size
        filename_size = SzArEx_GetFileNameUtf16(&db, i, nullptr);
        if (filename_size > PATH_MAX)
        {
            continue;
        }

        SzArEx_GetFileNameUtf16(&db, i, fileName);

        std::filesystem::path fileNamePath((char16_t*)fileName);
        std::string fileExtension = fileNamePath.has_extension() ? fileNamePath.extension().string() : "";
        fileExtension = to_lower_str(fileExtension);
        if (fileExtension == ".z64" ||
            fileExtension == ".v64" ||
            fileExtension == ".n64" ||
            fileExtension == ".ndd" ||
            fileExtension == ".d64")
        {
            uint32_t blockIndex = 0xFFFFFFFF;
            uint8_t* outBuffer  = nullptr;
            size_t   outBufferSize = 0;
            size_t   offset = 0;
            size_t   outSizeProcessed = 0;

            res = SzArEx_Extract(&db, &lookStream.vt, i,
                                    &blockIndex, &outBuffer, &outBufferSize,
                                    &offset, &outSizeProcessed,
                                    &allocImp, &allocTempImp);
            if (res != SZ_OK)
            {
                error = "read_7zip_file Failed: SzArEx_Extract Failed: ";
                error += std::to_string(res);
                CoreSetError(error);

                SzArEx_Free(&db, &allocImp);
                ISzAlloc_Free(&allocImp, lookStream.buf);
                File_Close(&archiveStream.file);
                ISzAlloc_Free(&allocImp, outBuffer);
                return false;
            }

            *size              = outSizeProcessed;
            // we have to memcpy it into buf, otherwise
            // the free() in CoreOpenRom() crashes
            *buf               = (char*)malloc(outSizeProcessed);
            memcpy(*buf, (outBuffer + offset), outSizeProcessed);
            *extractedFileName = fileNamePath;
            *isDisk            = (fileExtension == ".ndd" || fileExtension == ".d64");

            SzArEx_Free(&db, &allocImp);
            ISzAlloc_Free(&allocImp, lookStream.buf);
            File_Close(&archiveStream.file);
            ISzAlloc_Free(&allocImp, outBuffer);
            return true;
        }
    }

    // cleanup
    SzArEx_Free(&db, &allocImp);
    ISzAlloc_Free(&allocImp, lookStream.buf);
    File_Close(&archiveStream.file);

    error = "read_7zip_file Failed: no valid ROMs found in 7zip!";
    CoreSetError(error);
    return false;
}

static bool read_raw_file(std::filesystem::path file, char** buf, int* size)
{
    std::string   error;
    std::ifstream fileStream;
    int           fileStreamLen;
    char*         fileStreamBuf;

    // attempt to open file
    fileStream.open(file, std::ios::binary);
    if (!fileStream.is_open())
    {
        error = "read_raw_file Failed: ";
        error += "failed to open file: ";
        error += strerror(errno);
        error += " (";
        error += std::to_string(errno);
        error += ")";
        CoreSetError(error);
        return false;
    }

    // attempt to retrieve file length
    fileStream.seekg(0, fileStream.end);
    fileStreamLen = fileStream.tellg();
    fileStream.seekg(0, fileStream.beg);

    // try to create buffer
    fileStreamBuf = (char*)malloc(fileStreamLen);
    if (fileStreamBuf == nullptr)
    {
        error = "read_raw_file Failed: ";
        error += "malloc failed!";
        CoreSetError(error);
        free(fileStreamBuf);
        fileStream.close();
        return false;
    }

    // read file
    fileStream.read(fileStreamBuf, fileStreamLen);

    *buf  = fileStreamBuf;
    *size = fileStreamLen;

    fileStream.close();
    return true;
}

static bool write_file(std::filesystem::path file, char* buf, int size)
{
    std::string   error;
    std::ofstream fileStream;

    // attempt to open file
    fileStream.open(file, std::ios::binary);
    if (!fileStream.is_open())
    {
        error = "write_file Failed: ";
        error += "failed to open file: ";
        error += strerror(errno);
        error += " (";
        error += std::to_string(errno);
        error += ")";
        CoreSetError(error);
        return false;
    }

    // write buffer to file
    fileStream.write(buf, size);
    fileStream.close();
    return true;
}

//
// Exported Functions
//

bool CoreOpenRom(std::filesystem::path file)
{
    std::string error;
    m64p_error  ret;
    char*       buf      = nullptr;
    int         buf_size = 0;
    std::string file_extension;

    if (!m64p::Core.IsHooked())
    {
        return false;
    }

    if (CoreHasRomOpen())
    {
        error = "CoreOpenRom Failed: ";
        error += "cannot open ROM when another ROM is already open!";
        CoreSetError(error);
        return false;
    }

    file_extension = file.has_extension() ? file.extension().string() : "";
    file_extension = to_lower_str(file_extension);

    if (file_extension == ".zip" || 
        file_extension == ".7z")
    {
        std::filesystem::path extracted_file;
        bool                  is_disk = false;

        if (file_extension == ".zip")
        {
            if (!read_zip_file(file, &extracted_file, &is_disk, &buf, &buf_size))
            {
                return false;
            }
        }
        else
        {
            if (!read_7zip_file(file, &extracted_file, &is_disk, &buf, &buf_size))
            {
                return false;
            }
        }

        if (is_disk)
        {
            std::filesystem::path disk_file;
            disk_file = CoreGetUserCacheDirectory();
            disk_file += "/extracted_disks/";
            disk_file += extracted_file.filename();

            // attempt to create extraction directory
            try
            {
                if (!std::filesystem::exists(disk_file.parent_path()))
                {
                    if (!std::filesystem::create_directory(disk_file.parent_path()))
                    {
                        throw std::exception();
                    }
                }
            }
            catch (...)
            {
                error = "CoreOpenRom Failed: ";
                error += "Failed to create \"";
                error += disk_file.parent_path().string();
                error += "\"!";
                CoreSetError(error);
                return false;
            }

            // attempt to write temporary file
            if (!write_file(disk_file, buf, buf_size))
            {
                return false;
            }

            CoreMediaLoaderSetDiskFile(disk_file);

            l_ExtractedDiskPath = disk_file;
        }

        l_HasDisk          = is_disk;
        l_HasExtractedDisk = is_disk;
    }
    else if (file_extension == ".d64" || 
             file_extension == ".ndd")
    {
        CoreMediaLoaderSetDiskFile(file);
        l_HasDisk          = true;
        l_HasExtractedDisk = false;
    }
    else
    {
        if (!read_raw_file(file, &buf, &buf_size))
        {
            return false;
        }

        l_HasDisk          = false;
        l_HasExtractedDisk = false;
    }

    if (l_HasDisk)
    {
        ret = m64p::Core.DoCommand(M64CMD_DISK_OPEN, 0, nullptr);
        error = "CoreOpenRom: m64p::Core.DoCommand(M64CMD_DISK_OPEN) Failed: ";
    }
    else
    {
        ret = m64p::Core.DoCommand(M64CMD_ROM_OPEN, buf_size, buf);
        error = "CoreOpenRom: m64p::Core.DoCommand(M64CMD_ROM_OPEN) Failed: ";
        free(buf);
    }

    if (ret != M64ERR_SUCCESS)
    {
        error += m64p::Core.ErrorMessage(ret);
        CoreSetError(error);
    }

    l_HasRomOpen = (ret == M64ERR_SUCCESS);

    if (l_HasRomOpen)
    {
        // store default ROM settings
        CoreStoreCurrentDefaultRomSettings();
        // apply rom settings overlay
        CoreApplyRomSettingsOverlay();
    }

    return l_HasRomOpen;
}

bool CoreHasRomOpen(void)
{
    return l_HasRomOpen;
}

bool CoreGetRomType(CoreRomType& type)
{
    std::string error;

    if (!l_HasRomOpen)
    {
        error = "CoreGetRomType Failed: ";
        error += "cannot retrieve ROM type when no ROM has been opened!";
        CoreSetError(error);
        return false;
    }

    type = l_HasDisk ? CoreRomType::Disk : CoreRomType::Cartridge;
    return true;
}

bool CoreCloseRom(void)
{
    std::string error;
    m64p_error ret;

    if (!m64p::Core.IsHooked())
    {
        return false;
    }

    if (!CoreHasRomOpen())
    {
        error = "CoreCloseRom Failed: ";
        error += "cannot close rom when no rom is open!";
        CoreSetError(error);
        return false;
    }

    if (!CoreClearCheats())
    {
        return false;
    }

    if (l_HasDisk)
    {
        ret = m64p::Core.DoCommand(M64CMD_DISK_CLOSE, 0, nullptr);
        error = "CoreCloseRom: m64p::Core.DoCommand(M64CMD_DISK_CLOSE) Failed: ";
    }
    else
    {
        ret = m64p::Core.DoCommand(M64CMD_ROM_CLOSE, 0, nullptr);
        error = "CoreCloseRom: m64p::Core.DoCommand(M64CMD_ROM_CLOSE) Failed: ";
    }

    if (ret != M64ERR_SUCCESS)
    {
        error += m64p::Core.ErrorMessage(ret);
        CoreSetError(error);
        return false;
    }

    // clear default ROM settings
    CoreClearCurrentDefaultRomSettings();

    // if removing the temporary extracted disk file
    // fails, then we shouldn't break the state of
    // whether we currently have an open ROM/disk,
    // so we can safely say the ROM/disk isn't open
    // anymore here
    l_HasRomOpen = false;

    // attempt to clean temporary extracted disk file
    if (l_HasExtractedDisk)
    {
        try
        {
            if (!std::filesystem::remove(l_ExtractedDiskPath))
            {
                throw std::exception();
            }
        }
        catch (...)
        {
            error = "CoreCloseRom: Failed to remove \"";
            error += l_ExtractedDiskPath.string();
            error += "\"!";
            CoreSetError(error);
            return false;
        }
    }

    return true;
}
