// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

// dvxResource.c  -- DVX resource runtime API
//
// Reads the resource block appended to DXE3 files. The resource
// block is located by reading the footer at the end of the file.

#include "dvxRes.h"
#include "../../../tools/dvxResWrite.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int32_t dvxResAppend(const char *path, const char *name, uint32_t type, const void *data, uint32_t dataSize) {
    return dvxResAppendEntry(path, name, type, data, dataSize);
}


void dvxResClose(DvxResHandleT *h) {
    if (h) {
        free(h->entries);
        free(h);
    }
}


static int dvxResEntryCmp(const void *a, const void *b) {
    return strcmp(((const DvxResDirEntryT *)a)->name, ((const DvxResDirEntryT *)b)->name);
}


const DvxResDirEntryT *dvxResFind(DvxResHandleT *h, const char *name) {
    if (!h || !name) {
        return NULL;
    }

    // Entries are sorted at open() time so bsearch gives O(log n) lookup.
    DvxResDirEntryT key;
    memset(&key, 0, sizeof(key));
    strncpy(key.name, name, sizeof(key.name) - 1);
    key.name[sizeof(key.name) - 1] = '\0';
    return (const DvxResDirEntryT *)bsearch(&key, h->entries, h->entryCount,
                                             sizeof(DvxResDirEntryT), dvxResEntryCmp);
}


DvxResHandleT *dvxResOpen(const char *path) {
    if (!path) {
        return NULL;
    }

    FILE *f = fopen(path, "rb");

    if (!f) {
        return NULL;
    }

    // Measure the file so the directory size read from the (possibly
    // corrupt/hostile) footer can be bounds-checked before allocating.
    if (fseek(f, 0, SEEK_END) != 0) {
        fclose(f);
        return NULL;
    }

    int64_t fileSize = ftell(f);

    // Read footer from end of file
    if (fseek(f, -(int32_t)sizeof(DvxResFooterT), SEEK_END) != 0) {
        fclose(f);
        return NULL;
    }

    DvxResFooterT footer;

    if (fread(&footer, sizeof(footer), 1, f) != 1) {
        fclose(f);
        return NULL;
    }

    if (footer.magic != DVX_RES_MAGIC || footer.entryCount == 0) {
        fclose(f);
        return NULL;
    }

    // Reject an entryCount that can't fit in the file: this both stops a
    // size_t overflow in the malloc below (entryCount * 48 wraps on a
    // 32-bit target) and prevents the fread from overrunning a tiny
    // allocation when the footer is corrupt.
    if (fileSize < 0 ||
        (int64_t)footer.dirOffset > fileSize ||
        (int64_t)footer.entryCount > fileSize / (int64_t)sizeof(DvxResDirEntryT)) {
        fclose(f);
        return NULL;
    }

    // Read directory
    if (fseek(f, footer.dirOffset, SEEK_SET) != 0) {
        fclose(f);
        return NULL;
    }

    DvxResDirEntryT *entries = (DvxResDirEntryT *)malloc(footer.entryCount * sizeof(DvxResDirEntryT));

    if (!entries) {
        fclose(f);
        return NULL;
    }

    if (fread(entries, sizeof(DvxResDirEntryT), footer.entryCount, f) != footer.entryCount) {
        free(entries);
        fclose(f);
        return NULL;
    }

    // Force-terminate names read from the file before any strcmp (qsort /
    // bsearch); a corrupt unterminated name would otherwise read past the
    // field, and past the allocation for the last entry.
    for (uint32_t i = 0; i < footer.entryCount; i++) {
        entries[i].name[DVX_RES_NAME_MAX - 1] = '\0';
    }

    fclose(f);

    DvxResHandleT *h = (DvxResHandleT *)malloc(sizeof(DvxResHandleT));

    if (!h) {
        free(entries);
        return NULL;
    }

    strncpy(h->path, path, sizeof(h->path) - 1);
    h->path[sizeof(h->path) - 1] = '\0';
    h->entries    = entries;
    h->entryCount = footer.entryCount;

    // Sort once so dvxResFind can bsearch.
    qsort(h->entries, h->entryCount, sizeof(DvxResDirEntryT), dvxResEntryCmp);

    return h;
}


void *dvxResRead(DvxResHandleT *h, const char *name, uint32_t *outSize) {
    const DvxResDirEntryT *entry = dvxResFind(h, name);

    if (!entry) {
        return NULL;
    }

    FILE *f = fopen(h->path, "rb");

    if (!f) {
        return NULL;
    }

    if (fseek(f, entry->offset, SEEK_SET) != 0) {
        fclose(f);
        return NULL;
    }

    void *buf = malloc(entry->size);

    if (!buf) {
        fclose(f);
        return NULL;
    }

    if (fread(buf, 1, entry->size, f) != entry->size) {
        free(buf);
        fclose(f);
        return NULL;
    }

    fclose(f);

    if (outSize) {
        *outSize = entry->size;
    }

    return buf;
}


int32_t dvxResRemove(const char *path, const char *name) {
    if (!path || !name) {
        return -1;
    }

    long dxeSize = dvxResDxeContentSize(path);

    if (dxeSize < 0) {
        return -1;
    }

    DvxResDirEntryT *entries    = NULL;
    uint32_t         count      = 0;
    uint8_t        **data       = NULL;
    int32_t          readResult = dvxResReadExisting(path, dxeSize, &entries, &count, &data);

    // Distinguish a read/OOM error from a file with no resources to remove.
    if (readResult < 0) {
        return -1;
    }

    if (!entries || count == 0) {
        return -1;
    }

    bool found = false;

    for (uint32_t i = 0; i < count; i++) {
        if (strcmp(entries[i].name, name) == 0) {
            free(data[i]);

            for (uint32_t j = i; j < count - 1; j++) {
                entries[j] = entries[j + 1];
                data[j]    = data[j + 1];
            }

            count--;
            found = true;
            break;
        }
    }

    if (!found) {
        for (uint32_t i = 0; i < count; i++) {
            free(data[i]);
        }

        free(data);
        free(entries);
        return -1;
    }

    int32_t result;

    if (count == 0) {
        // No resources left -- truncate to DXE content only
        FILE *f = fopen(path, "rb");

        if (!f) {
            free(data);
            free(entries);
            return -1;
        }

        uint8_t *dxeBuf = (uint8_t *)malloc((size_t)dxeSize);

        if (!dxeBuf) {
            fclose(f);
            free(data);
            free(entries);
            return -1;
        }

        if (fread(dxeBuf, 1, (size_t)dxeSize, f) != (size_t)dxeSize) {
            free(dxeBuf);
            fclose(f);
            free(data);
            free(entries);
            return -1;
        }

        fclose(f);
        f = fopen(path, "wb");

        if (f) {
            fwrite(dxeBuf, 1, (size_t)dxeSize, f);
            fclose(f);
            result = 0;
        } else {
            result = -1;
        }

        free(dxeBuf);
    } else {
        result = dvxResWriteBlock(path, dxeSize, entries, count, data);
    }

    for (uint32_t i = 0; i < count; i++) {
        free(data[i]);
    }

    free(data);
    free(entries);
    return result;
}
