#include <3ds.h>
#include <string.h>
#include <stdio.h>
#include "plugin.h"
#include "ifile.h"
#include "utils.h"

#define MEMPERM_RW (MEMPERM_READ | MEMPERM_WRITE)

u32  g_loadSaveSwapArgs[4];
char g_swapFileName[256];
u32  g_memBlockSize = 5 * 1024 * 1024;

Result     MemoryBlock__SetSize(u32 size) {
    PluginLoaderContext *ctx = &PluginLoaderCtx;
    MemoryBlock *memblock = &ctx->memblock;

    if (memblock->isReady)
        return MAKERESULT(RL_PERMANENT, RS_INVALIDSTATE, RM_LDR, RD_ALREADY_INITIALIZED);
    
    g_memBlockSize = size;
    return 0;
}

Result      MemoryBlock__IsReady(void)
{
    PluginLoaderContext *ctx = &PluginLoaderCtx;
    MemoryBlock *memblock = &ctx->memblock;

    if (memblock->isReady)
        return 0;

    Result  res;

    if (isN3DS || (ctx->pluginMemoryStrategy == PLG_STRATEGY_MODE3))
    {
        s64     appRegionSize = 0;
        s64     appRegionUsed = 0;
        u32     appRegionFree;
        u32     gameReserveSize;
        vu32*   appMemAllocPtr = (vu32 *)PA_FROM_VA_PTR(0x1FF80040);
        u32     appMemAlloc = *appMemAllocPtr;
        u32     temp;

        memblock->isAppRegion = true;

        svcGetSystemInfo(&appRegionSize, 0x10000, 0x80);
        svcGetSystemInfo(&appRegionUsed, 0, 1);

        appRegionFree = appRegionSize - appRegionUsed;

        // Check if appmemalloc reports the entire available memory
        if ((u32)appRegionSize == appMemAlloc)
            *appMemAllocPtr -= g_memBlockSize; ///< Remove plugin share from available memory

        gameReserveSize = appRegionFree - g_memBlockSize;

        // First reserve the game memory size (to avoid heap relocation)
        res = svcControlMemoryUnsafe((u32 *)&temp, 0x30000000,
                                    gameReserveSize, MEMOP_REGION_APP | MEMOP_ALLOC | MEMOP_LINEAR_FLAG, MEMPERM_RW);

        // Then allocate our plugin memory block
        if (R_SUCCEEDED(res))
            res = svcControlMemoryUnsafe((u32 *)&memblock->memblock, 0x07000000,
                                        g_memBlockSize, MEMOP_REGION_APP | MEMOP_ALLOC | MEMOP_LINEAR_FLAG, MEMPERM_RW);

        // Finally release game reserve block
        if (R_SUCCEEDED(res))
            res = svcControlMemoryUnsafe((u32 *)&temp, temp, gameReserveSize, MEMOP_FREE, 0);
    }
    else
    {
        memblock->isAppRegion = false;
        res = svcControlMemoryUnsafe((u32 *)&memblock->memblock, 0x07000000,
                                    g_memBlockSize, MEMOP_REGION_SYSTEM | MEMOP_ALLOC | MEMOP_LINEAR_FLAG, MEMPERM_RW);
    }

    if (R_FAILED(res)) {
        if (isN3DS || (ctx->pluginMemoryStrategy == PLG_STRATEGY_MODE3))
            PluginLoader__Error("Cannot map plugin memory.", res);
        else
            PluginLoader__Error("A console reboot is needed to\nclose extended memory games.\n\nPress [B] to reboot.", res);
        svcKernelSetState(7);
    }
    else
    {
        // Clear the memblock
        memset(memblock->memblock, 0, g_memBlockSize);
        memblock->isReady = true;

        /*if (isN3DS)
        {
            // Check if appmemalloc reports the entire available memory
            s64     appRegionSize = 0;
            vu32*   appMemAlloc = (vu32 *)PA_FROM_VA_PTR(0x1FF80040);

            svcGetSystemInfo(&appRegionSize, 0x10000, 0x80);
            if ((u32)appRegionSize == *appMemAlloc)
                *appMemAlloc -= g_memBlockSize; ///< Remove plugin share from available memory
        } */
    }

    return res;
}

Result      MemoryBlock__Free(void)
{
    MemoryBlock *memblock = &PluginLoaderCtx.memblock;

    if (!memblock->isReady)
        return 0;

    MemOp   memRegion = memblock->isAppRegion ? MEMOP_REGION_APP : MEMOP_REGION_SYSTEM;
    Result  res = svcControlMemoryUnsafe((u32 *)&memblock->memblock, (u32)memblock->memblock,
                                    g_memBlockSize, memRegion | MEMOP_FREE, 0);

    memblock->isReady = false;
    memblock->memblock = NULL;

    if (R_FAILED(res))
        PluginLoader__Error("Couldn't free memblock", res);

    return res;
}

#define FS_OPEN_RWC (FS_OPEN_READ | FS_OPEN_WRITE | FS_OPEN_CREATE)

Result      MemoryBlock__ToSwapFile(void)
{
    MemoryBlock *memblock = &PluginLoaderCtx.memblock;
    PluginLoaderContext *ctx = &PluginLoaderCtx;

    u64     written = 0;
    u64     toWrite = g_memBlockSize;
    IFile   file;
    Result  res = 0;

    svcFlushDataCacheRange(memblock->memblock, g_memBlockSize);
    res = IFile_Open(&file, ARCHIVE_SDMC, fsMakePath(PATH_EMPTY, ""),
                    fsMakePath(PATH_ASCII, g_swapFileName), FS_OPEN_RWC);

    if (R_FAILED(res)) {
        PluginLoader__Error("CRITICAL: Failed to open swap file.\n\nConsole will now reboot.", res);
        svcKernelSetState(7);
    }
    
    if (!ctx->isSwapFunctionset) {
        PluginLoader__Error("CRITICAL: Swap save function\nis not set.\n\nConsole will now reboot.", res);
        svcKernelSetState(7);
    }
    ctx->swapLoadChecksum = saveSwapFunc(memblock->memblock, memblock->memblock + g_memBlockSize, g_loadSaveSwapArgs);
    
    res = IFile_Write(&file, &written, memblock->memblock, toWrite, FS_WRITE_FLUSH);

    if (R_FAILED(res) || written != toWrite) {
        PluginLoader__Error("CRITICAL: Couldn't write swap to SD.\n\nConsole will now reboot.", res);
        svcKernelSetState(7);
    }

    IFile_Close(&file);
    return res;
}

Result      MemoryBlock__FromSwapFile(void)
{
    MemoryBlock *memblock = &PluginLoaderCtx.memblock;

    u64     read = 0;
    u64     toRead = g_memBlockSize;
    IFile   file;
    Result  res = 0;

    res = IFile_Open(&file, ARCHIVE_SDMC, fsMakePath(PATH_EMPTY, ""),
                    fsMakePath(PATH_ASCII, g_swapFileName), FS_OPEN_READ);

    if (R_FAILED(res)) {
        PluginLoader__Error("CRITICAL: Failed to open swap file.\n\nConsole will now reboot.", res);
        svcKernelSetState(7);
    }

    res = IFile_Read(&file, &read, memblock->memblock, toRead);

    if (R_FAILED(res) || read != toRead) {
        PluginLoader__Error("CRITICAL: Couldn't read swap from SD.\n\nConsole will now reboot.", res);
        svcKernelSetState(7);
    }
    
    u32 checksum = loadSwapFunc(memblock->memblock, memblock->memblock + g_memBlockSize, g_loadSaveSwapArgs);
    
    PluginLoaderContext *ctx = &PluginLoaderCtx;
    if (checksum != ctx->swapLoadChecksum) {
        res = -1;
        PluginLoader__Error("CRITICAL: Swap file is corrupted.\n\nConsole will now reboot.", res);
        svcKernelSetState(7); 
    }
    
    svcFlushDataCacheRange(memblock->memblock, g_memBlockSize);
    IFile_Close(&file);
    return res;
}

Result     MemoryBlock__MountInProcess(void)
{
    Handle          target = PluginLoaderCtx.target;
    Error           *error = &PluginLoaderCtx.error;
    PluginHeader    *header = &PluginLoaderCtx.header;
    MemoryBlock     *memblock = &PluginLoaderCtx.memblock;
    bool            isPrivate = PluginLoaderCtx.isMemPrivate;

    Result       res = 0;

    // Executable
    if (R_FAILED((res = svcMapProcessMemoryEx(target, 0x07000000, CUR_PROCESS_HANDLE, (u32) memblock->memblock, header->exeSize, isPrivate ? MAPEXFLAGS_PRIVATE : 0))))
    {
        error->message = "Couldn't map exe memory block";
        error->code = res;
        return res;
    }

    // Heap (to be used by the plugin)
    if (R_FAILED((res = svcMapProcessMemoryEx(target, header->heapVA, CUR_PROCESS_HANDLE, (u32) memblock->memblock + header->exeSize, header->heapSize, isPrivate ? MAPEXFLAGS_PRIVATE : 0))))
    {
        error->message = "Couldn't map heap memory block";
        error->code = res;
    }

    return res;
}

Result     MemoryBlock__UnmountFromProcess(void)
{
    Handle          target = PluginLoaderCtx.target;
    PluginHeader    *header = &PluginLoaderCtx.header;

    Result  res = 0;

    res |= svcUnmapProcessMemoryEx(target, 0x07000000, header->exeSize);
    res |= svcUnmapProcessMemoryEx(target, header->heapVA, header->heapSize);

    return res;
}

Result    MemoryBlock__SetSwapSettings(u32* func, bool isLoad, u32* params)
{
    u32* physAddr = PA_FROM_VA_PTR(isLoad ? (u32)loadSwapFunc : (u32)saveSwapFunc); //Bypass mem permissions

	memcpy(g_loadSaveSwapArgs, params, sizeof(g_loadSaveSwapArgs));
    
    int i = 0;
    for (; i < 32 && func[i] != 0xE320F000; i++)
        physAddr[i] = func[i];

    if (i >= 32) {
        return -1;
    }
    return 0;
}

void       MemoryBlock__ResetSwapSettings(void)
{
	u32* savePhysAddr = PA_FROM_VA_PTR((u32)saveSwapFunc); //Bypass mem permissions
	u32* loadPhysAddr = PA_FROM_VA_PTR((u32)loadSwapFunc);
    PluginLoaderContext *ctx = &PluginLoaderCtx;

	memset(g_loadSaveSwapArgs, 0, sizeof(g_loadSaveSwapArgs));

	savePhysAddr[0] = loadPhysAddr[0] = 0xE12FFF1E; // BX LR

	for (int i = 1; i < 32; i++) {
		savePhysAddr[i] = loadPhysAddr[i] = 0xE320F000; // NOP
	}

	strcpy(g_swapFileName, "/luma/plugins/.swap");
    ctx->isSwapFunctionset = false;

	svcInvalidateEntireInstructionCache();
}

PluginHeader* MemoryBlock__GetMappedPluginHeader() {
    MemoryBlock     *memblock = &PluginLoaderCtx.memblock;
    return memblock->isReady ? (PluginHeader*)memblock->memblock : NULL;
}