#include "quickjs.h"
#include "miniz.h"
#include "qjs_blob.h"

static JSClassID js_reader_class_id;
static JSClassID js_writer_class_id;

static void js_reader_finalizer(JSRuntime *rt, JSValue val) {
  mz_zip_archive *zip = JS_GetOpaque(val, js_reader_class_id);
  mz_zip_reader_end(zip);
  js_free_rt(rt,zip);
}

static void js_writer_finalizer(JSRuntime *rt, JSValue val) {
  mz_zip_archive *zip = JS_GetOpaque(val, js_writer_class_id);
  mz_zip_writer_finalize_archive(zip);
  mz_zip_writer_end(zip);
  js_free_rt(rt,zip);
}

static JSClassDef js_reader_class = {
  "zip reader",
  .finalizer = js_reader_finalizer,
};

static JSClassDef js_writer_class = {
  "zip writer",
  .finalizer = js_writer_finalizer,
};

static mz_zip_archive *js2reader(JSContext *js, JSValue v)
{
  return JS_GetOpaque(v, js_reader_class_id);
}

static mz_zip_archive *js2writer(JSContext *js, JSValue v)
{
  return JS_GetOpaque(v, js_writer_class_id);
}

static JSValue js_miniz_read(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  size_t len;
  void *data = js_get_blob_data(js, &len, argv[0]);
  if (!data)
    return JS_ThrowReferenceError(js, "Could not create data.\n");

  mz_zip_archive *zip = calloc(sizeof(*zip),1);
  int success = mz_zip_reader_init_mem(zip, data, len, 0);
  int err = mz_zip_get_last_error(zip);
  if (err)
    return JS_ThrowInternalError(js, "miniz error: %s\n", mz_zip_get_error_string(err));

  JSValue jszip = JS_NewObjectClass(js, js_reader_class_id);
  JS_SetOpaque(jszip, zip);
  return jszip;
}

static JSValue js_miniz_write(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  const char *file = JS_ToCString(js, argv[0]);
  if (!file)
    return JS_EXCEPTION;
  
  mz_zip_archive *zip = calloc(sizeof(*zip), 1);
  if (!zip) {
    JS_FreeCString(js, file);
    return JS_ThrowOutOfMemory(js);
  }
  
  mz_bool success = mz_zip_writer_init_file(zip, file, 0);
  JS_FreeCString(js, file);
  
  if (!success) {
    int err = mz_zip_get_last_error(zip);
    mz_zip_writer_end(zip);
    free(zip);
    return JS_ThrowInternalError(js, "Failed to initialize zip writer: %s", mz_zip_get_error_string(err));
  }
  
  JSValue jszip = JS_NewObjectClass(js, js_writer_class_id);
  JS_SetOpaque(jszip, zip);
  return jszip;
}

static JSValue js_miniz_compress(JSContext *js, JSValue this_val,
                                 int argc, JSValueConst *argv)
{
  if (argc < 1)
    return JS_ThrowTypeError(js,
            "compress needs a string or ArrayBuffer");

  /* ─── 1. Grab the input data ──────────────────────────────── */
  const char *cstring = NULL;
  size_t      in_len  = 0;
  const void *in_ptr  = NULL;

  if (JS_IsString(argv[0])) {
    /* String → UTF-8 bytes without the terminating NUL */
    cstring = JS_ToCStringLen(js, &in_len, argv[0]);
    if (!cstring)
      return JS_EXCEPTION;
    in_ptr = cstring;
  } else {           /* assume ArrayBuffer / TypedArray */
    in_ptr = js_get_blob_data(js, &in_len, argv[0]);
    if (!in_ptr)
      return JS_ThrowTypeError(js,
              "Argument must be a string or ArrayBuffer");
  }

  /* ─── 2. Allocate an output buffer big enough  ────────────── */
  mz_ulong out_len_est = mz_compressBound(in_len);
  void *out_buf = js_malloc(js, out_len_est);
  if (!out_buf) {
    if (cstring) JS_FreeCString(js, cstring);
    return JS_EXCEPTION;
  }

  /* ─── 3. Do the compression (MZ_DEFAULT_COMPRESSION = level 6) */
  mz_ulong out_len = out_len_est;
  int st = mz_compress2(out_buf, &out_len,
                        in_ptr, in_len, MZ_DEFAULT_COMPRESSION);

  /* clean-up for string input */
  if (cstring) JS_FreeCString(js, cstring);

  if (st != MZ_OK) {
    js_free(js, out_buf);
    return JS_ThrowInternalError(js,
            "miniz: compression failed (%d)", st);
  }

  /* ─── 4. Hand JavaScript a copy of the compressed data ────── */
  JSValue abuf = js_new_blob_stoned_copy(js, out_buf, out_len);
  js_free(js, out_buf);
  return abuf;
}

static JSValue js_miniz_decompress(JSContext *js,
                                   JSValueConst this_val,
                                   int argc,
                                   JSValueConst *argv)
{
  if (argc < 1)
    return JS_ThrowTypeError(js,
            "decompress: need compressed ArrayBuffer");

  /* grab compressed data */
  size_t in_len;
  void  *in_ptr = js_get_blob_data(js, &in_len, argv[0]);
  if (!in_ptr)
    return JS_ThrowTypeError(js,
            "decompress: first arg must be an ArrayBuffer");

  /* zlib header present → tell tinfl to parse it */
  size_t out_len = 0;
  void  *out_ptr = tinfl_decompress_mem_to_heap(
                     in_ptr, in_len, &out_len,
                     TINFL_FLAG_PARSE_ZLIB_HEADER);

  if (!out_ptr)
    return JS_ThrowInternalError(js,
            "miniz: decompression failed");

  /* wrap for JS */
  JSValue ret;
  int asString = (argc > 1) && JS_ToBool(js, argv[1]);

//  if (asString)
    ret = JS_NewStringLen(js, (const char *)out_ptr, out_len);
//  else
//    ret = JS_NewArrayBufferCopy(js, out_ptr, out_len);

#ifdef MZ_FREE
  MZ_FREE(out_ptr);
#else
  free(out_ptr);
#endif
  return ret;
}


static const JSCFunctionListEntry js_miniz_funcs[] = {
  JS_CFUNC_DEF("read", 1, js_miniz_read),
  JS_CFUNC_DEF("write", 1, js_miniz_write),
  JS_CFUNC_DEF("compress", 1, js_miniz_compress),
  JS_CFUNC_DEF("decompress", 1, js_miniz_decompress),
};

JSValue js_writer_add_file(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  if (argc < 2)
    return JS_ThrowTypeError(js, "add_file requires (path, arrayBuffer)");
  
  mz_zip_archive *zip = js2writer(js, self);
  const char *pathInZip = JS_ToCString(js, argv[0]);
  if (!pathInZip)
    return JS_ThrowTypeError(js, "Could not parse path argument");
  
  size_t dataLen;
  void *data = js_get_blob_data(js, &dataLen, argv[1]);
  if (!data) {
    JS_FreeCString(js, pathInZip);
    return JS_ThrowTypeError(js, "Second argument must be an ArrayBuffer");
  }

  int success = mz_zip_writer_add_mem(zip, pathInZip, data, dataLen, MZ_DEFAULT_COMPRESSION);
  JS_FreeCString(js, pathInZip);
  
  if (!success)
    return JS_ThrowInternalError(js, "Failed to add memory to zip");
  
  return JS_NULL;
}


static const JSCFunctionListEntry js_writer_funcs[] = {
  JS_CFUNC_DEF("add_file", 1, js_writer_add_file),
};

JSValue js_reader_mod(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  const char *file = JS_ToCString(js,argv[0]);
  if (!file)
    return JS_EXCEPTION;
  
  mz_zip_archive *zip = js2reader(js, self);
  if (!zip) {
    JS_FreeCString(js, file);
    return JS_ThrowInternalError(js, "Invalid zip reader");
  }
  
  mz_zip_archive_file_stat pstat;
  mz_uint index = mz_zip_reader_locate_file(zip, file, NULL, 0);
  
  if (index == (mz_uint)-1) {
    JS_FreeCString(js, file);
    return JS_ThrowReferenceError(js, "File '%s' not found in archive", file);
  }
  
  JS_FreeCString(js, file);
  
  if (!mz_zip_reader_file_stat(zip, index, &pstat)) {
    int err = mz_zip_get_last_error(zip);
    return JS_ThrowInternalError(js, "Failed to get file stats: %s", mz_zip_get_error_string(err));
  }
  
  return JS_NewFloat64(js, pstat.m_time);
}

JSValue js_reader_exists(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  const char *file = JS_ToCString(js,argv[0]);
  if (!file)
    return JS_EXCEPTION;
  
  mz_zip_archive *zip = js2reader(js, self);
  if (!zip) {
    JS_FreeCString(js, file);
    return JS_ThrowInternalError(js, "Invalid zip reader");
  }
  
  mz_uint index = mz_zip_reader_locate_file(zip, file, NULL, 0);
  JS_FreeCString(js,file);
  if (index == (mz_uint)-1) return JS_NewBool(js, 0);
  return JS_NewBool(js, 1);
}

JSValue js_reader_slurp(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  const char *file = JS_ToCString(js,argv[0]);
  if (!file)
    return JS_EXCEPTION;
  
  mz_zip_archive *zip = js2reader(js, self);
  if (!zip) {
    JS_FreeCString(js, file);
    return JS_ThrowInternalError(js, "Invalid zip reader");
  }
  
  size_t len;
  void *data = mz_zip_reader_extract_file_to_heap(zip, file, &len, 0);
  
  if (!data) {
    int err = mz_zip_get_last_error(zip);
    const char *filename = file;
    JS_FreeCString(js, file);
    return JS_ThrowInternalError(js, "Failed to extract file '%s': %s", filename, mz_zip_get_error_string(err));
  }
  
  JS_FreeCString(js, file);

  JSValue ret = js_new_blob_stoned_copy(js, data, len);
  free(data);
  return ret;
}

JSValue js_reader_list(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  mz_zip_archive *zip = js2reader(js, self);
  if (!zip)
    return JS_ThrowInternalError(js, "Invalid zip reader");
  
  mz_uint num_files = mz_zip_reader_get_num_files(zip);
  
  JSValue arr = JS_NewArray(js);
  if (JS_IsException(arr))
    return arr;
  
  mz_uint arr_index = 0;
  for (mz_uint i = 0; i < num_files; i++) {
    mz_zip_archive_file_stat file_stat;
    if (!mz_zip_reader_file_stat(zip, i, &file_stat))
      continue;
    
    JSValue filename = JS_NewString(js, file_stat.m_filename);
    if (JS_IsException(filename)) {
      JS_FreeValue(js, arr);
      return filename;
    }
    JS_SetPropertyUint32(js, arr, arr_index++, filename);
  }
  
  return arr;
}

JSValue js_reader_is_directory(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  if (argc < 1)
    return JS_ThrowTypeError(js, "is_directory requires a file index");
    
  int32_t index;
  if (JS_ToInt32(js, &index, argv[0]))
    return JS_EXCEPTION;
  
  mz_zip_archive *zip = js2reader(js, self);
  if (!zip)
    return JS_ThrowInternalError(js, "Invalid zip reader");
  
  return JS_NewBool(js, mz_zip_reader_is_file_a_directory(zip, index));
}

JSValue js_reader_get_filename(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  if (argc < 1)
    return JS_ThrowTypeError(js, "get_filename requires a file index");
    
  int32_t index;
  if (JS_ToInt32(js, &index, argv[0]))
    return JS_EXCEPTION;
  
  mz_zip_archive *zip = js2reader(js, self);
  if (!zip)
    return JS_ThrowInternalError(js, "Invalid zip reader");
  
  mz_zip_archive_file_stat file_stat;
  if (!mz_zip_reader_file_stat(zip, index, &file_stat))
    return JS_ThrowInternalError(js, "Failed to get file stats");
    
  return JS_NewString(js, file_stat.m_filename);
}

JSValue js_reader_count(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  mz_zip_archive *zip = js2reader(js, self);
  if (!zip)
    return JS_ThrowInternalError(js, "Invalid zip reader");
    
  return JS_NewUint32(js, mz_zip_reader_get_num_files(zip));
}

static const JSCFunctionListEntry js_reader_funcs[] = {
  JS_CFUNC_DEF("mod", 1, js_reader_mod),
  JS_CFUNC_DEF("exists", 1, js_reader_exists),
  JS_CFUNC_DEF("slurp", 1, js_reader_slurp),
  JS_CFUNC_DEF("list", 0, js_reader_list),
  JS_CFUNC_DEF("is_directory", 1, js_reader_is_directory),
  JS_CFUNC_DEF("get_filename", 1, js_reader_get_filename),
  JS_CFUNC_DEF("count", 0, js_reader_count),
};

JSValue js_miniz_use(JSContext *js)
{
  JS_NewClassID(&js_reader_class_id);
  JS_NewClass(JS_GetRuntime(js), js_reader_class_id, &js_reader_class);
  JSValue reader_proto = JS_NewObject(js);
  JS_SetPropertyFunctionList(js, reader_proto, js_reader_funcs, sizeof(js_reader_funcs) / sizeof(JSCFunctionListEntry));
  JS_SetClassProto(js, js_reader_class_id, reader_proto);

  JS_NewClassID(&js_writer_class_id);
  JS_NewClass(JS_GetRuntime(js), js_writer_class_id, &js_writer_class);
  JSValue writer_proto = JS_NewObject(js);
  JS_SetPropertyFunctionList(js, writer_proto, js_writer_funcs, sizeof(js_writer_funcs) / sizeof(JSCFunctionListEntry));
  JS_SetClassProto(js, js_writer_class_id, writer_proto);
  
  JSValue export = JS_NewObject(js);
  JS_SetPropertyFunctionList(js, export, js_miniz_funcs, sizeof(js_miniz_funcs)/sizeof(JSCFunctionListEntry));
  return export;
}
