#define QOP_IMPLEMENTATION
#include "qop.h"
#include "cell.h"

static JSClassID js_qop_archive_class_id;

static void js_qop_archive_finalizer(JSRuntime *rt, JSValue val) {
    qop_desc *qop = JS_GetOpaque(val, js_qop_archive_class_id);
    if (qop) {
        if (qop->hashmap) {
            js_free_rt(qop->hashmap);
        }
        qop_close(qop);
        js_free_rt(qop);
    }
}

static JSClassDef js_qop_archive_class = {
    "qop archive",
    .finalizer = js_qop_archive_finalizer,
};

static JSClassID js_qop_writer_class_id;

typedef struct {
    FILE *fh;
    qop_file *files;
    int len;
    int capacity;
    unsigned int size;
} qop_writer;

static void js_qop_writer_finalizer(JSRuntime *rt, JSValue val) {
    qop_writer *w = JS_GetOpaque(val, js_qop_writer_class_id);
    if (w) {
        if (w->fh) fclose(w->fh);
        if (w->files) js_free_rt(w->files);
        js_free_rt(w);
    }
}

static JSClassDef js_qop_writer_class = {
    "qop writer",
    .finalizer = js_qop_writer_finalizer,
};

static qop_writer *js2writer(JSContext *js, JSValue v) {
    return JS_GetOpaque(v, js_qop_writer_class_id);
}

// Helper functions for writer
static void write_16(unsigned int v, FILE *fh) {
    unsigned char b[2];
    b[0] = 0xff & (v);
    b[1] = 0xff & (v >> 8);
    fwrite(b, 2, 1, fh);
}

static void write_32(unsigned int v, FILE *fh) {
    unsigned char b[4];
    b[0] = 0xff & (v);
    b[1] = 0xff & (v >> 8);
    b[2] = 0xff & (v >> 16);
    b[3] = 0xff & (v >> 24);
    fwrite(b, 4, 1, fh);
}

static void write_64(unsigned long long v, FILE *fh) {
    unsigned char b[8];
    b[0] = 0xff & (v);
    b[1] = 0xff & (v >> 8);
    b[2] = 0xff & (v >> 16);
    b[3] = 0xff & (v >> 24);
    b[4] = 0xff & (v >> 32);
    b[5] = 0xff & (v >> 40);
    b[6] = 0xff & (v >> 48);
    b[7] = 0xff & (v >> 56);
    fwrite(b, 8, 1, fh);
}

static qop_desc *js2qop(JSContext *js, JSValue v) {
    return JS_GetOpaque(v, js_qop_archive_class_id);
}

static int js_qop_ensure_index(JSContext *js, qop_desc *qop) {
    if (qop->hashmap != NULL) return 1;
    void *buffer = js_malloc_rt(qop->hashmap_size);
    if (!buffer) return 0;
    int num = qop_read_index(qop, buffer);
    if (num == 0) {
        js_free_rt(buffer);
        return 0;
    }
    return 1;
}

JSC_CCALL(qop_open,
    size_t len;
    void *data = js_get_blob_data(js, &len, argv[0]);
    if (data == -1)
        ret = JS_EXCEPTION;
    else if (!data)
        ret = JS_RaiseDisrupt(js, "Empty blob");
    else {
        qop_desc *qop = js_malloc_rt(sizeof(qop_desc));
        if (!qop)
            ret = JS_RaiseOOM(js);
        else {
            int size = qop_open_data((const unsigned char *)data, len, qop);
            if (size == 0) {
                js_free_rt(qop);
                ret = JS_ThrowReferenceError(js, "Failed to open QOP archive from blob");
            } else {
                JSValue obj = JS_NewObjectClass(js, js_qop_archive_class_id);
                JS_SetOpaque(obj, qop);
                ret = obj;
            }
        }
    }
)

JSC_CCALL(qop_write,
    const char *path = JS_ToCString(js, argv[0]);
    if (!path) return JS_EXCEPTION;

    FILE *fh = fopen(path, "wb");
    JS_FreeCString(js, path);
    if (!fh) return JS_RaiseDisrupt(js, "Could not open file for writing");

    qop_writer *w = js_malloc_rt(sizeof(qop_writer));
    if (!w) {
        fclose(fh);
        return JS_RaiseOOM(js);
    }

    w->fh = fh;
    w->capacity = 1024;
    w->len = 0;
    w->size = 0;
    w->files = js_malloc_rt(sizeof(qop_file) * w->capacity);
    if (!w->files) {
        fclose(fh);
        js_free_rt(w);
        return JS_RaiseOOM(js);
    }

    JSValue obj = JS_NewObjectClass(js, js_qop_writer_class_id);
    JS_SetOpaque(obj, w);
    ret = obj;
)


static JSValue js_qop_close(JSContext *js, JSValue self, int argc, JSValue *argv) {
    qop_desc *qop = js2qop(js, self);
    if (!qop)
        return JS_RaiseDisrupt(js, "Invalid QOP archive");

    qop_close(qop);
    JS_SetOpaque(self, NULL); // Prevent double free
    return JS_NULL;
}



static JSValue js_qop_read(JSContext *js, JSValue self, int argc, JSValue *argv) {
    qop_desc *qop = js2qop(js, self);
    if (!qop)
        return JS_RaiseDisrupt(js, "Invalid QOP archive");

    const char *path = JS_ToCString(js, argv[0]);
    if (!path)
        return JS_EXCEPTION;

    if (!js_qop_ensure_index(js, qop)) {
        JS_FreeCString(js, path);
        return JS_RaiseDisrupt(js, "Failed to read QOP index");
    }

    qop_file *file = qop_find(qop, path);
    JS_FreeCString(js, path);

    if (!file) {
        return JS_NULL;
    }

    unsigned char *dest = js_malloc_rt(file->size);
    if (!dest)
        return JS_RaiseOOM(js);

    int bytes = qop_read(qop, file, dest);
    if (bytes == 0) {
        js_free_rt(dest);
        return JS_RaiseDisrupt(js, "Failed to read file");
    }

    JSValue blob = js_new_blob_stoned_copy(js, dest, bytes);
    js_free_rt(dest);
    return blob;
}

static JSValue js_qop_read_ex(JSContext *js, JSValue self, int argc, JSValue *argv) {
    qop_desc *qop = js2qop(js, self);
    if (!qop)
        return JS_RaiseDisrupt(js, "Invalid QOP archive");

    const char *path = JS_ToCString(js, argv[0]);
    if (!path)
        return JS_EXCEPTION;

    if (!js_qop_ensure_index(js, qop)) {
        JS_FreeCString(js, path);
        return JS_RaiseDisrupt(js, "Failed to read QOP index");
    }

    qop_file *file = qop_find(qop, path);
    JS_FreeCString(js, path);

    if (!file)
        return JS_NULL;

    unsigned int start;
    unsigned int len;
    if (JS_ToUint32(js, &start, argv[1]) < 0 || JS_ToUint32(js, &len, argv[2]) < 0)
        return JS_RaiseDisrupt(js, "Invalid start or len");

    unsigned char *dest = js_malloc_rt(len);
    if (!dest)
        return JS_RaiseOOM(js);

    int bytes = qop_read_ex(qop, file, dest, start, len);
    if (bytes == 0) {
        js_free_rt(dest);
        return JS_RaiseDisrupt(js, "Failed to read file part");
    }

    JSValue blob = js_new_blob_stoned_copy(js, dest, bytes);
    js_free_rt(dest);
    return blob;
}

static JSValue js_qop_list(JSContext *js, JSValue self, int argc, JSValue *argv) {
    qop_desc *qop = js2qop(js, self);
    if (!qop)
        return JS_RaiseDisrupt(js, "Invalid QOP archive");

    if (!js_qop_ensure_index(js, qop)) {
        return JS_RaiseDisrupt(js, "Failed to read QOP index");
    }

    JSValue arr = JS_NewArray(js);
    int count = 0;

    for (unsigned int i = 0; i < qop->hashmap_len; i++) {
        qop_file *file = &qop->hashmap[i];
        if (file->size == 0) continue; // empty slot

        char *path = js_malloc_rt(file->path_len);
        if (!path) {
            return JS_RaiseOOM(js);
        }

        int len = qop_read_path(qop, file, path);
        if (len == 0) {
            js_free_rt(path);
            continue; // skip on error
        }

        JSValue str = JS_NewStringLen(js, path, len - 1); // -1 for null terminator
        js_free_rt(path);
        JS_SetPropertyNumber(js, arr, count++, str);
    }

    return arr;
}

static JSValue js_qop_stat(JSContext *js, JSValue self, int argc, JSValue *argv) {
    qop_desc *qop = js2qop(js, self);
    if (!qop) return JS_RaiseDisrupt(js, "Invalid QOP archive");

    const char *path = JS_ToCString(js, argv[0]);
    if (!path) return JS_EXCEPTION;

    if (!js_qop_ensure_index(js, qop)) {
        JS_FreeCString(js, path);
        return JS_RaiseDisrupt(js, "Failed to read QOP index");
    }

    qop_file *file = qop_find(qop, path);
    JS_FreeCString(js, path);

    if (!file) return JS_NULL;

    JSValue obj = JS_NewObject(js);
    JS_SetPropertyStr(js, obj, "size", JS_NewInt64(js, file->size));
    JS_SetPropertyStr(js, obj, "modtime", JS_NewInt64(js, 0));
    JS_SetPropertyStr(js, obj, "isDirectory", JS_NewBool(js, 0));
    return obj;
}

static JSValue js_qop_is_directory(JSContext *js, JSValue self, int argc, JSValue *argv) {
    qop_desc *qop = js2qop(js, self);
    if (!qop) return JS_RaiseDisrupt(js, "Invalid QOP archive");

    const char *path = JS_ToCString(js, argv[0]);
    if (!path) return JS_EXCEPTION;

    if (!js_qop_ensure_index(js, qop)) {
        JS_FreeCString(js, path);
        return JS_RaiseDisrupt(js, "Failed to read QOP index");
    }

    // Check if any file starts with path + "/"
    size_t path_len = strlen(path);
    char *prefix = alloca(path_len + 2);
    memcpy(prefix, path, path_len);
    prefix[path_len] = '/';
    prefix[path_len + 1] = '\0';
    
    int found = 0;
    
    // This is inefficient but simple. QOP doesn't have a directory structure.
    // We iterate all files to check prefixes.
    for (unsigned int i = 0; i < qop->hashmap_len; i++) {
        qop_file *file = &qop->hashmap[i];
        if (file->size == 0) continue;

        // We need to read the path to check it
        // Optimization: check if we can read just the prefix? 
        // qop_read_path reads the whole path.
        
        // Let's read the path.
        char file_path[1024]; // MAX_PATH_LEN
        if (file->path_len > 1024) continue; // Should not happen based on spec
        
        qop_read_path(qop, file, file_path);
        if (strncmp(file_path, prefix, path_len + 1) == 0) {
            found = 1;
            break;
        }
    }
    
    JS_FreeCString(js, path);
    return JS_NewBool(js, found);
}

// Writer methods

static JSValue js_writer_add_file(JSContext *js, JSValue self, int argc, JSValue *argv) {
    qop_writer *w = js2writer(js, self);
    if (!w) return JS_RaiseDisrupt(js, "Invalid QOP writer");

    const char *path = JS_ToCString(js, argv[0]);
    if (!path) return JS_EXCEPTION;

    size_t data_len;
    void *data = js_get_blob_data(js, &data_len, argv[1]);
    if (data == (void*)-1) {
        JS_FreeCString(js, path);
        return JS_EXCEPTION;
    }
    if (!data) {
        JS_FreeCString(js, path);
        return JS_RaiseDisrupt(js, "No blob data present");
    }

    if (w->len >= w->capacity) {
        w->capacity *= 2;
        qop_file *new_files = realloc(w->files, sizeof(qop_file) * w->capacity);
        if (!new_files) {
            JS_FreeCString(js, path);
            return JS_RaiseOOM(js);
        }
        w->files = new_files;
    }

    // Strip leading "./"
    const char *archive_path = path;
    if (path[0] == '.' && path[1] == '/') {
        archive_path = path + 2;
    }

    unsigned long long hash = qop_hash(archive_path);
    int path_len = strlen(archive_path) + 1;

    // Write path
    fwrite(archive_path, 1, path_len, w->fh);
    
    // Write data
    fwrite(data, 1, data_len, w->fh);

    w->files[w->len] = (qop_file){
        .hash = hash,
        .offset = w->size,
        .size = (unsigned int)data_len,
        .path_len = (unsigned short)path_len,
        .flags = QOP_FLAG_NONE
    };
    
    w->size += data_len + path_len;
    w->len++;

    JS_FreeCString(js, path);
    return JS_NULL;
}

static JSValue js_writer_finalize(JSContext *js, JSValue self, int argc, JSValue *argv) {
    qop_writer *w = js2writer(js, self);
    if (!w || !w->fh) return JS_RaiseDisrupt(js, "Invalid QOP writer or already closed");

    unsigned int total_size = w->size + 12; // Header size
    
    for (int i = 0; i < w->len; i++) {
        write_64(w->files[i].hash, w->fh);
        write_32(w->files[i].offset, w->fh);
        write_32(w->files[i].size, w->fh);
        write_16(w->files[i].path_len, w->fh);
        write_16(w->files[i].flags, w->fh);
        total_size += 20;
    }

    // Magic "qopf"
    unsigned int magic = (((unsigned int)'q') <<  0 | ((unsigned int)'o') <<  8 | 
                          ((unsigned int)'p') << 16 | ((unsigned int)'f') << 24);

    write_32(w->len, w->fh);
    write_32(total_size, w->fh);
    write_32(magic, w->fh);

    fclose(w->fh);
    w->fh = NULL;
    
    return JS_NULL;
}

static const JSCFunctionListEntry js_qop_archive_funcs[] = {
    JS_CFUNC_DEF("close", 0, js_qop_close),
    JS_CFUNC_DEF("list", 0, js_qop_list),
    JS_CFUNC_DEF("read", 1, js_qop_read),
    JS_CFUNC_DEF("read_ex", 3, js_qop_read_ex),
    JS_CFUNC_DEF("stat", 1, js_qop_stat),
    JS_CFUNC_DEF("is_directory", 1, js_qop_is_directory),
};

static const JSCFunctionListEntry js_qop_writer_funcs[] = {
    JS_CFUNC_DEF("add_file", 2, js_writer_add_file),
    JS_CFUNC_DEF("finalize", 0, js_writer_finalize),
};

static const JSCFunctionListEntry js_qop_funcs[] = {
    MIST_FUNC_DEF(qop, open, 1),
    MIST_FUNC_DEF(qop, write, 1),
    JS_PROP_INT32_DEF("FLAG_NONE", QOP_FLAG_NONE, 0),
    JS_PROP_INT32_DEF("FLAG_COMPRESSED_ZSTD", QOP_FLAG_COMPRESSED_ZSTD, 0),
    JS_PROP_INT32_DEF("FLAG_COMPRESSED_DEFLATE", QOP_FLAG_COMPRESSED_DEFLATE, 0),
    JS_PROP_INT32_DEF("FLAG_ENCRYPTED", QOP_FLAG_ENCRYPTED, 0),
};

JSValue js_core_qop_use(JSContext *js) {
    JS_FRAME(js);
    JS_NewClassID(&js_qop_archive_class_id);
    JS_NewClass(js, js_qop_archive_class_id, &js_qop_archive_class);
    JS_ROOT(archive_proto, JS_NewObject(js));
    JS_SetPropertyFunctionList(js, archive_proto.val, js_qop_archive_funcs, countof(js_qop_archive_funcs));
    JS_SetClassProto(js, js_qop_archive_class_id, archive_proto.val);

    JS_NewClassID(&js_qop_writer_class_id);
    JS_NewClass(js, js_qop_writer_class_id, &js_qop_writer_class);
    JS_ROOT(writer_proto, JS_NewObject(js));
    JS_SetPropertyFunctionList(js, writer_proto.val, js_qop_writer_funcs, countof(js_qop_writer_funcs));
    JS_SetClassProto(js, js_qop_writer_class_id, writer_proto.val);

    JS_ROOT(mod, JS_NewObject(js));
    JS_SetPropertyFunctionList(js, mod.val, js_qop_funcs, countof(js_qop_funcs));
    JS_RETURN(mod.val);
}
