#include "qjs_io.h"
#include "jsffi.h"
#include "qjs_macros.h"
#include "cell.h"

#include "qjs_blob.h"

#include <physfs.h>
#include <string.h>
#include <stdlib.h>
#include "wildmatch.h"

// Helper function for array length using QuickJS
// JS_ArrayLength removed - use JS_ArrayLength directly

// PHYSFS_File free function
static void PHYSFS_File_free(JSRuntime *rt, PHYSFS_File *f)
{
  PHYSFS_close(f);
}

// Class definition for PHYSFS_File
QJSCLASS(PHYSFS_File,)

// Helper function for writing to PHYSFS
static size_t js_physfs_write(JSContext *js, PHYSFS_File *f, JSValue val)
{
  size_t len;
  size_t wrote;
  if (JS_IsString(val)) {
    const char *data = JS_ToCStringLen(js,&len,val);
    wrote = PHYSFS_writeBytes(f,data,len);
    JS_FreeCString(js,data);
  } else {
    unsigned char *data = js_get_blob_data(js,&len,val);
    wrote = PHYSFS_writeBytes(f,data,len);
  }

  if (wrote < len) wrote = -1;
  return wrote;
}

// Glob data structure for filesystem operations
struct globdata {
  JSContext *js;
  JSValue arr;
  char **globs;
  char *glob;
  int idx;
  int recurse;
};

// Callback for enumerate
static int enumerate_cb(void *udata, const char *dir, const char *fname)
{
  struct globdata *data = (struct globdata*)udata;
  char *path;
  int needfree = 0;

  if (dir[0] == 0) path = (char*)fname;
  else {
    size_t dlen = strlen(dir);
    int ends_slash = (dlen && dir[dlen - 1] == '/');
    path = malloc(dlen + strlen(fname) + (ends_slash ? 1 : 2));
    if (ends_slash) sprintf(path, "%s%s", dir, fname); else sprintf(path, "%s/%s", dir, fname);
    needfree = 1;
  }

  PHYSFS_Stat st;
  if (!PHYSFS_stat(path, &st)) {
    if (needfree) free(path);
    return 1;
  }

  /* If it's a directory and we're recursing, enumerate it further. */
  if (st.filetype == PHYSFS_FILETYPE_DIRECTORY && data->recurse)
    PHYSFS_enumerate(path, enumerate_cb, data);

  /* Add this item (file or directory) to the JS array. */
  JS_SetPropertyUint32(data->js, data->arr, data->idx++, JS_NewString(data->js, path));

  if (needfree) free(path);
  return 1; /* continue enumerating */
}

// I/O FUNCTIONS

JSC_SCALL(io_rm,
  if (!PHYSFS_delete(str)) ret = JS_ThrowReferenceError(js,"could not remove %s. %s", str, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
)

JSC_SCALL(io_mkdir,
  if (!PHYSFS_mkdir(str)) ret = JS_ThrowReferenceError(js,"could not make directory %s. %s", str, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
)

JSC_SCALL(io_exists, ret = JS_NewBool(js,PHYSFS_exists(str)); )

JSC_SCALL(io_stat,
  PHYSFS_Stat stat;
  if (!PHYSFS_stat(str, &stat))
    return JS_ThrowReferenceError(js, "%s", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));

  ret = JS_NewObject(js);
  JS_SetPropertyStr(js,ret,"filesize", number2js(js,stat.filesize));
  JS_SetPropertyStr(js,ret,"modtime", number2js(js,stat.modtime));
  JS_SetPropertyStr(js,ret,"createtime", number2js(js,stat.createtime));
  JS_SetPropertyStr(js,ret,"accesstime", number2js(js,stat.accesstime));
)

JSC_SCALL(io_slurpbytes,
  PHYSFS_File *f = PHYSFS_openRead(str);
  if (!f) {
    ret = JS_ThrowReferenceError(js,"physfs error when slurping %s: %s", str, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
    goto END;
  }
  PHYSFS_Stat stat;
  PHYSFS_stat(str,&stat);
  void *data = malloc(stat.filesize);
  PHYSFS_readBytes(f,data,stat.filesize);
  PHYSFS_close(f);  
  ret = js_new_blob_stoned_copy(js,data,stat.filesize);

  END:
)

JSC_SCALL(io_slurp,
  PHYSFS_File *f = PHYSFS_openRead(str);
  if (!f) {
    ret = JS_ThrowReferenceError(js,"physfs error when slurping %s: %s", str, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
    goto END;
  }
  PHYSFS_Stat stat;
  PHYSFS_stat(str,&stat);
  void *data = malloc(stat.filesize);
  PHYSFS_readBytes(f,data,stat.filesize);
  PHYSFS_close(f);  
  ret = JS_NewStringLen(js,data, stat.filesize);

  free(data);

  END:
)

JSC_SCALL(io_slurpwrite,
  PHYSFS_File *f = PHYSFS_openWrite(str);
  if (!f) {
    ret = JS_ThrowReferenceError(js,"could not write to %s: %s", str, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
    goto END;
  }
  size_t wrote = js_physfs_write(js,f,argv[1]);

  PHYSFS_close(f);  
  if (wrote == -1)
    ret = JS_ThrowReferenceError(js,"%s", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
  
  END:
)

JSC_CCALL(io_mount,
  const char *src = JS_ToCString(js, argv[0]);
  const char *mountpoint;
  if (JS_IsNull(argv[1]))
    mountpoint = NULL;
  else
    mountpoint = JS_ToCString(js, argv[1]);
    
  int prepend = 0;
  
  if (argc > 2 && !JS_IsNull(argv[2]))
    prepend = JS_ToBool(js, argv[2]);
  
  if (!PHYSFS_mount(src, mountpoint, prepend))
    ret = JS_ThrowReferenceError(js,"Unable to mount %s at %s: %s", src, mountpoint, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
  
  JS_FreeCString(js, src);
  if (mountpoint)
    JS_FreeCString(js, mountpoint);
)

JSC_SCALL(io_unmount,
  if (!PHYSFS_unmount(str)) ret = JS_ThrowReferenceError(js,"%s", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
)

JSC_SCALL(io_writepath,
  if (!PHYSFS_setWriteDir(str)) ret = JS_ThrowReferenceError(js,"%s", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
)

JSC_SSCALL(io_match,
  if (wildmatch(str, str2, WM_PATHNAME | WM_PERIOD | WM_WILDSTAR) == WM_MATCH)
    ret = JS_NewBool(js,1);
  else
    ret = JS_NewBool(js,0);
)

// Callback for globfs, with support for negative patterns (e.g. "!**/.git")
static int globfs_cb(struct globdata *data, char *dir, char *file)
{
  int needfree = 0;
  char *path;
  PHYSFS_Stat stat;
  char **glob;

  if (dir[0] == 0)
    path = file;
  else {
    path = malloc(strlen(dir) + strlen(file) + 2);
    path[0] = 0;
    strcat(path, dir);
    strcat(path, "/");
    strcat(path, file);
    needfree = 1;
  }

  // Grab filetype now
  PHYSFS_stat(path, &stat);

  if (stat.filetype == PHYSFS_FILETYPE_DIRECTORY) {
    // Check negative patterns first: if directory matches a "!…" pattern, skip it
    for (glob = data->globs; *glob != NULL; glob++) {
      if ((*glob)[0] == '!') {
        const char *neg_pattern = (*glob) + 1;
        if (wildmatch(neg_pattern, path, WM_WILDSTAR) == WM_MATCH)
          goto END;
      }
    }
    // Not excluded: recurse into this directory
    PHYSFS_enumerate(path, globfs_cb, data);
    goto END;
  }

  // It's a file—first see if it matches any negative pattern
  for (glob = data->globs; *glob != NULL; glob++) {
    if ((*glob)[0] == '!') {
      const char *neg_pattern = (*glob) + 1;
      if (wildmatch(neg_pattern, path, WM_WILDSTAR) == WM_MATCH)
        goto END;
    }
  }

  // Now check positive patterns ("**/*.cm", "**/*.ce", etc.)
  for (glob = data->globs; *glob != NULL; glob++) {
    if ((*glob)[0] == '!') 
      continue;
    if (wildmatch(*glob, path, WM_WILDSTAR) == WM_MATCH) {
      JS_SetPropertyUint32(data->js,
                           data->arr,
                           data->idx++,
                           JS_NewString(data->js, path));
      break;
    }
  }

  END:
  if (needfree) 
    free(path);
  return 1;
}

JSC_CCALL(io_globfs,
  ret = JS_NewArray(js);
  struct globdata data;
  data.js = js;
  data.arr = ret;
  data.idx = 0;
  int globs_len = JS_ArrayLength(js,argv[0]);
  const char *globs[globs_len+1];
  for (int i = 0; i < globs_len; i++) {
    JSValue g = JS_GetPropertyUint32(js,argv[0],i);
    globs[i] = JS_ToCString(js,g);
    JS_FreeValue(js,g);
  }
  
  globs[globs_len] = NULL;
  data.globs = globs;

  const char *path = NULL;
  if (!JS_IsNull(argv[1])) path = JS_ToCString(js,argv[1]);
  PHYSFS_enumerate(path, globfs_cb, &data);

  for (int i = 0; i < globs_len; i++)
    JS_FreeCString(js,globs[i]);

  ret = data.arr;
  JS_FreeCString(js,path);
)

JSC_SCALL(io_enumerate,
  /* First argument: str (directory name) */
  /* Second argument: boolean => recurse or not */
  ret = JS_NewArray(js);

  struct globdata data;
  data.js = js;
  data.arr = ret;
  data.idx = 0;
  data.glob = NULL;   /* not used here */
  data.globs = NULL;  /* not used here */
  data.recurse = 0;

  if (!JS_IsNull(argv[1])) /* parse second arg if provided */
    data.recurse = JS_ToBool(js, argv[1]);

  /* Enumerate the directory given by 'str'. */
  PHYSFS_enumerate(str, enumerate_cb, &data);

  /* Return the JS array we filled. */
  ret = data.arr;
)

JSC_CCALL(io_basedir, return JS_NewString(js,PHYSFS_getBaseDir()))

JSC_SSCALL(io_prefdir, return JS_NewString(js,PHYSFS_getPrefDir(str, str2)))

JSC_SCALL(io_open,
  PHYSFS_File *f = PHYSFS_openWrite(str);
  if (!f)
    ret = JS_ThrowReferenceError(js,"%s", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
  else
    ret = PHYSFS_File2js(js,f);
)

JSC_SCALL(io_realdir,
  const char *real = PHYSFS_getRealDir(str);
  if (!real)
    ret = JS_NULL;
  else
    ret = JS_NewString(js,real);
)

JSC_CCALL(io_searchpath,
  ret = JS_NewArray(js);
  char **paths = PHYSFS_getSearchPath();
  for (int i = 0; paths[i] != NULL; i++)
    JS_SetPropertyUint32(js,ret,i,JS_NewString(js,paths[i]));
  PHYSFS_freeList(paths);
)

JSC_SCALL(io_is_directory,
  PHYSFS_Stat stat;
  int good = PHYSFS_stat(str, &stat);
  if (!good)
   ret = JS_NewBool(js, 0);
  else
    ret = JS_NewBool(js, stat.filetype == PHYSFS_FILETYPE_DIRECTORY);
)

// FILE FUNCTIONS

JSC_CCALL(file_close,
  PHYSFS_File *f = js2PHYSFS_File(js,self);
  PHYSFS_close(f);
)

JSC_CCALL(file_write,
  PHYSFS_File *f = js2PHYSFS_File(js,self);
  size_t wrote = js_physfs_write(js,f,argv[0]);
  if (wrote == -1)
    return JS_ThrowReferenceError(js,"%s", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
)

JSC_CCALL(file_buffer,
  PHYSFS_File *f = js2PHYSFS_File(js,self);
  size_t size = js2number(js,argv[0]);
  if (!PHYSFS_setBuffer(f,size))
    return JS_ThrowReferenceError(js,"%s", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));  
)

JSC_CCALL(file_tell,
  PHYSFS_File *f = js2PHYSFS_File(js,self);
  size_t tell = PHYSFS_tell(f);
  if (tell == -1)
    return JS_ThrowReferenceError(js,"%s", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));

  return number2js(js,tell);
)

JSC_CCALL(file_eof,
  PHYSFS_File *f = js2PHYSFS_File(js,self);
  return JS_NewBool(js, PHYSFS_eof(f));
)

JSC_CCALL(io_mountpoint,
  const char *path = JS_ToCString(js,argv[0]);
  const char *point = PHYSFS_getMountPoint(path);
  if (point)
    return JS_NewString(js,point);
)

static const JSCFunctionListEntry js_io_funcs[] = {
  MIST_FUNC_DEF(io, rm, 1),
  MIST_FUNC_DEF(io, mkdir, 1),
  MIST_FUNC_DEF(io,stat,1),
  MIST_FUNC_DEF(io, globfs, 2),
  MIST_FUNC_DEF(io, match, 2),
  MIST_FUNC_DEF(io, exists, 1),
  MIST_FUNC_DEF(io, mount, 3),
  MIST_FUNC_DEF(io,unmount,1),
  MIST_FUNC_DEF(io,slurp,1),
  MIST_FUNC_DEF(io,slurpbytes,1),
  MIST_FUNC_DEF(io,slurpwrite,2),
  MIST_FUNC_DEF(io,writepath, 1),
  MIST_FUNC_DEF(io,basedir, 0),
  MIST_FUNC_DEF(io, prefdir, 2),
  MIST_FUNC_DEF(io, realdir, 1),
  MIST_FUNC_DEF(io, open, 1),
  MIST_FUNC_DEF(io, searchpath, 0),
  MIST_FUNC_DEF(io, enumerate, 2),
  MIST_FUNC_DEF(io, is_directory, 1),
  MIST_FUNC_DEF(io, mountpoint, 1),
};

static const JSCFunctionListEntry js_PHYSFS_File_funcs[] = {
  MIST_FUNC_DEF(file, close, 0),
  MIST_FUNC_DEF(file, write, 1),
  MIST_FUNC_DEF(file, buffer, 1),
  MIST_FUNC_DEF(file, tell, 0),
  MIST_FUNC_DEF(file, eof, 0),
};

JSValue js_io_use(JSContext *js) {
  QJSCLASSPREP_FUNCS(PHYSFS_File);
  
  JSValue mod = JS_NewObject(js);
  JS_SetPropertyFunctionList(js,mod,js_io_funcs,countof(js_io_funcs));
  
  return mod;
}
