#include <compat.h> // IWYU pragma: keep

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <uthash.h>

#ifdef __APPLE__
#include <sys/sysctl.h>
#elif defined(__linux__)
#include <sys/sysinfo.h>
#endif

#include "ant.h"
#include "errors.h"
#include "runtime.h"
#include "internal.h"
#include "silver/engine.h"
#include "modules/navigator.h"
#include "modules/symbol.h"
#include "gc/modules.h"

typedef enum {
  LOCK_MODE_EXCLUSIVE,
  LOCK_MODE_SHARED
} lock_mode_t;

typedef struct lock_entry {
  char *name;
  lock_mode_t mode;
  int shared_count;
  UT_hash_handle hh;
} lock_entry_t;

typedef struct lock_request {
  ant_t *js;
  char *name;
  lock_mode_t mode;
  ant_value_t callback;
  ant_value_t promise;
  struct lock_request *next;
} lock_request_t;

static lock_entry_t *locks = NULL;
static lock_request_t *pending_requests = NULL;

static int get_hardware_concurrency(void) {
#ifdef __APPLE__
  int count;
  size_t size = sizeof(count);
  if (sysctlbyname("hw.ncpu", &count, &size, NULL, 0) == 0) {
    return count;
  }
  return 1;
#elif defined(__linux__)
  int count = (int)sysconf(_SC_NPROCESSORS_ONLN);
  return count > 0 ? count : 1;
#elif defined(_WIN32) || defined(_WIN64)
  SYSTEM_INFO sysinfo;
  GetSystemInfo(&sysinfo);
  return (int)sysinfo.dwNumberOfProcessors;
#else
  return 1;
#endif
}

static const char *get_platform_string(void) {
#if defined(__APPLE__)
  return "MacIntel";
#elif defined(__linux__)
  return "Linux x86_64";
#elif defined(_WIN32) || defined(_WIN64)
  return "Win32";
#elif defined(__FreeBSD__)
  return "FreeBSD";
#else
  return "Unknown";
#endif
}

static lock_entry_t *find_lock(const char *name) {
  lock_entry_t *entry = NULL;
  HASH_FIND_STR(locks, name, entry);
  return entry;
}

static lock_entry_t *create_lock(const char *name, lock_mode_t mode) {
  lock_entry_t *entry = calloc(1, sizeof(lock_entry_t));
  if (!entry) return NULL;
  
  entry->name = strdup(name);
  entry->mode = mode;
  entry->shared_count = (mode == LOCK_MODE_SHARED) ? 1 : 0;
  
  HASH_ADD_STR(locks, name, entry);
  return entry;
}

static bool can_acquire_lock(const char *name, lock_mode_t mode) {
  lock_entry_t *entry = find_lock(name);
  if (!entry) return true;
  if (mode == LOCK_MODE_SHARED && entry->mode == LOCK_MODE_SHARED) return true;
  return false;
}

static void release_lock(const char *name) {
  lock_entry_t *entry = find_lock(name);
  if (!entry) return;
  
  if (entry->mode == LOCK_MODE_SHARED) {
    entry->shared_count--;
    if (entry->shared_count > 0) return;
  }
  
  HASH_DEL(locks, entry);
  free(entry->name);
  free(entry);
}

static void process_pending_requests(ant_t *js);

static ant_value_t make_lock_handler(ant_t *js, ant_value_t cfunc, ant_value_t lock_name, ant_value_t outer_promise) {
  ant_value_t fn_obj = js_mkobj(js);
  
  ant_value_t data_obj = js_mkobj(js);
  js_set(js, data_obj, "lockName", lock_name);
  js_set(js, data_obj, "outerPromise", outer_promise);
  js_set_slot(fn_obj, SLOT_DATA, data_obj);
  js_set_slot(fn_obj, SLOT_CFUNC, cfunc);
  
  return js_obj_to_func(fn_obj);
}

static ant_value_t lock_then_handler(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t current_func = js_getcurrentfunc(js);
  ant_value_t data_obj = js_get_slot(current_func, SLOT_DATA);
  
  ant_value_t lock_name_val = js_get(js, data_obj, "lockName");
  ant_value_t outer_promise = js_get(js, data_obj, "outerPromise");
  ant_value_t result_val = (nargs > 0) ? args[0] : js_mkundef();
  
  size_t name_len;
  char *name = js_getstr(js, lock_name_val, &name_len);
  
  if (name) release_lock(name);
  js_resolve_promise(js, outer_promise, result_val);
  process_pending_requests(js);
  
  return js_mkundef();
}

static ant_value_t lock_catch_handler(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t current_func = js_getcurrentfunc(js);
  ant_value_t data_obj = js_get_slot(current_func, SLOT_DATA);
  
  ant_value_t lock_name_val = js_get(js, data_obj, "lockName");
  ant_value_t outer_promise = js_get(js, data_obj, "outerPromise");
  ant_value_t error_val = (nargs > 0) ? args[0] : js_mkundef();
  
  size_t name_len;
  char *name = js_getstr(js, lock_name_val, &name_len);
  
  if (name) release_lock(name);
  
  js_reject_promise(js, outer_promise, error_val);
  process_pending_requests(js);
  
  return js_mkundef();
}

static void execute_lock_callback(ant_t *js, const char *name, lock_mode_t mode, ant_value_t callback, ant_value_t outer_promise) {
  ant_value_t lock_obj = js_mkobj(js);
  js_set(js, lock_obj, "name", js_mkstr(js, name, strlen(name)));
  js_set(js, lock_obj, "mode", js_mkstr(js, mode == LOCK_MODE_EXCLUSIVE ? "exclusive" : "shared", mode == LOCK_MODE_EXCLUSIVE ? 9 : 6));
  js_set_sym(js, lock_obj, get_toStringTag_sym(), js_mkstr(js, "Lock", 4));
  
  ant_value_t result = sv_vm_call(js->vm, js, callback, js_mkundef(), &lock_obj, 1, NULL, false);
  
  if (vtype(result) == T_ERR) {
    release_lock(name);
    js_reject_promise(js, outer_promise, result);
    process_pending_requests(js);
    return;
  }
  
  if (vtype(result) == T_PROMISE) {
    ant_value_t name_str = js_mkstr(js, name, strlen(name));
    ant_value_t on_resolve = make_lock_handler(js, js_mkfun(lock_then_handler), name_str, outer_promise);
    ant_value_t on_reject = make_lock_handler(js, js_mkfun(lock_catch_handler), name_str, outer_promise);
    js_promise_then(js, result, on_resolve, on_reject);
    return;
  }
  
  release_lock(name);
  js_resolve_promise(js, outer_promise, result);
  process_pending_requests(js);
}

static void process_pending_requests(ant_t *js) {
  lock_request_t *prev = NULL;
  lock_request_t *req = pending_requests;
  
  while (req) {
    if (can_acquire_lock(req->name, req->mode)) {
      lock_entry_t *entry = find_lock(req->name);
      
      if (entry && entry->mode == LOCK_MODE_SHARED && req->mode == LOCK_MODE_SHARED) {
        entry->shared_count++;
      } else {
        create_lock(req->name, req->mode);
      }
      
      lock_request_t *to_process = req;
      
      if (prev) {
        prev->next = req->next;
      } else {
        pending_requests = req->next;
      }
      req = req->next;
      
      execute_lock_callback(js, to_process->name, to_process->mode, to_process->callback, to_process->promise);
      free(to_process->name);
      free(to_process);
      return;
    } else {
      prev = req;
      req = req->next;
    }
  }
}

static ant_value_t locks_request(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "locks.request requires at least 2 arguments");
  }
  
  size_t name_len;
  char *name = js_getstr(js, args[0], &name_len);
  if (!name) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "First argument must be a string");
  }
  
  ant_value_t options = js_mkundef();
  ant_value_t callback;
  lock_mode_t mode = LOCK_MODE_EXCLUSIVE;
  bool if_available = false;
  
  if (nargs == 2) {
    callback = args[1];
  } else {
    options = args[1];
    callback = args[2];
    
    if (is_special_object(options)) {
      ant_value_t mode_val = js_get(js, options, "mode");
      if (vtype(mode_val) == T_STR) {
        size_t mode_len;
        char *mode_str = js_getstr(js, mode_val, &mode_len);
        if (mode_str && strcmp(mode_str, "shared") == 0) mode = LOCK_MODE_SHARED;
      }
      
      if (js_get(js, options, "ifAvailable") == js_true) if_available = true;
    }
  }
  
  if (vtype(callback) != T_FUNC) {
    return js_mkerr_typed(js, JS_ERR_TYPE, "Callback must be a function");
  }
  
  ant_value_t promise = js_mkpromise(js);
  
  if (if_available && !can_acquire_lock(name, mode)) {
    ant_value_t null_val = js_mknull();
    ant_value_t result = sv_vm_call(js->vm, js, callback, js_mkundef(), &null_val, 1, NULL, false);
    
    if (vtype(result) == T_PROMISE) {
      ant_value_t on_resolve = make_lock_handler(
        js, js_mkfun(lock_then_handler),
        js_mkstr(js, "", 0), promise
      );
      js_promise_then(js, result, on_resolve, js_mkundef());
      return promise;
    }
    js_resolve_promise(js, promise, result);
    return promise;
  }
  
  if (can_acquire_lock(name, mode)) {
    lock_entry_t *entry = find_lock(name);
    
    if (entry && entry->mode == LOCK_MODE_SHARED && mode == LOCK_MODE_SHARED) {
      entry->shared_count++;
    } else {
      create_lock(name, mode);
    }
    
    execute_lock_callback(js, name, mode, callback, promise);
  } else {
    lock_request_t *req = calloc(1, sizeof(lock_request_t));
    req->js = js;
    req->name = strdup(name);
    req->mode = mode;
    req->callback = callback;
    req->promise = promise;
    req->next = NULL;
    
    lock_request_t *tail = pending_requests;
    if (!tail) {
      pending_requests = req;
    } else {
      while (tail->next) tail = tail->next;
      tail->next = req;
    }
  }
  
  return promise;
}

static ant_value_t locks_query(ant_t *js, ant_value_t *args, int nargs) {
  (void)args;
  (void)nargs;
  
  ant_value_t result = js_mkobj(js);
  ant_value_t held_arr = js_mkarr(js);
  ant_value_t pending_arr = js_mkarr(js);
  
  lock_entry_t *entry, *tmp;
  HASH_ITER(hh, locks, entry, tmp) {
    ant_value_t lock_info = js_mkobj(js);
    js_set(js, lock_info, "name", js_mkstr(js, entry->name, strlen(entry->name)));
    js_set(js, lock_info, "mode", js_mkstr(js, entry->mode == LOCK_MODE_EXCLUSIVE ? "exclusive" : "shared", entry->mode == LOCK_MODE_EXCLUSIVE ? 9 : 6));
    js_arr_push(js, held_arr, lock_info);
  }
  
  lock_request_t *req = pending_requests;
  while (req) {
    ant_value_t req_info = js_mkobj(js);
    js_set(js, req_info, "name", js_mkstr(js, req->name, strlen(req->name)));
    js_set(js, req_info, "mode", js_mkstr(js, req->mode == LOCK_MODE_EXCLUSIVE ? "exclusive" : "shared", req->mode == LOCK_MODE_EXCLUSIVE ? 9 : 6));
    js_arr_push(js, pending_arr, req_info);
    req = req->next;
  }
  
  js_set(js, result, "held", held_arr);
  js_set(js, result, "pending", pending_arr);
  
  ant_value_t promise = js_mkpromise(js);
  js_resolve_promise(js, promise, result);
  
  return promise;
}

void init_navigator_module(void) {
  ant_t *js = rt->js;
  
  ant_value_t navigator_obj = js_mkobj(js);
  
  js_set(js, navigator_obj, "hardwareConcurrency", js_mknum((double)get_hardware_concurrency()));
  js_set(js, navigator_obj, "language", js_mkstr(js, "en-US", 5));
  
  ant_value_t languages_arr = js_mkarr(js);
  js_arr_push(js, languages_arr, js_mkstr(js, "en-US", 5));
  js_set(js, navigator_obj, "languages", languages_arr);
  
  const char *platform = get_platform_string();
  js_set(js, navigator_obj, "platform", js_mkstr(js, platform, strlen(platform)));
  
  char user_agent[64];
  snprintf(user_agent, sizeof(user_agent), "Ant/%s", ANT_VERSION);
  js_set(js, navigator_obj, "userAgent", js_mkstr(js, user_agent, strlen(user_agent)));
  
  ant_value_t locks_obj = js_mkobj(js);
  js_set(js, locks_obj, "request", js_mkfun(locks_request));
  js_set(js, locks_obj, "query", js_mkfun(locks_query));
  js_set_sym(js, locks_obj, get_toStringTag_sym(), js_mkstr(js, "LockManager", 11));
  js_set(js, navigator_obj, "locks", locks_obj);
  
  js_set_sym(js, navigator_obj, get_toStringTag_sym(), js_mkstr(js, "Navigator", 9));
  js_set(js, js_glob(js), "navigator", navigator_obj);
}

void gc_mark_navigator(ant_t *js, gc_mark_fn mark) {
  for (lock_request_t *req = pending_requests; req; req = req->next) {
    mark(js, req->callback);
    mark(js, req->promise);
  }
}
