#include "php_embed_runtime.h"

#include <Zend/zend_exceptions.h>
#include <Zend/zend_compile.h>
#include <Zend/zend_vm_opcodes.h>
#include <Zend/zend_stream.h>
#include <main/php_main.h>
#include <string.h>

static gboolean php_embed_initialized = FALSE;
static void (*valaphp_embed_previous_error_cb)(int type, zend_string *error_filename, const uint32_t error_lineno, zend_string *message) = NULL;
static GString *valaphp_embed_syntax_error_message = NULL;
static GHashTable *valaphp_embed_resource_store = NULL;
static GHashTable *valaphp_embed_resource_ids_by_ptr = NULL;
static guint valaphp_embed_next_resource_id = 1;
static const char *VALAPHP_EMBED_RESOURCE_MARKER = "__valaphp_embed_resource_id";

#ifdef VALAPHP_ENABLE_PWPHPBYTECODE
static void
valaphp_embed_ini_enable_pwphpbytecode_settings(HashTable *configuration_hash)
{
    zval error_prepend_string;
    ZVAL_NEW_STR(&error_prepend_string, zend_string_init(ZEND_STRL("PW-Embed SAPI error:"), 1));
    zend_hash_str_update(configuration_hash, ZEND_STRL("error_prepend_string"), &error_prepend_string);

    zval opcache_enable_cli;
    ZVAL_NEW_STR(&opcache_enable_cli, zend_string_init(ZEND_STRL("On"), 1));
    zend_hash_str_update(configuration_hash, ZEND_STRL("opcache.enable_cli"), &opcache_enable_cli);

    zval opcache_jit_buffer_size;
    ZVAL_NEW_STR(&opcache_jit_buffer_size, zend_string_init(ZEND_STRL("256M"), 1));
    zend_hash_str_update(configuration_hash, ZEND_STRL("opcache.jit_buffer_size"), &opcache_jit_buffer_size);

    zval opcache_jit;
    ZVAL_NEW_STR(&opcache_jit, zend_string_init(ZEND_STRL("function"), 1));
    zend_hash_str_update(configuration_hash, ZEND_STRL("opcache.jit"), &opcache_jit);

    zval opcache_save_comments;
    ZVAL_NEW_STR(&opcache_save_comments, zend_string_init(ZEND_STRL("0"), 1));
    zend_hash_str_update(configuration_hash, ZEND_STRL("opcache.save_comments"), &opcache_save_comments);
}
#endif

static const char *
valaphp_embed_error_type_name(int type)
{
    switch (type) {
    case E_PARSE:
        return "Parse error";
    case E_ERROR:
        return "Fatal error";
    case E_WARNING:
        return "Warning";
    case E_NOTICE:
        return "Notice";
    default:
        return "PHP error";
    }
}

static void
valaphp_embed_capture_error_cb(int type, zend_string *error_filename, const uint32_t error_lineno, zend_string *message)
{
    if (valaphp_embed_syntax_error_message != NULL) {
        if (valaphp_embed_syntax_error_message->len > 0)
            g_string_append_c(valaphp_embed_syntax_error_message, '\n');
        g_string_append_printf(
            valaphp_embed_syntax_error_message,
            "%s: %s in %s on line %u",
            valaphp_embed_error_type_name(type),
            message != NULL ? ZSTR_VAL(message) : "unknown PHP syntax error",
            error_filename != NULL ? ZSTR_VAL(error_filename) : "unknown",
            error_lineno
        );
    }
}

static char *
valaphp_embed_take_exception_message(const char *function_name)
{
    if (!EG(exception))
        return g_strdup_printf("PHP %s failed", function_name);

    zend_object *exception = EG(exception);
    zval rv;
    zval *message = zend_read_property(exception->ce, exception, "message", sizeof("message") - 1, TRUE, &rv);
    zend_string *message_string = message != NULL ? zval_get_string(message) : NULL;
    char *result = NULL;

    if (message_string != NULL) {
        result = g_strdup_printf("%s", ZSTR_VAL(message_string));
        zend_string_release(message_string);
    } else {
        result = g_strdup_printf("PHP %s failed", function_name);
    }

    zend_clear_exception();
    return result;
}

static gboolean
valaphp_embed_zend_string_matches_path(zend_string *filename, const char *path)
{
    return filename != NULL && path != NULL && strcmp(ZSTR_VAL(filename), path) == 0;
}

static void
valaphp_embed_remove_compiled_declarations_for_path(const char *path)
{
    if (path == NULL || path[0] == '\0')
        return;

    char *canonical_path = g_canonicalize_filename(path, NULL);
    GPtrArray *function_keys = g_ptr_array_new_with_free_func((GDestroyNotify) zend_string_release);
    zend_string *name = NULL;
    zend_function *function = NULL;

    ZEND_HASH_FOREACH_STR_KEY_PTR(CG(function_table), name, function) {
        if (name != NULL
            && function != NULL
            && function->type == ZEND_USER_FUNCTION
            && (valaphp_embed_zend_string_matches_path(function->op_array.filename, path)
                || valaphp_embed_zend_string_matches_path(function->op_array.filename, canonical_path))) {
            g_ptr_array_add(function_keys, zend_string_copy(name));
        }
    } ZEND_HASH_FOREACH_END();

    for (guint i = 0; i < function_keys->len; i++) {
        zend_string *key = (zend_string *) g_ptr_array_index(function_keys, i);
        zend_hash_del(CG(function_table), key);
    }
    g_ptr_array_unref(function_keys);

    GPtrArray *class_keys = g_ptr_array_new_with_free_func((GDestroyNotify) zend_string_release);
    zend_class_entry *class_entry = NULL;

    ZEND_HASH_FOREACH_STR_KEY_PTR(CG(class_table), name, class_entry) {
        if (name != NULL
            && class_entry != NULL
            && class_entry->type == ZEND_USER_CLASS
            && (valaphp_embed_zend_string_matches_path(class_entry->info.user.filename, path)
                || valaphp_embed_zend_string_matches_path(class_entry->info.user.filename, canonical_path))) {
            g_ptr_array_add(class_keys, zend_string_copy(name));
        }
    } ZEND_HASH_FOREACH_END();

    for (guint i = 0; i < class_keys->len; i++) {
        zend_string *key = (zend_string *) g_ptr_array_index(class_keys, i);
        zend_hash_del(CG(class_table), key);
    }
    g_ptr_array_unref(class_keys);
    g_free(canonical_path);
}

static void
valaphp_embed_stored_resource_free(gpointer data)
{
    zval *stored = (zval *) data;
    if (stored == NULL)
        return;
    zval_ptr_dtor(stored);
    g_free(stored);
}

static void
valaphp_embed_resource_store_ensure(void)
{
    if (valaphp_embed_resource_store == NULL) {
        valaphp_embed_resource_store = g_hash_table_new_full(
            g_direct_hash,
            g_direct_equal,
            NULL,
            valaphp_embed_stored_resource_free
        );
    }
    if (valaphp_embed_resource_ids_by_ptr == NULL) {
        valaphp_embed_resource_ids_by_ptr = g_hash_table_new(
            g_direct_hash,
            g_direct_equal
        );
    }
}

static Phpzval *
valaphp_embed_resource_to_php_zval(zval *value)
{
    guint id = valaphp_embed_store_resource_zval(value);

    Phpzval *result = php_zval_object();
    Phpzval *resource_id = php_zval_integer((gint64) id);
    php_zval_set(result, VALAPHP_EMBED_RESOURCE_MARKER, resource_id);
    php_zval_unref(resource_id);
    return result;
}

guint
valaphp_embed_store_resource_zval(zval *value)
{
    if (value == NULL || Z_TYPE_P(value) != IS_RESOURCE)
        return 0;

    valaphp_embed_resource_store_ensure();

    zend_resource *resource = Z_RES_P(value);
    /* By-ref calls can convert the same resource repeatedly; keep one wrapper id per Zend resource. */
    guint id = GPOINTER_TO_UINT(g_hash_table_lookup(valaphp_embed_resource_ids_by_ptr, resource));

    if (id == 0) {
        id = valaphp_embed_next_resource_id++;
        zval *stored = g_new0(zval, 1);
        ZVAL_COPY(stored, value);
        g_hash_table_insert(valaphp_embed_resource_store, GUINT_TO_POINTER(id), stored);
        g_hash_table_insert(valaphp_embed_resource_ids_by_ptr, resource, GUINT_TO_POINTER(id));
    }

    return id;
}

gboolean
valaphp_embed_resource_id_to_zval(guint resource_id, zval *return_value)
{
    if (resource_id == 0 || return_value == NULL)
        return FALSE;

    zval *stored = valaphp_embed_resource_store != NULL
        ? (zval *) g_hash_table_lookup(valaphp_embed_resource_store, GUINT_TO_POINTER(resource_id))
        : NULL;
    if (stored == NULL)
        return FALSE;

    ZVAL_COPY(return_value, stored);
    return TRUE;
}

void
valaphp_embed_resource_id_remove(guint resource_id)
{
    if (resource_id == 0 || valaphp_embed_resource_store == NULL)
        return;

    gpointer key = GUINT_TO_POINTER(resource_id);
    zval *stored = (zval *) g_hash_table_lookup(valaphp_embed_resource_store, key);
    if (stored != NULL && Z_TYPE_P(stored) == IS_RESOURCE && valaphp_embed_resource_ids_by_ptr != NULL)
        g_hash_table_remove(valaphp_embed_resource_ids_by_ptr, Z_RES_P(stored));

    g_hash_table_remove(valaphp_embed_resource_store, key);
}

static gboolean
valaphp_embed_php_zval_to_resource(Phpzval *value, zval *return_value)
{
    if (value == NULL || php_zval_get_value_type(value) != PHP_VALUE_TYPE_OBJECT)
        return FALSE;

    Phpzval *resource_id = php_zval_get(value, VALAPHP_EMBED_RESOURCE_MARKER);
    if (resource_id == NULL)
        return FALSE;

    gboolean found = FALSE;
    if (php_zval_get_value_type(resource_id) == PHP_VALUE_TYPE_INT) {
        guint id = (guint) php_zval_as_int(resource_id);
        if (valaphp_embed_resource_id_to_zval(id, return_value))
            found = TRUE;
    }

    php_zval_unref(resource_id);
    return found;
}

static gboolean
valaphp_embed_key_is_unsigned_index(const char *key, zend_ulong *index)
{
    if (key == NULL || *key == '\0')
        return FALSE;

    if (key[0] == '0' && key[1] != '\0')
        return FALSE;

    zend_ulong value = 0;
    for (const char *ch = key; *ch != '\0'; ch++) {
        if (*ch < '0' || *ch > '9')
            return FALSE;
        value = (value * 10) + (zend_ulong) (*ch - '0');
    }

    *index = value;
    return TRUE;
}

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

    ZEND_HASH_FOREACH_KEY_VAL(hash, numeric_key, string_key, entry) {
        Phpzval *child = valaphp_embed_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 {
            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);
            }
        }
        php_zval_unref(child);
    } ZEND_HASH_FOREACH_END();

    return result;
}

Phpzval *
valaphp_embed_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_RESOURCE:
        return valaphp_embed_resource_to_php_zval(value);
    case IS_ARRAY:
        return valaphp_hash_to_php_zval(Z_ARRVAL_P(value), FALSE);
    case IS_OBJECT:
        return valaphp_hash_to_php_zval(Z_OBJPROP_P(value), TRUE);
    default:
        return php_zval_null_value();
    }
}

gboolean
valaphp_embed_call_php_function(const char *function_name, zval *params, uint32_t param_count, zval *return_value, char **error_message)
{
    if (error_message != NULL)
        *error_message = NULL;

    if (function_name == NULL) {
        if (error_message != NULL)
            *error_message = g_strdup("PHP function name is null");
        return FALSE;
    }

    if (!valaphp_embed_ensure_initialized(error_message))
        return FALSE;

    zval php_function_name;
    ZVAL_STRING(&php_function_name, function_name);
    ZVAL_UNDEF(return_value);

    zend_result status = call_user_function(CG(function_table), NULL, &php_function_name, return_value, param_count, params);

    zval_ptr_dtor(&php_function_name);

    if (status != SUCCESS || EG(exception)) {
        if (error_message != NULL)
            *error_message = valaphp_embed_take_exception_message(function_name);
        else if (EG(exception))
            zend_clear_exception();
        if (!Z_ISUNDEF_P(return_value))
            zval_ptr_dtor(return_value);
        return FALSE;
    }

    return TRUE;
}

static void
valaphp_embed_try_clean_output_buffer(void)
{
    zval ignored;
    char *ignored_error = NULL;
    if (valaphp_embed_call_php_function("ob_get_clean", NULL, 0, &ignored, &ignored_error))
        zval_ptr_dtor(&ignored);
    g_free(ignored_error);
}

char *
valaphp_embed_call_php_function_output_to_string(const char *function_name, zval *params, uint32_t param_count, char **error_message)
{
    zval return_value;
    if (!valaphp_embed_call_php_function("ob_start", NULL, 0, &return_value, error_message))
        return NULL;
    zval_ptr_dtor(&return_value);

    if (!valaphp_embed_call_php_function(function_name, params, param_count, &return_value, error_message)) {
        valaphp_embed_try_clean_output_buffer();
        return NULL;
    }
    zval_ptr_dtor(&return_value);

    if (!valaphp_embed_call_php_function("ob_get_clean", NULL, 0, &return_value, error_message))
        return NULL;

    char *result = valaphp_embed_zval_to_allocated_string(&return_value);
    zval_ptr_dtor(&return_value);
    return result;
}

char *
valaphp_embed_zval_to_allocated_string(zval *return_value)
{
    if (Z_TYPE_P(return_value) == IS_STRING)
        return g_strndup(Z_STRVAL_P(return_value), Z_STRLEN_P(return_value));

    if (Z_TYPE_P(return_value) == IS_NULL || Z_ISUNDEF_P(return_value))
        return g_strdup("");

    zend_string *string_value = zval_get_string(return_value);
    char *result = g_strndup(ZSTR_VAL(string_value), ZSTR_LEN(string_value));
    zend_string_release(string_value);
    return result;
}

static void
valaphp_embed_shutdown(void)
{
    if (valaphp_embed_resource_ids_by_ptr != NULL) {
        g_hash_table_destroy(valaphp_embed_resource_ids_by_ptr);
        valaphp_embed_resource_ids_by_ptr = NULL;
    }
    if (valaphp_embed_resource_store != NULL) {
        g_hash_table_destroy(valaphp_embed_resource_store);
        valaphp_embed_resource_store = NULL;
    }
    if (php_embed_initialized) {
        php_embed_shutdown();
        php_embed_initialized = FALSE;
    }
}

gboolean
valaphp_embed_ensure_initialized(char **error_message)
{
    if (php_embed_initialized)
        return TRUE;

    char *argv[] = { "valaphp-runtime-embed", NULL };
    php_embed_module.phpinfo_as_text = 1;
#ifdef VALAPHP_ENABLE_PWPHPBYTECODE
    PG(display_startup_errors) = 1;
    PG(during_request_startup) = 1;
    php_embed_module.ini_defaults = valaphp_embed_ini_enable_pwphpbytecode_settings;
    php_embed_module.name = "cli";
#endif

    if (php_embed_init(1, argv) != SUCCESS) {
        if (error_message != NULL)
            *error_message = g_strdup("Failed to initialize embedded PHP");
        return FALSE;
    }

    php_embed_initialized = TRUE;
    atexit(valaphp_embed_shutdown);
    return TRUE;
}

gboolean
valaphp_embed_syntax_check_file(const char *path, char **error_message)
{
    if (error_message != NULL)
        *error_message = NULL;

    if (path == NULL || path[0] == '\0') {
        if (error_message != NULL)
            *error_message = g_strdup("PHP syntax check path is empty");
        return FALSE;
    }

    if (!valaphp_embed_ensure_initialized(error_message))
        return FALSE;

    zend_file_handle file_handle;
    zend_stream_init_filename(&file_handle, path);

    valaphp_embed_syntax_error_message = g_string_new(NULL);
    valaphp_embed_previous_error_cb = zend_error_cb;
    zend_error_cb = valaphp_embed_capture_error_cb;

    zend_result result = FAILURE;
    zend_try {
        result = php_lint_script(&file_handle);
    } zend_catch {
        result = FAILURE;
    } zend_end_try();

    zend_error_cb = valaphp_embed_previous_error_cb;
    valaphp_embed_previous_error_cb = NULL;
    zend_destroy_file_handle(&file_handle);
    valaphp_embed_remove_compiled_declarations_for_path(path);

    if (result != SUCCESS) {
        if (error_message != NULL) {
            if (valaphp_embed_syntax_error_message != NULL && valaphp_embed_syntax_error_message->len > 0)
                *error_message = g_strdup(valaphp_embed_syntax_error_message->str);
            else
                *error_message = g_strdup_printf("PHP syntax check failed for %s", path);
        }
        if (valaphp_embed_syntax_error_message != NULL)
            g_string_free(valaphp_embed_syntax_error_message, TRUE);
        valaphp_embed_syntax_error_message = NULL;
        if (EG(exception))
            zend_clear_exception();
        return FALSE;
    }

    if (valaphp_embed_syntax_error_message != NULL)
        g_string_free(valaphp_embed_syntax_error_message, TRUE);
    valaphp_embed_syntax_error_message = NULL;
    return TRUE;
}

static const char *
valaphp_embed_op_type_name(uint8_t op_type)
{
    switch (op_type) {
    case IS_UNUSED:
        return "unused";
    case IS_CONST:
        return "const";
    case IS_TMP_VAR:
        return "tmp";
    case IS_VAR:
        return "var";
    case IS_CV:
        return "cv";
    default:
        return "unknown";
    }
}

static void
valaphp_embed_append_zval_literal(GString *out, const zval *literal)
{
    if (literal == NULL) {
        g_string_append(out, " literal=<null>");
        return;
    }

    switch (Z_TYPE_P(literal)) {
    case IS_STRING: {
        zend_string *value = Z_STR_P(literal);
        char *raw = g_strndup(ZSTR_VAL(value), ZSTR_LEN(value));
        char *escaped = g_strescape(raw, NULL);
        g_string_append_printf(
            out,
            " literal=string(len=%zu value=\"%s\")",
            (size_t) ZSTR_LEN(value),
            escaped != NULL ? escaped : ""
        );
        g_free(escaped);
        g_free(raw);
        break;
    }
    case IS_LONG:
        g_string_append_printf(out, " literal=int(%" G_GINT64_FORMAT ")", (gint64) Z_LVAL_P(literal));
        break;
    case IS_DOUBLE: {
        char buf[G_ASCII_DTOSTR_BUF_SIZE];
        g_ascii_dtostr(buf, sizeof(buf), Z_DVAL_P(literal));
        g_string_append_printf(out, " literal=float(%s)", buf);
        break;
    }
    case IS_TRUE:
        g_string_append(out, " literal=bool(true)");
        break;
    case IS_FALSE:
        g_string_append(out, " literal=bool(false)");
        break;
    case IS_NULL:
        g_string_append(out, " literal=null");
        break;
    default:
        g_string_append_printf(out, " literal=%s", zend_zval_type_name(literal));
        break;
    }
}

static void
valaphp_embed_append_op_operand(GString *out, const zend_op_array *op_array, const zend_op *opline, znode_op operand, uint8_t op_type)
{
    g_string_append_printf(out, "%s", valaphp_embed_op_type_name(op_type));

    if (op_type == IS_CONST) {
        const zval *literal = NULL;
        long literal_index = -1;
        const zval *literals_begin = op_array->literals;
        const zval *literals_end = op_array->literals != NULL ? op_array->literals + op_array->last_literal : NULL;
        const zval *runtime_literal = RT_CONSTANT(opline, operand);

        if (literals_begin != NULL && runtime_literal >= literals_begin && runtime_literal < literals_end) {
            literal = runtime_literal;
            literal_index = (long) (runtime_literal - literals_begin);
        } else if (operand.constant < (uint32_t) op_array->last_literal) {
            literal = CT_CONSTANT_EX(op_array, operand.constant);
            literal_index = (long) operand.constant;
        }

        g_string_append_printf(out, "(raw=%u", operand.constant);
        if (literal_index >= 0)
            g_string_append_printf(out, " index=%ld", literal_index);
        g_string_append_c(out, ')');
        valaphp_embed_append_zval_literal(out, literal);
        return;
    }

    if (op_type == IS_CV) {
        uint32_t var_num = EX_VAR_TO_NUM(operand.var);
        zend_string *var_name = var_num < (uint32_t) op_array->last_var ? op_array->vars[var_num] : NULL;
        if (var_name != NULL) {
            g_string_append_printf(out, "($%s)", ZSTR_VAL(var_name));
            return;
        }
    }

    g_string_append_printf(out, "(%u)", operand.var);
}

static gint
valaphp_embed_lookup_opcode_id(const char *name)
{
    if (name == NULL || name[0] == '\0')
        return -1;

    uint8_t opcode = zend_get_opcode_id(name, strlen(name));
    const char *roundtrip_name = zend_get_opcode_name(opcode);
    if (roundtrip_name != NULL && strcmp(roundtrip_name, name) == 0 && opcode < 255)
        return (gint) opcode;

    if (g_str_has_prefix(name, "ZEND_")) {
        const char *short_name = name + 5;
        opcode = zend_get_opcode_id(short_name, strlen(short_name));
        roundtrip_name = zend_get_opcode_name(opcode);
        if (roundtrip_name != NULL && strcmp(roundtrip_name, name) == 0 && opcode < 255)
            return (gint) opcode;
    }

    for (guint i = 0; i <= G_MAXUINT8; i++) {
        roundtrip_name = zend_get_opcode_name((uint8_t) i);
        if (roundtrip_name != NULL && strcmp(roundtrip_name, name) == 0)
            return (gint) i;
    }

    return -1;
}

static void
valaphp_embed_append_op_array_dump(GString *out, const char *label, const zend_op_array *op_array);

static gboolean
valaphp_embed_op_array_matches_file(const zend_op_array *candidate, const zend_op_array *main_op_array)
{
    if (candidate == NULL || main_op_array == NULL)
        return FALSE;
    if (candidate->filename == NULL || main_op_array->filename == NULL)
        return FALSE;
    return zend_string_equals(candidate->filename, main_op_array->filename);
}

static void
valaphp_embed_append_user_function_op_array_dump(GString *out, const char *label_prefix, zend_function *function, const zend_op_array *main_op_array)
{
    if (function == NULL || function->type != ZEND_USER_FUNCTION)
        return;
    if (!valaphp_embed_op_array_matches_file(&function->op_array, main_op_array))
        return;

    const char *function_name = function->op_array.function_name != NULL
        ? ZSTR_VAL(function->op_array.function_name)
        : "{anonymous}";
    char *label = g_strdup_printf("%s%s", label_prefix != NULL ? label_prefix : "function:", function_name);
    g_string_append_c(out, '\n');
    valaphp_embed_append_op_array_dump(out, label, &function->op_array);
    g_free(label);
}

static void
valaphp_embed_append_declared_op_array_dumps(GString *out, const zend_op_array *main_op_array)
{
    zend_string *name = NULL;
    zend_function *function = NULL;
    zend_class_entry *class_entry = NULL;
    GHashTable *dumped_op_arrays = g_hash_table_new(g_direct_hash, g_direct_equal);
    GHashTable *dumped_labels = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);

    ZEND_HASH_FOREACH_STR_KEY_PTR(CG(function_table), name, function) {
        (void) name;
        if (function != NULL && !g_hash_table_contains(dumped_op_arrays, &function->op_array)) {
            valaphp_embed_append_user_function_op_array_dump(out, "function:", function, main_op_array);
            if (function->type == ZEND_USER_FUNCTION)
                g_hash_table_add(dumped_op_arrays, &function->op_array);
        }
    } ZEND_HASH_FOREACH_END();

    ZEND_HASH_FOREACH_STR_KEY_PTR(CG(class_table), name, class_entry) {
        if (class_entry == NULL || class_entry->type != ZEND_USER_CLASS)
            continue;
        const char *class_name = class_entry->name != NULL
            ? ZSTR_VAL(class_entry->name)
            : (name != NULL ? ZSTR_VAL(name) : "{anonymous-class}");
        zend_string *method_name = NULL;
        zend_function *method = NULL;
        ZEND_HASH_FOREACH_STR_KEY_PTR(&class_entry->function_table, method_name, method) {
            if (method == NULL || method->type != ZEND_USER_FUNCTION)
                continue;
            const char *method_label = method->op_array.function_name != NULL
                ? ZSTR_VAL(method->op_array.function_name)
                : (method_name != NULL ? ZSTR_VAL(method_name) : "{anonymous-method}");
            char *label_prefix = g_strdup_printf("class:%s::", class_name);
            char *label = g_strdup_printf("%s%s", label_prefix, method_label);
            if (valaphp_embed_op_array_matches_file(&method->op_array, main_op_array)
                && !g_hash_table_contains(dumped_op_arrays, &method->op_array)
                && !g_hash_table_contains(dumped_labels, label)) {
                g_hash_table_add(dumped_op_arrays, &method->op_array);
                g_hash_table_add(dumped_labels, g_strdup(label));
                g_string_append_c(out, '\n');
                valaphp_embed_append_op_array_dump(out, label, &method->op_array);
            }
            g_free(label);
            g_free(label_prefix);
        } ZEND_HASH_FOREACH_END();
    } ZEND_HASH_FOREACH_END();

    g_hash_table_unref(dumped_labels);
    g_hash_table_unref(dumped_op_arrays);
}

static void
valaphp_embed_append_op_array_dump(GString *out, const char *label, const zend_op_array *op_array)
{
    if (op_array == NULL)
        return;

    g_string_append_printf(
        out,
        "op_array %s file=%s function=%s line_start=%u line_end=%u opcodes=%u vars=%d tmps=%u literals=%d\n",
        label != NULL ? label : "main",
        op_array->filename != NULL ? ZSTR_VAL(op_array->filename) : "",
        op_array->function_name != NULL ? ZSTR_VAL(op_array->function_name) : "{main}",
        op_array->line_start,
        op_array->line_end,
        op_array->last,
        op_array->last_var,
        op_array->T,
        op_array->last_literal
    );

    for (int i = 0; i < op_array->last_var; i++) {
        zend_string *var_name = op_array->vars[i];
        if (var_name != NULL)
            g_string_append_printf(out, "  cv[%d] $%s\n", i, ZSTR_VAL(var_name));
    }

    for (uint32_t i = 0; i < op_array->last; i++) {
        const zend_op *opline = &op_array->opcodes[i];
        const char *opcode_name = zend_get_opcode_name(opline->opcode);
        gint opcode_id = valaphp_embed_lookup_opcode_id(opcode_name);
        g_string_append_printf(
            out,
            "  #%04u %-24s opcode=%u lookup_id=%d line=%u ext=%u result=",
            i,
            opcode_name != NULL ? opcode_name : "UNKNOWN",
            (guint) opline->opcode,
            opcode_id,
            opline->lineno,
            opline->extended_value
        );
        valaphp_embed_append_op_operand(out, op_array, opline, opline->result, opline->result_type);
        g_string_append(out, " op1=");
        valaphp_embed_append_op_operand(out, op_array, opline, opline->op1, opline->op1_type);
        g_string_append(out, " op2=");
        valaphp_embed_append_op_operand(out, op_array, opline, opline->op2, opline->op2_type);
        g_string_append_c(out, '\n');
    }
}

char *
valaphp_embed_op_array_dump_file(const char *path, char **error_message)
{
    if (error_message != NULL)
        *error_message = NULL;

    if (path == NULL || path[0] == '\0') {
        if (error_message != NULL)
            *error_message = g_strdup("PHP op_array dump path is empty");
        return NULL;
    }

    if (!valaphp_embed_ensure_initialized(error_message))
        return NULL;

    gchar *source = NULL;
    gsize source_len = 0;
    if (!g_file_get_contents(path, &source, &source_len, NULL)) {
        if (error_message != NULL)
            *error_message = g_strdup_printf("PHP op_array dump could not read %s", path);
        return NULL;
    }
    zend_string *source_string = zend_string_init(source, source_len, 0);
    g_free(source);

    valaphp_embed_syntax_error_message = g_string_new(NULL);
    valaphp_embed_previous_error_cb = zend_error_cb;
    zend_error_cb = valaphp_embed_capture_error_cb;

    zend_op_array *op_array = NULL;
    zend_try {
        op_array = compile_string(source_string, path, ZEND_COMPILE_POSITION_AT_SHEBANG);
    } zend_catch {
        op_array = NULL;
    } zend_end_try();

    zend_error_cb = valaphp_embed_previous_error_cb;
    valaphp_embed_previous_error_cb = NULL;
    zend_string_release(source_string);

    if (op_array == NULL) {
        if (error_message != NULL) {
            if (valaphp_embed_syntax_error_message != NULL && valaphp_embed_syntax_error_message->len > 0)
                *error_message = g_strdup(valaphp_embed_syntax_error_message->str);
            else if (EG(exception))
                *error_message = valaphp_embed_take_exception_message("zend_compile_file");
            else
                *error_message = g_strdup_printf("PHP op_array compile failed for %s", path);
        }
        if (valaphp_embed_syntax_error_message != NULL)
            g_string_free(valaphp_embed_syntax_error_message, TRUE);
        valaphp_embed_syntax_error_message = NULL;
        if (EG(exception))
            zend_clear_exception();
        return NULL;
    }

    GString *out = g_string_new(NULL);
    valaphp_embed_append_op_array_dump(out, "main", op_array);
    valaphp_embed_append_declared_op_array_dumps(out, op_array);

    /* compile_string() may register declarations from the compiled source in
     * compiler-global tables.  Destroying the returned op_array here can abort
     * for files that contain only or mostly declarations, while php2vala only
     * needs a short-lived diagnostic dump.  Leave Zend to reclaim process-local
     * compiler allocations at shutdown. */

    if (valaphp_embed_syntax_error_message != NULL)
        g_string_free(valaphp_embed_syntax_error_message, TRUE);
    valaphp_embed_syntax_error_message = NULL;
    return g_string_free(out, FALSE);
}

char *
valaphp_embed_opcode_name(guint opcode)
{
    if (opcode > G_MAXUINT8)
        return g_strdup("ZEND_UNKNOWN");

    const char *name = zend_get_opcode_name((uint8_t) opcode);
    return g_strdup(name != NULL ? name : "ZEND_UNKNOWN");
}

gint
valaphp_embed_opcode_id(const char *name)
{
    if (name == NULL || name[0] == '\0')
        return -1;

    return valaphp_embed_lookup_opcode_id(name);
}

void
valaphp_embed_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, (double) php_zval_as_double(value));
        break;

    case PHP_VALUE_TYPE_STRING: {
        gchar *string_value = php_zval_as_string(value);
        ZVAL_STRING(return_value, string_value != NULL ? string_value : "");
        g_free(string_value);
        break;
    }

    case PHP_VALUE_TYPE_ARRAY: {
        array_init(return_value);
        int count = php_zval_get_key_count(value);

        for (int i = 0; i < count; i++) {
            gchar *key = php_zval_get_key_at(value, i);
            if (key == NULL)
                continue;

            Phpzval *child = php_zval_get(value, key);
            zval php_child;
            valaphp_embed_zval_to_zend_zval(child, &php_child);

            zend_ulong index;
            if (valaphp_embed_key_is_unsigned_index(key, &index))
                zend_hash_index_update(Z_ARRVAL_P(return_value), index, &php_child);
            else
                zend_hash_str_update(Z_ARRVAL_P(return_value), key, strlen(key), &php_child);

            if (child != NULL)
                php_zval_unref(child);
            g_free(key);
        }
        break;
    }

    case PHP_VALUE_TYPE_OBJECT: {
        if (valaphp_embed_php_zval_to_resource(value, return_value))
            break;

        object_init(return_value);
        int count = php_zval_get_key_count(value);

        for (int i = 0; i < count; i++) {
            gchar *key = php_zval_get_key_at(value, i);
            if (key == NULL)
                continue;

            Phpzval *child = php_zval_get(value, key);
            zval php_child;
            valaphp_embed_zval_to_zend_zval(child, &php_child);
            add_property_zval(return_value, key, &php_child);

            if (child != NULL)
                php_zval_unref(child);
            g_free(key);
        }
        break;
    }
    }
}
