#include "call/zval_array_to_zval.h"

#include "php_embed_runtime.h"

#include <string.h>

/* Needed while valaphp-runtime-embed may be built against an older installed
 * valaphp-runtime-core header during local source-tree testing. */
extern void php_zval_assign_from(Phpzval *self, Phpzval *value);

typedef struct {
    zval *params;
    Phpzval **children;
    gboolean *by_ref;
    uint32_t param_count;
} ValaphpEmbedCallParams;

static zend_function *
valaphp_embed_find_function(const char *function_name)
{
    if (function_name == NULL)
        return NULL;
    return (zend_function *) zend_hash_str_find_ptr(CG(function_table), function_name, strlen(function_name));
}

static gboolean
valaphp_embed_arg_is_by_ref(zend_function *function, uint32_t index)
{
    if (function == NULL)
        return FALSE;
    return ARG_SHOULD_BE_SENT_BY_REF(function, index + 1) != 0;
}

static void
valaphp_embed_copy_back_by_ref_params(ValaphpEmbedCallParams *call_params)
{
    if (call_params == NULL || call_params->params == NULL || call_params->children == NULL || call_params->by_ref == NULL)
        return;

    for (uint32_t i = 0; i < call_params->param_count; i++) {
        if (!call_params->by_ref[i] || call_params->children[i] == NULL)
            continue;

        zval *value = Z_ISREF(call_params->params[i]) ? Z_REFVAL(call_params->params[i]) : &call_params->params[i];
        Phpzval *updated = valaphp_embed_zend_zval_to_php_zval(value);
        php_zval_assign_from(call_params->children[i], updated);
        php_zval_unref(updated);
    }
}

static ValaphpEmbedCallParams
valaphp_embed_params_from_php_zval_array(const char *function_name, Phpzval *args)
{
    uint32_t count = 0;
    if (args != NULL)
        count = (uint32_t) php_zval_count(args);

    ValaphpEmbedCallParams call_params = { 0 };
    call_params.param_count = count;
    if (count == 0)
        return call_params;

    zend_function *function = valaphp_embed_find_function(function_name);
    call_params.params = g_new0(zval, count);
    call_params.children = g_new0(Phpzval *, count);
    call_params.by_ref = g_new0(gboolean, count);
    for (uint32_t i = 0; i < count; i++) {
        Phpzval *child = php_zval_get_index(args, (int) i);
        call_params.children[i] = child;
        valaphp_embed_zval_to_zend_zval(child, &call_params.params[i]);
        if (valaphp_embed_arg_is_by_ref(function, i)) {
            ZVAL_MAKE_REF(&call_params.params[i]);
            call_params.by_ref[i] = TRUE;
        }
    }

    return call_params;
}

static void
valaphp_embed_params_free(ValaphpEmbedCallParams *call_params)
{
    if (call_params == NULL || call_params->params == NULL)
        return;

    for (uint32_t i = 0; i < call_params->param_count; i++) {
        zval_ptr_dtor(&call_params->params[i]);
        if (call_params->children != NULL && call_params->children[i] != NULL)
            php_zval_unref(call_params->children[i]);
    }
    g_free(call_params->params);
    g_free(call_params->children);
    g_free(call_params->by_ref);
    call_params->params = NULL;
    call_params->children = NULL;
    call_params->by_ref = NULL;
    call_params->param_count = 0;
}

Phpzval *
valaphp_embed_call_zval_array_to_zval(const char *function_name, Phpzval *args, char **error_message)
{
    if (!valaphp_embed_ensure_initialized(error_message))
        return NULL;

    ValaphpEmbedCallParams call_params = valaphp_embed_params_from_php_zval_array(function_name, args);
    zval return_value;

    if (!valaphp_embed_call_php_function(function_name, call_params.params, call_params.param_count, &return_value, error_message)) {
        valaphp_embed_params_free(&call_params);
        return NULL;
    }

    valaphp_embed_copy_back_by_ref_params(&call_params);
    Phpzval *result = valaphp_embed_zend_zval_to_php_zval(&return_value);
    zval_ptr_dtor(&return_value);
    valaphp_embed_params_free(&call_params);
    return result;
}

char *
valaphp_embed_call_zval_array_output_to_string(const char *function_name, Phpzval *args, char **error_message)
{
    if (!valaphp_embed_ensure_initialized(error_message))
        return NULL;

    ValaphpEmbedCallParams call_params = valaphp_embed_params_from_php_zval_array(function_name, args);
    char *result = valaphp_embed_call_php_function_output_to_string(function_name, call_params.params, call_params.param_count, error_message);
    if (result != NULL)
        valaphp_embed_copy_back_by_ref_params(&call_params);
    valaphp_embed_params_free(&call_params);
    return result;
}
