#include "cell.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#include "pd_api.h"

// Global Playdate API pointers - defined in main_playdate.c
extern const struct playdate_file *pd_file;

// --- Helper Functions ---

// Convert JS value to SDFile* (casting via intptr_t/int)
static SDFile* js2fd(JSContext *ctx, JSValueConst val)
{
    int32_t fd_int;
    if (JS_ToInt32(ctx, &fd_int, val) < 0) {
        JS_ThrowTypeError(ctx, "Expected file descriptor number");
        return NULL;
    }
    return (SDFile*)(intptr_t)fd_int;
}

static time_t pd_time_to_unix(FileStat *fs) {
    struct tm t = {0};
    t.tm_year = fs->m_year - 1900;
    t.tm_mon = fs->m_month - 1;
    t.tm_mday = fs->m_day;
    t.tm_hour = fs->m_hour;
    t.tm_min = fs->m_minute;
    t.tm_sec = fs->m_second;
    return mktime(&t);
}

// --- Implementation ---

JSC_SCALL(fd_open,
    FileOptions flags = kFileRead;
    
    // Parse optional flags argument
    if (argc > 1 && JS_IsText(argv[1])) {
        const char *flag_str = JS_ToCString(js, argv[1]);
        flags = 0;
        
        if (strchr(flag_str, 'r')) flags |= kFileRead;
        if (strchr(flag_str, 'w')) flags |= kFileWrite;
        if (strchr(flag_str, 'a')) flags |= kFileAppend;
        if (strchr(flag_str, '+')) {
            // Read+Write not directly mapped, assume Write covers it or combine?
            // Playdate docs say kFileRead|kFileWrite is valid
            flags |= kFileRead | kFileWrite;
        }
        
        JS_FreeCString(js, flag_str);
    }
    
    SDFile* file = pd_file->open(str, flags);
    if (!file) {
        const char* err = pd_file->geterr();
        ret = JS_ThrowInternalError(js, "open failed: %s", err ? err : "unknown error");
    } else {
        ret = JS_NewInt32(js, (int32_t)(intptr_t)file);
    }
)

JSC_CCALL(fd_write,
    SDFile* fd = js2fd(js, argv[0]);
    if (!fd) return JS_EXCEPTION;
    
    size_t len;
    int wrote;
    if (JS_IsText(argv[1])) {
        const char *data = JS_ToCStringLen(js, &len, argv[1]);
        if (!data) return JS_EXCEPTION;
        wrote = pd_file->write(fd, data, (unsigned int)len);
        JS_FreeCString(js, data);
    } else {
        void *data = js_get_blob_data(js, &len, argv[1]);
        if (data == (void*)-1)
            return JS_EXCEPTION;
        wrote = pd_file->write(fd, data, (unsigned int)len);
    }

    if (wrote < 0) {
        const char* err = pd_file->geterr();
        return JS_ThrowInternalError(js, "write failed: %s", err ? err : "unknown error");
    }

    return JS_NewInt64(js, wrote);
)

JSC_CCALL(fd_read,
    SDFile* fd = js2fd(js, argv[0]);
    if (!fd) return JS_EXCEPTION;
    
    size_t size = 4096;
    if (argc > 1)
        size = js2number(js, argv[1]);
    
    void *buf = malloc(size);
    if (!buf)
        return JS_ThrowInternalError(js, "malloc failed");
    
    int bytes_read = pd_file->read(fd, buf, (unsigned int)size);
    if (bytes_read < 0) {
        free(buf);
        const char* err = pd_file->geterr();
        return JS_ThrowInternalError(js, "read failed: %s", err ? err : "unknown error");
    }
    
    ret = js_new_blob_stoned_copy(js, buf, bytes_read);
    free(buf);
    return ret;
)

JSC_SCALL(fd_slurp,
    FileStat st;
    if (pd_file->stat(str, &st) != 0) {
        // const char* err = pd_file->geterr(); // stat usually returns -1 on error
        return JS_ThrowInternalError(js, "stat failed for %s", str);
    }
    
    if (st.isdir)
        return JS_ThrowTypeError(js, "path is a directory");
    
    size_t size = st.size;
    if (size == 0)
        return js_new_blob_stoned_copy(js, NULL, 0);
    
    SDFile* fd = pd_file->open(str, kFileRead);
    if (!fd) {
        const char* err = pd_file->geterr();
        return JS_ThrowInternalError(js, "open failed: %s", err ? err : "unknown error");
    }
    
    void *data = malloc(size);
    if (!data) {
        pd_file->close(fd);
        return JS_ThrowInternalError(js, "malloc failed");
    }

    int bytes_read = pd_file->read(fd, data, (unsigned int)size);
    pd_file->close(fd);

    if (bytes_read < 0) {
        free(data);
        return JS_ThrowInternalError(js, "read failed");
    }

    ret = js_new_blob_stoned_copy(js, data, bytes_read);
    free(data);
)

JSC_CCALL(fd_lseek,
    SDFile* fd = js2fd(js, argv[0]);
    if (!fd) return JS_EXCEPTION;
    
    int offset = (int)js2number(js, argv[1]);
    int whence = SEEK_SET;
    
    if (argc > 2) {
        const char *whence_str = JS_ToCString(js, argv[2]);
        if (strcmp(whence_str, "cur") == 0) whence = SEEK_CUR;
        else if (strcmp(whence_str, "end") == 0) whence = SEEK_END;
        JS_FreeCString(js, whence_str);
    }
    
    if (pd_file->seek(fd, offset, whence) != 0) {
        const char* err = pd_file->geterr();
        return JS_ThrowInternalError(js, "seek failed: %s", err ? err : "unknown error");
    }
    
    // tell to get new pos
    int new_pos = pd_file->tell(fd);
    return JS_NewInt64(js, new_pos);
)

JSC_CCALL(fd_getcwd,
    // Playdate is always at root of its sandbox
    return JS_NewString(js, "/");
)

JSC_SCALL(fd_rmdir,
    int recursive = 0;
    if (argc > 1)
        recursive = JS_ToBool(js, argv[1]);
    
    if (pd_file->unlink(str, recursive) != 0) {
        const char* err = pd_file->geterr();
        ret = JS_ThrowInternalError(js, "could not remove %s: %s", str, err ? err : "unknown error");
    }
)

JSC_SCALL(fd_mkdir,
    if (pd_file->mkdir(str) != 0) {
        const char* err = pd_file->geterr();
        ret = JS_ThrowInternalError(js, "could not make directory %s: %s", str, err ? err : "unknown error");
    }
)

JSC_SCALL(fd_mv,
    if (argc < 2)
        ret = JS_ThrowTypeError(js, "fd.mv requires 2 arguments: old path and new path");
    else if (!JS_IsText(argv[1]))
        ret = JS_ThrowTypeError(js, "second argument must be a string (new path)");
    else {
        const char *new_path = JS_ToCString(js, argv[1]);
        if (pd_file->rename(str, new_path) != 0) {
            const char* err = pd_file->geterr();
            ret = JS_ThrowInternalError(js, "could not rename %s to %s: %s", str, new_path, err ? err : "unknown error");
        }
        JS_FreeCString(js, new_path);
    }
)

JSC_SCALL(fd_symlink,
    // Not supported
    if (argc >= 2 && JS_IsText(argv[1])) {
        // consume arg
        JS_FreeCString(js, JS_ToCString(js, argv[1])); 
    }
    ret = JS_ThrowInternalError(js, "symlink not supported on Playdate");
)

JSC_SCALL(fd_unlink,
    if (pd_file->unlink(str, 0) != 0) {
         const char* err = pd_file->geterr();
         ret = JS_ThrowInternalError(js, "could not remove file %s: %s", str, err ? err : "unknown error");
    }
)

JSC_SCALL(fd_rm,
    // Recursive remove
    if (pd_file->unlink(str, 1) != 0) {
        const char* err = pd_file->geterr();
        ret = JS_ThrowInternalError(js, "could not remove %s: %s", str, err ? err : "unknown error");
    }
)

JSC_CCALL(fd_fsync,
    SDFile* fd = js2fd(js, argv[0]);
    if (!fd) return JS_EXCEPTION;
    
    if (pd_file->flush(fd) != 0) {
        const char* err = pd_file->geterr();
        return JS_ThrowInternalError(js, "fsync failed: %s", err ? err : "unknown error");
    }
    
    return JS_NULL;
)

JSC_CCALL(fd_close,
    SDFile* fd = js2fd(js, argv[0]);
    if (!fd) return JS_EXCEPTION;
    
    if (pd_file->close(fd) != 0) {
        const char* err = pd_file->geterr();
        return JS_ThrowInternalError(js, "close failed: %s", err ? err : "unknown error");
    }
    
    return JS_NULL;
)

JSC_CCALL(fd_fstat,
    // fstat not supported by Playdate API
    SDFile* fd = js2fd(js, argv[0]);
    if (!fd) return JS_EXCEPTION;
    
    // We can't get stat from SDFile*. Return minimal object or error?
    // fd.c returns full object.
    // Return empty object with error property? Or just size 0?
    // Let's return empty object.
    JSValue obj = JS_NewObject(js);
    return obj;
)

JSC_SCALL(fd_stat,
    FileStat st;
    if (pd_file->stat(str, &st) != 0) {
        return JS_NewObject(js); // Return empty on failure/not found
    }

    JSValue obj = JS_NewObject(js);
    JS_SetPropertyStr(js, obj, "size", JS_NewInt64(js, st.size));
    // Approximate mode
    JS_SetPropertyStr(js, obj, "mode", JS_NewInt32(js, st.isdir ? 0040755 : 0100644)); 
    JS_SetPropertyStr(js, obj, "uid", JS_NewInt32(js, 0));
    JS_SetPropertyStr(js, obj, "gid", JS_NewInt32(js, 0));
    
    time_t ts = pd_time_to_unix(&st);
    JS_SetPropertyStr(js, obj, "atime", JS_NewInt64(js, ts));
    JS_SetPropertyStr(js, obj, "mtime", JS_NewInt64(js, ts));
    JS_SetPropertyStr(js, obj, "ctime", JS_NewInt64(js, ts));
    
    JS_SetPropertyStr(js, obj, "nlink", JS_NewInt32(js, 1));
    JS_SetPropertyStr(js, obj, "ino", JS_NewInt64(js, 0));
    JS_SetPropertyStr(js, obj, "dev", JS_NewInt32(js, 0));
    JS_SetPropertyStr(js, obj, "rdev", JS_NewInt32(js, 0));
    JS_SetPropertyStr(js, obj, "blksize", JS_NewInt32(js, 512));
    JS_SetPropertyStr(js, obj, "blocks", JS_NewInt64(js, (st.size + 511) / 512));
    
    JS_SetPropertyStr(js, obj, "isFile", JS_NewBool(js, !st.isdir));
    JS_SetPropertyStr(js, obj, "isDirectory", JS_NewBool(js, st.isdir));
    JS_SetPropertyStr(js, obj, "isSymlink", JS_NewBool(js, 0));
    JS_SetPropertyStr(js, obj, "isFIFO", JS_NewBool(js, 0));
    JS_SetPropertyStr(js, obj, "isSocket", JS_NewBool(js, 0));
    JS_SetPropertyStr(js, obj, "isCharDevice", JS_NewBool(js, 0));
    JS_SetPropertyStr(js, obj, "isBlockDevice", JS_NewBool(js, 0));
    
    return obj;
)

struct listfiles_ctx {
    JSContext *js;
    JSValue array;
    int index;
};

static void listfiles_cb(const char *path, void *userdata) {
    struct listfiles_ctx *ctx = (struct listfiles_ctx*)userdata;
    if (strcmp(path, ".") == 0 || strcmp(path, "..") == 0) return;
    
    // Playdate listfiles returns just the name, but sometimes with slash for dir?
    // Docs say "names of files".
    
    JS_SetPropertyNumber(ctx->js, ctx->array, ctx->index++, JS_NewString(ctx->js, path));
}

JSC_SCALL(fd_readdir,
    JSValue ret_arr = JS_NewArray(js);
    struct listfiles_ctx ctx = { js, ret_arr, 0 };
    
    if (pd_file->listfiles(str, listfiles_cb, &ctx, 0) != 0) {
        const char* err = pd_file->geterr();
        JS_FreeValue(js, ret_arr);
        return JS_ThrowInternalError(js, "listfiles failed: %s", err ? err : "unknown error");
    }
    
    ret = ret_arr;
)

JSC_CCALL(fd_is_file,
    const char *path = JS_ToCString(js, argv[0]);
    if (!path) return JS_EXCEPTION;
    
    FileStat st;
    int res = pd_file->stat(path, &st);
    JS_FreeCString(js, path);
    
    if (res != 0) return JS_NewBool(js, 0);
    return JS_NewBool(js, !st.isdir);
)

JSC_CCALL(fd_is_dir,
    const char *path = JS_ToCString(js, argv[0]);
    if (!path) return JS_EXCEPTION;
    
    FileStat st;
    int res = pd_file->stat(path, &st);
    JS_FreeCString(js, path);
    
    if (res != 0) return JS_NewBool(js, 0);
    return JS_NewBool(js, st.isdir);
)

JSC_CCALL(fd_slurpwrite,
    size_t len;
    const char *data = js_get_blob_data(js, &len, argv[1]);

    if (data == (const char *)-1)
        return JS_EXCEPTION;
    
    const char *str = JS_ToCString(js, argv[0]);
    if (!str) return JS_EXCEPTION;
    
    SDFile* fd = pd_file->open(str, kFileWrite | kFileRead); // Truncates by default? Playdate docs: kFileWrite truncates.
    JS_FreeCString(js, str);
    
    if (!fd) {
        const char* err = pd_file->geterr();
        return JS_ThrowInternalError(js, "open failed: %s", err ? err : "unknown error");
    }
    
    int written = pd_file->write(fd, data, (unsigned int)len);
    pd_file->close(fd);
    
    if (written != (int)len) {
        const char* err = pd_file->geterr();
        return JS_ThrowInternalError(js, "write failed: %s", err ? err : "unknown error");
    }
    
    return JS_NULL;
)

// Helper for recursive enumeration
static void visit_directory_pd(JSContext *js, JSValue results, int *result_count, const char *curr_path, const char *rel_prefix, int recurse) {
    // We can't use listfiles with arbitrary userdata easily if we need to pass multiple args without struct
    // We'll reuse listfiles but we need a way to pass context.
    // The context struct can hold current path info.
    
    // Actually, listfiles takes a callback.
    // We need to implement recursion.
    // But listfiles doesn't give us full paths in callback, just names.
    // So we can reconstruct.
    
    // We need a nested struct or helper.
}

struct enum_ctx {
    JSContext *js;
    JSValue results;
    int *count;
    const char *curr_path;
    const char *rel_prefix;
    int recurse;
};

static void enum_cb(const char *name, void *userdata) {
    struct enum_ctx *ctx = (struct enum_ctx*)userdata;
    if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) return;
    
    char item_rel[512]; // PATH_MAX
    if (ctx->rel_prefix && strlen(ctx->rel_prefix) > 0) {
        snprintf(item_rel, sizeof(item_rel), "%s/%s", ctx->rel_prefix, name);
    } else {
        strcpy(item_rel, name);
    }
    
    JS_SetPropertyNumber(ctx->js, ctx->results, (*ctx->count)++, JS_NewString(ctx->js, item_rel));
    
    if (ctx->recurse) {
        // Check if directory
        char child_path[512];
        snprintf(child_path, sizeof(child_path), "%s/%s", ctx->curr_path, name);
        
        // Remove trailing slash if name has it? Playdate names sometimes have trailing slash for dirs
        // But stat should handle it.
        
        FileStat st;
        if (pd_file->stat(child_path, &st) == 0 && st.isdir) {
             struct enum_ctx subctx = *ctx;
             subctx.curr_path = child_path;
             subctx.rel_prefix = item_rel;
             pd_file->listfiles(child_path, enum_cb, &subctx, 0);
        }
    }
}

JSC_SCALL(fd_enumerate,
    const char *path = str;
    if (!path) path = ".";
    int recurse = 0;

    if (argc > 1)
        recurse = JS_ToBool(js, argv[1]);

    JSValue results = JS_NewArray(js);
    int result_count = 0;

    FileStat st;
    if (pd_file->stat(path, &st) == 0 && st.isdir) {
        struct enum_ctx ctx = { js, results, &result_count, path, "", recurse };
        pd_file->listfiles(path, enum_cb, &ctx, 0);
    }

    ret = results;
)

JSC_CCALL(fd_realpath,
    const char *path = JS_ToCString(js, argv[0]);
    if (!path) return JS_EXCEPTION;
    // Playdate has no realpath, just return path
    JSValue res = JS_NewString(js, path);
    JS_FreeCString(js, path);
    return res;
)

JSC_CCALL(fd_is_link,
    const char *path = JS_ToCString(js, argv[0]);
    JS_FreeCString(js, path);
    return JS_NewBool(js, 0);
)

JSC_CCALL(fd_readlink,
    const char *path = JS_ToCString(js, argv[0]);
    JS_FreeCString(js, path);
    return JS_ThrowInternalError(js, "readlink not supported");
)

static const JSCFunctionListEntry js_fd_funcs[] = {
    MIST_FUNC_DEF(fd, open, 2),
    MIST_FUNC_DEF(fd, write, 2),
    MIST_FUNC_DEF(fd, read, 2),
    MIST_FUNC_DEF(fd, slurp, 1),
    MIST_FUNC_DEF(fd, slurpwrite, 2),
    MIST_FUNC_DEF(fd, lseek, 3),
    MIST_FUNC_DEF(fd, getcwd, 0),
    MIST_FUNC_DEF(fd, rmdir, 2),
    MIST_FUNC_DEF(fd, unlink, 1),
    MIST_FUNC_DEF(fd, mkdir, 1),
    MIST_FUNC_DEF(fd, mv, 2),
    MIST_FUNC_DEF(fd, rm, 1),
    MIST_FUNC_DEF(fd, fsync, 1),
    MIST_FUNC_DEF(fd, close, 1),
    MIST_FUNC_DEF(fd, stat, 1),
    MIST_FUNC_DEF(fd, fstat, 1),
    MIST_FUNC_DEF(fd, readdir, 1),
    MIST_FUNC_DEF(fd, is_file, 1),
    MIST_FUNC_DEF(fd, is_dir, 1),
    MIST_FUNC_DEF(fd, is_link, 1),
    MIST_FUNC_DEF(fd, enumerate, 2),
    MIST_FUNC_DEF(fd, symlink, 2),
    MIST_FUNC_DEF(fd, realpath, 1),
    MIST_FUNC_DEF(fd, readlink, 1),
};

JSValue js_fd_use(JSContext *js) {
    JSValue mod = JS_NewObject(js);
    JS_SetPropertyFunctionList(js, mod, js_fd_funcs, countof(js_fd_funcs));
    return mod;
}
