#ifndef DS_COMMON_H
#define DS_COMMON_H

#include "php.h"
#include "zend_exceptions.h"
#include "ext/standard/php_var.h"
#include "ext/spl/spl_exceptions.h"
#include "zend_smart_str.h"
#include "../php_ds.h"

/**
 * Used for consistent naming when working with callables.
 */
#define FCI_PARAMS zend_fcall_info fci, zend_fcall_info_cache fci_cache
#define FCI_ARGS   fci, fci_cache

/**
 * Used for consistent naming when working with variadics.
 */
#define VA_PARAMS zend_long argc, zval *argv
#define VA_ARGS   argc, argv

/**
 * Default namespace.
 */
#define PHP_DS_NS_NAME "Ds\\"
#define PHP_DS_NS(cls) PHP_DS_NS_NAME #cls

/**
 * This is useful for functions that require the string and its length.
 */
#define STR_AND_LEN(str) str, sizeof(str) - 1

/**
 * Slightly faster check when the given zval is probably a boolean.
 */
#define EXPECTED_BOOL_IS_TRUE(z) (Z_TYPE_P(z) != IS_FALSE && zend_is_true(z))

/**
 * Combined class, name, and arginfo method entry.
 */
#define PHP_DS_ME(cls, name) \
    PHP_ME(cls, name, arginfo_##cls##_##name, ZEND_ACC_PUBLIC)

/**
 *
 */
#define DTOR_AND_UNDEF(z)           \
do {                                \
    zval *_z = z;                   \
    if (_z && ! Z_ISUNDEF_P(_z)) {  \
        zval_ptr_dtor(_z);          \
        ZVAL_UNDEF(_z);             \
    }                               \
} while (0)

/**
 *
 */
#define SET_AS_RETURN_AND_UNDEF(z)          \
do {                                        \
    zval *_z = z;                           \
    if (return_value) {                     \
        ZVAL_COPY_VALUE(return_value, _z);  \
        ZVAL_UNDEF(_z);                     \
    } else {                                \
        if ( ! Z_ISUNDEF_P(_z)) {           \
            zval_ptr_dtor(_z);              \
            ZVAL_UNDEF(_z);                 \
        }                                   \
    }                                       \
} while (0)

/**
 * Swaps two zval's.
 */
#define SWAP_ZVAL(a, b) \
do {                    \
    zval _t = a;        \
    a = b;              \
    b = _t;             \
} while (0)

/**
 * Adds the given zval "val" to "sum".
 */
#if PHP_MAJOR_VERSION < 8 || PHP_MAJOR_VERSION == 8 && PHP_MINOR_VERSION < 3
#define DS_ADD_TO_SUM(val, sum)                                         \
do {                                                                    \
    if (Z_TYPE_P(val) == IS_LONG || Z_TYPE_P(val) == IS_DOUBLE) {       \
        fast_add_function(sum, sum, val);                               \
    } else {                                                            \
        zval _num;                                                      \
        ZVAL_COPY(&_num, val);                                          \
        convert_scalar_to_number(&_num);                                \
        fast_add_function(sum, sum, &_num);                             \
    }                                                                   \
} while (0)
#else
#define DS_ADD_TO_SUM(val, sum)                                         \
do {                                                                    \
    if (Z_TYPE_P(val) == IS_LONG || Z_TYPE_P(val) == IS_DOUBLE) {       \
        add_function(sum, sum, val);                                    \
    } else {                                                            \
        zval _num;                                                      \
        ZVAL_COPY(&_num, val);                                          \
        convert_scalar_to_number(&_num);                                \
        add_function(sum, sum, &_num);                                  \
    }                                                                   \
} while (0)
#endif

/**
 * Used to replace a buffer with a new one.
 */
#define FREE_AND_REPLACE(ptr, repl) \
do {                                \
    void *_repl = repl;             \
    efree(ptr);                     \
    ptr = _repl;                    \
} while (0)

/**
 * Copies 'len' values from 'src' into 'dst'.
 */
#define COPY_ZVAL_BUFFER(dst, src, len)     \
do {                                        \
    zval *_src = src;                       \
    zval *_dst = dst;                       \
    zval *_end = src + len;                 \
    for (; _src != _end; ++_src, ++_dst) {  \
        ZVAL_COPY(_dst, _src);              \
    }                                       \
} while (0)

/**
 * Used to determine if a string zval is equal to a string literal.
 * Eg. ZVAL_EQUALS_STRING(value, "test")
 */
#define ZVAL_EQUALS_STRING(z, s) zend_string_equals_literal(Z_STR_P(z), s)

/**
 * Copies a zval into the return_value.
 */
#define RETURN_ZVAL_COPY(z)          \
do {                                 \
    zval *_z = z;                    \
    if (_z) {                        \
        ZVAL_COPY(return_value, _z); \
    }                                \
    return;                          \
} while (0)

#define SERIALIZE_SET_ZSTR(s)                                           \
    *buffer = (unsigned char *) estrndup(ZSTR_VAL((s)), ZSTR_LEN((s))); \
    *length = ZSTR_LEN((s));

#define PHP_DS_SERIALIZE_FUNCIONS(name) \
int name##_serialize(                   \
    zval                    *object,    \
    unsigned char          **buffer,    \
    size_t                  *length,    \
    zend_serialize_data     *data       \
);                                      \
int name##_unserialize(                 \
    zval                    *object,    \
    zend_class_entry        *ce,        \
    const unsigned char     *buffer,    \
    size_t                   length,    \
    zend_unserialize_data   *data       \
)

/** EXCEPTIONS **************************************************************/

#define ARRAY_ACCESS_BY_KEY_NOT_SUPPORTED() ds_throw_exception( \
    zend_ce_error, \
    "Array access by key is not supported")

#define INDEX_OUT_OF_RANGE(index, max) ds_throw_exception( \
    spl_ce_OutOfRangeException, \
    max == 0 \
        ? "Index out of range: %d" \
        : "Index out of range: %d, expected 0 <= x <= %d", \
    index, \
    max - 1)

#define OFFSET_OUT_OF_BOUNDS() ds_throw_exception( \
    spl_ce_OutOfBoundsException, \
    "Offset out of bounds")

#define ARRAY_ACCESS_PUSH_NOT_SUPPORTED() ds_throw_exception( \
    spl_ce_OutOfBoundsException, \
    "Array access push syntax is not supported")

#define KEY_NOT_FOUND() ds_throw_exception( \
    spl_ce_OutOfBoundsException, \
    "Key not found")

#define OBJ_HASH_MUST_BE_SCALAR(z) ds_throw_exception( \
    spl_ce_UnexpectedValueException, \
    "Object hash must be scalar, %s given", zend_get_type_by_const(Z_TYPE_P(z)))

#define VALUE_MUST_BE_INTEGER(z) ds_throw_exception( \
    spl_ce_UnexpectedValueException, \
    "Value must be of type integer, %d given", zend_get_type_by_const(Z_TYPE_P(z)))

#define NOT_ALLOWED_WHEN_EMPTY() ds_throw_exception( \
    spl_ce_UnderflowException, \
    "Unexpected empty state")

#define ARRAY_OR_TRAVERSABLE_REQUIRED() ds_throw_exception( \
    spl_ce_InvalidArgumentException, \
    "Value must be an array or traversable object")

#define INTEGER_INDEX_REQUIRED(z) ds_throw_exception( \
    zend_ce_type_error, \
    "Index must be of type integer, %s given", zend_get_type_by_const(Z_TYPE_P(z)))

#define INTEGER_LENGTH_REQUIRED(z) ds_throw_exception( \
    zend_ce_type_error, \
    "Length must be of type integer, %s given", zend_get_type_by_const(Z_TYPE_P(z)))

#define ITERATION_BY_REF_NOT_SUPPORTED() ds_throw_exception( \
    zend_ce_error, \
    "Iterating by reference is not supported")

#define ACCESS_BY_REF_NOT_ALLOWED() ds_throw_exception( \
    zend_ce_error, \
    "Access by reference is not allowed")

#define UNSERIALIZE_ERROR() ds_throw_exception( \
    zend_ce_error, \
    "Failed to unserialize data")

#define RECONSTRUCTION_NOT_ALLOWED() ds_throw_exception( \
    zend_ce_error, \
    "Immutable objects may not be reconstructed")

#define MUTABILITY_NOT_ALLOWED() ds_throw_exception( \
    zend_ce_error, \
    "Immutable objects may not be changed")

// https://bugs.php.net/bug.php?id=80816
#if PHP_VERSION_ID >= 80100
#define spl_ce_Aggregate     zend_ce_aggregate
#define spl_ce_ArrayAccess   zend_ce_arrayaccess
#define spl_ce_Countable     zend_ce_countable
#define spl_ce_Iterator      zend_ce_iterator
#define spl_ce_Serializable  zend_ce_serializable
#define spl_ce_Stringable    zend_ce_stringable
#define spl_ce_Traversable   zend_ce_traversable
#endif

/**
 *
 */
void ds_throw_exception(zend_class_entry *ce, const char *format, ...);

/*****************************************************************************/

/**
 * Returns the next power of 2 greater than or equal to n.
 */
uint32_t ds_next_power_of_2(uint32_t n, uint32_t min);

/**
 * Similar to 'implode', joins a zval buffer using an optional 'glue'.
 * Use NULL and 0 for 'str' and 'len' to indicate an optional glue.
 */
zend_string *ds_join_zval_buffer(
    zval        *buffer,
    zend_long    size,
    char        *str,
    size_t       len
);


/**
 * Normalizes input parameters for slicing so that the implementation can focus
 * on the actual slicing. Takes care of negative values, length > size etc.
 */
void ds_normalize_slice_args(
    zend_long *offset,
    zend_long *length,
    zend_long size
);

/**
 * Allocates a zval buffer of a specified length.
 */
zval *ds_allocate_zval_buffer(zend_long length);

/**
 * Reallocates a zval buffer to a specified length.
 *
 * @param  buffer
 * @param  length The resulting length of the buffer.
 * @param  used   Number of slots currently in use in the given buffer.
 */
zval *ds_reallocate_zval_buffer(zval *buffer, zend_long length, zend_long current, zend_long used);

/**
 * Sorts a zval buffer in place using the default internal compare_func.
 */
void ds_sort_zval_buffer(zval *buffer, zend_long size);

/**
 * Sorts a zval buffer in place using a user-provided, global compare function.
 */
void ds_user_sort_zval_buffer(zval *buffer, zend_long size);

/**
 * Reverses zvals between two ranges, usually a range within a buffer.
 */
void ds_reverse_zval_range(zval *x, zval *y);

/**
 * Determines if a zval is set, ie. 'isset' and 'empty'.
 */
int ds_zval_isset(zval *value, int check_empty);

/**
 * Determines if a zval is an array.
 */
bool ds_is_array(zval *value);

/**
 * Determines if an array uses keys, similar to how json_encode does it.
 */
bool ds_php_array_uses_keys(HashTable *ht);

/**
 * Determines if a zval is an object and implements Traversable.
 */
bool ds_is_traversable(zval *value);

/**
 *
 */
void smart_str_appendz(smart_str *buffer, zval *value);

/**
 *
 */
bool ds_special_is_equal(zval *op1, zval *op2);

#endif
