#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <signal.h>
#include <stdarg.h>
#include <assert.h>
#include <inttypes.h>
#include <limits.h>
#include <errno.h>
#include <sys/stat.h>
#include <time.h>

#ifdef HAVE_MIMALLOC
#include <mimalloc.h>
#endif

#ifdef __APPLE__
#include <sys/types.h>
#include <sys/sysctl.h>
#endif

#ifdef _WIN32
#include <windows.h>
#endif

#include <SDL3/SDL_atomic.h>

#define WOTA_IMPLEMENTATION
#include "wota.h"
#include "qjs_wota.h"

#define BLOB_IMPLEMENTATION
#include "blob.h"

#include "physfs.h"
#include "stb_ds.h"
#include "jsffi.h"
#include "cell.h"
#include "timer.h"

#ifdef TRACY_ENABLE
#include <tracy/TracyC.h>
#endif

#if defined(__APPLE__)
#include <malloc/malloc.h>
#define MALLOC_OVERHEAD 0
#elif defined(_WIN32)
#include <malloc.h>
#define MALLOC_OVERHEAD 8
#elif defined(__linux__) || defined(__GLIBC__)
#define _GNU_SOURCE
#include <malloc.h>
#define MALLOC_OVERHEAD 8
#else
#define MALLOC_OVERHEAD 0
#endif

#define likely(x)   __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

int tracy_profiling_enabled = 0;

#define ENGINE "engine.cm"

static cell_rt **ready_queue = NULL;
static SDL_Semaphore *ready_sem;
static SDL_SpinLock queue_lock = 0;

static cell_rt **main_queue = NULL;
static SDL_Semaphore *main_sem;
static SDL_SpinLock main_queue_lock = 0;

static SDL_Mutex *actors_mutex = NULL;
static struct { char *key; cell_rt *value; } *actors = NULL;
static unsigned char *zip_buffer_global = NULL;
static char *prosperon = NULL;
cell_rt *root_cell = NULL;

static SDL_AtomicInt shutting_down;
static SDL_AtomicInt runners_count;

static inline uint64_t now_ns()
{
  return SDL_GetTicksNS();
}

static void exit_handler(void)
{
  SDL_SetAtomicInt(&shutting_down, 1);
  
  /* Signal all waiting threads */
  int count = SDL_GetAtomicInt(&runners_count);
  for (int i = 0; i < count; i++)
    SDL_SignalSemaphore(ready_sem);
  
  /* Signal main thread in case it's waiting */
  SDL_SignalSemaphore(main_sem);

  /* Wait for all runner threads to exit */
  while (SDL_GetAtomicInt(&runners_count) > 0) {
    SDL_Delay(10);
  }
  
  if (ready_sem)
    SDL_DestroySemaphore(ready_sem);
  if (main_sem)
    SDL_DestroySemaphore(main_sem);
  if (actors_mutex)
    SDL_DestroyMutex(actors_mutex);
    
  SDL_Quit();
  exit(0);
}

void actor_free(cell_rt *actor)
{
  // Delete it out of actors first so it can no longer get messages
  SDL_LockMutex(actors_mutex);
  shdel(actors, actor->id);
  SDL_UnlockMutex(actors_mutex);
  
  // If in a queue, remove it
  SDL_LockSpinlock(&queue_lock);
  for (int i = 0; i < arrlen(ready_queue); i++) {
    if (ready_queue[i] == actor) {
      arrdel(ready_queue, i);
      break;
    }
  }
  SDL_UnlockSpinlock(&queue_lock);
  
  SDL_LockSpinlock(&main_queue_lock);
  for (int i = 0; i < arrlen(main_queue); i++) {
    if (main_queue[i] == actor) {
      arrdel(main_queue, i);
      break;
    }
  }
  SDL_UnlockSpinlock(&main_queue_lock);
  
  // Do not go forward with actor destruction until the actor is completely free
  SDL_LockMutex(actor->msg_mutex);
  SDL_LockMutex(actor->mutex);

  JSContext *js = actor->context;
  
  JS_FreeValue(js, actor->idx_buffer);
  JS_FreeValue(js, actor->message_handle);
  JS_FreeValue(js, actor->on_exception);
  JS_FreeValue(js, actor->unneeded);
  JS_FreeAtom(js, actor->actor_sym);
  
  SDL_RemoveTimer(actor->ar);

  /* Free timer callbacks stored in actor */
  for (int i = 0; i < hmlen(actor->timers); i++) {
    JS_FreeValue(js, actor->timers[i].value);
  }
  
  hmfree(actor->timers);
  arrfree(actor->module_registry);
  
  /* Free all letters in the queue */
  for (int i = 0; i < arrlen(actor->letters); i++) {
    if (actor->letters[i].type == LETTER_BLOB) {
      blob_destroy(actor->letters[i].blob_data);
    } else if (actor->letters[i].type == LETTER_CALLBACK) {
      JS_FreeValue(js, actor->letters[i].callback);
    }
  }
  
  arrfree(actor->letters);
  
  JSRuntime *rt = JS_GetRuntime(js);
  JS_SetInterruptHandler(rt, NULL, NULL);  
  JS_FreeContext(js);
  JS_FreeRuntime(rt);
  free(actor->id);
  
  SDL_UnlockMutex(actor->mutex);
  SDL_DestroyMutex(actor->mutex);
  SDL_UnlockMutex(actor->msg_mutex);
  SDL_DestroyMutex(actor->msg_mutex);
  
#ifdef HAVE_MIMALLOC
  mi_heap_destroy(actor->heap);
#endif
  free(actor);
  
  SDL_LockMutex(actors_mutex);
  if (shlen(actors) == 0)
    exit(0);
  SDL_UnlockMutex(actors_mutex);
}

void js_dofree(JSRuntime *rt, void *opaque, void *ptr)
{
  js_free_rt(rt, ptr);
}

static size_t js_mi_malloc_usable_size(const void *ptr)
{
  #if defined(__APPLE__)
  return malloc_size(ptr);
  #elif defined(_WIN32)
  return _msize((void *)ptr);
  #elif defined(EMSCRIPTEN)
  return 0;
  #elif defined(__linux__) || defined(__GLIBC__)
  return malloc_usable_size((void *)ptr);
  #else
  return malloc_usable_size((void *)ptr);
  #endif
}

void *js_mi_malloc(JSMallocState *s, size_t sz) {
  void *ptr;
  assert(sz != 0);
  if (unlikely(s->malloc_size + sz > s->malloc_limit)) return NULL;
  
#ifdef HAVE_MIMALLOC
  cell_rt *actor = (cell_rt*)s->opaque;
  ptr = mi_heap_malloc(actor->heap, sz);
#else
  ptr = malloc(sz);
#endif
  if (!ptr) return NULL;
  
  s->malloc_count++;
  s->malloc_size += js_mi_malloc_usable_size(ptr) + MALLOC_OVERHEAD;
  
#ifdef TRACY_ENABLE
  if (tracy_profiling_enabled) {
#ifdef HAVE_MIMALLOC
    cell_rt *actor = (cell_rt*)s->opaque;
    TracyCAllocN(ptr, js_mi_malloc_usable_size(ptr) + MALLOC_OVERHEAD, actor->name);
#else
    TracyCAllocN(ptr, js_mi_malloc_usable_size(ptr) + MALLOC_OVERHEAD, "actor");
#endif
  }
#endif
  
  return ptr;
}

void js_mi_free(JSMallocState *s, void *p) {
  if (!p) return;
  
  s->malloc_count--;
  s->malloc_size -= js_mi_malloc_usable_size(p) + MALLOC_OVERHEAD;
  
#ifdef TRACY_ENABLE
  if (tracy_profiling_enabled) {
#ifdef HAVE_MIMALLOC
    cell_rt *actor = s->opaque;
    TracyCFreeN(p, actor->name);
#else
    TracyCFreeN(p, "actor");
#endif
  }
#endif
  
#ifdef HAVE_MIMALLOC
  mi_free(p);
#else
  free(p);
#endif
}

void *js_mi_realloc(JSMallocState *s, void *p, size_t sz) {
  size_t old_size;
  
  if (!p) return sz ? js_mi_malloc(s, sz) : NULL;
  
  old_size = js_mi_malloc_usable_size(p);
  if (!sz) {
    s->malloc_count--;
    s->malloc_size -= old_size + MALLOC_OVERHEAD;
#ifdef TRACY_ENABLE
    if (tracy_profiling_enabled) {
#ifdef HAVE_MIMALLOC
      cell_rt *actor = (cell_rt*)s->opaque;
      TracyCFreeN(p, actor->name);
#else
      TracyCFreeN(p, "actor");
#endif
    }
#endif
#ifdef HAVE_MIMALLOC
    mi_free(p);
#else
    free(p);
#endif
    return NULL;
  }
  
  if (s->malloc_size + sz - old_size > s->malloc_limit) return NULL;
  
#ifdef TRACY_ENABLE
  if (tracy_profiling_enabled) {
#ifdef HAVE_MIMALLOC
    cell_rt *actor = (cell_rt*)s->opaque;
    TracyCFreeN(p, actor->name);
#else
    TracyCFreeN(p, "actor");
#endif
  }
#endif
  
#ifdef HAVE_MIMALLOC
  cell_rt *actor = (cell_rt*)s->opaque;
  p = mi_heap_realloc(actor->heap, p, sz);
#else
  p = realloc(p, sz);
#endif
  if (!p) return NULL;
  
  s->malloc_size += js_mi_malloc_usable_size(p) - old_size;
  
#ifdef TRACY_ENABLE
  if (tracy_profiling_enabled) {
#ifdef HAVE_MIMALLOC
    cell_rt *actor = (cell_rt*)s->opaque;
    TracyCAllocN(p, js_mi_malloc_usable_size(p) + MALLOC_OVERHEAD, actor->name);
#else
    TracyCAllocN(p, js_mi_malloc_usable_size(p) + MALLOC_OVERHEAD, "actor");
#endif
  }
#endif
  
  return p;
}

#ifdef HAVE_MIMALLOC
static const JSMallocFunctions mimalloc_funcs = {
  js_mi_malloc,
  js_mi_free,
  js_mi_realloc,
  js_mi_malloc_usable_size
};
#endif

static void free_zip(void)
{
  free(zip_buffer_global);
  zip_buffer_global = NULL;
}

int prosperon_mount_core(void)
{
  size_t size;
  char exe_path[PATH_MAX];
  
  // Get the full path of the executable
  const char *base_dir = PHYSFS_getBaseDir();
  if (base_dir) {
    snprintf(exe_path, sizeof(exe_path), "%s%s", base_dir, PHYSFS_getDirSeparator());
    
    // Extract just the executable name from argv[0]
    const char *exe_name = strrchr(prosperon, '/');
    if (!exe_name) exe_name = strrchr(prosperon, '\\');
    if (exe_name) exe_name++; else exe_name = prosperon;
    
    strncat(exe_path, exe_name, sizeof(exe_path) - strlen(exe_path) - 1);
  } else {
    strncpy(exe_path, prosperon, sizeof(exe_path) - 1);
    exe_path[sizeof(exe_path) - 1] = '\0';
  }
  
  FILE *f = fopen(exe_path, "rb");
  if (!f) return perror("fopen"), 0;
  if (fseek(f, 0, SEEK_END) != 0) return perror("fseek"), fclose(f), 0;
  size = ftell(f);
  if (size < 0) return perror("ftell"), fclose(f), 0;
  zip_buffer_global = malloc(size);
  if (!zip_buffer_global) return perror("malloc"), fclose(f), 0;
  rewind(f);
  if (fread(zip_buffer_global, 1, size, f) != size) {
    perror("fread");
    free(zip_buffer_global);
    fclose(f);
    return 0;
  }
  fclose(f);

  long max_comment_len = 0xFFFF;
  long eocd_search_start = (size > max_comment_len + 22) ? size - (max_comment_len + 22) : 0;
  long eocd_pos = -1;
  for (long i = size - 22; i >= eocd_search_start; i--)
    if (zip_buffer_global[i] == 'P' && zip_buffer_global[i + 1] == 'K' &&
        zip_buffer_global[i + 2] == 0x05 && zip_buffer_global[i + 3] == 0x06) {
      eocd_pos = i;
      break;
    }
  if (eocd_pos < 0) {
    free(zip_buffer_global);
    return 0;
  }

  uint16_t comment_length = zip_buffer_global[eocd_pos + 20] | (zip_buffer_global[eocd_pos + 21] << 8);
  int eocd_size = 22 + comment_length;
  uint32_t cd_size = zip_buffer_global[eocd_pos + 12] | (zip_buffer_global[eocd_pos + 13] << 8) |
                     (zip_buffer_global[eocd_pos + 14] << 16) | (zip_buffer_global[eocd_pos + 15] << 24);
  uint32_t cd_offset_rel = zip_buffer_global[eocd_pos + 16] | (zip_buffer_global[eocd_pos + 17] << 8) |
                           (zip_buffer_global[eocd_pos + 18] << 16) | (zip_buffer_global[eocd_pos + 19] << 24);
  uint32_t appended_zip_size = cd_offset_rel + cd_size + eocd_size;
  long zip_offset = size - appended_zip_size;
  if (zip_offset < 0 || zip_offset >= size) return fprintf(stderr, "Invalid zip offset: %ld\n", zip_offset), free(zip_buffer_global), 0;

  int ret = PHYSFS_mountMemory(zip_buffer_global + zip_offset, appended_zip_size, free_zip, "core.zip", NULL, 0);
  if (!ret) {
    printf("COULD NOT MOUNT! Reason: %s\n", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
  }
  return ret;
}

void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
{
  if (actor->disrupt) return;
  JS_FreeValue(actor->context, actor->unneeded);
  
  if (!JS_IsFunction(actor->context, fn)) {
    actor->unneeded = JS_NULL;
    goto END;
  }
  
  actor->unneeded = JS_DupValue(actor->context, fn);
  actor->ar_secs = seconds;
  
  END:
  if (actor->ar) {
    SDL_RemoveTimer(actor->ar);
    actor->ar = 0;
  }
  set_actor_state(actor);
}

cell_rt *create_actor(void *wota)
{
  cell_rt *actor = calloc(sizeof(*actor), 1);
#ifdef HAVE_MIMALLOC
  actor->heap = mi_heap_new();
#endif
  actor->init_wota = wota;
  actor->idx_buffer = JS_NULL;
  actor->message_handle = JS_NULL;
  actor->unneeded = JS_NULL;
  actor->on_exception = JS_NULL;
  actor->actor_sym = JS_ATOM_NULL;

  arrsetcap(actor->letters, 5);

  actor->mutex = SDL_CreateMutex();      /* Protects JSContext + state */
  actor->msg_mutex = SDL_CreateMutex();  /* Mailbox queue lock */
  
  /* Lock actor->mutex while initializing JS runtime. */
  SDL_LockMutex(actor->mutex);
  script_startup(actor);
  set_actor_state(actor);
  SDL_UnlockMutex(actor->mutex);  

  return actor;
}

cell_rt *get_actor(char *id)
{
  SDL_LockMutex(actors_mutex);
  int idx = shgeti(actors, id);
  if (idx == -1) {
    SDL_UnlockMutex(actors_mutex);
    return NULL;
  }
  cell_rt *actor = actors[idx].value;
  SDL_UnlockMutex(actors_mutex);
  return actor;
}

const char *register_actor(const char *id, cell_rt *actor, int mainthread, double ar)
{
  SDL_LockMutex(actors_mutex);
  if (shgeti(actors, id) != -1) {
    SDL_UnlockMutex(actors_mutex);
    return "Actor with given ID already exists.";
  }
  actor->main_thread_only = mainthread;
  actor->id = strdup(id);
  actor->ar_secs = ar;
  shput(actors, id, actor);
  SDL_UnlockMutex(actors_mutex);
  return NULL;
}

const char *send_message(const char *id, void *msg)
{
  cell_rt *target = get_actor(id);
  if (!target) {
    blob_destroy((blob *)msg);
    return "Could not get actor from id.";
  }
  
  letter l;
  l.type = LETTER_BLOB;
  l.blob_data = (blob *)msg;
  
  SDL_LockMutex(target->msg_mutex);

  arrput(target->letters, l);
  if (target->ar) {
    SDL_RemoveTimer(target->ar);
    target->ar = 0;
  }
  SDL_UnlockMutex(target->msg_mutex);
  
  set_actor_state(target);
  
  return NULL;
}

static Uint32 actor_remove_cb(cell_rt *actor, Uint32 id, Uint32 interval)
{
  actor->disrupt = 1;
  
  if (!JS_IsNull(actor->unneeded)) {
    SDL_LockMutex(actor->mutex);
    JSValue ret = JS_Call(actor->context, actor->unneeded, JS_NULL, 0, NULL);
    uncaught_exception(actor->context, ret);
    SDL_UnlockMutex(actor->mutex);
  }
  
  actor_free(actor);
  return 0;
}

/* Timer callback adds an event to the queue under evt_mutex. */
Uint32 actor_delay_cb(cell_rt *actor, SDL_TimerID id, Uint32 interval)
{
  SDL_LockMutex(actor->msg_mutex);
  int idx = hmgeti(actor->timers, id);
  if (idx == -1) goto END;
  
  JSValue cb = actor->timers[idx].value;
  hmdel(actor->timers, id);
  actor_clock(actor, cb);
  JS_FreeValue(actor->context, cb);

  END:
  SDL_UnlockMutex(actor->msg_mutex);  
  return 0;
}

void set_actor_state(cell_rt *actor)
{
  if (actor->disrupt) {
    actor_free(actor);
    return;
  }
  SDL_LockMutex(actor->msg_mutex);
  
  switch(actor->state) {
    case ACTOR_RUNNING:
    case ACTOR_READY:
    if (actor->ar) {
      SDL_RemoveTimer(actor->ar);
      actor->ar = 0;
    }
    break;
      
    case ACTOR_IDLE:
      if (arrlen(actor->letters)) {
        actor->state = ACTOR_READY;
        if (actor->main_thread_only) {
          SDL_LockSpinlock(&main_queue_lock);
          arrput(main_queue, actor);
          SDL_UnlockSpinlock(&main_queue_lock);
          SDL_SignalSemaphore(main_sem);
        } else {
          SDL_LockSpinlock(&queue_lock);
          arrput(ready_queue, actor);
          SDL_UnlockSpinlock(&queue_lock);
          SDL_SignalSemaphore(ready_sem);
        }
      } else if (!arrlen(actor->letters) && !hmlen(actor->timers))
        actor->ar = SDL_AddTimerNS(actor->ar_secs*1e9, actor_remove_cb, actor);
      break;
  }

  SDL_UnlockMutex(actor->msg_mutex);
}

void actor_turn(cell_rt *actor)
{
  SDL_LockMutex(actor->mutex);
  
#ifdef TRACY_ENABLE
  int entered = 0;
  if (tracy_profiling_enabled && TracyCIsConnected) {
    TracyCFiberEnter(actor->name);
    entered = 1;
  }
#endif

  actor->state = ACTOR_RUNNING;  
  
  TAKETURN:

  SDL_LockMutex(actor->msg_mutex);
  JSValue result;
  if (!arrlen(actor->letters)) {
    SDL_UnlockMutex(actor->msg_mutex);
    goto ENDTURN;
  }
  letter l = actor->letters[0];
  arrdel(actor->letters, 0);
  SDL_UnlockMutex(actor->msg_mutex);
  
  if (l.type == LETTER_BLOB) { 
    // Create a JS blob from the C blob
    size_t size = l.blob_data->length / 8; // Convert bits to bytes
    JSValue arg = js_new_blob_stoned_copy(actor->context, l.blob_data->data, size);
    blob_destroy(l.blob_data);
    result = JS_Call(actor->context, actor->message_handle, JS_NULL, 1, &arg);
    uncaught_exception(actor->context, result);
    JS_FreeValue(actor->context, arg);
  } else if (l.type == LETTER_CALLBACK) {
    result = JS_Call(actor->context, l.callback, JS_NULL, 0, NULL);
    uncaught_exception(actor->context, result);
    JS_FreeValue(actor->context, l.callback);
  }
  
  if (actor->disrupt) goto ENDTURN;

  // If there are no waiting threads, bail. otherwise, try for another turn
  SDL_LockSpinlock(&queue_lock);
  int someone_else_waiting = (arrlen(ready_queue) > 0);
  SDL_UnlockSpinlock(&queue_lock);
  
  if (!someone_else_waiting) goto TAKETURN;
  
  ENDTURN:
  actor->state = ACTOR_IDLE;  
  
#ifdef TRACY_ENABLE
  if (tracy_profiling_enabled && entered)
    TracyCFiberLeave(actor->name);
#endif

  set_actor_state(actor);
  
  SDL_UnlockMutex(actor->mutex);
}

void actor_clock(cell_rt *actor, JSValue fn)
{
  SDL_LockMutex(actor->msg_mutex);
  letter l;
  l.type = LETTER_CALLBACK;
  l.callback = JS_DupValue(actor->context, fn);
  arrput(actor->letters, l);
  SDL_UnlockMutex(actor->msg_mutex);
  set_actor_state(actor);
}

/* JS function that schedules a timer. */
JSValue js_actor_delay(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  if (!JS_IsFunction(js, argv[0]))
    return JS_ThrowReferenceError(js, "Argument must be a function.");

  cell_rt *actor = JS_GetContextOpaque(js);
  double seconds;
  JS_ToFloat64(js, &seconds, argv[1]);
  if (seconds <= 0) {
    actor_clock(actor, argv[0]);
    return JS_NULL;
  }

  SDL_LockMutex(actor->msg_mutex);
  uint32_t id = SDL_AddTimerNS(seconds*1e9, actor_delay_cb, actor);    
  JSValue cb = JS_DupValue(js, argv[0]);
  hmput(actor->timers, id, cb);
  SDL_UnlockMutex(actor->msg_mutex);
  return JS_NewUint32(js, id);
}

JSValue js_actor_removetimer(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  cell_rt *actor = JS_GetContextOpaque(js);
  uint32_t timer_id;
  JS_ToUint32(js, &timer_id, argv[0]);
  if (timer_id == -1) return JS_NULL;
  
  SDL_RemoveTimer(timer_id);
  
  JSValue cb = JS_NULL;
  
  SDL_LockMutex(actor->msg_mutex);
  int id = hmgeti(actor->timers, timer_id);
  if (id != -1) {
    cb = actor->timers[id].value;
    hmdel(actor->timers, timer_id);
  }
  SDL_UnlockMutex(actor->msg_mutex);
  
  JS_FreeValue(js,cb);

  return JS_NULL;
}

// Wrapper struct to keep the array pointer stable
typedef struct {
#ifdef TRACY_ENABLE
  TracyCZoneCtx *arr; // stb_ds dynamic array
#else
  void *arr;
#endif
} tracy_stack_t;

// Global TLS ID for the Tracy stack
static SDL_TLSID tracy_stack_id = {0};

// Cleanup function for the wrapper struct
static void tracy_cleanup_stack(void *value)
{
  tracy_stack_t *stack = value;
  if (stack) {
    arrfree(stack->arr);
    free(stack);
  }
}

// Get or initialize the thread-local Tracy stack
static tracy_stack_t *get_tracy_stack(void)
{
  tracy_stack_t *stack = SDL_GetTLS(&tracy_stack_id);
  if (!stack) {
    stack = malloc(sizeof(tracy_stack_t));
    stack->arr = NULL; // stb_ds starts with NULL
    arrsetcap(stack->arr, 5); // Initial capacity
    SDL_SetTLS(&tracy_stack_id, stack, tracy_cleanup_stack);
  }
  return stack;
}

void tracy_call_hook(JSContext *js, JSValue fn)
{
#ifdef TRACY_ENABLE
  if (!tracy_profiling_enabled)
    return;

  tracy_stack_t *stack = get_tracy_stack();
  js_debug debug;
  js_debug_info(js, fn, &debug);

  uint64_t srcloc = ___tracy_alloc_srcloc(debug.line, debug.filename, strlen(debug.filename), debug.name, strlen(debug.name), debug.unique);
  arrput(stack->arr, ___tracy_emit_zone_begin_alloc(srcloc, 1));

  free_js_debug_info(js, &debug);
#endif
}

void tracy_end_hook(JSContext *js, JSValue fn)
{
#ifdef TRACY_ENABLE
  if (!tracy_profiling_enabled)
    return;

  tracy_stack_t *stack = get_tracy_stack();
  if (arrlen(stack->arr) > 0)
    ___tracy_emit_zone_end(arrpop(stack->arr));
#endif
}

void actor_disrupt(cell_rt *crt)
{
  crt->disrupt = 1;
  if (crt->state != ACTOR_RUNNING)
    actor_free(crt);
}

static int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt)
{
  return SDL_GetAtomicInt(&shutting_down) || crt->disrupt;
}

void script_startup(cell_rt *prt)
{
  JSRuntime *rt;

#ifdef HAVE_MIMALLOC
  rt = JS_NewRuntime2(&mimalloc_funcs, prt);
#else
  rt = JS_NewRuntime();
#endif

  JSContext *js = JS_NewContextRaw(rt);
  JS_SetInterruptHandler(rt, actor_interrupt_cb, prt);
  
#ifdef TRACY_ENABLE
  if (tracy_profiling_enabled) {
    js_debug_sethook(js, tracy_call_hook, JS_HOOK_CALL);
    js_debug_sethook(js, tracy_end_hook, JS_HOOK_RET);
  }
#endif  
  
  JS_AddIntrinsicBaseObjects(js);
  JS_AddIntrinsicEval(js);
  JS_AddIntrinsicRegExp(js);
  JS_AddIntrinsicJSON(js);
  JS_AddIntrinsicMapSet(js);

  JS_SetContextOpaque(js, prt);
  prt->context = js;
  ffi_load(js);
  
  PHYSFS_File *eng = PHYSFS_openRead(ENGINE);
  if (!eng) {
    printf("ERROR: Could not open file %s! %s\n", ENGINE, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
    return;
  }
  PHYSFS_Stat stat;
  PHYSFS_stat(ENGINE, &stat);
  char *data = malloc(stat.filesize+1);
  PHYSFS_readBytes(eng, data, stat.filesize);
  PHYSFS_close(eng);
  data[stat.filesize] = 0;

  prt->state = ACTOR_RUNNING;
  JSValue v = JS_Eval(js, data, (size_t)stat.filesize, ENGINE, 0);
  uncaught_exception(js, v);
  prt->state = ACTOR_IDLE;
  set_actor_state(prt);
}

int uncaught_exception(JSContext *js, JSValue v)
{
  cell_rt *rt = JS_GetContextOpaque(js);
  SDL_LockMutex(rt->mutex);
  
  if (!JS_HasException(js)) {
    JS_FreeValue(js,v);
    SDL_UnlockMutex(rt->mutex);
    return 1;
  }
  
  JSValue exp = JS_GetException(js);
  JSValue ret = JS_Call(js, rt->on_exception, JS_NULL, 1, &exp);
  JS_FreeValue(js,ret);  
  JS_FreeValue(js, exp);
  JS_FreeValue(js,v);
  SDL_UnlockMutex(rt->mutex);
  return 0;
}

static int actor_runner(void *data)
{
  SDL_AddAtomicInt(&runners_count, 1);
  
  while (!SDL_GetAtomicInt(&shutting_down)) {
    SDL_WaitSemaphore(ready_sem);  
    SDL_LockSpinlock(&queue_lock);

    cell_rt *actor = NULL;
    if (arrlen(ready_queue) > 0) {
      actor = ready_queue[0];
      arrdel(ready_queue, 0);
    }
    SDL_UnlockSpinlock(&queue_lock);
    
    if (actor)
      actor_turn(actor);
  }
  
  SDL_AddAtomicInt(&runners_count, -1);
  return 0;
}

static void signal_handler(int sig)
{
  const char *str = NULL;
  switch (sig) {
    case SIGABRT: str = "SIGABRT"; break;
    case SIGFPE:  str = "SIGFPE";  break;
    case SIGILL:  str = "SIGILL";  break;
    case SIGINT:  str = "SIGINT";  break;
    case SIGSEGV: str = "SIGSEGV"; break;
    case SIGTERM: str = "SIGTERM"; break;
  }
  if (!str) return;
  
  exit_handler();
}

static void add_runners(int n)
{
  /* Launch runner threads */
  for (int i = 0; i < n; i++) {
    char threadname[128];
    snprintf(threadname, sizeof(threadname), "actor runner %d", i);
    SDL_Thread *thread = SDL_CreateThread(actor_runner, threadname, NULL);
    SDL_DetachThread(thread);
    /* Thread is detached, no need to track */
  }
}

static void loop()
{
  int msgs = 0;
  while (!SDL_GetAtomicInt(&shutting_down)) {
    SDL_WaitSemaphore(main_sem);
    SDL_LockSpinlock(&main_queue_lock);
    cell_rt *actor = NULL;
    if (arrlen(main_queue) > 0) {
      actor = main_queue[0];
      arrdel(main_queue, 0);
    }
    SDL_UnlockSpinlock(&main_queue_lock);
    msgs++;
    actor_turn(actor);
    continue;
  }
}

int main(int argc, char **argv)
{
  int profile_enabled = 0;
  int script_start = 1;

  /* Check for --profile flag */
  if (argc > 1 && strcmp(argv[1], "--profile") == 0) {
    profile_enabled = 1; script_start = 2;
#ifndef TRACY_ENABLE
    printf("Warning: --profile flag was specified but Tracy profiling is not compiled in\n");
#endif
  }

  if (!SDL_Init(SDL_INIT_EVENTS | SDL_INIT_GAMEPAD)) {
    printf("CRITICAL ERROR: %s\n", SDL_GetError());
    exit(1);
  }
  
#ifdef TRACY_ENABLE
  tracy_profiling_enabled = profile_enabled;
#endif

  prosperon = argv[0];
  PHYSFS_init(argv[0]);
  
  /* Mount core.zip first - this is critical! */
  int mounted = prosperon_mount_core();
  if (!mounted) mounted = PHYSFS_mount("core.zip", NULL, 0);
  if (!mounted) mounted = PHYSFS_mount("scripts", NULL, 0);
  if (!mounted) {
    printf("ERROR: Could not mount core. Reason: %s\n", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
    return 1;
  }

  /* Search for .cell directory up the tree */
  char *search_dir = SDL_GetCurrentDirectory();
  char *cell_parent_dir = NULL;
  struct stat st;
  
  while (search_dir && strlen(search_dir) > 1) {
    char test_path[PATH_MAX];
    snprintf(test_path, sizeof(test_path), "%s/.cell", search_dir);
    if (stat(test_path, &st) == 0 && S_ISDIR(st.st_mode)) {
      cell_parent_dir = strdup(search_dir);
      break;
    }
    char *last_sep = strrchr(search_dir, '/');
    if (!last_sep || last_sep == search_dir) {
      if (stat("/.cell", &st) == 0 && S_ISDIR(st.st_mode))
        cell_parent_dir = strdup("/");
      break;
    }
    *last_sep = '\0';
  }
  
  if (cell_parent_dir) {
    /* 1) Strip any trailing slash from cell_parent_dir (except if it's just "/") */
    size_t proj_len = strlen(cell_parent_dir);
    if (proj_len > 1 && cell_parent_dir[proj_len - 1] == '/')
      cell_parent_dir[proj_len - 1] = '\0';
 
    char scriptpath[PATH_MAX];
    snprintf(scriptpath, sizeof(scriptpath), "%s/scripts", cell_parent_dir);
    
    char cellpath[PATH_MAX];
    snprintf(cellpath, sizeof(cellpath), "%s/.cell/modules", cell_parent_dir);
    
    PHYSFS_mount(scriptpath, NULL, 1);
    PHYSFS_mount(cellpath, NULL, 0);
    PHYSFS_mount(cell_parent_dir, NULL, 0);    
    PHYSFS_setWriteDir(cell_parent_dir);  

    free(cell_parent_dir);
  } else {
    // Not in a project - use CELLPATH after confirming ..
    // TODO: implement
    printf("Could not find project! Exiting!\n");
    exit(1);
  }
  
  SDL_free(search_dir);

  /* Create the initial actor from the command line */
  int actor_argc = argc - script_start;
  char **actor_argv = argv + script_start;

  WotaBuffer startwota;
  wota_buffer_init(&startwota, 5);
  wota_write_record(&startwota, 2);
  wota_write_text(&startwota, "program");
  wota_write_text(&startwota, actor_argv[0]);
  wota_write_text(&startwota, "arg");
  wota_write_array(&startwota, actor_argc - 1);
  for (int i = 1; i < actor_argc; i++)
    wota_write_text(&startwota, actor_argv[i]);
    
    
  /* Initialize synchronization primitives */
  ready_sem = SDL_CreateSemaphore(0);
  main_sem = SDL_CreateSemaphore(0);
  actors_mutex = SDL_CreateMutex();
  SDL_SetAtomicInt(&shutting_down, 0);
  SDL_SetAtomicInt(&runners_count, 0);    
  
  add_runners(SDL_GetNumLogicalCPUCores());
    
  root_cell = create_actor(startwota.data);

  /* Set up signal and exit handlers */
  signal(SIGINT, signal_handler);
  signal(SIGTERM, signal_handler);
  signal(SIGSEGV, signal_handler);
  signal(SIGABRT, signal_handler);
  
  loop();

  return 0;
}

int actor_exists(const char *id)
{
  SDL_LockMutex(actors_mutex);
  int idx = shgeti(actors,id);
  SDL_UnlockMutex(actors_mutex);
  if (idx == -1)
    return 0;
  else
    return 1;
}

int JS_ArrayLength(JSContext *js, JSValue a)
{
  JSValue length = JS_GetPropertyStr(js, a, "length");
  int len;
  JS_ToInt32(js,&len,length);
  JS_FreeValue(js,length);
  return len;
}