/*
 * 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/>.
 */
#define CORE_INTERNAL
#include "CachedRomHeaderAndSettings.hpp"
#include "MediaLoader.hpp"
#include "Directories.hpp"
#include "RomSettings.hpp"
#include "m64p/Api.hpp"
#include "Archive.hpp"
#include "Cheats.hpp"
#include "String.hpp"
#include "Error.hpp"
#include "File.hpp"
#include "Rom.hpp"

#include <string>
#include <cstdlib>
#include <cstring>

//
// Local Variables
//

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

//
// Exported Functions
//

bool CoreOpenRom(std::filesystem::path file)
{
    std::string error;
    m64p_error  ret;
    std::vector<char> buf;
    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 = CoreLowerString(file_extension);

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

        if (!CoreReadArchiveFile(file, extracted_file, is_disk, buf))
        {
            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()) &&
                    !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 (!CoreWriteFile(disk_file, buf))
            {
                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 (!CoreReadFile(file, buf))
        {
            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.data());
        error = "CoreOpenRom: m64p::Core.DoCommand(M64CMD_ROM_OPEN) Failed: ";
    }

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

    l_HasRomOpen = (ret == M64ERR_SUCCESS);

    if (l_HasRomOpen)
    {
        // store ROM path
        l_RomPath = file;
        // store default ROM settings
        CoreStoreCurrentDefaultRomSettings();
        // apply rom settings overlay
        CoreApplyRomSettingsOverlay();
        // update cached rom header and settings entry
        CoreUpdateCachedRomHeaderAndSettings(file);
    }

    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 CoreGetRomPath(std::filesystem::path& path)
{
    std::string error;

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

    path = l_RomPath;
    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;
}
