#include "pwphpbytecode_runtime.h"

#include "php_embed_runtime.h"

#include <php.h>
#include <Zend/zend_exceptions.h>
#include <main/php_output.h>
#include <string.h>

static GHashTable *loaded_loader_symbols = NULL;

static void
set_error(char **error_message, const char *message)
{
    if (error_message != NULL)
        *error_message = g_strdup(message);
}

gboolean
valaphp_pwphpbytecode_ensure_initialized(char **error_message)
{
    if (error_message != NULL)
        *error_message = NULL;

    return valaphp_embed_ensure_initialized(error_message);
}

gboolean
valaphp_pwphpbytecode_begin_load_class(const char *loader_id, gboolean *should_load, char **error_message)
{
    if (error_message != NULL)
        *error_message = NULL;
    if (should_load != NULL)
        *should_load = FALSE;

    if (loader_id == NULL || loader_id[0] == '\0')
        return TRUE;

    if (!valaphp_pwphpbytecode_ensure_initialized(error_message))
        return FALSE;

    if (loaded_loader_symbols == NULL)
        loaded_loader_symbols = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);

    if (g_hash_table_contains(loaded_loader_symbols, loader_id))
        return TRUE;

    if (should_load != NULL)
        *should_load = TRUE;
    return TRUE;
}

void
valaphp_pwphpbytecode_finish_load_class(const char *loader_id)
{
    if (loader_id == NULL || loader_id[0] == '\0')
        return;

    if (loaded_loader_symbols == NULL)
        loaded_loader_symbols = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);

    g_hash_table_add(loaded_loader_symbols, g_strdup(loader_id));
}

static gboolean
hash_has_string_or_sparse_keys(HashTable *hash)
{
    zend_ulong numeric_key;
    zend_string *string_key;
    zval *entry;
    zend_ulong expected_key = 0;

    ZEND_HASH_FOREACH_KEY_VAL(hash, numeric_key, string_key, entry) {
        if (string_key != NULL || numeric_key != expected_key)
            return TRUE;
        expected_key++;
    } ZEND_HASH_FOREACH_END();

    return FALSE;
}

static Phpzval *zend_zval_to_php_zval(zval *value);

static Phpzval *
zend_hash_to_php_zval(HashTable *hash, gboolean force_object)
{
    zend_ulong numeric_key;
    zend_string *string_key;
    zval *entry;
    gboolean as_object = force_object || hash_has_string_or_sparse_keys(hash);
    Phpzval *result = as_object ? php_zval_object() : php_zval_array();

    ZEND_HASH_FOREACH_KEY_VAL(hash, numeric_key, string_key, entry) {
        Phpzval *child = zend_zval_to_php_zval(entry);
        if (as_object) {
            if (string_key != NULL) {
                php_zval_set(result, ZSTR_VAL(string_key), child);
            } else {
                char *key = g_strdup_printf("%lu", (unsigned long) numeric_key);
                php_zval_set(result, key, child);
                g_free(key);
            }
        } else {
            php_zval_append(result, child);
        }
        php_zval_unref(child);
    } ZEND_HASH_FOREACH_END();

    return result;
}

static Phpzval *
zend_zval_to_php_zval(zval *value)
{
    switch (Z_TYPE_P(value)) {
    case IS_NULL:
        return php_zval_null_value();
    case IS_FALSE:
        return php_zval_boolean(FALSE);
    case IS_TRUE:
        return php_zval_boolean(TRUE);
    case IS_LONG:
        return php_zval_integer((gint64) Z_LVAL_P(value));
    case IS_DOUBLE:
        return php_zval_double_number((gdouble) Z_DVAL_P(value));
    case IS_STRING:
        return php_zval_string_value(Z_STRVAL_P(value));
    case IS_ARRAY:
        return zend_hash_to_php_zval(Z_ARRVAL_P(value), FALSE);
    case IS_OBJECT:
        return zend_hash_to_php_zval(Z_OBJPROP_P(value), TRUE);
    default:
        return php_zval_null_value();
    }
}

static void
php_zval_to_zend_zval(Phpzval *value, zval *return_value)
{
    if (value == NULL) {
        ZVAL_NULL(return_value);
        return;
    }

    switch (php_zval_get_value_type(value)) {
    case PHP_VALUE_TYPE_NULL:
        ZVAL_NULL(return_value);
        break;
    case PHP_VALUE_TYPE_BOOL:
        ZVAL_BOOL(return_value, php_zval_as_bool(value));
        break;
    case PHP_VALUE_TYPE_INT:
        ZVAL_LONG(return_value, (zend_long) php_zval_as_int(value));
        break;
    case PHP_VALUE_TYPE_DOUBLE:
        ZVAL_DOUBLE(return_value, php_zval_as_double(value));
        break;
    case PHP_VALUE_TYPE_STRING:
        ZVAL_STRING(return_value, php_zval_as_string(value));
        break;
    case PHP_VALUE_TYPE_ARRAY: {
        array_init(return_value);
        int count = php_zval_count(value);
        for (int i = 0; i < count; i++) {
            Phpzval *child = php_zval_get_index(value, i);
            zval zend_child;
            php_zval_to_zend_zval(child, &zend_child);
            add_next_index_zval(return_value, &zend_child);
            if (child != NULL)
                php_zval_unref(child);
        }
        break;
    }
    case PHP_VALUE_TYPE_OBJECT: {
        array_init(return_value);
        int count = php_zval_get_key_count(value);
        for (int i = 0; i < count; i++) {
            const char *key = php_zval_get_key_at(value, i);
            if (key == NULL)
                continue;
            Phpzval *child = php_zval_get(value, key);
            zval zend_child;
            php_zval_to_zend_zval(child, &zend_child);
            add_assoc_zval(return_value, key, &zend_child);
            if (child != NULL)
                php_zval_unref(child);
        }
        break;
    }
    default:
        ZVAL_NULL(return_value);
        break;
    }
}

static zval *
params_from_php_zval_array(Phpzval *args, uint32_t *param_count)
{
    uint32_t count = 0;
    if (args != NULL)
        count = (uint32_t) php_zval_count(args);

    *param_count = count;
    if (count == 0)
        return NULL;

    zval *params = g_new0(zval, count);
    for (uint32_t i = 0; i < count; i++) {
        Phpzval *child = php_zval_get_index(args, (int) i);
        php_zval_to_zend_zval(child, &params[i]);
        if (child != NULL)
            php_zval_unref(child);
    }
    return params;
}

static void
params_free(zval *params, uint32_t param_count)
{
    if (params == NULL)
        return;

    for (uint32_t i = 0; i < param_count; i++)
        zval_ptr_dtor(&params[i]);
    g_free(params);
}

static gboolean
call_static_method(const char *class_name, const char *method_name, zval *params, uint32_t param_count, zval *return_value, char **error_message)
{
    if (class_name == NULL || class_name[0] == '\0' || method_name == NULL || method_name[0] == '\0') {
        set_error(error_message, "pwphpbytecode class or method name is empty");
        return FALSE;
    }

    char *callback_name = g_strdup_printf("%s::%s", class_name, method_name);
    zval callback;
    ZVAL_STRING(&callback, callback_name);
    ZVAL_UNDEF(return_value);

    int status = SUCCESS;
    zend_try {
        status = call_user_function(EG(function_table), NULL, &callback, return_value, param_count, params);
    } zend_catch {
        status = FAILURE;
    } zend_end_try();

    zval_ptr_dtor(&callback);
    g_free(callback_name);

    if (status != SUCCESS || EG(exception)) {
        if (EG(exception))
            zend_clear_exception();
        if (!Z_ISUNDEF_P(return_value))
            zval_ptr_dtor(return_value);
        set_error(error_message, "pwphpbytecode static method call failed");
        return FALSE;
    }

    return TRUE;
}

static Phpzval *
call_static_method_zval_internal(const char *class_name, const char *method_name, Phpzval *args, gboolean flush_output, char **error_message)
{
    if (error_message != NULL)
        *error_message = NULL;

    if (!valaphp_pwphpbytecode_ensure_initialized(error_message))
        return NULL;

    uint32_t param_count = 0;
    zval *params = params_from_php_zval_array(args, &param_count);
    zval return_value;

    if (!call_static_method(class_name, method_name, params, param_count, &return_value, error_message)) {
        params_free(params, param_count);
        return NULL;
    }

    Phpzval *result = zend_zval_to_php_zval(&return_value);
    zval_ptr_dtor(&return_value);
    params_free(params, param_count);
    if (flush_output)
        php_output_flush_all();
    return result;
}

Phpzval *
valaphp_pwphpbytecode_call_static_method_zval(const char *class_name, const char *method_name, Phpzval *args, char **error_message)
{
    return call_static_method_zval_internal(class_name, method_name, args, TRUE, error_message);
}

char *
valaphp_pwphpbytecode_call_static_method_output_to_string(const char *class_name, const char *method_name, Phpzval *args, char **error_message)
{
    if (error_message != NULL)
        *error_message = NULL;

    if (!valaphp_pwphpbytecode_ensure_initialized(error_message))
        return NULL;

    php_output_start_default();
    Phpzval *result = call_static_method_zval_internal(class_name, method_name, args, FALSE, error_message);
    if (result == NULL) {
        php_output_discard();
        return NULL;
    }
    php_zval_unref(result);

    zval contents;
    ZVAL_NULL(&contents);
    char *output = g_strdup("");
    if (php_output_get_contents(&contents) == SUCCESS && Z_TYPE(contents) == IS_STRING) {
        g_free(output);
        output = g_strndup(Z_STRVAL(contents), Z_STRLEN(contents));
    }
    zval_ptr_dtor(&contents);
    php_output_discard();
    return output;
}
