#ifndef SV_ITERATION_H
#define SV_ITERATION_H

#include "ant.h"
#include "async.h"
#include "utf8.h"
#include "property.h"
#include "silver/engine.h"
#include "modules/symbol.h"
#include "modules/collections.h"

#define SV_ITER_GENERIC  0
#define SV_ITER_ARRAY    1
#define SV_ITER_MAP      2
#define SV_ITER_SET      3
#define SV_ITER_STRING   4

static inline ant_value_t sv_op_for_in(sv_vm_t *vm, ant_t *js) {
  ant_value_t obj = vm->stack[--vm->sp];
  ant_value_t keys = js_for_in_keys(js, obj);
  if (is_err(keys)) return keys;
  vm->stack[vm->sp++] = keys;
  return tov(0);
}

static inline bool sv_is_map_iter(
  ant_t *js, ant_value_t obj,
  map_iterator_state_t **out_state,
  iter_type_t *out_type
) {
  if (vtype(obj) != T_OBJ) return false;
  if (!g_map_iter_proto || js_get_proto(js, obj) != g_map_iter_proto) return false;
  
  ant_value_t state_val = js_get_slot(obj, SLOT_ITER_STATE);
  if (vtype(state_val) == T_UNDEF) return false;
  
  map_iterator_state_t *st = (map_iterator_state_t *)(uintptr_t)js_getnum(state_val);
  if (!st) return false;
  
  *out_state = st;
  *out_type = st->type;
  
  return true;
}

static inline bool sv_is_set_iter(
  ant_t *js, ant_value_t obj,
  set_iterator_state_t **out_state,
  iter_type_t *out_type
) {
  if (vtype(obj) != T_OBJ) return false;
  if (!g_set_iter_proto || js_get_proto(js, obj) != g_set_iter_proto) return false;
  
  ant_value_t state_val = js_get_slot(obj, SLOT_ITER_STATE);
  if (vtype(state_val) == T_UNDEF) return false;
  
  set_iterator_state_t *st = (set_iterator_state_t *)(uintptr_t)js_getnum(state_val);
  if (!st) return false;
  
  *out_state = st;
  *out_type = st->type;
  
  return true;
}

static inline ant_value_t sv_op_for_of(sv_vm_t *vm, ant_t *js) {
  ant_value_t iterable = vm->stack[--vm->sp];

  if (vtype(iterable) == T_ARR) {
    vm->stack[vm->sp++] = iterable;
    vm->stack[vm->sp++] = tov(0);
    vm->stack[vm->sp++] = tov(SV_ITER_ARRAY);
    return tov(0);
  }

  if (vtype(iterable) == T_STR) {
    if (str_is_heap_rope(iterable) || str_is_heap_builder(iterable)) {
      iterable = str_materialize(js, iterable);
      if (is_err(iterable)) return iterable;
    }
    vm->stack[vm->sp++] = iterable;
    vm->stack[vm->sp++] = tov(0);
    vm->stack[vm->sp++] = tov(SV_ITER_STRING);
    return tov(0);
  }

  ant_value_t iter_fn = js_get_sym(js, iterable, get_iterator_sym());
  if (!is_callable(iter_fn)) return js_mkerr(js, "not iterable");
  
  ant_value_t iterator = sv_vm_call(vm, js, iter_fn, iterable, NULL, 0, NULL, false);
  if (is_err(iterator)) return iterator;

  map_iterator_state_t *map_st;
  iter_type_t map_type;
  if (sv_is_map_iter(js, iterator, &map_st, &map_type)) {
    vm->stack[vm->sp++] = ANT_PTR(map_st);
    vm->stack[vm->sp++] = tov((double)map_type);
    vm->stack[vm->sp++] = tov(SV_ITER_MAP);
    return tov(0);
  }

  set_iterator_state_t *set_st;
  iter_type_t set_type;
  if (sv_is_set_iter(js, iterator, &set_st, &set_type)) {
    vm->stack[vm->sp++] = ANT_PTR(set_st);
    vm->stack[vm->sp++] = tov((double)set_type);
    vm->stack[vm->sp++] = tov(SV_ITER_SET);
    return tov(0);
  }

  ant_value_t next_method = js_getprop_fallback(js, iterator, "next");
  vm->stack[vm->sp++] = iterator;
  vm->stack[vm->sp++] = next_method;
  vm->stack[vm->sp++] = tov(SV_ITER_GENERIC);
  return tov(0);
}

static inline ant_value_t sv_op_for_await_of(sv_vm_t *vm, ant_t *js) {
  ant_value_t iterable = vm->stack[--vm->sp];
  ant_value_t iter_fn = js_get_sym(js, iterable, get_asyncIterator_sym());

  if (!is_callable(iter_fn)) {
    iter_fn = js_get_sym(js, iterable, get_iterator_sym());
    if (!is_callable(iter_fn)) return js_mkerr(js, "not iterable");
  }

  ant_value_t iterator = sv_vm_call(vm, js, iter_fn, iterable, NULL, 0, NULL, false);
  if (is_err(iterator)) return iterator;
  ant_value_t next_method = js_getprop_fallback(js, iterator, "next");
  vm->stack[vm->sp++] = iterator;
  vm->stack[vm->sp++] = next_method;
  vm->stack[vm->sp++] = tov(SV_ITER_GENERIC);
  return tov(0);
}

static inline ant_value_t sv_iter_result_get_named(
  ant_t *js,
  ant_value_t result,
  const char *interned,
  const char *key,
  ant_offset_t key_len
) {
  ant_object_t *ptr = is_object_type(result) 
    ? js_obj_ptr(js_as_obj(result)) 
    : NULL;
    
  ant_value_t out = js_mkundef();
  bool should_fallback = false;

  if (interned && sv_try_get_shape_data_prop(js, ptr, interned, &out, &should_fallback)) return out;
  return sv_getprop_fallback_len(js, result, key, key_len);
}

static inline void sv_iter_result_unpack(
  ant_t *js, ant_value_t result,
  ant_value_t *out_done, ant_value_t *out_value
) {
  *out_done = sv_iter_result_get_named(js, result, js->intern.done, "done", 4);
  *out_value = sv_iter_result_get_named(js, result, js->intern.value, "value", 5);
}

static inline ant_value_t sv_iter_advance(
  sv_vm_t *vm, ant_t *js, int hint, ant_value_t *out_value, bool *out_done
) {
  int tag = hint ? hint : (int)js_getnum(vm->stack[vm->sp - 1]);
  switch (tag) {
  case SV_ITER_ARRAY: {
    ant_value_t arr = vm->stack[vm->sp - 3];
    int idx = (int)js_getnum(vm->stack[vm->sp - 2]);
    ant_offset_t len = js_arr_len(js, arr);
    if (idx >= (int)len) {
      *out_value = js_mkundef();
      *out_done = true;
    } else {
      *out_value = js_arr_get(js, arr, (ant_offset_t)idx);
      *out_done = false;
      vm->stack[vm->sp - 2] = tov(idx + 1);
    }
    return tov(0);
  }

  case SV_ITER_MAP: {
    map_iterator_state_t *st =
      (map_iterator_state_t *)(uintptr_t)js_getnum(vm->stack[vm->sp - 3]);
    if (!st->current) {
      *out_value = js_mkundef();
      *out_done = true;
    } else {
      map_entry_t *entry = st->current;
      ant_value_t value;
      switch (st->type) {
      case ITER_TYPE_MAP_VALUES:
        value = entry->value;
        break;
      case ITER_TYPE_MAP_KEYS:
        value = entry->key_val;
        break;
      case ITER_TYPE_MAP_ENTRIES: {
        ant_value_t pair = js_mkarr(js);
        js_arr_push(js, pair, entry->key_val);
        js_arr_push(js, pair, entry->value);
        value = pair;
        break;
      }
      default:
        value = js_mkundef();
      }
      st->current = entry->hh.next;
      *out_value = value;
      *out_done = false;
    }
    return tov(0);
  }

  case SV_ITER_SET: {
    set_iterator_state_t *st =
      (set_iterator_state_t *)(uintptr_t)js_getnum(vm->stack[vm->sp - 3]);
    if (!st->current) {
      *out_value = js_mkundef();
      *out_done = true;
    } else {
      set_entry_t *entry = st->current;
      ant_value_t value;
      if (st->type == ITER_TYPE_SET_ENTRIES) {
        ant_value_t pair = js_mkarr(js);
        js_arr_push(js, pair, entry->value);
        js_arr_push(js, pair, entry->value);
        value = pair;
      } else {
        value = entry->value;
      }
      st->current = entry->hh.next;
      *out_value = value;
      *out_done = false;
    }
    return tov(0);
  }

  case SV_ITER_STRING: {
    ant_value_t str = vm->stack[vm->sp - 3];
    int idx = (int)js_getnum(vm->stack[vm->sp - 2]);
    ant_offset_t slen = str_len_fast(js, str);
    if (idx >= (int)slen) {
      *out_value = js_mkundef();
      *out_done = true;
    } else {
      ant_offset_t off = vstr(js, str, NULL);
      utf8proc_int32_t cp;
      ant_offset_t cb_len = (ant_offset_t)utf8_next(
        (const utf8proc_uint8_t *)(uintptr_t)(off + idx),
        (utf8proc_ssize_t)(slen - idx),
        &cp
      );
      *out_value = js_mkstr(js, (const void *)(uintptr_t)(off + idx), cb_len);
      *out_done = false;
      vm->stack[vm->sp - 2] = tov(idx + (int)cb_len);
    }
    return tov(0);
  }

  default: {
    ant_value_t next_method = vm->stack[vm->sp - 2];
    ant_value_t iterator = vm->stack[vm->sp - 3];
    if (!is_callable(next_method))
      return js_mkerr(js, "iterator.next is not a function");
    ant_value_t result = sv_vm_call(vm, js, next_method, iterator, NULL, 0, NULL, false);
    if (is_err(result)) return result;
    if (!is_object_type(result))
      return js_mkerr_typed(js, JS_ERR_TYPE, "Iterator result is not an object");
    ant_value_t done = js_mkundef();
    sv_iter_result_unpack(js, result, &done, out_value);
    if (is_err(done)) return done;
    if (is_err(*out_value)) return *out_value;
    *out_done = js_truthy(js, done);
    return tov(0);
  }}
}

static inline ant_value_t sv_op_iter_next(sv_vm_t *vm, ant_t *js, uint8_t *ip) {
  int hint = (int)sv_get_u8(ip + 1);
  ant_value_t value;
  bool done = false;
  
  ant_value_t status = sv_iter_advance(vm, js, hint, &value, &done);
  if (is_err(status)) return status;
  vm->stack[vm->sp++] = value;
  vm->stack[vm->sp++] = done ? js_true : js_false;
  
  return tov(0);
}

static inline void sv_op_iter_get_value(sv_vm_t *vm, ant_t *js) {
  ant_value_t obj = vm->stack[--vm->sp];
  ant_value_t done = js_mkundef();
  ant_value_t value = js_mkundef();
  sv_iter_result_unpack(js, obj, &done, &value);
  vm->stack[vm->sp++] = value;
  vm->stack[vm->sp++] = mkval(T_BOOL, js_truthy(js, done));
}

static inline void sv_op_iter_close(sv_vm_t *vm, ant_t *js) {
  int tag = (int)js_getnum(vm->stack[vm->sp - 1]);
  if (tag == SV_ITER_GENERIC) {
    ant_value_t iterator = vm->stack[vm->sp - 3];
    ant_value_t return_fn = js_getprop_fallback(js, iterator, "return");
    if (is_callable(return_fn))
      sv_vm_call(vm, js, return_fn, iterator, NULL, 0, NULL, false);
  }
  vm->sp -= 3;
}

static inline ant_value_t sv_op_destructure_init(sv_vm_t *vm, ant_t *js) {
  return sv_op_for_of(vm, js);
}

static inline void sv_op_destructure_close(sv_vm_t *vm, ant_t *js) {
  sv_op_iter_close(vm, js);
}

static inline ant_value_t sv_op_destructure_next(sv_vm_t *vm, ant_t *js) {
  ant_value_t value;
  bool done = false;
  
  ant_value_t status = sv_iter_advance(vm, js, 0, &value, &done);
  if (is_err(status)) return status;
  vm->stack[vm->sp++] = done ? js_mkundef() : value;
  
  return tov(0);
}

static inline ant_value_t sv_op_destructure_rest(sv_vm_t *vm, ant_t *js) {
  ant_value_t rest = js_mkarr(js);
  for (;;) {
    ant_value_t value;
    bool done = false;
    ant_value_t status = sv_iter_advance(vm, js, 0, &value, &done);
    if (is_err(status)) return status;
    if (done) break;
    js_arr_push(js, rest, value);
  }
  vm->stack[vm->sp++] = rest;
  return tov(0);
}

static inline ant_value_t sv_op_iter_call(sv_vm_t *vm, ant_t *js, uint8_t *ip) {
  ant_value_t method = vm->stack[vm->sp - 1];
  ant_value_t iterator = vm->stack[vm->sp - 4];
  if (!is_callable(method))
    return js_mkerr(js, "iterator method is not callable");
  ant_value_t result = sv_vm_call(vm, js, method, iterator, NULL, 0, NULL, false);
  if (is_err(result)) return result;
  vm->stack[vm->sp++] = result;
  return tov(0);
}

static inline sv_await_result_t sv_op_await_iter_next(sv_vm_t *vm, ant_t *js) {
  sv_await_result_t out = {
    .state = SV_AWAIT_READY,
    .value = js_mkundef(),
    .handoff = false,
  };
  ant_value_t next_method = vm->stack[vm->sp - 2];
  ant_value_t iterator = vm->stack[vm->sp - 3];
  if (!is_callable(next_method))
    return (sv_await_result_t){ .state = SV_AWAIT_ERROR, .value = js_mkerr(js, "iterator.next is not a function"), .handoff = false };
  ant_value_t result = sv_vm_call(vm, js, next_method, iterator, NULL, 0, NULL, false);
  if (is_err(result))
    return (sv_await_result_t){ .state = SV_AWAIT_ERROR, .value = result, .handoff = false };
  if (vtype(result) == T_PROMISE) {
    vm->suspended_entry_fp = vm->fp;
    vm->suspended_saved_fp = vm->fp - 1;
    sv_await_result_t awaited = sv_await_value(vm, js, result);
    if (awaited.state != SV_AWAIT_SUSPENDED || awaited.handoff) {
      vm->suspended_entry_fp = -1;
      vm->suspended_saved_fp = -1;
    }
    if (awaited.state != SV_AWAIT_READY) return awaited;
    result = awaited.value;
  }
  ant_value_t done = js_mkundef();
  ant_value_t value = js_mkundef();
  sv_iter_result_unpack(js, result, &done, &value);
  if (is_err(done))
    return (sv_await_result_t){ .state = SV_AWAIT_ERROR, .value = done, .handoff = false };
  if (is_err(value))
    return (sv_await_result_t){ .state = SV_AWAIT_ERROR, .value = value, .handoff = false };
  if (vtype(value) == T_PROMISE) {
    vm->suspended_entry_fp = vm->fp;
    vm->suspended_saved_fp = vm->fp - 1;
    sv_await_result_t awaited_val = sv_await_value(vm, js, value);
    if (awaited_val.state != SV_AWAIT_SUSPENDED || awaited_val.handoff) {
      vm->suspended_entry_fp = -1;
      vm->suspended_saved_fp = -1;
    }
    if (awaited_val.state != SV_AWAIT_READY) return awaited_val;
    value = awaited_val.value;
  }
  vm->stack[vm->sp++] = value;
  vm->stack[vm->sp++] = mkval(T_BOOL, js_truthy(js, done));
  return out;
}

#endif
