#ifndef DS_HTABLE_H
#define DS_HTABLE_H

#include "../common.h"

#define DS_HTABLE_MIN_CAPACITY  8  // Must be a power of 2

/**
 * Marker to indicate an invalid index in the buffer.
 */
#define DS_HTABLE_INVALID_INDEX ((uint32_t) -1)

/**
 * Determines the calculated hash of a bucket, before mod. We're using the key's
 * zval uint32_t "next" to store this value so that we don't need another member
 * in the bucket struct.
 */
#define DS_HTABLE_BUCKET_HASH(_bucket) (Z_NEXT((_bucket)->key))

/**
 * Determines the buffer index of the next bucket in the collision chain.
 * An invalid index indicates that it's the last bucket in the chain.
 */
#define DS_HTABLE_BUCKET_NEXT(_bucket) (Z_NEXT((_bucket)->value))

/**
 * Determines if a bucket has been deleted.
 */
#define DS_HTABLE_BUCKET_DELETED(_bucket) (Z_ISUNDEF((_bucket)->key))

/**
 * Finds the start of the collision chain for a given hash.
 * An invalid index indicates that a chain doesn't exist.
 */
#define DS_HTABLE_BUCKET_LOOKUP(t, h) ((t)->lookup[h & ((t)->capacity - 1)])

/**
 * Determines if a table is packed, ie. doesn't have deleted buckets.
 */
#define DS_HTABLE_IS_PACKED(t) ((t)->size == (t)->next)

/**
 * Rehashes a bucket into a table.
 *
 * 1. Determine where the bucket's chain would start.
 * 2. Set the bucket's next bucket to be the start of the chain.
 * 3. Set the start of the chain to the bucket's position in the buffer.
 *
 * This means that the next bucket can come before another in the buffer,
 * because a rehash unshifts the bucket into the chain.
 */
#define DS_HTABLE_BUCKET_REHASH(table, bucket, mask, idx)   \
do {                                                        \
    ds_htable_bucket_t *_bucket = bucket;                   \
    ds_htable_t        *_table  = table;                    \
                                                            \
    uint32_t  hash = DS_HTABLE_BUCKET_HASH(_bucket);        \
    uint32_t *head = &_table->lookup[hash & (mask)];        \
                                                            \
    DS_HTABLE_BUCKET_NEXT(_bucket) = *head;                 \
    *head = idx;                                            \
} while (0)

/**
 * Copies a bucket's state into another, including: key, value, hash and next.
 */
#define DS_HTABLE_BUCKET_COPY(dst, src)                         \
do {                                                            \
    ds_htable_bucket_t *_src = src;                             \
    ds_htable_bucket_t *_dst = dst;                             \
    ZVAL_COPY(&_dst->key, &_src->key);                          \
    ZVAL_COPY(&_dst->value, &_src->value);                      \
    DS_HTABLE_BUCKET_NEXT(_dst) = DS_HTABLE_BUCKET_NEXT(_src);  \
    DS_HTABLE_BUCKET_HASH(_dst) = DS_HTABLE_BUCKET_HASH(_src);  \
} while (0)

/**
 * Marks a bucket as deleted, destructing both the key and the value.
 */
#define DS_HTABLE_BUCKET_DELETE(b)                          \
    DTOR_AND_UNDEF(&(b)->value);                            \
    DTOR_AND_UNDEF(&(b)->key);                              \
    DS_HTABLE_BUCKET_NEXT((b)) = DS_HTABLE_INVALID_INDEX

#define DS_HTABLE_FOREACH_BUCKET(h, b)                  \
do {                                                    \
    ds_htable_t *_h = h;                                \
    ds_htable_bucket_t *_x = _h->buckets;               \
    ds_htable_bucket_t *_y = _h->buckets + _h->next;    \
    for (; _x < _y; ++_x) {                             \
        if (DS_HTABLE_BUCKET_DELETED(_x)) continue;     \
        b = _x;

#define DS_HTABLE_FOREACH_BUCKET_REVERSED(h, b)         \
do {                                                    \
    ds_htable_t *_h  = h;                               \
    ds_htable_bucket_t *_x = _h->buckets;               \
    ds_htable_bucket_t *_y = _x + _h->next - 1;         \
    for (; _y >= _x; --_y) {                            \
        if (DS_HTABLE_BUCKET_DELETED(_y)) continue;     \
        b = _y;

#define DS_HTABLE_FOREACH(h, i, k, v)                   \
do {                                                    \
    uint32_t _i;                                        \
    uint32_t _n = (h)->size;                            \
    ds_htable_bucket_t *_b = (h)->buckets;              \
                                                        \
    for (_i = 0; _i < _n; ++_b) {                       \
        if (DS_HTABLE_BUCKET_DELETED(_b)) continue;     \
        k = &_b->key;                                   \
        v = &_b->value;                                 \
        i = _i++;

#define DS_HTABLE_FOREACH_KEY(h, k)                     \
do {                                                    \
    ds_htable_t *_h = h;                                \
    ds_htable_bucket_t *_x = _h->buckets;               \
    ds_htable_bucket_t *_y = _h->buckets + _h->next;    \
    for (; _x < _y; ++_x) {                             \
        if (DS_HTABLE_BUCKET_DELETED(_x)) continue;     \
        k = &_x->key;

#define DS_HTABLE_FOREACH_VALUE(h, v)                   \
do {                                                    \
    ds_htable_t *_h = h;                                \
    ds_htable_bucket_t *_x = _h->buckets;               \
    ds_htable_bucket_t *_y = _h->buckets + _h->next;    \
    for (; _x < _y; ++_x) {                             \
        if (DS_HTABLE_BUCKET_DELETED(_x)) continue;     \
        v = &_x->value;

#define DS_HTABLE_FOREACH_KEY_VALUE(h, k, v)    \
do {                                                    \
    ds_htable_t *_h = h;                                \
    ds_htable_bucket_t *_x = _h->buckets;               \
    ds_htable_bucket_t *_y = _h->buckets + _h->next;    \
    for (; _x < _y; ++_x) {                             \
        if (DS_HTABLE_BUCKET_DELETED(_x)) continue;     \
        k = &_x->key;                                   \
        v = &_x->value;

#define DS_HTABLE_FOREACH_END() \
    }                           \
} while (0)

typedef struct _ds_htable_bucket_t {
    zval key;
    zval value;
} ds_htable_bucket_t;

typedef struct _ds_htable_t {
    ds_htable_bucket_t  *buckets;       // Buffer for the buckets
    uint32_t            *lookup;        // Separated hash lookup table
    uint32_t             next;          // Next open index in the bucket buffer
    uint32_t             size;          // Number of active buckets in the table
    uint32_t             capacity;      // Length of the bucket buffer
    uint32_t             min_deleted;   // First deleted bucket buffer index
} ds_htable_t;

ds_htable_t *ds_htable();
zval *ds_htable_values(ds_htable_t *table);

void ds_htable_ensure_capacity(ds_htable_t *table, uint32_t capacity);

void ds_htable_sort(ds_htable_t *table, compare_func_t compare_func);
void ds_htable_sort_by_key(ds_htable_t *table);
void ds_htable_sort_by_value(ds_htable_t *table);
void ds_htable_sort_by_pair(ds_htable_t *table);
void ds_htable_sort_callback_by_key(ds_htable_t *table);
void ds_htable_sort_callback_by_value(ds_htable_t *table);

ds_htable_bucket_t *ds_htable_lookup_by_value(ds_htable_t *h, zval *key);
ds_htable_bucket_t *ds_htable_lookup_by_key(ds_htable_t *h, zval *key);
ds_htable_bucket_t *ds_htable_lookup_by_position(ds_htable_t *table, uint32_t position);

bool ds_htable_lookup_or_next(ds_htable_t *table, zval *key, ds_htable_bucket_t **return_value);
bool ds_htable_has_keys(ds_htable_t *h, VA_PARAMS);
bool ds_htable_has_key(ds_htable_t *table, zval *key);
bool ds_htable_has_values(ds_htable_t *h, VA_PARAMS);
bool ds_htable_has_value(ds_htable_t *h, zval *value);
int  ds_htable_remove(ds_htable_t *h, zval *key, zval *return_value);
void ds_htable_put(ds_htable_t *h, zval *key, zval *value);
void ds_htable_to_array(ds_htable_t *h, zval *arr);
void ds_htable_free(ds_htable_t *h);
zval *ds_htable_get(ds_htable_t *h, zval *key);
ds_htable_t *ds_htable_slice(ds_htable_t *table, zend_long index, zend_long length);

void ds_htable_clear(ds_htable_t *h);
bool ds_htable_isset(ds_htable_t *h, zval *key, bool check_empty);
ds_htable_t *ds_htable_clone(ds_htable_t *source);

zend_string *ds_htable_join_keys(ds_htable_t *table, const char* glue, const size_t len);

void ds_htable_reverse(ds_htable_t *table);
ds_htable_t *ds_htable_reversed(ds_htable_t *table);

ds_htable_bucket_t *ds_htable_first(ds_htable_t *table);
ds_htable_bucket_t *ds_htable_last(ds_htable_t *table);

ds_htable_t *ds_htable_map(ds_htable_t *table, FCI_PARAMS);
ds_htable_t *ds_htable_filter(ds_htable_t *table);
ds_htable_t *ds_htable_filter_callback(ds_htable_t *table, FCI_PARAMS);

void ds_htable_apply(ds_htable_t *table, FCI_PARAMS);
void ds_htable_reduce(ds_htable_t *table, FCI_PARAMS, zval *initial, zval *return_value);

ds_htable_t *ds_htable_xor(ds_htable_t *table, ds_htable_t *other);
ds_htable_t *ds_htable_diff(ds_htable_t *table, ds_htable_t *other);
ds_htable_t *ds_htable_intersect(ds_htable_t *table, ds_htable_t *other);
ds_htable_t *ds_htable_merge(ds_htable_t *table, ds_htable_t *other);

int ds_htable_serialize(ds_htable_t *table, unsigned char **buffer, size_t *buf_len, zend_serialize_data *data);
int ds_htable_unserialize(ds_htable_t *table, const unsigned char *buffer, size_t length, zend_unserialize_data *data);

#endif
