// 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.

// dvxres.c  -- DVX resource tool
//
// Manages resource blocks appended to DXE3 files (.app, .wgt, .lib).
// Builds on both DOS (DJGPP) and Linux (GCC) with no dependencies
// beyond standard C.
//
// Commands:
//   dvxres add   <file> <name> <type> <data|@file>
//   dvxres build <file> <manifest.res>
//   dvxres list  <file>
//   dvxres get   <file> <name> [outfile]
//   dvxres strip <file>
//
// The resource block is appended after the DXE3 content:
//   [resource data entries]
//   [resource directory]
//   [footer with magic + directory offset + count]

#include "dvxResWrite.h"

#include <ctype.h>
#include <stdbool.h>

// ============================================================
// Prototypes
// ============================================================

static int  cmdAdd(const char *dxePath, const char *name, const char *typeStr, const char *dataArg);
static int  cmdBuild(const char *dxePath, const char *manifestPath);
static int  cmdGet(const char *dxePath, const char *name, const char *outPath);
static int  cmdList(const char *dxePath);
static int  cmdStrip(const char *dxePath);
static int  parseType(const char *typeStr);
static void usage(void);


static int cmdAdd(const char *dxePath, const char *name, const char *typeStr, const char *dataArg) {
    int type = parseType(typeStr);

    if (type < 0) {
        fprintf(stderr, "Unknown resource type: %s (use icon, text, or binary)\n", typeStr);
        return 1;
    }

    if (strlen(name) >= DVX_RES_NAME_MAX) {
        fprintf(stderr, "Resource name too long (max %d chars)\n", DVX_RES_NAME_MAX - 1);
        return 1;
    }

    // Prepare resource data
    uint8_t *newData = NULL;
    uint32_t newSize = 0;

    if (type == DVX_RES_TEXT) {
        newSize = (uint32_t)strlen(dataArg) + 1;
        newData = (uint8_t *)malloc(newSize);

        if (!newData) {
            fprintf(stderr, "Out of memory\n");
            return 1;
        }

        memcpy(newData, dataArg, newSize);
    } else {
        // Binary/icon: dataArg is a file path (strip leading @ if present)
        const char *filePath = dataArg;

        if (filePath[0] == '@') {
            filePath++;
        }

        int32_t readLen = 0;
        newData = (uint8_t *)platformReadFile(filePath, &readLen);

        if (!newData) {
            fprintf(stderr, "Cannot open or read data file: %s\n", filePath);
            return 1;
        }

        newSize = (uint32_t)readLen;
    }

    int result = dvxResAppendEntry(dxePath, name, (uint32_t)type, newData, newSize);
    free(newData);

    if (result != 0) {
        fprintf(stderr, "Failed to write resources to: %s\n", dxePath);
        return 1;
    }

    printf("Added '%s' (%s, %u bytes) to %s\n", name, typeStr, (unsigned)newSize, dxePath);
    return 0;
}


static int cmdBuild(const char *dxePath, const char *manifestPath) {
    FILE *mf = fopen(manifestPath, "r");

    if (!mf) {
        fprintf(stderr, "Cannot open manifest: %s\n", manifestPath);
        return 1;
    }

    long dxeSize = dvxResDxeContentSize(dxePath);

    if (dxeSize < 0) {
        fclose(mf);
        fprintf(stderr, "Cannot open: %s\n", dxePath);
        return 1;
    }

    // Strip any existing resources first -- start fresh
    DvxResDirEntryT *entries = NULL;
    uint32_t         count   = 0;
    uint8_t        **data    = NULL;

    char line[1024];
    int  lineNum = 0;

    while (fgets(line, sizeof(line), mf)) {
        lineNum++;

        // Strip trailing newline/CR
        size_t len = strlen(line);

        while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
            line[--len] = '\0';
        }

        // Skip empty lines and comments
        char *p = line;

        while (*p && isspace((unsigned char)*p)) {
            p++;
        }

        if (*p == '\0' || *p == '#') {
            continue;
        }

        // Parse: name type data
        // name and type are whitespace-delimited
        // data is either a quoted string or a filename (rest of line)
        char name[DVX_RES_NAME_MAX];
        char typeStr[16];
        char dataArg[512];

        if (sscanf(p, "%31s %15s", name, typeStr) != 2) {
            fprintf(stderr, "%s:%d: expected: name type data\n", manifestPath, lineNum);
            goto fail;
        }

        // Advance past name and type to get data
        char *dp = p;

        // Skip name
        while (*dp && !isspace((unsigned char)*dp)) {
            dp++;
        }

        while (*dp && isspace((unsigned char)*dp)) {
            dp++;
        }

        // Skip type
        while (*dp && !isspace((unsigned char)*dp)) {
            dp++;
        }

        while (*dp && isspace((unsigned char)*dp)) {
            dp++;
        }

        if (*dp == '\0') {
            fprintf(stderr, "%s:%d: missing data argument\n", manifestPath, lineNum);
            goto fail;
        }

        // Handle quoted strings: strip quotes
        if (*dp == '"') {
            dp++;
            char *end = strrchr(dp, '"');

            if (end) {
                *end = '\0';
            }
        }

        strncpy(dataArg, dp, sizeof(dataArg) - 1);
        dataArg[sizeof(dataArg) - 1] = '\0';

        int type = parseType(typeStr);

        if (type < 0) {
            fprintf(stderr, "%s:%d: unknown type: %s\n", manifestPath, lineNum, typeStr);
            goto fail;
        }

        // Prepare data
        uint8_t *newData = NULL;
        uint32_t newSize = 0;

        if (type == DVX_RES_TEXT) {
            newSize = (uint32_t)strlen(dataArg) + 1;
            newData = (uint8_t *)malloc(newSize);

            if (!newData) {
                fprintf(stderr, "Out of memory\n");
                goto fail;
            }

            memcpy(newData, dataArg, newSize);
        } else {
            int32_t readLen = 0;
            newData = (uint8_t *)platformReadFile(dataArg, &readLen);

            if (!newData) {
                fprintf(stderr, "%s:%d: cannot open or read: %s\n", manifestPath, lineNum, dataArg);
                goto fail;
            }

            newSize = (uint32_t)readLen;
        }

        // Append.  Use tmp pointers so a realloc failure doesn't
        // strand the existing blocks as unreachable leaks.  As a
        // host build-time tool, abort on OOM -- nothing useful to
        // recover to.
        DvxResDirEntryT *newEntries = (DvxResDirEntryT *)realloc(entries, (count + 1) * sizeof(DvxResDirEntryT));

        if (!newEntries) {
            fprintf(stderr, "dvxres: out of memory\n");
            exit(1);
        }

        entries = newEntries;

        uint8_t **newDataArr = (uint8_t **)realloc(data, (count + 1) * sizeof(uint8_t *));

        if (!newDataArr) {
            fprintf(stderr, "dvxres: out of memory\n");
            exit(1);
        }

        data = newDataArr;

        memset(&entries[count], 0, sizeof(DvxResDirEntryT));
        snprintf(entries[count].name, DVX_RES_NAME_MAX, "%s", name);
        entries[count].type = (uint32_t)type;
        entries[count].size = newSize;
        data[count] = newData;
        count++;

        printf("  %s (%s, %u bytes)\n", name, typeStr, (unsigned)newSize);
    }

    fclose(mf);

    if (count == 0) {
        printf("No resources in manifest.\n");
        free(entries);
        free(data);
        return 0;
    }

    int result = dvxResWriteBlock(dxePath, dxeSize, entries, count, data);

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

    free(data);
    free(entries);

    if (result != 0) {
        fprintf(stderr, "Failed to write resources to: %s\n", dxePath);
        return 1;
    }

    printf("Built %u resource(s) into %s\n", (unsigned)count, dxePath);
    return 0;

fail:
    fclose(mf);

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

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


static int cmdGet(const char *dxePath, const char *name, const char *outPath) {
    DvxResHandleT *h = dvxResOpen(dxePath);

    if (!h) {
        fprintf(stderr, "No resources in %s\n", dxePath);
        return 1;
    }

    uint32_t size = 0;
    void    *buf  = dvxResRead(h, name, &size);

    dvxResClose(h);

    if (!buf) {
        fprintf(stderr, "Resource not found: %s\n", name);
        return 1;
    }

    if (outPath) {
        FILE *f = fopen(outPath, "wb");

        if (!f) {
            free(buf);
            fprintf(stderr, "Cannot write: %s\n", outPath);
            return 1;
        }

        fwrite(buf, 1, size, f);
        fclose(f);
        printf("Extracted '%s' (%u bytes) to %s\n", name, (unsigned)size, outPath);
    } else {
        // Write to stdout
        fwrite(buf, 1, size, stdout);
    }

    free(buf);
    return 0;
}


static int cmdList(const char *dxePath) {
    FILE *f = fopen(dxePath, "rb");

    if (!f) {
        fprintf(stderr, "Cannot open: %s\n", dxePath);
        return 1;
    }

    fseek(f, -(long)sizeof(DvxResFooterT), SEEK_END);
    DvxResFooterT footer;

    if (fread(&footer, sizeof(footer), 1, f) != 1 || footer.magic != DVX_RES_MAGIC) {
        fclose(f);
        printf("No resources in %s\n", dxePath);
        return 0;
    }

    fseek(f, footer.dirOffset, SEEK_SET);

    printf("Resources in %s (%u entries):\n", dxePath, (unsigned)footer.entryCount);
    printf("  %-31s %-8s %s\n", "Name", "Type", "Size");
    printf("  %-31s %-8s %s\n", "-------------------------------", "--------", "--------");

    for (uint32_t i = 0; i < footer.entryCount; i++) {
        DvxResDirEntryT entry;

        if (fread(&entry, sizeof(entry), 1, f) != 1) {
            break;
        }

        const char *typeStr = "unknown";

        if (entry.type == DVX_RES_ICON) {
            typeStr = "icon";
        } else if (entry.type == DVX_RES_TEXT) {
            typeStr = "text";
        } else if (entry.type == DVX_RES_BINARY) {
            typeStr = "binary";
        }

        printf("  %-31s %-8s %u\n", entry.name, typeStr, (unsigned)entry.size);
    }

    fclose(f);
    return 0;
}


static int cmdStrip(const char *dxePath) {
    long dxeSize = dvxResDxeContentSize(dxePath);

    if (dxeSize < 0) {
        fprintf(stderr, "Cannot open: %s\n", dxePath);
        return 1;
    }

    // Read file size
    FILE *f = fopen(dxePath, "rb");

    if (!f) {
        return 1;
    }

    fseek(f, 0, SEEK_END);
    long fileSize = ftell(f);
    fclose(f);

    if (fileSize == dxeSize) {
        printf("No resources to strip.\n");
        return 0;
    }

    // Read DXE content
    f = fopen(dxePath, "rb");

    if (!f) {
        fprintf(stderr, "Cannot open: %s\n", dxePath);
        return 1;
    }

    uint8_t *buf = (uint8_t *)malloc(dxeSize);

    if (!buf) {
        fclose(f);
        fprintf(stderr, "Out of memory\n");
        return 1;
    }

    if (fread(buf, 1, dxeSize, f) != (size_t)dxeSize) {
        fprintf(stderr, "Short read on DXE file\n");
        free(buf);
        fclose(f);
        return 1;
    }

    fclose(f);

    // Rewrite truncated
    f = fopen(dxePath, "wb");

    if (!f) {
        free(buf);
        return 1;
    }

    fwrite(buf, 1, dxeSize, f);
    fclose(f);
    free(buf);

    printf("Stripped %ld bytes of resources from %s\n", fileSize - dxeSize, dxePath);
    return 0;
}


static int parseType(const char *typeStr) {
    if (strcmp(typeStr, "icon") == 0 || strcmp(typeStr, "image") == 0) {
        return DVX_RES_ICON;
    }

    if (strcmp(typeStr, "text") == 0) {
        return DVX_RES_TEXT;
    }

    if (strcmp(typeStr, "binary") == 0) {
        return DVX_RES_BINARY;
    }

    return -1;
}


static void usage(void) {
    fprintf(stderr, "DVX Resource Tool\n\n");
    fprintf(stderr, "Usage:\n");
    fprintf(stderr, "  dvxres add   <file> <name> <type> <data|@file>\n");
    fprintf(stderr, "  dvxres build <file> <manifest.res>\n");
    fprintf(stderr, "  dvxres list  <file>\n");
    fprintf(stderr, "  dvxres get   <file> <name> [outfile]\n");
    fprintf(stderr, "  dvxres strip <file>\n\n");
    fprintf(stderr, "Types: icon (or image), text, binary\n\n");
    fprintf(stderr, "For 'add' with type 'text', <data> is the string value.\n");
    fprintf(stderr, "For 'add' with type 'icon' or 'binary', <data> is a file path.\n\n");
    fprintf(stderr, "Manifest format (one per line):\n");
    fprintf(stderr, "  name  type  data\n");
    fprintf(stderr, "  # lines starting with # are comments\n");
}


int main(int argc, char **argv) {
    if (argc < 3) {
        usage();
        return 1;
    }

    const char *cmd  = argv[1];
    const char *file = argv[2];

    if (strcmp(cmd, "add") == 0) {
        if (argc < 6) {
            fprintf(stderr, "Usage: dvxres add <file> <name> <type> <data>\n");
            return 1;
        }

        return cmdAdd(file, argv[3], argv[4], argv[5]);
    }

    if (strcmp(cmd, "build") == 0) {
        if (argc < 4) {
            fprintf(stderr, "Usage: dvxres build <file> <manifest.res>\n");
            return 1;
        }

        return cmdBuild(file, argv[3]);
    }

    if (strcmp(cmd, "list") == 0) {
        return cmdList(file);
    }

    if (strcmp(cmd, "get") == 0) {
        if (argc < 4) {
            fprintf(stderr, "Usage: dvxres get <file> <name> [outfile]\n");
            return 1;
        }

        return cmdGet(file, argv[3], argc >= 5 ? argv[4] : NULL);
    }

    if (strcmp(cmd, "strip") == 0) {
        return cmdStrip(file);
    }

    fprintf(stderr, "Unknown command: %s\n", cmd);
    usage();
    return 1;
}
