#include "php.h"

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

static inline int msgpack_convert_long_to_properties(HashTable *ht, zval *object, HashTable **properties, HashPosition *prop_pos, unsigned int key_index, zval *val, HashTable *var) /* {{{ */ {
    zval key_zv;
    HashTable *props = *properties;

    if (props != NULL) {
        zval *data, tplval, *dataval, prop_key_zv;
        zend_string *prop_key;
        zend_ulong prop_key_index;
        const char *class_name, *prop_name;
        size_t prop_len;

        for (;; zend_hash_move_forward_ex(props, prop_pos)) {
            if (zend_hash_get_current_key_ex(props, &prop_key, &prop_key_index, prop_pos) == HASH_KEY_IS_STRING) {
                zend_unmangle_property_name_ex(prop_key, &class_name, &prop_name, &prop_len);
                ZVAL_NEW_STR(&prop_key_zv, prop_key);
                if (var == NULL || !zend_hash_str_exists(var, prop_name, prop_len)) {
                    if ((data = zend_hash_find(ht, prop_key)) != NULL) {
                        switch (Z_TYPE_P(data)) {
                            case IS_ARRAY:
                            {
                                HashTable *dataht;
                                dataht = HASH_OF(val);

                                if ((dataval = zend_hash_index_find(dataht, prop_key_index)) == NULL) {
                                    MSGPACK_WARNING("[msgpack] (%s) "
                                            "can't get data value by index",
                                            __FUNCTION__);
                                    return FAILURE;
                                }

                                if (msgpack_convert_array(&tplval, data, dataval) == SUCCESS) {
                                    zend_hash_move_forward_ex(props, prop_pos);
                                    zend_update_property(Z_OBJCE_P(object), OBJ_FOR_PROP(object), prop_name, prop_len, &tplval);
                                    return SUCCESS;
                                }
                                return FAILURE;
                            }
                            case IS_OBJECT:
                            {
                                if (msgpack_convert_object(&tplval, data, val) == SUCCESS) {
                                    zend_hash_move_forward_ex(props, prop_pos);
                                    zend_update_property(Z_OBJCE_P(object), OBJ_FOR_PROP(object), prop_name, prop_len, &tplval);
                                    return SUCCESS;
                                }
                                return FAILURE;
                            }
                            default:
                                zend_hash_move_forward_ex(props, prop_pos);
                                zend_update_property(Z_OBJCE_P(object), OBJ_FOR_PROP(object), prop_name, prop_len, val);
                            return SUCCESS;
                        }
                    }
                }
            } else {
                break;
            }
        }
        *properties = NULL;
    }
    ZVAL_LONG(&key_zv, key_index);
#if PHP_VERSION_ID < 80000
    zend_std_write_property(object, &key_zv, val, NULL);
#else
    {
        zend_string *key = zval_get_string(&key_zv);
        zend_std_write_property(Z_OBJ_P(object), key, val, NULL);
        zend_string_release(key);
    }
#endif
    return SUCCESS;
}
/* }}} */

static inline int msgpack_convert_string_to_properties(zval *object, zend_string *key, zval *val, HashTable *var)/* {{{ */ {
    zend_class_entry *ce = Z_OBJCE_P(object);
    HashTable *propers = Z_OBJPROP_P(object);
    zend_string *prot_name, *priv_name;
    zval pub_name;
    int return_code;

    ZVAL_STR(&pub_name, key);
    priv_name = zend_mangle_property_name(ZSTR_VAL(ce->name), ZSTR_LEN(ce->name), ZSTR_VAL(key), ZSTR_LEN(key), 1);
    prot_name = zend_mangle_property_name("*", 1, ZSTR_VAL(key), ZSTR_LEN(key), 1);

    if (zend_hash_find(propers, priv_name) != NULL) {
        zend_update_property_ex(ce, OBJ_FOR_PROP(object), key, val);
        return_code = SUCCESS;
    } else if (zend_hash_find(propers, prot_name) != NULL) {
        zend_update_property_ex(ce, OBJ_FOR_PROP(object), key, val);
        return_code = SUCCESS;
    } else {
#if PHP_VERSION_ID < 80000
        zend_std_write_property(object, &pub_name, val, NULL);
#else
        zend_std_write_property(Z_OBJ_P(object), key, val, NULL);
#endif
        return_code = FAILURE;
    }
    zend_hash_add(var, Z_STR(pub_name), val);

    zend_string_release(priv_name);
    zend_string_release(prot_name);

    return return_code;
}
/* }}} */

int msgpack_convert_array(zval *return_value, zval *tpl, zval *value) /* {{{ */ {
    zend_string *key;
    int key_type;
    zend_ulong key_index;
    zval *data;
    HashTable *ht, *htval;

    if (Z_TYPE_P(tpl) != IS_ARRAY) {
        MSGPACK_WARNING("[msgpack] (%s) template is not array", __FUNCTION__);
        return FAILURE;
    }

    if (Z_TYPE_P(value) == IS_INDIRECT) {
        value = Z_INDIRECT_P(value);
    }

    ht = HASH_OF(tpl);
    array_init(return_value);

    if (zend_hash_num_elements(ht) == 0) {
        MSGPACK_WARNING("[msgpack] (%s) template array length is 0", __FUNCTION__);
        return FAILURE;
    }

    /* string */
    if (ht->nNumOfElements != ht->nNextFreeElement) {
        HashPosition valpos;

        htval = HASH_OF(value);

        if (!htval) {
            MSGPACK_WARNING("[msgpack] (%s) input data is not array", __FUNCTION__);
            return FAILURE;
        }

        zend_hash_internal_pointer_reset_ex(htval, &valpos);
        ZEND_HASH_FOREACH_KEY_VAL(ht, key_index, key, data) {
            if (key) {
                zval *dataval;
                int (*convert_function)(zval *, zval *, zval *) = NULL;
                switch (Z_TYPE_P(data)) {
                    case IS_ARRAY:
                        convert_function = msgpack_convert_array;
                        break;
                    case IS_OBJECT:
                        // case IS_STRING:
                        convert_function = msgpack_convert_object;
                        break;
                    default:
                        break;
                }

                if ((dataval = zend_hash_get_current_data_ex(htval, &valpos)) == NULL) {
                    MSGPACK_WARNING("[msgpack] (%s) can't get data", __FUNCTION__);
                    return FAILURE;
                }

                if (Z_TYPE_P(dataval) == IS_INDIRECT) {
                    dataval = Z_INDIRECT_P(dataval);
                }

                if (convert_function) {
                    zval rv;
                    if (convert_function(&rv, data, dataval) != SUCCESS) {
                        return FAILURE;
                    }
                    zend_symtable_update(Z_ARRVAL_P(return_value), key, &rv);
                } else {
                    Z_TRY_ADDREF_P(dataval);
                    zend_symtable_update(Z_ARRVAL_P(return_value), key, dataval);
                }
            }
            zend_hash_move_forward_ex(htval, &valpos);
        } ZEND_HASH_FOREACH_END();

        return SUCCESS;
    } else {
        /* index */
        zval *arydata;
        HashPosition pos;
        int (*convert_function)(zval *, zval *, zval *) = NULL;

        if (Z_TYPE_P(value) != IS_ARRAY) {
            MSGPACK_WARNING("[msgpack] (%s) unserialized data must be array.", __FUNCTION__);
            return FAILURE;
        }

        zend_hash_internal_pointer_reset_ex(ht, &pos);
        key_type = zend_hash_get_current_key_ex(ht, &key, &key_index, &pos);
        if (key_type == HASH_KEY_NON_EXISTENT) {
            MSGPACK_WARNING(
                    "[msgpack] (%s) first element in template array is empty",
                    __FUNCTION__);
            return FAILURE;
        }

        if ((data = zend_hash_get_current_data_ex(ht, &pos)) == NULL) {
            MSGPACK_WARNING("[msgpack] (%s) invalid template: empty array?", __FUNCTION__);
            return FAILURE;
        }

        switch (Z_TYPE_P(data)) {
            case IS_ARRAY:
                convert_function = msgpack_convert_array;
                break;
            case IS_OBJECT:
            case IS_STRING:
                convert_function = msgpack_convert_object;
                break;
            default:
                break;
        }

        htval = HASH_OF(value);
        if (zend_hash_num_elements(htval) == 0) {
            MSGPACK_WARNING("[msgpack] (%s) array length is 0 in unserialized data", __FUNCTION__);
            return FAILURE;
        }

        ZEND_HASH_FOREACH_KEY_VAL_IND(htval, key_index, key, arydata) {
            if (key) {
                MSGPACK_WARNING("[msgpack] (%s) key is string", __FUNCTION__);
                return FAILURE;
            } else {
                zval rv;
                if (convert_function) {
                    if (convert_function(&rv, data, arydata) != SUCCESS) {
                        MSGPACK_WARNING(
                                "[msgpack] (%s) "
                                "convert failure in HASH_KEY_IS_LONG "
                                "in indexed array",
                                __FUNCTION__);
                        return FAILURE;
                    }
                    add_next_index_zval(return_value, &rv);
                } else {
                    Z_TRY_ADDREF_P(arydata);
                    add_next_index_zval(return_value, arydata);
                }
            }
        } ZEND_HASH_FOREACH_END();
        return SUCCESS;
    }

    return FAILURE;
}
/* }}} */

int msgpack_convert_object(zval *return_value, zval *tpl, zval *value) /* {{{ */ {
    zend_class_entry *ce;

    switch (Z_TYPE_P(tpl)) {
        case IS_STRING:
            ce = zend_lookup_class(Z_STR_P(tpl));
            if (ce == NULL) {
                MSGPACK_ERROR("[msgpack] (%s) Class '%s' not found",
                              __FUNCTION__, Z_STRVAL_P(tpl));
                return FAILURE;
            }
            break;
        case IS_OBJECT:
            ce = Z_OBJCE_P(tpl);
            break;
        default:
            MSGPACK_ERROR("[msgpack] (%s) object type is unsupported",
                          __FUNCTION__);
            return FAILURE;
    }

    if (Z_TYPE_P(value) == IS_INDIRECT) {
        value = Z_INDIRECT_P(value);
    }

    if (Z_TYPE_P(value) == IS_OBJECT) {
        zend_class_entry *vce = Z_OBJCE_P(value);
        if (zend_string_equals(ce->name, vce->name)) {
            ZVAL_COPY(return_value, value);
            return SUCCESS;
        }
    }

    object_init_ex(return_value, ce);

    /* Run the constructor if there is one */
    if (ce->constructor && (ce->constructor->common.fn_flags & ZEND_ACC_PUBLIC)) {
        zval retval;
        zend_fcall_info fci;
        zend_fcall_info_cache fcc;

        memset(&fci, 0, sizeof(fci));
        memset(&fcc, 0, sizeof(fcc));

        fci.size = sizeof(fci);
#if PHP_VERSION_ID < 70100
        fci.function_table = EG(function_table);
#endif
        fci.object = Z_OBJ_P(return_value);
        fci.retval = &retval;
#if PHP_VERSION_ID < 80000
        fci.no_separation = 1;
#endif

#if PHP_VERSION_ID < 70300
        fcc.initialized = 1;
#endif
        fcc.function_handler = ce->constructor;
#if PHP_VERSION_ID < 70100
        fcc.calling_scope = EG(scope);
#else
        fcc.calling_scope = zend_get_executed_scope();
#endif
        fcc.called_scope = Z_OBJCE_P(return_value);
        fcc.object = Z_OBJ_P(return_value);

        if (zend_call_function(&fci, &fcc) == FAILURE) {
            MSGPACK_WARNING(
                "[msgpack] (%s) Invocation of %s's constructor failed",
                __FUNCTION__, ZSTR_VAL(ce->name));

            return FAILURE;
        }
    }

    switch (Z_TYPE_P(value)) {
        case IS_ARRAY:
         {
            int num;
            HashTable *ht, *ret, *var = NULL;
            zend_string *str_key;
            zval *data;
            zend_ulong num_key;

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

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

            /* string - php_only mode? */
            if (ht->nNumOfElements != ht->nNextFreeElement || ht->nNumOfElements != ret->nNumOfElements) {
                HashTable *properties = NULL;
                HashPosition prop_pos;

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

                ZEND_HASH_FOREACH_STR_KEY_VAL(ht, str_key, data) {
                    if (str_key) {
                        if (msgpack_convert_string_to_properties(return_value, str_key, data, var) != SUCCESS) {
                            MSGPACK_WARNING("[msgpack] (%s) "
                                    "illegal offset type, skip this decoding",
                                    __FUNCTION__);
                        }
                    }
                } ZEND_HASH_FOREACH_END();

                /* index */
#if PHP_VERSION_ID < 80000
                properties = Z_OBJ_HT_P(return_value)->get_properties(return_value);
#else
                properties = Z_OBJ_HT_P(return_value)->get_properties(Z_OBJ_P(return_value));
#endif
                if (HASH_OF(tpl)) {
                    properties = HASH_OF(tpl);
                }
                zend_hash_internal_pointer_reset_ex(properties, &prop_pos);


                ZEND_HASH_FOREACH_KEY_VAL(ht, num_key, str_key, data) {
                    if (str_key == NULL) {
                        if (msgpack_convert_long_to_properties(ret, return_value, &properties, &prop_pos, num_key, data, var) != SUCCESS) {
                            MSGPACK_WARNING("[msgpack] (%s) "
                                    "illegal offset type, skip this decoding",
                                    __FUNCTION__);
                        }
                    }
                } ZEND_HASH_FOREACH_END();

                zend_hash_destroy(var);
                FREE_HASHTABLE(var);
            } else {
                int (*convert_function)(zval *, zval *, zval *) = NULL;
                const char *class_name, *prop_name;
                size_t prop_len;
                zval *aryval;

                num_key = 0;
                ZEND_HASH_FOREACH_STR_KEY_VAL(ret, str_key, data) {
                    aryval = zend_hash_index_find(ht, num_key);

                    if (data == NULL) {
                        MSGPACK_WARNING("[msgpack] (%s) can't get data value by index", __FUNCTION__);
                        return FAILURE;
                    }

                    if (Z_TYPE_P(data) == IS_INDIRECT) {
                        data = Z_INDIRECT_P(data);
                    }

                    switch (Z_TYPE_P(data)) {
                        case IS_ARRAY:
                            convert_function = msgpack_convert_array;
                            break;
                        case IS_OBJECT:
                            //case IS_STRING: -- may have default values of
                            // class members, so it's not wise to allow
                            convert_function = msgpack_convert_object;
                            break;
                    }

                    zend_unmangle_property_name_ex(str_key, &class_name, &prop_name, &prop_len);

                    if (convert_function) {
                        zval nv;
                        if (convert_function(&nv, data, aryval) != SUCCESS) {
                            MSGPACK_WARNING("[msgpack] (%s) "
                                "convert failure in convert_object",
                                __FUNCTION__);
                            return FAILURE;
                        }

                        zend_update_property_ex(ce, OBJ_FOR_PROP(return_value), str_key, &nv);
                        zval_ptr_dtor(&nv);
                    } else  {
                        zend_update_property(ce, OBJ_FOR_PROP(return_value), prop_name, prop_len, aryval);
                    }
                    num_key++;
                } ZEND_HASH_FOREACH_END();
          }
            break;
        }
           default:
        {
            HashTable *properties = NULL;
            HashPosition prop_pos;

#if PHP_VERSION_ID < 80000
            properties = Z_OBJ_HT_P(return_value)->get_properties(return_value);
#else
            properties = Z_OBJ_HT_P(return_value)->get_properties(Z_OBJ_P(return_value));
#endif
            zend_hash_internal_pointer_reset_ex(properties, &prop_pos);

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

    return SUCCESS;
}
/* }}} */

int msgpack_convert_template(zval *return_value, zval *tpl, zval *value) /* {{{ */ {
    switch (Z_TYPE_P(tpl)) {
        case IS_ARRAY:
            return msgpack_convert_array(return_value, tpl, value);
        case IS_STRING:
        case IS_OBJECT:
            return msgpack_convert_object(return_value, tpl, value);
        default:
            MSGPACK_ERROR("[msgpack] (%s) Template type is unsupported",
                          __FUNCTION__);
            return FAILURE;
    }
}
/* }}} */

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 * vim600: noet sw=4 ts=4 fdm=marker
 * vim<600: noet sw=4 ts=4
 */
