#include <3ds.h>
#include "plugin.h"
#include <string.h>
#include "csvc.h"

static Handle   plgLdrHandle;
static Handle   plgLdrArbiter;
static s32*     plgEvent;
static s32*     plgReply;
static int      plgLdrRefCount;
static bool     plgIsN3DS;
static OnPlgLdrEventCb_t    onPlgLdrEventCb = NULL;

static Result   PLGLDR__GetArbiter(void)
{
    Result res = 0;

    u32 *cmdbuf = getThreadCommandBuffer();

    cmdbuf[0] = IPC_MakeHeader(9, 0, 0);
    if (R_SUCCEEDED((res = svcSendSyncRequest(plgLdrHandle))))
    {
        res = cmdbuf[1];
        plgLdrArbiter = cmdbuf[3];
    }
    return res;
}

Result  plgLdrInit(void)
{
    s64    out = 0;
    Result res = 0;

    svcGetSystemInfo(&out, 0x10000, 0x201);
    plgIsN3DS = out != 0;
    if (AtomicPostIncrement(&plgLdrRefCount) == 0)
        res = svcConnectToPort(&plgLdrHandle, "plg:ldr");

    if (R_SUCCEEDED(res)
       && R_SUCCEEDED((res = PLGLDR__GetArbiter())))
    {
        PluginHeader *header = (PluginHeader *)0x07000000;

        plgEvent = header->plgldrEvent;
        plgReply = header->plgldrReply;
    }
    else
        plgLdrExit();

    return res;
}

void    plgLdrExit(void)
{
    if (AtomicDecrement(&plgLdrRefCount))
        return;

    if (plgLdrHandle)
        svcCloseHandle(plgLdrHandle);
    if (plgLdrArbiter)
        svcCloseHandle(plgLdrArbiter);
    plgLdrHandle = plgLdrArbiter = 0;
}

Result  PLGLDR__IsPluginLoaderEnabled(bool *isEnabled)
{
    Result res = 0;

    u32 *cmdbuf = getThreadCommandBuffer();

    cmdbuf[0] = IPC_MakeHeader(2, 0, 0);
    if (R_SUCCEEDED((res = svcSendSyncRequest(plgLdrHandle))))
    {
        res = cmdbuf[1];
        *isEnabled = cmdbuf[2];
    }
    return res;
}

Result  PLGLDR__SetPluginLoaderState(bool enabled)
{
    Result res = 0;

    u32 *cmdbuf = getThreadCommandBuffer();

    cmdbuf[0] = IPC_MakeHeader(3, 1, 0);
    cmdbuf[1] = (u32)enabled;

    if (R_SUCCEEDED((res = svcSendSyncRequest(plgLdrHandle))))
    {
        res = cmdbuf[1];
    }
    return res;
}

Result  PLGLDR__SetPluginLoadParameters(PluginLoadParameters *parameters)
{
    Result res = 0;

    u32 *cmdbuf = getThreadCommandBuffer();

    cmdbuf[0] = IPC_MakeHeader(4, 2, 4);
    cmdbuf[1] = (u32)parameters->noFlash | (((u32)parameters->pluginMemoryStrategy) << 8);
    cmdbuf[2] = parameters->lowTitleId;
    cmdbuf[3] = IPC_Desc_Buffer(256, IPC_BUFFER_R);
    cmdbuf[4] = (u32)parameters->path;
    cmdbuf[5] = IPC_Desc_Buffer(32 * sizeof(u32), IPC_BUFFER_R);
    cmdbuf[6] = (u32)parameters->config;

    if (R_SUCCEEDED((res = svcSendSyncRequest(plgLdrHandle))))
    {
        res = cmdbuf[1];
    }
    return res;
}

Result  PLGLDR__DisplayMenu(PluginMenu *menu)
{
    Result res = 0;

    u32 nbItems = menu->nbItems;
    u32 *cmdbuf = getThreadCommandBuffer();

    cmdbuf[0] = IPC_MakeHeader(5, 1, 8);
    cmdbuf[1] = nbItems;
    cmdbuf[2] = IPC_Desc_Buffer(nbItems, IPC_BUFFER_RW);
    cmdbuf[3] = (u32)menu->states;
    cmdbuf[4] = IPC_Desc_Buffer(MAX_BUFFER, IPC_BUFFER_R);
    cmdbuf[5] = (u32)menu->title;
    cmdbuf[6] = IPC_Desc_Buffer(MAX_BUFFER * nbItems, IPC_BUFFER_R);
    cmdbuf[7] = (u32)menu->items;
    cmdbuf[8] = IPC_Desc_Buffer(MAX_BUFFER * nbItems, IPC_BUFFER_R);
    cmdbuf[9] = (u32)menu->hints;

    if (R_SUCCEEDED((res = svcSendSyncRequest(plgLdrHandle))))
    {
        res = cmdbuf[1];
    }

    return res;
}

Result  PLGLDR__DisplayMessage(const char *title, const char *body)
{
    Result res = 0;

    u32 *cmdbuf = getThreadCommandBuffer();

    cmdbuf[0] = IPC_MakeHeader(6, 0, 4);
    cmdbuf[1] = IPC_Desc_Buffer(strlen(title), IPC_BUFFER_R);
    cmdbuf[2] = (u32)title;
    cmdbuf[3] = IPC_Desc_Buffer(strlen(body), IPC_BUFFER_R);
    cmdbuf[4] = (u32)body;

    if (R_SUCCEEDED((res = svcSendSyncRequest(plgLdrHandle))))
    {
        res = cmdbuf[1];
    }
    return res;
}

Result  PLGLDR__DisplayErrMessage(const char *title, const char *body, u32 error)
{
    Result res = 0;

    u32 *cmdbuf = getThreadCommandBuffer();

    cmdbuf[0] = IPC_MakeHeader(7, 1, 4);
    cmdbuf[1] = error;
    cmdbuf[2] = IPC_Desc_Buffer(strlen(title), IPC_BUFFER_R);
    cmdbuf[3] = (u32)title;
    cmdbuf[4] = IPC_Desc_Buffer(strlen(body), IPC_BUFFER_R);
    cmdbuf[5] = (u32)body;

    if (R_SUCCEEDED((res = svcSendSyncRequest(plgLdrHandle))))
    {
        res = cmdbuf[1];
    }
    return res;
}

Result  PLGLDR__GetVersion(u32 *version)
{
    Result res = 0;

    u32 *cmdbuf = getThreadCommandBuffer();

    cmdbuf[0] = IPC_MakeHeader(8, 0, 0);

    if (R_SUCCEEDED((res = svcSendSyncRequest(plgLdrHandle))))
    {
        if (cmdbuf[0] != IPC_MakeHeader(8, 2, 0))
            return 0xD900182F;

        res = cmdbuf[1];
        if (version)
            *version = cmdbuf[2];
    }
    return res;
}

Result  PLGLDR__GetPluginPath(char *path)
{
    if (path == NULL)
        return MAKERESULT(28, 7, 254, 1014); ///< Usage, App, Invalid argument

    Result res = 0;

    u32 *cmdbuf = getThreadCommandBuffer();

    cmdbuf[0] = IPC_MakeHeader(10, 0, 2);
    cmdbuf[1] = IPC_Desc_Buffer(255, IPC_BUFFER_RW);
    cmdbuf[2] = (u32)path;

    if (R_SUCCEEDED((res = svcSendSyncRequest(plgLdrHandle))))
    {
        res = cmdbuf[1];
    }
    return res;
}

Result  PLGLDR__SetRosalinaMenuBlock(bool shouldBlock)
{
    Result res = 0;

    u32 *cmdbuf = getThreadCommandBuffer();

    cmdbuf[0] = IPC_MakeHeader(11, 1, 0);
    cmdbuf[1] = (u32)shouldBlock;

    if (R_SUCCEEDED((res = svcSendSyncRequest(plgLdrHandle))))
    {
        res = cmdbuf[1];
    }
    return res;
}

Result  PLGLDR__SetSwapSettings(char* swapPath, void* saveFunc, void* loadFunc, void* args)
{
	Result res = 0;

	const char* trueSwapPath;
	if (swapPath) trueSwapPath = swapPath;
	else trueSwapPath = "\0";

	u32 buf[4] = { 0 };
	u32* trueArgs;
	if (args) trueArgs = args;
	else trueArgs = buf;

	u32* cmdbuf = getThreadCommandBuffer();

	cmdbuf[0] = IPC_MakeHeader(12, 2, 4);
	cmdbuf[1] = (saveFunc) ? svcConvertVAToPA(saveFunc, false) | (1 << 31) : 0;
	cmdbuf[2] = (loadFunc) ? svcConvertVAToPA(loadFunc, false) | (1 << 31) : 0;
	cmdbuf[3] = IPC_Desc_Buffer(4 * sizeof(u32), IPC_BUFFER_R);
	cmdbuf[4] = (u32)trueArgs;
	cmdbuf[5] = IPC_Desc_Buffer(strlen(trueSwapPath) + 1, IPC_BUFFER_R);
	cmdbuf[6] = (u32)trueSwapPath;

	if (R_SUCCEEDED((res = svcSendSyncRequest(plgLdrHandle))))
	{
		res = cmdbuf[1];
	}
	return res;
}

Result  PLGLDR__SetExeLoadSettings(void* loadFunc, void* args)
{
	Result res = 0;

	u32 buf[4] = { 0 };
	u32* trueArgs;
	if (args) trueArgs = args;
	else trueArgs = buf;

	u32* cmdbuf = getThreadCommandBuffer();

	cmdbuf[0] = IPC_MakeHeader(13, 1, 2);
	cmdbuf[1] = (loadFunc) ? svcConvertVAToPA(loadFunc, false) | (1 << 31) : 0;
	cmdbuf[2] = IPC_Desc_Buffer(4 * sizeof(u32), IPC_BUFFER_R);
	cmdbuf[3] = (u32)trueArgs;

	if (R_SUCCEEDED((res = svcSendSyncRequest(plgLdrHandle))))
	{
		res = cmdbuf[1];
	}
	return res;
}


void    PLGLDR__SetEventCallback(OnPlgLdrEventCb_t cb)
{
    onPlgLdrEventCb = cb;
}

static s32      __ldrex__(s32 *addr)
{
    s32 val;
    do
        val = __ldrex(addr);
    while (__strex(addr, val));

    return val;
}

static void     __strex__(s32 *addr, s32 val)
{
    do
        __ldrex(addr);
    while (__strex(addr, val));
}

void    PLGLDR__Status(void)
{
    s32  event = __ldrex__(plgEvent); // exclusive read necessary?

    if (event <= 0)
        return;

    if (onPlgLdrEventCb)
        onPlgLdrEventCb(event);

    __strex__(plgReply, PLG_OK);
    __strex__(plgEvent, PLG_WAIT);

    if (event < PLG_ABOUT_TO_SWAP)
        return;

    svcArbitrateAddress(plgLdrArbiter, (u32)plgReply, ARBITRATION_SIGNAL, 1, 0);
    if (event == PLG_ABOUT_TO_SWAP)
        svcArbitrateAddress(plgLdrArbiter, (u32)plgEvent, ARBITRATION_WAIT_IF_LESS_THAN, PLG_OK, 0);
    else if (event == PLG_ABOUT_TO_EXIT)
    {
        plgLdrExit();
        svcExitThread();
    }
}
