#include "php.h"

#include "php_msgpack.h"
#include "msgpack_convert.h"
#include "msgpack_errors.h"

#if (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION < 3)
#   define Z_REFCOUNT_P(pz)    ((pz)->refcount)
#   define Z_SET_ISREF_P(pz)   (pz)->is_ref = 1
#   define Z_UNSET_ISREF_P(pz) (pz)->is_ref = 0
#endif

#define MSGPACK_CONVERT_COPY_ZVAL(_pz, _ppz)   \
    ALLOC_INIT_ZVAL(_pz);                      \
    *(_pz) = **(_ppz);                         \
    if (PZVAL_IS_REF(*(_ppz))) {               \
        if (Z_REFCOUNT_P(*(_ppz)) > 0) {       \
            zval_copy_ctor(_pz);               \
        } else {                               \
            FREE_ZVAL(*(_ppz));                \
        }                                      \
        INIT_PZVAL(_pz);                       \
        Z_SET_ISREF_P(_pz);                    \
    } else {                                   \
        zval_copy_ctor(_pz);                   \
        INIT_PZVAL(_pz);                       \
    }

#define MSGPACK_CONVERT_UPDATE_PROPERTY(_ht, _key, _key_len, _val, _var)  \
    if (zend_symtable_update(                                             \
            _ht, _key, _key_len, &_val, sizeof(_val), NULL) == SUCCESS) { \
        zend_hash_add(_var, _key, _key_len, &_val, sizeof(_val), NULL);   \
        return SUCCESS;                                                   \
    }

inline int msgpack_convert_long_to_properties(
    HashTable *ht, HashTable **properties, HashPosition *prop_pos,
    uint key_index, zval *val, HashTable *var)
{
    if (*properties != NULL)
    {
        char *prop_key;
        uint prop_key_len;
        ulong prop_key_index;

        for (;; zend_hash_move_forward_ex(*properties, prop_pos))
        {
            if (zend_hash_get_current_key_ex(
                    *properties, &prop_key, &prop_key_len,
                    &prop_key_index, 0, prop_pos) == HASH_KEY_IS_STRING)
            {
                if (var == NULL ||
                    !zend_hash_exists(var, prop_key, prop_key_len))
                {
                    zend_hash_move_forward_ex(*properties, prop_pos);
                    return zend_symtable_update(
                        ht, prop_key, prop_key_len,
                        &val, sizeof(val), NULL);
                }
            }
            else
            {
                break;
            }
        }

        *properties = NULL;
    }

    return zend_hash_index_update(ht, key_index, &val, sizeof(val), NULL);
}

inline int msgpack_convert_string_to_properties(
    zval *object, char *key, uint key_len, zval *val, HashTable *var)
{
    zval **data = NULL;
    HashTable *ht;
    zend_class_entry *ce;
    char *prot_name, *priv_name;
    int prop_name_len;
    TSRMLS_FETCH();

    ht = HASH_OF(object);
    ce = zend_get_class_entry(object TSRMLS_CC);

    /* private */
    zend_mangle_property_name(
        &priv_name, &prop_name_len, ce->name, ce->name_length, key, key_len, 1);
    if (zend_hash_find(
            ht, priv_name, prop_name_len, (void **)&data) == SUCCESS)
    {
        MSGPACK_CONVERT_UPDATE_PROPERTY(ht, priv_name, prop_name_len, val, var);
    }

    /* protected */
    zend_mangle_property_name(
        &prot_name, &prop_name_len, "*", 1, key, key_len, 1);
    if (zend_hash_find(
            ht, prot_name, prop_name_len, (void **)&data) == SUCCESS)
    {
        MSGPACK_CONVERT_UPDATE_PROPERTY(ht, prot_name, prop_name_len, val, var);
    }

    /* public */
    MSGPACK_CONVERT_UPDATE_PROPERTY(ht, key, key_len, val, var);

    return FAILURE;
}

int msgpack_convert_object(zval *return_value, zval *object, zval **value)
{
    zend_class_entry *ce, **pce;
    HashTable *properties = NULL;
    HashPosition prop_pos;
    TSRMLS_FETCH();

    switch (Z_TYPE_P(object))
    {
        case IS_STRING:
            if (zend_lookup_class(
                    Z_STRVAL_P(object), Z_STRLEN_P(object),
                    &pce TSRMLS_CC) != SUCCESS)
            {
                MSGPACK_ERROR("[msgpack] (%s) Class '%s' not found",
                              __FUNCTION__, Z_STRVAL_P(object));
                return FAILURE;
            }
            ce = *pce;
            break;
        case IS_OBJECT:
            ce = zend_get_class_entry(object TSRMLS_CC);
            break;
        default:
            MSGPACK_ERROR("[msgpack] (%s) Object type is unsupported",
                          __FUNCTION__);
            return FAILURE;
    }

    if (Z_TYPE_PP(value) == IS_OBJECT)
    {
        zend_class_entry *vce;

        vce = zend_get_class_entry(*value TSRMLS_CC);
        if (strcmp(ce->name, vce->name) == 0)
        {
            *return_value = **value;
            zval_copy_ctor(return_value);
            zval_ptr_dtor(value);
            return SUCCESS;
        }
    }

    object_init_ex(return_value, ce);
    properties = Z_OBJ_HT_P(return_value)->get_properties(
        return_value TSRMLS_CC);
    if (HASH_OF(object))
    {
        properties = HASH_OF(object);
    }
    zend_hash_internal_pointer_reset_ex(properties, &prop_pos);

    switch (Z_TYPE_PP(value))
    {
        case IS_ARRAY:
        {
            char *key;
            uint key_len;
            int key_type;
            ulong key_index;
            zval **data;
            HashPosition pos;
            HashTable *ht, *ret;
            HashTable *var = NULL;
            int num;

            ht = HASH_OF(*value);
            ret = HASH_OF(return_value);

            num = zend_hash_num_elements(ht);
            if (num <= 0)
            {
                zval_ptr_dtor(value);
                break;
            }

            ALLOC_HASHTABLE(var);
            zend_hash_init(var, num, NULL, NULL, 0);

            /* string */
            if (ht->nNumOfElements != ht->nNextFreeElement)
            {
                zend_hash_internal_pointer_reset_ex(ht, &pos);
                for (;; zend_hash_move_forward_ex(ht, &pos))
                {
                    key_type = zend_hash_get_current_key_ex(
                        ht, &key, &key_len, &key_index, 0, &pos);

                    if (key_type == HASH_KEY_NON_EXISTANT)
                    {
                        break;
                    }

                    if (zend_hash_get_current_data_ex(
                            ht, (void *)&data, &pos) != SUCCESS)
                    {
                        continue;
                    }

                    if (key_type == HASH_KEY_IS_STRING)
                    {
                        zval *val;
                        MSGPACK_CONVERT_COPY_ZVAL(val, data);
                        if (msgpack_convert_string_to_properties(
                                return_value, key, key_len, val, var) != SUCCESS)
                        {
                            zval_ptr_dtor(&val);
                            MSGPACK_WARNING(
                                "[msgpack] (%s) "
                                "illegal offset type, skip this decoding",
                                __FUNCTION__);
                        }
                    }
                }
            }

            /* index */
            zend_hash_internal_pointer_reset_ex(ht, &pos);
            for (;; zend_hash_move_forward_ex(ht, &pos))
            {
                key_type = zend_hash_get_current_key_ex(
                    ht, &key, &key_len, &key_index, 0, &pos);

                if (key_type == HASH_KEY_NON_EXISTANT)
                {
                    break;
                }

                if (zend_hash_get_current_data_ex(
                        ht, (void *)&data, &pos) != SUCCESS)
                {
                    continue;
                }

                switch (key_type)
                {
                    case HASH_KEY_IS_LONG:
                    {
                        zval *val;
                        MSGPACK_CONVERT_COPY_ZVAL(val, data);
                        if (msgpack_convert_long_to_properties(
                                ret, &properties, &prop_pos,
                                key_index, val, var) != SUCCESS)
                        {
                            zval_ptr_dtor(&val);
                            MSGPACK_WARNING(
                                "[msgpack] (%s) "
                                "illegal offset type, skip this decoding",
                                __FUNCTION__);
                        }
                        break;
                    }
                    case HASH_KEY_IS_STRING:
                        break;
                    default:
                        MSGPACK_WARNING(
                            "[msgpack] (%s) key is not string nor array",
                            __FUNCTION__);
                        break;
                }
            }

            zend_hash_destroy(var);
            FREE_HASHTABLE(var);

            zval_ptr_dtor(value);
            break;
        }
        default:
            if (msgpack_convert_long_to_properties(
                    HASH_OF(return_value), &properties, &prop_pos,
                    0, *value, NULL) != SUCCESS)
            {
                MSGPACK_WARNING(
                    "[msgpack] (%s) illegal offset type, skip this decoding",
                    __FUNCTION__);
            }
            break;
    }

    return SUCCESS;
}