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

// dvxPlatformUtil.c  -- small cross-build platform utilities
//
// Functions here are linked into every DVX build variant: DOS dvx.exe,
// the BASIC standalone compiler BASCOMP.EXE, and the host-side test
// binaries.  Kept independent of the VESA / DPMI / INT-33h code in
// dvxPlatformDos.c so minimal builds without the full platform layer
// can still use them.

#include "dvxPlat.h"
#include "thirdparty/stb_ds_wrap.h"

#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <unistd.h>

#ifdef __DJGPP__
#include <dir.h>
#endif

// Permission bits for directories created by dvxMakeDirs (rwxr-xr-x).
#define MKDIR_MODE 0755

// Transfer block used by platformCopyFile.
#define COPY_FILE_CHUNK 4096


const char *dvxSkipWs(const char *s) {
    if (!s) {
        return s;
    }

    while (*s == ' ' || *s == '\t') {
        s++;
    }

    return s;
}


// Bounded formatted append: format fmt/args into buf starting at byte
// offset pos, never writing past bufSize.  Returns the new position
// (pos + bytes written) on success, or -1 if the result was truncated,
// vsnprintf failed, or pos was already out of range.  Feeding a -1 back in
// as pos propagates, so a chain of appends collapses to a single
// truncation check on the final return value.
int32_t dvxStrAppendf(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) {
    if (!buf || pos < 0 || pos >= bufSize) {
        return -1;
    }

    va_list args;
    int32_t avail   = bufSize - pos;
    int32_t written;

    va_start(args, fmt);
    written = (int32_t)vsnprintf(buf + pos, (size_t)avail, fmt, args);
    va_end(args);

    if (written < 0 || written >= avail) {
        return -1;
    }

    return pos + written;
}


int32_t dvxTrimRight(char *buf) {
    if (!buf) {
        return 0;
    }

    int32_t len = (int32_t)strlen(buf);

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

    return len;
}


bool dvxHasExt(const char *name, const char *ext) {
    if (!name || !ext) {
        return false;
    }

    size_t nameLen = strlen(name);
    size_t extLen  = strlen(ext);

    if (nameLen < extLen) {
        return false;
    }

    return strcasecmp(name + nameLen - extLen, ext) == 0;
}


char **dvxReadDir(const char *dirPath) {
    if (!dirPath) {
        return NULL;
    }

    DIR *dir = opendir(dirPath);

    if (!dir) {
        return NULL;
    }

    char          **entries = NULL;
    struct dirent  *ent;

    while ((ent = readdir(dir)) != NULL) {
        // Skip "." and ".."
        if (ent->d_name[0] == '.' &&
            (ent->d_name[1] == '\0' ||
             (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) {
            continue;
        }

        arrput(entries, strdup(ent->d_name));
    }

    closedir(dir);
    return entries;
}


void dvxReadDirFree(char **entries) {
    if (!entries) {
        return;
    }

    int32_t n = (int32_t)arrlen(entries);

    for (int32_t i = 0; i < n; i++) {
        free(entries[i]);
    }

    arrfree(entries);
}


int32_t platformChdir(const char *path) {
#ifdef __DJGPP__
    if (path[0] && path[1] == ':') {
        int drive = toupper((unsigned char)path[0]) - 'A';
        setdisk(drive);
    }
#endif

    return chdir(path) == 0 ? 0 : -1;
}


bool platformCopyFile(const char *srcPath, const char *dstPath) {
    FILE *src = fopen(srcPath, "rb");

    if (!src) {
        return false;
    }

    FILE *dst = fopen(dstPath, "wb");

    if (!dst) {
        fclose(src);
        return false;
    }

    char   buf[COPY_FILE_CHUNK];
    size_t n;
    bool   ok = true;

    while (ok && (n = fread(buf, 1, sizeof(buf), src)) > 0) {
        ok = (fwrite(buf, 1, n, dst) == n);
    }

    ok = ok && !ferror(src);
    ok = (fclose(dst) == 0) && ok;
    fclose(src);

    if (!ok) {
        remove(dstPath);
    }

    return ok;
}


const char *platformPathBaseName(const char *path) {
    if (!path) {
        return "";
    }

    const char *sep = platformPathDirEnd(path);
    return sep ? sep + 1 : path;
}


// Return the last path separator in `path`, or NULL if none.
//
// Platform-specific, because what counts as a separator varies:
//   - DJGPP/DOS and Win32 accept both '/' and '\\' -- we have to check
//     both and return whichever appears last.
//   - Unix-like systems use only '/'.  '\\' is a legal filename
//     character there, so treating it as a separator would incorrectly
//     split paths containing literal backslashes.
char *platformPathDirEnd(const char *path) {
#if defined(__DJGPP__) || defined(_WIN32) || defined(_WIN64)
    char *fwd  = strrchr(path, '/');
    char *back = strrchr(path, '\\');

    if (back > fwd) {
        return back;
    }

    return fwd;
#else
    return strrchr(path, '/');
#endif
}


// Creates a directory and all parent directories that don't exist.
// Walks the path left-to-right, creating each component.  mkdir() on
// an existing directory returns EEXIST, which we silently ignore.
// DJGPP-style "C:\" drive letters are handled by skipping the colon
// so we don't try to mkdir("C:") -- harmless on POSIX paths that
// don't start with "X:".
int32_t platformMkdirRecursive(const char *path) {
    char buf[DVX_MAX_PATH];
    strncpy(buf, path, sizeof(buf) - 1);
    buf[sizeof(buf) - 1] = '\0';

    for (char *p = buf; *p; p++) {
        // Skip drive letter (e.g. "C:\")
        if (p == buf + 1 && *p == ':') {
            continue;
        }

        if (*p == '/' || *p == '\\') {
            *p = '\0';

            if (buf[0] != '\0') {
                mkdir(buf, MKDIR_MODE);
            }

            *p = '/';
        }
    }

    // Create the final directory
    if (mkdir(buf, MKDIR_MODE) != 0 && errno != EEXIST) {
        return -1;
    }

    return 0;
}


// Remove carriage returns in-place.  Used when reading DOS-format
// text files on hosts that don't auto-strip them.
int32_t platformStripLineEndings(char *buf, int32_t len) {
    int32_t dst = 0;

    for (int32_t src = 0; src < len; src++) {
        if (buf[src] != '\r') {
            buf[dst++] = buf[src];
        }
    }

    buf[dst] = '\0';
    return dst;
}


char *platformReadFile(const char *path, int32_t *outLen) {
    if (outLen) {
        *outLen = 0;
    }

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

    if (!f) {
        return NULL;
    }

    if (fseek(f, 0, SEEK_END) != 0) {
        fclose(f);
        return NULL;
    }

    long size = ftell(f);

    if (size < 0) {
        fclose(f);
        return NULL;
    }

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

    char *buf = (char *)malloc((size_t)size + 1);

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

    size_t got = fread(buf, 1, (size_t)size, f);
    fclose(f);

    if (got != (size_t)size) {
        free(buf);
        return NULL;
    }

    buf[size] = '\0';

    if (outLen) {
        *outLen = (int32_t)size;
    }

    return buf;
}
