#include "errors.h"
#include "internal.h"
#include "descriptors.h"
#include "silver/engine.h"
#include "modules/io.h"
#include "highlight.h"

#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <limits.h>
#include <crprintf.h>

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <unistd.h>
#include <sys/ioctl.h>
#endif

typedef struct { char *buf; size_t size; } errbuf_t;

static void print_error_value(ant_t *js, ant_value_t value, ant_value_t fallback_stack, const char *prefix) {
  ant_value_t obj = is_err(value) ? js_as_obj(value) : value;
  const char *stack = NULL; bool no_stack = false;
  
  if (vtype(obj) == T_OBJ) {
    ant_value_t err_type = js_get_slot(obj, SLOT_ERR_TYPE);
    no_stack = vtype(err_type) == T_NUM && ((int)js_getnum(err_type) & JS_ERR_NO_STACK);
    if (!no_stack) stack = get_str_prop(js, obj, "stack", 5, NULL);
  }
  
  if (!no_stack && !stack && vtype(fallback_stack) == T_STR) {
    ant_offset_t slen;
    ant_offset_t soff = vstr(js, fallback_stack, &slen);
    stack = (const char *)(uintptr_t)(soff);
  }
  
  if (prefix) fputs(prefix, stderr);
  
  if (stack) {
    fputs(stack, stderr);
    size_t n = strlen(stack);
    if (n == 0 || stack[n - 1] != '\n') fputc('\n', stderr);
  } else if (vtype(obj) == T_OBJ) {
    const char *name = get_str_prop(js, obj, "name", 4, NULL);
    const char *msg = get_str_prop(js, obj, "message", 7, NULL);
    
    if (name && msg) fprintf(stderr, "%s%s%s: %s%s%s\n", C_RED, name, C_RESET, C_BOLD, msg, C_RESET);
    else if (name) fprintf(stderr, "%s%s%s\n", C_RED, name, C_RESET);
    else fprintf(stderr, "[object Error]\n");
  } 
  
  else fprintf(stderr, "%s\n", js_str(js, value));
}

bool print_uncaught_throw(ant_t *js) {
  if (!js->thrown_exists) return false;
  print_error_value(js, js->thrown_value, js->thrown_stack, NULL);
  
  js->thrown_exists = false;
  js->thrown_value = js_mkundef();
  js->thrown_stack = js_mkundef();
  
  return true;
}

bool print_unhandled_promise_rejection(ant_t *js, ant_value_t value) {
  print_error_value(js, value, js_mkundef(), "Uncaught (in promise) ");
  return true;
}

bool js_mark_errorlike_no_stack(ant_t *js, ant_value_t value) {
  if (vtype(value) != T_OBJ) return false;
  if (js_get_slot(value, SLOT_ERROR_BRAND) == js_true) return false;

  const char *name = get_str_prop(js, value, "name", 4, NULL);
  const char *message = get_str_prop(js, value, "message", 7, NULL);
  if ((!name || !*name) && (!message || !*message)) return false;

  ant_value_t err_type = js_get_slot(value, SLOT_ERR_TYPE);
  int base_type = vtype(err_type) == T_NUM ? (int)js_getnum(err_type) : JS_ERR_GENERIC;

  js_set(js, value, "stack", js_mkundef());
  js_set_slot(value, SLOT_ERR_TYPE, js_mknum((double)(base_type | JS_ERR_NO_STACK)));
  
  return true;
}

static const char *get_error_type_name(js_err_type_t err_type) {
  static const char *names[] = {
    [JS_ERR_GENERIC]   = "Error",
    [JS_ERR_TYPE]      = "TypeError",
    [JS_ERR_SYNTAX]    = "SyntaxError",
    [JS_ERR_REFERENCE] = "ReferenceError",
    [JS_ERR_RANGE]     = "RangeError",
    [JS_ERR_EVAL]      = "EvalError",
    [JS_ERR_URI]       = "URIError",
    [JS_ERR_INTERNAL]  = "InternalError",
    [JS_ERR_AGGREGATE] = "AggregateError",
  };

  return names[err_type] ?: "Error";
}

static bool ensure_errbuf_capacity(errbuf_t *eb, size_t needed) {
  if (needed <= eb->size) return true;

  size_t new_size = eb->size;
  while (new_size < needed) {
    size_t next = new_size * 2;
    if (next < new_size) return false;
    new_size = next;
  }

  char *next_buf = (char *)realloc(eb->buf, new_size);
  if (!next_buf) return false;
  eb->buf = next_buf;
  eb->size = new_size;
  return true;
}

__attribute__((format(printf, 3, 4)))
static size_t append_errbuf_fmt(errbuf_t *eb, size_t used, const char *fmt, ...) {
  int max_attempts = 3;
  int attempt = 0;

  for (;;) {
    if (!ensure_errbuf_capacity(eb, used + 1)) {
      return eb->size ? eb->size - 1 : used;
    }

    size_t remaining = eb->size - used;
    va_list ap;
    va_start(ap, fmt);
    int written = vsnprintf(eb->buf + used, remaining, fmt, ap);
    va_end(ap);

    if (written < 0) return used;
    if ((size_t)written < remaining) return used + (size_t)written;

    if (!ensure_errbuf_capacity(eb, used + (size_t)written + 1)) {
      if (++attempt >= max_attempts) return eb->size ? eb->size - 1 : used;
    }
  }
}

static inline size_t remaining_capacity(size_t used, size_t total) {
  return used >= total ? 0 : total - used;
}

static size_t append_error_header(errbuf_t *eb, ant_t *js, size_t used, int line, int col) {
  const char *file = (js->errsite.valid && js->errsite.filename) ? js->errsite.filename : js->filename;
  if (file) return append_errbuf_fmt(eb, used, "%s:%d:%d\n", file, line, col);
  return append_errbuf_fmt(eb, used, "<eval>:%d:%d\n", line, col);
}

static void get_line_col(const char *code, ant_offset_t code_len, ant_offset_t pos, int *out_line, int *out_col) {
  if (!out_line || !out_col) return;

  *out_line = 1;
  *out_col = 1;

  if (!code || code_len <= 0 || pos <= 0) return;
  if (pos > code_len) pos = code_len;

  for (ant_offset_t i = 0; i < pos; i++) {
    char ch = code[i];
    if (ch == '\0') break;
    if (ch == '\n') {
      *out_line += 1;
      *out_col = 1;
    } else *out_col += 1;
  }
}

static void get_error_line(
  const char *code, ant_offset_t clen, ant_offset_t pos, char *buf, size_t bufsize,
  int *line_start_col, ant_offset_t *out_line_start, ant_offset_t *out_line_end
) {
  if (!code || bufsize == 0) {
    if (bufsize > 0) buf[0] = '\0';
    if (line_start_col) *line_start_col = 1;
    if (out_line_start) *out_line_start = 0;
    if (out_line_end) *out_line_end = 0;
    return;
  }

  if (pos > clen) pos = clen;

  if (clen == 0) {
    buf[0] = '\0';
    if (line_start_col) *line_start_col = 1;
    if (out_line_start) *out_line_start = 0;
    if (out_line_end) *out_line_end = 0;
    return;
  }

  ant_offset_t line_start = pos;
  while (line_start > 0 && code[line_start - 1] != '\n') {
    line_start--;
  }

  ant_offset_t line_end = pos;
  while (line_end < clen && code[line_end] != '\n' && code[line_end] != '\0') {
    line_end++;
  }

  ant_offset_t line_len = line_end - line_start;
  if (line_len >= bufsize) line_len = (ant_offset_t)(bufsize - 1);

  memcpy(buf, &code[line_start], line_len);
  buf[line_len] = '\0';
  
  if (line_start_col) *line_start_col = (int)(pos - line_start) + 1;
  if (out_line_start) *out_line_start = line_start;
  if (out_line_end) *out_line_end = line_end;
}

static size_t append_error_value(errbuf_t *eb, ant_t *js, size_t used, ant_value_t value) {
  const char *name = "Error";
  const char *msg = NULL;

  ant_offset_t name_len = 5;
  ant_offset_t msg_len = 0;

  static const void *type_dispatch[] = {
    [T_STR] = &&l_type_str,
    [T_OBJ] = &&l_type_obj,
    [T_FUNC] = &&l_type_default,
    [T_ARR] = &&l_type_default,
    [T_PROMISE] = &&l_type_default,
    [T_GENERATOR] = &&l_type_default,
    [T_BIGINT] = &&l_type_default,
    [T_NUM] = &&l_type_default,
    [T_BOOL] = &&l_type_default,
    [T_SYMBOL] = &&l_type_default,
    [T_CFUNC] = &&l_type_default,
    [T_TYPEDARRAY] = &&l_type_default,
    [T_ERR] = &&l_type_default,
    [T_UNDEF] = &&l_type_default,
    [T_NULL] = &&l_type_default,
  };

  uint8_t t = vtype(value);
  if (t < sizeof(type_dispatch) / sizeof(type_dispatch[0]) && type_dispatch[t]) {
    goto *type_dispatch[t];
  }
  goto l_type_default;

  l_type_str:
    msg = (const char *)(uintptr_t)(vstr(js, value, &msg_len));
    goto l_type_done;

  l_type_obj:
    name = get_str_prop(js, value, "name", 4, &name_len);
    if (!name) {
      name = "Error";
      name_len = 5;
    }
    msg = get_str_prop(js, value, "message", 7, &msg_len);
    goto l_type_done;

  l_type_default:
    msg = js_str(js, value);
    msg_len = msg ? (ant_offset_t)strlen(msg) : 0;
    goto l_type_done;

  l_type_done:

  static const void *dispatch[] = { &&l_with_msg, &&l_name_only };
  int key = msg ? 0 : 1;
  goto *dispatch[key];

  l_with_msg:
    return append_errbuf_fmt(eb, used,
      ERR_FMT,
      (int)name_len, name, (int)msg_len, msg
    );

  l_name_only:
    return append_errbuf_fmt(eb, used,
      ERR_NAME_ONLY,
      (int)name_len, name
    );
}

static int count_digits_int(int v) {
  int n = 1;
  while (v >= 10) { v /= 10; n++; }
  return n;
}

static void append_error_caret(errbuf_t *eb, size_t *n, int error_col, int span_cols) {
  if (span_cols < 1) span_cols = 1;
  if (!ensure_errbuf_capacity(eb, *n + (size_t)error_col + (size_t)span_cols + 2)) return;
  if (*n >= eb->size - 1) return;

  size_t remaining = eb->size - *n;
  for (int i = 1; i < error_col && remaining > 1; i++) {
    eb->buf[(*n)++] = ' ';
    remaining--;
  }
  
  for (int i = 0; i < span_cols && remaining > 1; i++) {
    eb->buf[(*n)++] = '^';
    remaining--;
  }
  
  eb->buf[*n] = '\0';
}

static int error_span_cols_for_line(ant_offset_t src_pos, ant_offset_t span_len, ant_offset_t line_start, ant_offset_t line_end) {
  if (line_end < line_start) return 1;
  if (src_pos < line_start) src_pos = line_start;
  if (src_pos > line_end) src_pos = line_end;
  if (span_len <= 0) return 1;

  ant_offset_t span_end = src_pos + span_len;
  if (span_end < src_pos) span_end = src_pos;
  if (span_end > line_end) span_end = line_end;

  ant_offset_t width = span_end - src_pos;
  if (width <= 0) width = 1;
  if (width > INT_MAX) width = INT_MAX;
  return (int)width;
}

static int error_terminal_columns(void) {
#ifdef _WIN32
  HANDLE h = GetStdHandle(STD_ERROR_HANDLE);
  if (h == NULL || h == INVALID_HANDLE_VALUE) h = GetStdHandle(STD_OUTPUT_HANDLE);
  if (h && h != INVALID_HANDLE_VALUE) {
    CONSOLE_SCREEN_BUFFER_INFO csbi;
    if (GetConsoleScreenBufferInfo(h, &csbi)) {
      int cols = (int)(csbi.srWindow.Right - csbi.srWindow.Left + 1);
      if (cols > 0) return cols;
    }
  }
#else
  struct winsize ws;
  if (ioctl(STDERR_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
    return (int)ws.ws_col;
  }
  if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
    return (int)ws.ws_col;
  }
#endif

  const char *cols_env = getenv("COLUMNS");
  if (cols_env && *cols_env) {
    char *end = NULL;
    long cols = strtol(cols_env, &end, 10);
    if (end != cols_env && cols > 0 && cols <= INT_MAX) return (int)cols;
  }
  return 120;
}

static int error_context_src_cols_limit(int gutter_w) {
  int cols = error_terminal_columns();
  int half = cols / 2;
  if (half < 40) half = 40;
  int budget = half - (gutter_w + 3);
  if (budget < 20) budget = 20;
  return budget;
}

static const char *error_frame_name(ant_t *js, sv_frame_t *frame, sv_func_t *func) {
  if (func && func->name && func->name[0]) return func->name;
  if (js && frame && vtype(frame->callee) == T_FUNC) {
    ant_offset_t name_len = 0;
    const char *name = get_str_prop(js, js_func_obj(frame->callee), "name", 4, &name_len);
    if (name && name_len > 0) return name;
  }
  return "<anonymous>";
}

typedef struct {
  const char *name;
  const char *file;
  int line;
  int col;
  int index;
  int depth;
} js_vm_frame_view_t;

typedef bool 
  (*js_vm_frame_visitor_fn)
  (ant_t *js, const js_vm_frame_view_t *view, void *ctx);

static bool error_skip_bottom_wrapper_frame(const js_vm_frame_view_t *view) {
  return 
    view &&
    view->index == 0 &&
    view->depth > 0 &&
    strcmp(view->name, "<anonymous>") == 0 &&
    view->line == 1 &&
    view->col == 1;
}

static bool error_fill_vm_frame_view(
  ant_t *js, sv_vm_t *vm, int depth, int i, 
  const char *fallback_file, js_vm_frame_view_t *out
) {
  if (!js || !vm || i < 0 || i > depth || !out) return false;

  sv_frame_t *frame = &vm->frames[i];
  sv_func_t *func = frame->func;

  out->name = error_frame_name(js, frame, func);
  out->file = (func && func->filename) ? func->filename : fallback_file;
  out->line = (func && func->source_line > 0) ? func->source_line : 1;
  out->col = 1;
  out->index = i;
  out->depth = depth;

  if (func && func->srcpos && frame->ip) {
    uint32_t l, c;
    if (sv_lookup_srcpos(func, (int)(frame->ip - func->code), &l, &c)) {
      out->line = (int)l; out->col = (int)c;
    }
  }

  return true;
}

static void error_visit_vm_stack_frames(
  ant_t *js, const char *fallback_file, js_vm_frame_visitor_fn visitor, void *ctx
) {
  if (!js || !visitor) return;
  sv_vm_t *vm = sv_vm_get_active(js);
  if (!vm) return;

  int depth = vm->fp;
  for (int i = depth; i >= 0; i--) {
    js_vm_frame_view_t view;
    if (!error_fill_vm_frame_view(js, vm, depth, i, fallback_file, &view)) continue;
    if (error_skip_bottom_wrapper_frame(&view)) continue;
    if (!visitor(js, &view, ctx)) break;
  }
}

typedef struct {
  errbuf_t *eb;
  size_t *n;
  size_t remaining;
  const char *dim;
  const char *reset;
} error_frame_errbuf_ctx_t;

static bool error_visit_frame_append_errbuf(ant_t *js, const js_vm_frame_view_t *view, void *ctx) {
  error_frame_errbuf_ctx_t *c = (error_frame_errbuf_ctx_t *)ctx;
  *c->n = append_errbuf_fmt(
    c->eb, *c->n,
    "\n    at %s %s(%s:%d:%d)%s",
    view->name, c->dim, view->file, view->line, view->col, c->reset
  );
  c->remaining = remaining_capacity(*c->n, c->eb->size);
  return c->remaining > 20;
}

ant_value_t js_capture_raw_stack(ant_t *js) {
  errbuf_t eb = { malloc(4096), 4096 };
  if (!eb.buf) return js_mkundef();
  eb.buf[0] = '\0';

  size_t n = 0;
  const char *file = (js->errsite.valid && js->errsite.filename)
    ? js->errsite.filename
    : (js->filename ? js->filename : "<eval>");

  sv_vm_t *vm = sv_vm_get_active(js);
  if (vm && vm->fp >= 0) {
    error_frame_errbuf_ctx_t ctx = { &eb, &n, remaining_capacity(n, eb.size), "", "" };
    error_visit_vm_stack_frames(js, file, error_visit_frame_append_errbuf, &ctx);
  }

  ant_value_t stack_str = js_mkstr(js, eb.buf, n);
  free(eb.buf);
  return stack_str;
}

static bool error_visit_frame_print_file(ant_t *js, const js_vm_frame_view_t *view, void *ctx) {
  FILE *out = (FILE *)ctx;
  fprintf(
    out, "  at %s (%s%s:%d:%d%s)\n",
    view->name, C_GRAY, view->file, view->line, view->col, C_RESET
  );
  return true;
}

static bool append_error_context(
  errbuf_t *eb, size_t *n,
  const char *src, ant_offset_t src_len,
  ant_offset_t src_pos,
  int error_line_no, int error_col, int error_span_cols
) {
  if (!src || src_len <= 0 || !n) return false;
  if (src_pos < 0) src_pos = 0;
  if (src_pos > src_len) src_pos = src_len;

  ant_offset_t err_line_start = src_pos;
  while (err_line_start > 0 && src[err_line_start - 1] != '\n') err_line_start--;
  
  ant_offset_t err_line_end = src_pos;
  while (err_line_end < src_len && src[err_line_end] != '\n' && src[err_line_end] != '\0') err_line_end++;

  ant_offset_t ctx_start = err_line_start;
  int first_line_no = error_line_no;
  for (int i = 0; i < 5 && ctx_start > 0; i++) {
    ant_offset_t prev = ctx_start - 1;
    if (src[prev] == '\n') prev--;
    while (prev >= 0 && src[prev] != '\n') prev--;
    
    ctx_start = prev + 1;
    first_line_no--;
    if (first_line_no < 1) { first_line_no = 1; break; }
  }

  int gutter_w = count_digits_int(error_line_no);
  int src_cols_limit = error_context_src_cols_limit(gutter_w);

  char tagged[4096]; char rendered[8192];
  highlight_state hl_state = HL_STATE_INIT;

  ant_offset_t cur = ctx_start;
  int line_no = first_line_no;

  while (cur <= err_line_end && cur < src_len) {
    ant_offset_t ls = cur, le = cur;
    while (le < src_len && src[le] != '\n' && src[le] != '\0') le++;
    
    int line_len = (int)(le - ls);
    bool was_clipped = (src_cols_limit > 0 && line_len > src_cols_limit);

    if (!io_no_color) {
      highlight_js_line_clipped(
        src + ls, (size_t)line_len, (size_t)src_cols_limit,
        tagged, sizeof(tagged), &hl_state
      );
      crsprintf_stateful(rendered, sizeof(rendered), NULL, tagged);
      *n = append_errbuf_fmt(eb, *n, "\n%*d | %s", gutter_w, line_no, rendered);
    } else {
      int shown = was_clipped ? src_cols_limit : line_len;
      *n = append_errbuf_fmt(eb, *n, "\n%*d | %.*s", gutter_w, line_no, shown, src + ls);
    }

    if (was_clipped) *n = append_errbuf_fmt(eb, *n, "...");
    if (ls == err_line_start) {
      *n = append_errbuf_fmt(eb, *n, "\n%*s   ", gutter_w, "");
      int caret_col = error_col;
      int caret_span = error_span_cols;
      if (was_clipped) {
        int max_col = src_cols_limit > 0 ? src_cols_limit : 1;
        if (caret_col > max_col) caret_col = max_col;
        if (caret_span > max_col - caret_col + 1) caret_span = max_col - caret_col + 1;
        if (caret_span < 1) caret_span = 1;
      }
      append_error_caret(eb, n, caret_col, caret_span);
    }

    if (le >= src_len || src[le] == '\0') break;
    cur = le + 1; line_no++;
  }

  return true;
}

static void format_error_stack(errbuf_t *eb, ant_t *js, size_t *n, int line, int col, bool include_source_line, const char *error_line, int error_col, int error_span_cols) {
  if (!ensure_errbuf_capacity(eb, *n + 1)) return;

  const char *dim = C_GRAY;
  const char *reset = C_RESET;

  if (include_source_line && error_line && error_line[0] && *n < eb->size) {
    *n = append_errbuf_fmt(eb, *n, "\n%s\n", error_line);
    append_error_caret(eb, n, error_col, error_span_cols);
  }

  size_t remaining = remaining_capacity(*n, eb->size);
  if (remaining > 20) {
    const char *file = (js->errsite.valid && js->errsite.filename)
      ? js->errsite.filename
      : (js->filename ? js->filename : "<eval>");
      
    sv_vm_t *vm = sv_vm_get_active(js);
    int depth = vm ? vm->fp : -1;
    
    if (depth >= 0) {
      error_frame_errbuf_ctx_t ctx = { eb, n, remaining, dim, reset };
      error_visit_vm_stack_frames(js, file, error_visit_frame_append_errbuf, &ctx);
      remaining = ctx.remaining;
    }

    if (depth <= 0 && remaining > 20) {
      *n = append_errbuf_fmt(eb, *n,
        "\n    at %s%s:%d:%d%s",
        dim, file, line, col, reset
      );
      remaining = remaining_capacity(*n, eb->size);
    }

    if (remaining > 60 && js->filename && strcmp(js->filename, "[eval]") != 0) {
      *n = append_errbuf_fmt(eb, *n,
        "\n    at silver.sv_execute_frame %s(ant:internal/silver/engine:323:5)%s",
        dim, reset
      );
      remaining = remaining_capacity(*n, eb->size);
    }

    if (remaining > 40 && js->filename && strcmp(js->filename, "[eval]") != 0) {
      *n = append_errbuf_fmt(eb, *n, "\n    at %sant:internal/call:13635:14%s", dim, reset);
    }
  }

  eb->buf[eb->size - 1] = '\0';
}

void js_set_error_site(ant_t *js, const char *src, ant_offset_t src_len, const char *filename, ant_offset_t off, ant_offset_t span_len) {
  if (!js) return;
  
  js->errsite.src = src;
  js->errsite.src_len = src_len;
  js->errsite.filename = filename;
  js->errsite.off = off < 0 ? 0 : off;
  js->errsite.span_len = span_len < 0 ? 0 : span_len;
  js->errsite.valid = (src != NULL && src_len >= 0);
}

void js_get_call_location(ant_t *js, const char **out_filename, int *out_line, int *out_col) {
  if (!js) return;
  if (!js->errsite.valid) js_set_error_site_from_vm_top(js);
  
  if (out_filename) *out_filename = (js->errsite.valid && js->errsite.filename) ? js->errsite.filename : js->filename;
  if (out_line) *out_line = 1;
  if (out_col)  *out_col  = 1;
  
  if (js->errsite.valid && js->errsite.src) get_line_col(
    js->errsite.src, js->errsite.src_len, 
    js->errsite.off, out_line, out_col
  );
}

void js_clear_error_site(ant_t *js) {
  if (!js) return;
  memset(&js->errsite, 0, sizeof(js->errsite));
}

static void resolve_error_site(
  ant_t *js, const char **out_src, ant_offset_t *out_src_len,
  ant_offset_t *out_src_pos, ant_offset_t *out_span_len
) {
  if (js && !js->errsite.valid) js_set_error_site_from_vm_top(js);

  const char *src = NULL;
  ant_offset_t src_len = 0;
  ant_offset_t src_pos = 0;
  ant_offset_t span_len = 0;

  if (js->errsite.valid) {
    src = js->errsite.src;
    src_len = js->errsite.src_len;
    src_pos = js->errsite.off;
    span_len = js->errsite.span_len;
  }

  if (out_src) *out_src = src;
  if (out_src_len) *out_src_len = src_len;
  if (out_src_pos) *out_src_pos = src_pos;
  if (out_span_len) *out_span_len = span_len;
}

typedef struct {
  const char *src;
  ant_offset_t src_len;
  ant_offset_t src_pos;
  int error_col;
  int error_span_cols;
  int line;
  int col;
  char error_line[256];
} js_error_render_site_t;

static void js_prepare_error_render_site(ant_t *js, js_error_render_site_t *site) {
  if (!site) return;
  memset(site, 0, sizeof(*site));
  site->line = 1;
  site->col = 1;
  site->error_col = 1;
  site->error_span_cols = 1;

  ant_offset_t src_span_len = 0;
  ant_offset_t line_start = 0, line_end = 0;
  resolve_error_site(js, &site->src, &site->src_len, &site->src_pos, &src_span_len);

  get_line_col(site->src, site->src_len, site->src_pos, &site->line, &site->col);
  get_error_line(
    site->src, site->src_len, site->src_pos,
    site->error_line, sizeof(site->error_line),
    &site->error_col, &line_start, &line_end
  );
  site->error_span_cols = error_span_cols_for_line(site->src_pos, src_span_len, line_start, line_end);
}

typedef enum {
  JS_STACK_TEXT_FROM_ERROR_OBJECT = 0,
  JS_STACK_TEXT_FROM_THROW_VALUE = 1,
} js_stack_text_kind_t;

static ant_value_t js_build_stack_text(ant_t *js, js_stack_text_kind_t kind, ant_value_t value) {
  js_error_render_site_t site;
  js_prepare_error_render_site(js, &site);

  errbuf_t eb = { malloc(4096), 4096 };
  if (!eb.buf) return js_mkundef();
  eb.buf[0] = '\0';

  size_t n = 0;
  n = append_error_header(&eb, js, 0, site.line, site.col);

  if (n > 0 && eb.buf[n - 1] == '\n') {
    n--;
    eb.buf[n] = '\0';
  }

  bool rendered_context = append_error_context(
    &eb, &n, site.src, site.src_len, site.src_pos,
    site.line, site.error_col, site.error_span_cols
  );

  if (!rendered_context && site.error_line[0]) {
    n = append_errbuf_fmt(&eb, n, "\n%s\n", site.error_line);
    append_error_caret(&eb, &n, site.error_col, site.error_span_cols);
  }

  n = append_errbuf_fmt(&eb, n, "\n");

  if (kind == JS_STACK_TEXT_FROM_ERROR_OBJECT) {
    const char *err_name = "Error";
    const char *err_msg = NULL;
    
    ant_offset_t name_len = 5, msg_len = 0; 
    const char *n_str = get_str_prop(js, value, "name", 4, &name_len);
    
    if (n_str) err_name = n_str;
    err_msg = get_str_prop(js, value, "message", 7, &msg_len);

    if (err_msg) {
      n = append_errbuf_fmt(&eb, n,
        "%s%.*s%s: %s%.*s%s",
        C_RED, (int)name_len, err_name, C_RESET,
        C_BOLD, (int)msg_len, err_msg, C_RESET);
    } else {
      n = append_errbuf_fmt(&eb, n,
        "%s%.*s%s",
        C_RED, (int)name_len, err_name, C_RESET);
    }
  } else n = append_error_value(&eb, js, n, value);

  format_error_stack(
    &eb, js, &n, site.line, site.col, false,
    site.error_line, site.error_col, site.error_span_cols
  );

  ant_value_t stack_str = js_mkstr(js, eb.buf, n);
  free(eb.buf);
  return stack_str;
}

void js_capture_stack(ant_t *js, ant_value_t err_obj) {
  ant_value_t stack_str = js_build_stack_text(js, JS_STACK_TEXT_FROM_ERROR_OBJECT, err_obj);
  if (vtype(stack_str) != T_STR) return;

  js_set(js, err_obj, "stack", stack_str);
  js_set_descriptor(js, js_as_obj(err_obj), "stack", 5, JS_DESC_W | JS_DESC_C);
  js_clear_error_site(js);
}

js_err_type_t get_error_type(ant_t *js) {
  if (!js->thrown_exists) return JS_ERR_GENERIC;
  ant_value_t err_type = js_get_slot(js->thrown_value, SLOT_ERR_TYPE);
  if (vtype(err_type) != T_NUM) return JS_ERR_GENERIC;
  return (js_err_type_t)((int)js_getnum(err_type) & ~JS_ERR_NO_STACK);
}

__attribute__((format(printf, 4, 5)))
ant_value_t js_create_error(ant_t *js, js_err_type_t err_type, ant_value_t props, const char *xx, ...) {
  va_list ap;
  char error_msg[256] = {0};

  bool no_stack = (err_type & JS_ERR_NO_STACK) != 0;
  js_err_type_t base_type = (js_err_type_t)(err_type & ~JS_ERR_NO_STACK);

  va_start(ap, xx);
    vsnprintf(error_msg, sizeof(error_msg), xx, ap);
  va_end(ap);

  const char *err_name = get_error_type_name(base_type);
  size_t err_name_len = strlen(err_name);
  size_t msg_len = strlen(error_msg);

  ant_value_t err_obj = js_mkobj(js);
  js_set(js, err_obj, "name", js_mkstr(js, err_name, err_name_len));
  js_set(js, err_obj, "message", js_mkstr(js, error_msg, msg_len));
  js_set_slot(err_obj, SLOT_ERR_TYPE, js_mknum((double)err_type));

  int props_type = vtype(props);
  if ((JS_TPFLG(props_type) & T_SPECIAL_OBJECT_MASK) != 0) {
    js_merge_obj(js, err_obj, props);
  }
  ant_value_t proto = js_get_ctor_proto(js, err_name, err_name_len);
  int proto_type = vtype(proto);
  if ((JS_TPFLG(proto_type) & T_SPECIAL_OBJECT_MASK) != 0) {
    js_set_proto_init(err_obj, proto);
  }

  js->thrown_exists = true;
  js->thrown_value = err_obj;

  if (!no_stack) {
    js_capture_stack(js, err_obj);
  }

  js_clear_error_site(js);
  return mkval(T_ERR, vdata(err_obj));
}

ant_value_t js_make_error_silent(ant_t *js, js_err_type_t err_type, const char *message) {
  bool had_throw = js->thrown_exists;
  
  ant_value_t saved_value = had_throw ? js->thrown_value : js_mkundef();
  ant_value_t saved_stack = had_throw ? js->thrown_stack : js_mkundef();

  js_create_error(js, err_type, js_mkundef(), "%s", message);
  ant_value_t err = js->thrown_value;

  js->thrown_exists = had_throw;
  js->thrown_value = saved_value;
  js->thrown_stack = saved_stack;
  
  return err;
}

ant_value_t js_throw(ant_t *js, ant_value_t value) {
  if (vtype(value) == T_OBJ) {
    ant_value_t existing = js_get(js, value, "stack");
    if (vtype(existing) == T_STR) {
      js->thrown_exists = true;
      js->thrown_value = value;
      js_clear_error_site(js);
      return mkval(T_ERR, vdata(value));
    }
    ant_value_t slot = js_get_slot(value, SLOT_ERR_TYPE);
    if (vtype(slot) == T_NUM && ((int)js_getnum(slot) & JS_ERR_NO_STACK)) {
      js->thrown_exists = true;
      js->thrown_value = value;
      js_clear_error_site(js);
      return mkval(T_ERR, vdata(value));
    }
  }

  ant_value_t stack_str = js_build_stack_text(js, JS_STACK_TEXT_FROM_THROW_VALUE, value);
  if (vtype(stack_str) != T_STR) {
    js->thrown_exists = true;
    js->thrown_value = value;
    js_clear_error_site(js);
    return mkval(T_ERR, 0);
  }

  if (vtype(value) == T_OBJ) {
    js_set(js, value, "stack", stack_str);
    js_set_descriptor(js, js_as_obj(value), "stack", 5, JS_DESC_W | JS_DESC_C);
  }

  js->thrown_exists = true;
  js->thrown_value = value;
  js->thrown_stack = stack_str;
  js_clear_error_site(js);
  
  return mkval(T_ERR, 0);
}

enum { 
  CS_FILE = 0,
  CS_LINE,
  CS_COL,
  CS_NAME
};

static ant_value_t callsite_field(ant_t *js, int field) {
  ant_value_t data = js_get_slot(js->this_val, SLOT_DATA);
  if (vtype(data) != T_ARR) return js_mkundef();
  return js_arr_get(js, data, field);
}

static ant_value_t callsite_getFileName(ant_t *js, ant_value_t *args, int nargs) { return callsite_field(js, CS_FILE); }
static ant_value_t callsite_getLineNumber(ant_t *js, ant_value_t *args, int nargs) { return callsite_field(js, CS_LINE); }
static ant_value_t callsite_getColumnNumber(ant_t *js, ant_value_t *args, int nargs) { return callsite_field(js, CS_COL); }
static ant_value_t callsite_getFunctionName(ant_t *js, ant_value_t *args, int nargs) { return callsite_field(js, CS_NAME); }

static ant_value_t callsite_getTypeName(ant_t *js, ant_value_t *args, int nargs) { return js_mknull(); }
static ant_value_t callsite_getMethodName(ant_t *js, ant_value_t *args, int nargs) { return callsite_field(js, CS_NAME); }

static ant_value_t callsite_isNative(ant_t *js, ant_value_t *args, int nargs) { return js_false; }
static ant_value_t callsite_isToplevel(ant_t *js, ant_value_t *args, int nargs) { return js_false; }
static ant_value_t callsite_isEval(ant_t *js, ant_value_t *args, int nargs) { return js_false; }
static ant_value_t callsite_isConstructor(ant_t *js, ant_value_t *args, int nargs) { return js_false; }
static ant_value_t callsite_getEvalOrigin(ant_t *js, ant_value_t *args, int nargs) { return js_mkundef(); }
static ant_value_t callsite_getThis(ant_t *js, ant_value_t *args, int nargs) { return js_mkundef(); }

static ant_value_t callsite_toString(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t name = callsite_field(js, CS_NAME);
  ant_value_t file = callsite_field(js, CS_FILE);
  ant_value_t line = callsite_field(js, CS_LINE);
  ant_value_t col  = callsite_field(js, CS_COL);

  const char *n = js_str(js, name);
  const char *f = js_str(js, file);
  int l = vtype(line) == T_NUM ? (int)js_getnum(line) : 0;
  int c = vtype(col)  == T_NUM ? (int)js_getnum(col)  : 0;

  char buf[512];
  int len = snprintf(buf, sizeof(buf), "%s (%s:%d:%d)", n, f, l, c);
  if (len < 0) len = 0;
  return js_mkstr(js, buf, (size_t)len);
}

typedef struct {
  ant_t *js;
  ant_value_t arr;
  ant_value_t proto;
} callsite_build_ctx_t;

static bool callsite_visit_frame(ant_t *js, const js_vm_frame_view_t *view, void *ctx) {
  callsite_build_ctx_t *c = (callsite_build_ctx_t *)ctx;

  ant_value_t data = js_mkarr(js);
  js_arr_push(js, data, js_mkstr(js, view->file, strlen(view->file)));
  js_arr_push(js, data, js_mknum((double)view->line));
  js_arr_push(js, data, js_mknum((double)view->col));
  js_arr_push(js, data, js_mkstr(js, view->name, strlen(view->name)));

  ant_value_t site = js_mkobj(js);
  js_set_proto_init(site, c->proto);
  js_set_slot(site, SLOT_DATA, data);

  js_arr_push(js, c->arr, site);
  return true;
}

ant_value_t js_build_callsite_array(ant_t *js) {
  ant_value_t proto = js_mkobj(js);
  
  js_set(js, proto, "getFileName",     js_mkfun(callsite_getFileName));
  js_set(js, proto, "getLineNumber",   js_mkfun(callsite_getLineNumber));
  js_set(js, proto, "getColumnNumber", js_mkfun(callsite_getColumnNumber));
  js_set(js, proto, "getFunctionName", js_mkfun(callsite_getFunctionName));
  js_set(js, proto, "getTypeName",     js_mkfun(callsite_getTypeName));
  js_set(js, proto, "getMethodName",   js_mkfun(callsite_getMethodName));
  js_set(js, proto, "isNative",        js_mkfun(callsite_isNative));
  js_set(js, proto, "isToplevel",      js_mkfun(callsite_isToplevel));
  js_set(js, proto, "isEval",          js_mkfun(callsite_isEval));
  js_set(js, proto, "isConstructor",   js_mkfun(callsite_isConstructor));
  js_set(js, proto, "getEvalOrigin",   js_mkfun(callsite_getEvalOrigin));
  js_set(js, proto, "getThis",         js_mkfun(callsite_getThis));
  js_set(js, proto, "toString",        js_mkfun(callsite_toString));

  ant_value_t arr = js_mkarr(js);
  callsite_build_ctx_t ctx = { js, arr, proto };

  const char *file = (js->errsite.valid && js->errsite.filename)
    ? js->errsite.filename
    : (js->filename ? js->filename : "<eval>");

  error_visit_vm_stack_frames(js, file, callsite_visit_frame, &ctx);
  return arr;
}

void js_print_stack_trace_vm(ant_t *js, FILE *stream) {
  const char *fallback_file = js->filename ? js->filename : "<unknown>";
  if (!stream) return;
  
  error_visit_vm_stack_frames(
    js, fallback_file, 
    error_visit_frame_print_file, stream
  );
}
