#include "vbdri.h"
#include "image.h"
#include "disadiff.h"
#include "vdisadiff.h"
#include "bdri.h"
#include "vff.h"
#include "ui.h"

#define VBDRI_MAX_ENTRIES   8192 // Completely arbitrary

#define VFLAG_UNKNOWN       (1UL<<28)
#define VFLAG_HOMEBREW      (1UL<<29)
#define VFLAG_ESHOP         (1UL<<30)
#define VFLAG_SYSTEM        (1UL<<31)
#define VFLAG_TICKDIR       (VFLAG_UNKNOWN|VFLAG_HOMEBREW|VFLAG_ESHOP|VFLAG_SYSTEM)

#define NAME_TIE            "%016llX"
#define NAME_TIE_LEN        16
#define NAME_TIK            "%016llX.%08lX.tik" // title id / console id
#define NAME_TIK_LEN        (16 + 1 + 8 + 4)

#define PART_PATH           "D:/partitionA.bin"


typedef struct {
    u8 type; // 0 for eshop, 1 for homebrew, 2 for system, 3 for unknown
    u32 size;
    u8  console_id[4];
} PACKED_STRUCT TickInfoEntry;

// only for the main directory
static const VirtualFile VTickDbFileTemplates[] = {
    { "system"  , 0x00000000, 0x00000000, 0xFF, VFLAG_DIR | VFLAG_SYSTEM },
    { "homebrew", 0x00000000, 0x00000000, 0xFF, VFLAG_DIR | VFLAG_HOMEBREW },
    { "eshop"   , 0x00000000, 0x00000000, 0xFF, VFLAG_DIR | VFLAG_ESHOP },
    { "unknown" , 0x00000000, 0x00000000, 0xFF, VFLAG_DIR | VFLAG_UNKNOWN },
};

static bool is_tickdb;
static u32 num_entries = 0;
static u8* title_ids = NULL;
static TickInfoEntry* tick_info = NULL;
static u8* cached_entry = NULL;
static int cache_index;

void DeinitVBDRIDrive(void) {
    free(title_ids);
    free(tick_info);
    free(cached_entry);
    title_ids = NULL;
    tick_info = NULL;
    cached_entry = NULL;
    num_entries = 0;
    cache_index = -1;
}

bool SortVBDRITickets() {
    if (!CheckVBDRIDrive() || !is_tickdb)
        return false;

    if (tick_info)
        return true;

    tick_info = (TickInfoEntry*) malloc(num_entries * sizeof(TickInfoEntry));
    if (!tick_info)
        return false;

    ShowString("Sorting tickets, please wait ...");

    for (u32 i = 0; i < num_entries - 1; i++) {
        Ticket* ticket;
        if (ReadTicketFromDB(PART_PATH, title_ids + (i * 8), &ticket) != 0) {
            free(tick_info);
            tick_info = NULL;
            return false;
        }
        tick_info[i].type = (ticket->commonkey_idx > 1) ? 3 :
            ((ValidateTicketSignature(ticket) != 0) ? 1 : ((ticket->commonkey_idx == 1) ? 2 : 0));
        tick_info[i].size = GetTicketSize(ticket);
        memcpy(tick_info[i].console_id, ticket->console_id, 4);
        free(ticket);
    }

    ClearScreenF(true, false, COLOR_STD_BG);

    return true;
}

u64 InitVBDRIDrive(void) { // prerequisite: .db file mounted as virtual diff image
    u64 mount_state = CheckVDisaDiffDrive();
    if (!(mount_state & SYS_DIFF)) return 0;
    is_tickdb = (mount_state & SYS_TICKDB);

    DeinitVBDRIDrive();

    num_entries = min((is_tickdb ? GetNumTickets(PART_PATH) : GetNumTitleInfoEntries(PART_PATH)) + 1, VBDRI_MAX_ENTRIES);
    title_ids = (u8*) malloc(num_entries * 8);
    if (!title_ids ||
        ((is_tickdb ? ListTicketTitleIDs(PART_PATH, title_ids, num_entries) : ListTitleInfoEntryTitleIDs(PART_PATH, title_ids, num_entries)) != 0)) {
        DeinitVBDRIDrive();
        return 0;
    }

    if (!is_tickdb && ((cached_entry = malloc(sizeof(TitleInfoEntry))) == NULL)) {
        DeinitVBDRIDrive();
        return 0;
    }

    return mount_state;
}

u64 CheckVBDRIDrive(void) {
    u64 mount_state = CheckVDisaDiffDrive();
    return (title_ids && (mount_state & SYS_DIFF) && (!is_tickdb || (mount_state & SYS_TICKDB))) ?
        mount_state : 0;
}

bool ReadVBDRIDir(VirtualFile* vfile, VirtualDir* vdir) {
    if (!CheckVBDRIDrive())
        return false;

    if (vdir->flags & VFLAG_TICKDIR) { // ticket dir
        if (!is_tickdb || (!tick_info && !SortVBDRITickets()))
            return false;

        while (++vdir->index < (int) num_entries) {
            u32 type = tick_info[vdir->index].type;
            u64 tid = getbe64(title_ids + (vdir->index * 8));

            if ((tid == 0) || !(
                ((vdir->flags & VFLAG_ESHOP) && (type == 0)) ||
                ((vdir->flags & VFLAG_HOMEBREW) && (type == 1)) ||
                ((vdir->flags & VFLAG_SYSTEM) && (type == 2)) ||
                ((vdir->flags & VFLAG_UNKNOWN) && (type == 3))))
                continue;

            memset(vfile, 0, sizeof(VirtualFile));
            snprintf(vfile->name, 32, NAME_TIK, tid, getle32(tick_info[vdir->index].console_id));
            vfile->offset = vdir->index; // "offset" is the internal buffer index
            vfile->size = tick_info[vdir->index].size;
            vfile->keyslot = 0xFF;
            vfile->flags = (vdir->flags | VFLAG_DELETABLE) & ~VFLAG_DIR;

            return true; // found
        }
    } else { // root dir
        if (is_tickdb) {
            int n_templates = sizeof(VTickDbFileTemplates) / sizeof(VirtualFile);
            const VirtualFile* templates = VTickDbFileTemplates;
            while (++vdir->index < n_templates) {
                // copy current template to vfile
                memcpy(vfile, templates + vdir->index, sizeof(VirtualFile));
                return true; // found
            }
        } else { // title dbs display all entries in root
            while (++vdir->index < (int) num_entries) {
                u64 tid = getbe64(title_ids + (vdir->index * 8));
                if (tid == 0)
                    continue;
                memset(vfile, 0, sizeof(VirtualFile));
                snprintf(vfile->name, 32, NAME_TIE, tid);
                vfile->offset = vdir->index; // "offset" is the internal buffer index
                vfile->size = sizeof(TitleInfoEntry);
                vfile->keyslot = 0xFF;
                vfile->flags = (vdir->flags | VFLAG_DELETABLE) & ~VFLAG_DIR;

                return true;
            }
        }
    }

    return false;
}

bool GetNewVBDRIFile(VirtualFile* vfile, VirtualDir* vdir, const char* path) {

    size_t path_len = strlen(path), buf_len = (is_tickdb ? NAME_TIK_LEN : NAME_TIE_LEN) + 2;
    u64 tid;
    u32 console_id;
    char c;

    char buf[buf_len];
    strcpy(buf, path + path_len - buf_len + 1);


    if (( is_tickdb && (sscanf(buf, "/" NAME_TIK "%c", &tid, &console_id, &c) != 2)) ||
        (!is_tickdb && (sscanf(buf, "/" NAME_TIE "%c", &tid, &c) != 1)) ||
        (tid == 0))
        return false;
    tid = getbe64((u8*)&tid);

    int entry_index = -1;
    for (u32 i = 0; i < num_entries; i++) {
        if ((entry_index == -1) && (*((u64*)(void*)(title_ids + 8 * i)) == 0))
            entry_index = i;

        if (memcmp(&tid, title_ids + 8 * i, 8) == 0)
            return false;
    }

    if (entry_index == -1) {
        if (num_entries == VBDRI_MAX_ENTRIES)
            return false;

        u32 new_num_entries = min(num_entries + 128, VBDRI_MAX_ENTRIES);
        u8* new_title_ids = realloc(title_ids, new_num_entries * 8);
        if (!new_title_ids)
            return false;
        if (tick_info) {
            TickInfoEntry* new_tick_info = realloc(tick_info, new_num_entries * sizeof(TickInfoEntry));
            if (!new_tick_info)
                return false;
            tick_info = new_tick_info;
        }

        entry_index = num_entries;
        num_entries = new_num_entries;
        title_ids = new_title_ids;

        memset(title_ids + entry_index * 8, 0, (num_entries - entry_index) * 8);
    }

    u32 size = is_tickdb ? TICKET_COMMON_SIZE : sizeof(TitleInfoEntry);
    u8 entry[size];
    if (is_tickdb)
        *((u32*)(void*)(entry + 0x2A8)) = 0xAC000000;
    if ((is_tickdb ? AddTicketToDB(PART_PATH, (u8*)&tid, (Ticket*)(void*)entry, false) :
        AddTitleInfoEntryToDB(PART_PATH, (u8*)&tid, (TitleInfoEntry*)(void*)entry, false)) != 0)
        return false;

    memcpy(title_ids + entry_index * 8, &tid, 8);

    if (tick_info) {
        tick_info[entry_index].type = 3;
        tick_info[entry_index].size = TICKET_COMMON_SIZE;
        memset(tick_info[entry_index].console_id, 0, 4);
    }

    memset(vfile, 0, sizeof(VirtualFile));
    strcpy(vfile->name, buf);
    vfile->offset = entry_index; // "offset" is the internal buffer index
    vfile->size = size;
    vfile->keyslot = 0xFF;
    vfile->flags = (vdir->flags | VFLAG_DELETABLE) & ~VFLAG_DIR;

    return true;
}

int ReadVBDRIFile(const VirtualFile* vfile, void* buffer, u64 offset, u64 count) {
    if ((int) vfile->offset == cache_index) {
        memcpy(buffer, cached_entry + offset, count);
        return 0;
    }

    if (is_tickdb && (cache_index != -1))
        free(cached_entry);
    if ((is_tickdb ? ReadTicketFromDB(PART_PATH, title_ids + vfile->offset * 8, (Ticket**) &cached_entry) :
        ReadTitleInfoEntryFromDB(PART_PATH, title_ids + vfile->offset * 8, (TitleInfoEntry*) cached_entry)) != 0)
        return 1;
    cache_index = (int) vfile->offset;

    memcpy(buffer, cached_entry + offset, count);
    return 0;
}

int WriteVBDRIFile(VirtualFile* vfile, const void* buffer, u64 offset, u64 count) {
    bool resize = false;

    if (offset + count > vfile->size) {
        if (!is_tickdb || (!tick_info && !SortVBDRITickets()))
            return false;
        vfile->size = offset + count;
        resize = true;
    }

    if (is_tickdb && (cache_index != -1))
        free(cached_entry);
    if ((is_tickdb ? ReadTicketFromDB(PART_PATH, title_ids + vfile->offset * 8, (Ticket**) &cached_entry) :
        ReadTitleInfoEntryFromDB(PART_PATH, title_ids + vfile->offset * 8, (TitleInfoEntry*) cached_entry)) != 0) {
        if (resize) vfile->size = tick_info[vfile->offset].size;
        return 1;
    }
    cache_index = (int) vfile->offset;

    if (resize) {
        u8* new_cached_entry = realloc(cached_entry, vfile->size);
        if (!new_cached_entry) {
            vfile->size = tick_info[vfile->offset].size;
            return 1;
        }

        cached_entry = new_cached_entry;

        if (RemoveTicketFromDB(PART_PATH, title_ids + vfile->offset * 8) != 0) {
            vfile->size = tick_info[vfile->offset].size;
            return 1;
        }
    }

    memcpy(cached_entry + offset, buffer, count);

    if ((is_tickdb ? AddTicketToDB(PART_PATH, title_ids + vfile->offset * 8, (Ticket*)(void*)cached_entry, true) :
        AddTitleInfoEntryToDB(PART_PATH, title_ids + vfile->offset * 8, (TitleInfoEntry*)(void*)cached_entry, true)) != 0) {
        if (resize) vfile->size = tick_info[vfile->offset].size;
        if (is_tickdb) {
            free(cached_entry);
            cached_entry = NULL;
        }
        cache_index = -1;
        return 1;
    }

    if (resize) tick_info[vfile->offset].size = vfile->size;

    if (tick_info && ((offset <= 0x1F1 && offset + count > 0x1F1) || (cached_entry[0x1F1] == 0 && offset <= 0x104 && offset + count > 4)))
        tick_info[vfile->offset].type = (cached_entry[0x1F1] > 1) ? 3 :
            ((ValidateTicketSignature((Ticket*)(void*)cached_entry) != 0) ? 1 : ((cached_entry[0x1F1] == 1) ? 2 : 0));

    return 0;
}

int DeleteVBDRIFile(const VirtualFile* vfile) {
    int ret = (int) (is_tickdb ? RemoveTicketFromDB(PART_PATH, title_ids + vfile->offset * 8) : RemoveTitleInfoEntryFromDB(PART_PATH, title_ids + vfile->offset * 8));

    if (ret == 0) {
        if ((int) vfile->offset == cache_index) {
            cache_index = -1;
            if (is_tickdb) {
                free(cached_entry);
                cached_entry = NULL;
            }
        }

        memset(title_ids + vfile->offset * 8, 0, 8);
    }

    return ret;
}

u64 GetVBDRIDriveSize(void) {
    return CheckVBDRIDrive() ? fvx_qsize(PART_PATH) : 0;
}
