#include "../../common.h"

#include "../../ds/ds_htable.h"
#include "php_htable_iterator.h"

static ds_htable_bucket_t *find_starting_bucket(ds_htable_t *table)
{
    ds_htable_bucket_t *bucket = table->buckets;

    if (table->size != 0) {
        ds_htable_bucket_t *last = table->buckets + table->capacity;

        while (bucket != last && DS_HTABLE_BUCKET_DELETED(bucket)) {
            ++bucket;
        }
    }

    return bucket;
}

static void php_ds_htable_iterator_dtor(zend_object_iterator *iter)
{
    ds_htable_iterator_t *iterator = (ds_htable_iterator_t *) iter;

    OBJ_RELEASE(iterator->obj);
    DTOR_AND_UNDEF(&iterator->intern.data);
}

static int php_ds_htable_iterator_valid(zend_object_iterator *iter)
{
    ds_htable_iterator_t *iterator = (ds_htable_iterator_t *) iter;
    uint32_t size                  = iterator->table->size;
    uint32_t position              = iterator->position;

    return position < size ? SUCCESS : FAILURE;
}

static zval *php_ds_htable_iterator_get_current_value(zend_object_iterator *iter)
{
    ds_htable_iterator_t *iterator = (ds_htable_iterator_t *) iter;
    ds_htable_bucket_t   *bucket   = iterator->bucket;

    if ( ! DS_HTABLE_BUCKET_DELETED(bucket)) {
        return &bucket->value;
    }

    return NULL;
}

static zval *php_ds_htable_iterator_get_current_keyval(zend_object_iterator *iter)
{
    ds_htable_iterator_t *iterator = (ds_htable_iterator_t *) iter;
    ds_htable_bucket_t   *bucket   = iterator->bucket;

    if ( ! DS_HTABLE_BUCKET_DELETED(bucket)) {
        return &bucket->key;
    }

    return NULL;
}

static void php_ds_htable_iterator_get_current_key(zend_object_iterator *iter, zval *key)
{
    ds_htable_iterator_t *iterator = (ds_htable_iterator_t *) iter;
    ds_htable_bucket_t   *bucket   = iterator->bucket;

    if ( ! DS_HTABLE_BUCKET_DELETED(bucket)) {
        ZVAL_COPY(key, &bucket->key);
    }
}

static zval *php_ds_htable_iterator_get_current_pair(zend_object_iterator *iter)
{
    ds_htable_iterator_t *iterator = (ds_htable_iterator_t *) iter;
    ds_htable_bucket_t   *bucket   = iterator->bucket;

    if ( ! DS_HTABLE_BUCKET_DELETED(bucket)) {

        zval *key = &bucket->key;
        zval *val = &bucket->value;

        zval *arr = &iterator->intern.data;

        Z_TRY_ADDREF_P(key);
        Z_TRY_ADDREF_P(val);

        array_init_size(arr, 2);

        add_next_index_zval(arr, key);
        add_next_index_zval(arr, val);

        return arr;
    }

    return NULL;
}

static void php_ds_htable_iterator_get_current_pos(zend_object_iterator *iter, zval *key)
{
    ZVAL_LONG(key, ((ds_htable_iterator_t *) iter)->position);
}

static void php_ds_htable_iterator_move_forward(zend_object_iterator *iter)
{
    ds_htable_iterator_t *iterator = (ds_htable_iterator_t *) iter;

    if (++iterator->position < iterator->table->size) {
        do {
            ++iterator->bucket;
        } while (DS_HTABLE_BUCKET_DELETED(iterator->bucket));
    }
}

static void php_ds_htable_iterator_rewind(zend_object_iterator *iter)
{
    ds_htable_iterator_t *iterator = (ds_htable_iterator_t *) iter;

    iterator->position = 0;
    iterator->bucket   = find_starting_bucket(iterator->table);
}

static zend_object_iterator_funcs php_ds_htable_get_value_iterator_funcs = {
    php_ds_htable_iterator_dtor,
    php_ds_htable_iterator_valid,
    php_ds_htable_iterator_get_current_value, // value
    php_ds_htable_iterator_get_current_pos,   // key
    php_ds_htable_iterator_move_forward,
    php_ds_htable_iterator_rewind
};

static zend_object_iterator_funcs php_ds_htable_get_key_iterator_funcs = {
    php_ds_htable_iterator_dtor,
    php_ds_htable_iterator_valid,
    php_ds_htable_iterator_get_current_keyval, // value
    php_ds_htable_iterator_get_current_pos,    // key
    php_ds_htable_iterator_move_forward,
    php_ds_htable_iterator_rewind
};

static zend_object_iterator_funcs php_ds_htable_get_pair_iterator_funcs = {
    php_ds_htable_iterator_dtor,
    php_ds_htable_iterator_valid,
    php_ds_htable_iterator_get_current_pair, // value
    php_ds_htable_iterator_get_current_pos,  // key
    php_ds_htable_iterator_move_forward,
    php_ds_htable_iterator_rewind
};

static zend_object_iterator_funcs php_ds_htable_get_assoc_iterator_funcs = {
    php_ds_htable_iterator_dtor,
    php_ds_htable_iterator_valid,
    php_ds_htable_iterator_get_current_value, // value
    php_ds_htable_iterator_get_current_key,   // key
    php_ds_htable_iterator_move_forward,
    php_ds_htable_iterator_rewind
};


static zend_object_iterator *php_ds_htable_create_htable_iterator(
    zval *obj,
    ds_htable_t *table,
    zend_object_iterator_funcs *funcs,
    int by_ref
) {
    ds_htable_iterator_t *iterator;

    if (by_ref) {
        ITERATION_BY_REF_NOT_SUPPORTED();
        return NULL;
    }

    iterator = ecalloc(1, sizeof(ds_htable_iterator_t));

    zend_iterator_init((zend_object_iterator*) iterator);

    ZVAL_UNDEF(&iterator->intern.data);

    iterator->intern.funcs  = funcs;
    iterator->table         = table;
    iterator->obj           = Z_OBJ_P(obj);

    // Add a reference to the object so that it doesn't get collected when
    // the iterated object is implict, eg. foreach ($obj->getInstance() as $value){ ... }
#if PHP_VERSION_ID >= 70300
    GC_ADDREF(iterator->obj);
#else
    ++GC_REFCOUNT(iterator->obj);
#endif

    return (zend_object_iterator *) iterator;
}

zend_object_iterator *php_ds_htable_get_value_iterator_ex(
    zend_class_entry *ce,
    zval *obj,
    int by_ref,
    ds_htable_t *table
){
    return php_ds_htable_create_htable_iterator(
        obj, table, &php_ds_htable_get_value_iterator_funcs, by_ref);
}

zend_object_iterator *php_ds_htable_get_key_iterator_ex(
    zend_class_entry *ce,
    zval *obj,
    int by_ref,
    ds_htable_t *table
){
    return php_ds_htable_create_htable_iterator(
        obj, table, &php_ds_htable_get_key_iterator_funcs, by_ref);
}

zend_object_iterator *php_ds_htable_get_pair_iterator_ex(
    zend_class_entry *ce,
    zval *obj,
    int by_ref,
    ds_htable_t *table
){
    return php_ds_htable_create_htable_iterator(
        obj, table, &php_ds_htable_get_pair_iterator_funcs, by_ref);
}

zend_object_iterator *php_ds_htable_get_assoc_iterator_ex(
    zend_class_entry *ce,
    zval *obj,
    int by_ref,
    ds_htable_t *table
){
    return php_ds_htable_create_htable_iterator(
        obj, table, &php_ds_htable_get_assoc_iterator_funcs, by_ref);
}
