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

#include "ant.h"
#include "errors.h"
#include "runtime.h"
#include "internal.h"
#include "silver/engine.h"

#include "modules/reflect.h"
#include "modules/symbol.h"

static ant_value_t reflect_get(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkundef();
  
  ant_value_t target = args[0];
  ant_value_t key = args[1];
  
  if (!is_object_type(target)) return js_mkundef();

  ant_value_t prop_key = key;
  if (is_object_type(prop_key)) {
    prop_key = js_to_primitive(js, prop_key, 1);
    if (is_err(prop_key)) return prop_key;
  }

  if (vtype(prop_key) == T_SYMBOL)
    return js_get_sym(js, target, prop_key);

  if (vtype(prop_key) != T_STR) {
    prop_key = js_tostring_val(js, prop_key);
    if (is_err(prop_key)) return prop_key;
  }

  char *key_str = js_getstr(js, prop_key, NULL);
  if (!key_str) return js_mkundef();
  
  return js_get(js, target, key_str);
}

static ant_value_t reflect_set(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 3) return js_false;
  
  ant_value_t target = args[0];
  ant_value_t key = args[1];
  ant_value_t value = args[2];
  
  int t = vtype(target);
  if (t != T_OBJ && t != T_FUNC) return js_false;
  
  if (vtype(key) != T_STR) return js_false;
  
  char *key_str = js_getstr(js, key, NULL);
  if (!key_str) return js_false;
  
  js_set(js, target, key_str, value);
  return js_true; 
}

static ant_value_t reflect_has(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_false;
  
  ant_value_t target = args[0];
  ant_value_t key = args[1];
  
  int t = vtype(target);
  if (t != T_OBJ && t != T_FUNC) return js_false;
  
  if (vtype(key) != T_STR) return js_false;
  
  size_t key_len;
  char *key_str = js_getstr(js, key, &key_len);
  if (!key_str) return js_false;
  
  ant_offset_t off = lkp_proto(js, target, key_str, key_len);
  return js_bool(off > 0);
}

static ant_value_t reflect_delete_property(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_false;
  
  ant_value_t target = args[0];
  ant_value_t key = args[1];
  
  int t = vtype(target);
  if (t != T_OBJ && t != T_FUNC) return js_false;
  
  if (vtype(key) != T_STR) return js_false;
  
  char *key_str = js_getstr(js, key, NULL);
  if (!key_str) return js_false;
  
  ant_value_t del_result = js_delete_prop(js, target, key_str, strlen(key_str));
  bool deleted = !is_err(del_result) && js_truthy(js, del_result);
  return js_bool(deleted);
}

static ant_value_t reflect_own_keys(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkarr(js);
  
  ant_value_t target = args[0];
  
  int t = vtype(target);
  if (t != T_OBJ && t != T_FUNC) {
    return js_mkerr(js, "Reflect.ownKeys called on non-object");
  }
  
  ant_value_t keys_arr = js_mkarr(js);
  ant_iter_t iter = js_prop_iter_begin(js, target);
  const char *key; size_t key_len; ant_value_t value;
  
  while (js_prop_iter_next(&iter, &key, &key_len, &value)) {
    js_arr_push(js, keys_arr, js_mkstr(js, key, key_len));
  }
  
  js_prop_iter_end(&iter);
  return keys_arr;
}

static ant_value_t reflect_construct(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) {
    return js_mkerr(js, "Reflect.construct requires at least 2 arguments");
  }
  
  ant_value_t target = args[0];
  ant_value_t args_arr = args[1];
  ant_value_t new_target = (nargs >= 3) ? args[2] : target;
  
  if (vtype(target) != T_FUNC && vtype(target) != T_CFUNC) {
    return js_mkerr(js, "Reflect.construct: first argument must be a constructor");
  }
  
  if (vtype(new_target) != T_FUNC && vtype(new_target) != T_CFUNC) {
    return js_mkerr(js, "Reflect.construct: third argument must be a constructor");
  }
  
  ant_value_t length_val = js_get(js, args_arr, "length");
  int arg_count = 0;
  if (vtype(length_val) == T_NUM) {
    arg_count = (int)js_getnum(length_val);
  }
  
  ant_value_t *call_args = NULL;
  if (arg_count > 0) {
    call_args = malloc(arg_count * sizeof(ant_value_t));
    if (!call_args) return js_mkerr(js, "Out of memory");
    
    for (int i = 0; i < arg_count; i++) {
      char idx[16];
      snprintf(idx, sizeof(idx), "%d", i);
      call_args[i] = js_get(js, args_arr, idx);
    }
  }
  
  ant_value_t new_obj = js_mkobj(js);
  ant_value_t proto = js_get(js, new_target, "prototype");
  if (vtype(proto) == T_OBJ) js_set_proto_init(new_obj, proto);

  ant_value_t saved_new_target = js->new_target;
  js->new_target = new_target;

  ant_value_t result = sv_vm_call(js->vm, js, target, new_obj, call_args, arg_count, NULL, true);
  js->new_target = saved_new_target;
  
  if (call_args) free(call_args);
  if (is_object_type(result)) return result;
  
  return new_obj;
}

static ant_value_t reflect_apply(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 3) {
    return js_mkerr(js, "Reflect.apply requires 3 arguments");
  }
  
  ant_value_t target = args[0];
  ant_value_t this_arg = args[1];
  ant_value_t args_arr = args[2];
  
  if (vtype(target) != T_FUNC && vtype(target) != T_CFUNC) {
    return js_mkerr(js, "Reflect.apply: first argument must be a function");
  }

  if (vtype(args_arr) == T_UNDEF || vtype(args_arr) == T_NULL) return js_mkerr_typed(
    js, JS_ERR_TYPE,
    "Reflect.apply: third argument must be an array-like object"
  );

  ant_value_t result;
  if (vtype(args_arr) == T_ARR) {
    ant_value_t *call_args = NULL;
    int arg_count = extract_array_args(js, args_arr, &call_args);
    result = sv_vm_call_explicit_this(
      js->vm, js, target, this_arg, 
      call_args, arg_count
    );
    if (call_args) free(call_args);
    return result;
  }
  
  ant_value_t length_val = js_get(js, args_arr, "length");
  int arg_count = 0;
  if (vtype(length_val) == T_NUM) {
    arg_count = (int)js_getnum(length_val);
  }
  
  ant_value_t *call_args = NULL;
  if (arg_count > 0) {
    call_args = malloc(arg_count * sizeof(ant_value_t));
    if (!call_args) return js_mkerr(js, "Out of memory");
    
    for (int i = 0; i < arg_count; i++) {
      char idx[16];
      snprintf(idx, sizeof(idx), "%d", i);
      call_args[i] = js_get(js, args_arr, idx);
    }
  }
  
  result = sv_vm_call_explicit_this(
    js->vm, js, target, this_arg, 
    call_args, arg_count
  );
  
  if (call_args) free(call_args);
  return result;
}

static ant_value_t reflect_get_own_property_descriptor(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkundef();
  
  ant_value_t target = args[0];
  ant_value_t key = args[1];
  
  int t = vtype(target);
  if (t != T_OBJ && t != T_FUNC) return js_mkundef();
  
  if (vtype(key) != T_STR) return js_mkundef();
  
  size_t key_len;
  char *key_str = js_getstr(js, key, &key_len);
  if (!key_str) return js_mkundef();
  
  ant_offset_t off = lkp(js, target, key_str, key_len);
  if (off <= 0) return js_mkundef();
  
  ant_value_t value = js_get(js, target, key_str);
  ant_value_t desc = js_mkobj(js);
  js_set(js, desc, "value", value);
  js_set(js, desc, "writable", js_true);
  js_set(js, desc, "enumerable", js_true);
  js_set(js, desc, "configurable", js_true);
  
  return desc;
}

static ant_value_t reflect_define_property(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 3) return js_false;
  return js_define_property(js, args[0], args[1], args[2], true);
}

static ant_value_t reflect_get_prototype_of(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) {
    return js_mkerr(js, "Reflect.getPrototypeOf requires an argument");
  }
  
  ant_value_t target = args[0];
  
  if (!is_object_type(target)) {
    return js_mkerr(js, "Reflect.getPrototypeOf: argument must be an object");
  }
  
  return js_get_proto(js, target);
}

static ant_value_t reflect_set_prototype_of(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_false;
  
  ant_value_t target = args[0];
  ant_value_t proto = args[1];
  
  if (!is_object_type(target)) return js_false;
  if (!is_object_type(proto) && vtype(proto) != T_NULL) return js_false;
  if (vtype(proto) != T_NULL && proto_chain_contains(js, proto, target)) return js_false;
  
  js_set_proto_wb(js, target, proto);
  
  return js_true;
}

static ant_value_t reflect_is_extensible(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_false;
  
  ant_value_t target = args[0];
  int t = vtype(target);
  
  if (t != T_OBJ && t != T_FUNC) return js_false;

  ant_object_t *obj = js_obj_ptr(js_as_obj(target));
  if (!obj) return js_false;
  if (obj->frozen || obj->sealed) return js_false;
  return js_bool(obj->extensible);
}

static ant_value_t reflect_prevent_extensions(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_false;
  
  ant_value_t target = args[0];
  int t = vtype(target);
  
  if (t != T_OBJ && t != T_FUNC) return js_false;

  ant_object_t *obj = js_obj_ptr(js_as_obj(target));
  if (!obj) return js_false;
  obj->extensible = 0;
  return js_true;
}

void init_reflect_module(void) {
  ant_t *js = rt->js;
  ant_value_t reflect_obj = js_mkobj(js);
  
  js_set(js, reflect_obj, "get", js_mkfun(reflect_get));
  js_set(js, reflect_obj, "set", js_mkfun(reflect_set));
  js_set(js, reflect_obj, "has", js_mkfun(reflect_has));
  js_set(js, reflect_obj, "deleteProperty", js_mkfun(reflect_delete_property));
  js_set(js, reflect_obj, "ownKeys", js_mkfun(reflect_own_keys));
  js_set(js, reflect_obj, "construct", js_mkfun(reflect_construct));
  js_set(js, reflect_obj, "apply", js_mkfun(reflect_apply));
  js_set(js, reflect_obj, "getOwnPropertyDescriptor", js_mkfun(reflect_get_own_property_descriptor));
  js_set(js, reflect_obj, "defineProperty", js_mkfun(reflect_define_property));
  js_set(js, reflect_obj, "getPrototypeOf", js_mkfun(reflect_get_prototype_of));
  js_set(js, reflect_obj, "setPrototypeOf", js_mkfun(reflect_set_prototype_of));
  js_set(js, reflect_obj, "isExtensible", js_mkfun(reflect_is_extensible));
  js_set(js, reflect_obj, "preventExtensions", js_mkfun(reflect_prevent_extensions));
  
  js_set_sym(js, reflect_obj, get_toStringTag_sym(), js_mkstr(js, "Reflect", 7));
  js_set(js, js_glob(js), "Reflect", reflect_obj);
}
