/*
*   This file is part of Luma3DS
*   Copyright (C) 2016-2020 Aurora Wright, TuxSH
*
*   This program is free software: you can redistribute it and/or modify
*   it under the terms of the GNU General Public License as published by
*   the Free Software Foundation, either version 3 of the License, or
*   (at your option) any later version.
*
*   This program is distributed in the hope that it will be useful,
*   but WITHOUT ANY WARRANTY; without even the implied warranty of
*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*   GNU General Public License for more details.
*
*   You should have received a copy of the GNU General Public License
*   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*
*   Additional Terms 7.b and 7.c of GPLv3 apply to this file:
*       * Requiring preservation of specified reasonable legal notices or
*         author attributions in that material or in the Appropriate Legal
*         Notices displayed by works containing it.
*       * Prohibiting misrepresentation of the origin of that material,
*         or requiring that modified versions of such material be marked in
*         reasonable ways as different from the original version.
*/

/* This file was entirely written by fincs */

#include <3ds.h>
#include "3dsx.h"
#include "memory.h"

#define Log_PrintP(...) ((void)0)

#define MAXRELOCS 512
static _3DSX_Reloc s_relocBuf[MAXRELOCS];
u32 ldrArgvBuf[ARGVBUF_SIZE/4];

#define SEC_ASSERT(x) do { if (!(x)) { Log_PrintP("Assertion failed: %s", #x); return false; } } while (0)

typedef struct
{
    void* segPtrs[3]; // code, rodata & data
    u32 segAddrs[3];
    u32 segSizes[3];
} _3DSX_LoadInfo;

static inline u32 TranslateAddr(u32 off, _3DSX_LoadInfo* d, u32* offsets)
{
    if (off < offsets[0])
        return d->segAddrs[0] + off;
    if (off < offsets[1])
        return d->segAddrs[1] + off - offsets[0];
    return d->segAddrs[2] + off - offsets[1];
}

bool Ldr_Get3dsxSize(u32* pSize, IFile *file)
{
    _3DSX_Header hdr;

    if (IFile_Read2(file, &hdr, sizeof(hdr), 0) != sizeof(hdr))
    {
        Log_PrintP("Cannot read 3DSX header");
        return false;
    }

    if (hdr.magic != _3DSX_MAGIC)
    {
        Log_PrintP("Not a valid 3DSX file");
        return false;
    }

    u32 segSizes[3];
    segSizes[0] = (hdr.codeSegSize+0xFFF) &~ 0xFFF;
    segSizes[1] = (hdr.rodataSegSize+0xFFF) &~ 0xFFF;
    segSizes[2] = (hdr.dataSegSize+0xFFF) &~ 0xFFF;
    SEC_ASSERT(segSizes[0] >= hdr.codeSegSize);
    SEC_ASSERT(segSizes[1] >= hdr.rodataSegSize);
    SEC_ASSERT(segSizes[2] >= hdr.dataSegSize);

    // TODO: Check int overflow
    *pSize = segSizes[0] + segSizes[1] + segSizes[2] + 0x1000; // One extra page reserved for settings/etc

    return true;
}

static inline u32 min(u32 a, u32 b)
{
    return a < b ? a : b;
}

Handle Ldr_CodesetFrom3dsx(const char* name, u32* codePages, u32 baseAddr, IFile *file, u64 tid)
{
    u32 i,j,k,m;
    Result res;
    _3DSX_Header hdr;
    IFile_Read2(file, &hdr, sizeof(hdr), 0);

    _3DSX_LoadInfo d;
    d.segSizes[0] = (hdr.codeSegSize+0xFFF) &~ 0xFFF;
    d.segSizes[1] = (hdr.rodataSegSize+0xFFF) &~ 0xFFF;
    d.segSizes[2] = (hdr.dataSegSize+0xFFF) &~ 0xFFF;
    d.segPtrs[0] = codePages;
    d.segPtrs[1] = (char*)d.segPtrs[0] + d.segSizes[0];
    d.segPtrs[2] = (char*)d.segPtrs[1] + d.segSizes[1];
    d.segAddrs[0] = baseAddr;
    d.segAddrs[1] = d.segAddrs[0] + d.segSizes[0];
    d.segAddrs[2] = d.segAddrs[1] + d.segSizes[1];

    u32 offsets[2] = { d.segSizes[0], d.segSizes[0] + d.segSizes[1] };
    u32* segLimit = d.segPtrs[2] + d.segSizes[2];

    u32 readOffset = hdr.headerSize;

    u32 nRelocTables = hdr.relocHdrSize/4;
    SEC_ASSERT((3*4*nRelocTables) <= 0x1000);
    u32* extraPage = (u32*)((char*)d.segPtrs[2] + d.segSizes[2]);
    u32 extraPageAddr = d.segAddrs[2] + d.segSizes[2];

    // Read the relocation headers
    for (i = 0; i < 3; i ++)
    {
        if (IFile_Read2(file, &extraPage[i*nRelocTables], hdr.relocHdrSize, readOffset) != hdr.relocHdrSize)
        {
            Log_PrintP("Cannot read relheader %d", i);
            return 0;
        }
        readOffset += hdr.relocHdrSize;
    }

    // Read the code segment
    if (IFile_Read2(file, d.segPtrs[0], hdr.codeSegSize, readOffset) != hdr.codeSegSize)
    {
        Log_PrintP("Cannot read code segment");
        return 0;
    }
    readOffset += hdr.codeSegSize;

    // Read the rodata segment
    if (IFile_Read2(file, d.segPtrs[1], hdr.rodataSegSize, readOffset) != hdr.rodataSegSize)
    {
        Log_PrintP("Cannot read rodata segment");
        return 0;
    }
    readOffset += hdr.rodataSegSize;

    // Read the data segment
    u32 dataLoadSegSize = hdr.dataSegSize - hdr.bssSize;
    if (IFile_Read2(file, d.segPtrs[2], dataLoadSegSize, readOffset) != dataLoadSegSize)
    {
        Log_PrintP("Cannot read data segment");
        return 0;
    }
    readOffset += dataLoadSegSize;

    // Relocate the segments
    for (i = 0; i < 3; i ++)
    {
        for (j = 0; j < nRelocTables; j ++)
        {
            int nRelocs = extraPage[i*nRelocTables + j];
            if (j >= (sizeof(_3DSX_RelocHdr)/4))
            {
                // Not using this header
                readOffset += nRelocs;
                continue;
            }

            u32* pos = (u32*)d.segPtrs[i];
            u32* endPos = pos + (d.segSizes[i]/4);
            SEC_ASSERT(endPos <= segLimit);

            while (nRelocs)
            {
                u32 toDo = nRelocs > MAXRELOCS ? MAXRELOCS : nRelocs;
                nRelocs -= toDo;

                u32 readSize = toDo*sizeof(_3DSX_Reloc);
                if (IFile_Read2(file, s_relocBuf, readSize, readOffset) != readSize)
                {
                    Log_PrintP("Cannot read reloc table (%d,%d)", i, j);
                    return 0;
                }
                readOffset += readSize;

                for (k = 0; k < toDo && pos < endPos; k ++)
                {
                    pos += s_relocBuf[k].skip;
                    u32 nPatches = s_relocBuf[k].patch;
                    for (m = 0; m < nPatches && pos < endPos; m ++)
                    {
                        u32 inAddr = baseAddr + 4*(pos - codePages);
                        u32 origData = *pos;
                        u32 subType = origData >> (32-4);
                        u32 addr = TranslateAddr(origData &~ 0xF0000000, &d, offsets);
                        //Log_PrintP("%08lX<-%08lX", inAddr, addr);
                        switch (j)
                        {
                            case 0:
                            {
                                if (subType != 0)
                                {
                                    Log_PrintP("Unsupported absolute reloc subtype (%lu)", subType);
                                    return 0;
                                }
                                *pos = addr;
                                break;
                            }
                            case 1:
                            {
                                u32 data = addr - inAddr;
                                switch (subType)
                                {
                                    case 0: *pos = data;            break; // 32-bit signed offset
                                    case 1: *pos = data &~ BIT(31); break; // 31-bit signed offset
                                    default:
                                        Log_PrintP("Unsupported relative reloc subtype (%lu)", subType);
                                        return 0;
                                }
                                break;
                            }
                        }
                        pos++;
                    }
                }
            }
        }
    }

    // Detect and fill _prm structure
    PrmStruct* pst = (PrmStruct*) &codePages[1];
    if (pst->magic == _PRM_MAGIC)
    {
        memset(extraPage, 0, 0x1000);
        memcpy(extraPage, ldrArgvBuf, sizeof(ldrArgvBuf));
        pst->pSrvOverride = extraPageAddr + 0xFFC;
        pst->pArgList = extraPageAddr;
        pst->runFlags |= RUNFLAG_APTCHAINLOAD;
        s64 dummy;
        bool isN3DS = svcGetSystemInfo(&dummy, 0x10001, 0) == 0;
        if (isN3DS)
        {
            pst->heapSize = 48*1024*1024;
            pst->linearHeapSize = 64*1024*1024;
        } else
        {
            pst->heapSize = 24*1024*1024;
            pst->linearHeapSize = 32*1024*1024;
        }
    }

    // Create the codeset
    CodeSetHeader csh;
    memset(&csh, 0, sizeof(csh));
    memcpy(csh.name, name, 8);
    csh.program_id      = tid;
    csh.text_addr       = d.segAddrs[0];
    csh.text_size       = d.segSizes[0] >> 12;
    csh.ro_addr         = d.segAddrs[1];
    csh.ro_size         = d.segSizes[1] >> 12;
    csh.rw_addr         = d.segAddrs[2];
    csh.rw_size         = (d.segSizes[2] >> 12) + 1; // One extra page reserved for settings/etc
    csh.text_size_total = csh.text_size;
    csh.ro_size_total   = csh.ro_size;
    csh.rw_size_total   = csh.rw_size;
    Handle hCodeset = 0;
    res = svcCreateCodeSet(&hCodeset, &csh, (u32)d.segPtrs[0], (u32)d.segPtrs[1], (u32)d.segPtrs[2]);
    if (res)
    {
        Log_PrintP("svcCreateCodeSet: %08lX", res);
        return 0;
    }

    return hCodeset;
}
