#include "phpef_runtime_embed_value_conversion.h"

#include <Zend/zend_exceptions.h>
#include <ext/spl/spl_exceptions.h>
#include <glib.h>
#include <string.h>

static const gchar *
phpef_runtime_embed_normalized_php_class_name(const gchar *class_name)
{
    if (class_name == NULL) {
        return "Exception";
    }
    while (*class_name == '\\') {
        class_name++;
    }
    return *class_name != '\0' ? class_name : "Exception";
}

static zend_class_entry *
phpef_runtime_embed_zend_exception_class_for_php_class(const gchar *class_name)
{
    const gchar *normalized = phpef_runtime_embed_normalized_php_class_name(class_name);

    if (g_ascii_strcasecmp(normalized, "Error") == 0) {
        return zend_ce_error != NULL ? zend_ce_error : zend_ce_exception;
    }
    if (g_ascii_strcasecmp(normalized, "TypeError") == 0) {
        return zend_ce_type_error != NULL ? zend_ce_type_error : zend_ce_exception;
    }
    if (g_ascii_strcasecmp(normalized, "ArgumentCountError") == 0) {
        return zend_ce_argument_count_error != NULL ? zend_ce_argument_count_error : zend_ce_exception;
    }
    if (g_ascii_strcasecmp(normalized, "RuntimeException") == 0) {
        return spl_ce_RuntimeException != NULL ? spl_ce_RuntimeException : zend_ce_exception;
    }
    if (g_ascii_strcasecmp(normalized, "UnexpectedValueException") == 0) {
        return spl_ce_UnexpectedValueException != NULL ? spl_ce_UnexpectedValueException : zend_ce_exception;
    }
    if (g_ascii_strcasecmp(normalized, "LogicException") == 0) {
        return spl_ce_LogicException != NULL ? spl_ce_LogicException : zend_ce_exception;
    }
    if (g_ascii_strcasecmp(normalized, "InvalidArgumentException") == 0) {
        return spl_ce_InvalidArgumentException != NULL ? spl_ce_InvalidArgumentException : zend_ce_exception;
    }
    if (g_ascii_strcasecmp(normalized, "DomainException") == 0) {
        return spl_ce_DomainException != NULL ? spl_ce_DomainException : zend_ce_exception;
    }
    if (g_ascii_strcasecmp(normalized, "LengthException") == 0) {
        return spl_ce_LengthException != NULL ? spl_ce_LengthException : zend_ce_exception;
    }
    if (g_ascii_strcasecmp(normalized, "OutOfRangeException") == 0) {
        return spl_ce_OutOfRangeException != NULL ? spl_ce_OutOfRangeException : zend_ce_exception;
    }
    if (g_ascii_strcasecmp(normalized, "OutOfBoundsException") == 0) {
        return spl_ce_OutOfBoundsException != NULL ? spl_ce_OutOfBoundsException : zend_ce_exception;
    }
    if (g_ascii_strcasecmp(normalized, "OverflowException") == 0) {
        return spl_ce_OverflowException != NULL ? spl_ce_OverflowException : zend_ce_exception;
    }
    if (g_ascii_strcasecmp(normalized, "RangeException") == 0) {
        return spl_ce_RangeException != NULL ? spl_ce_RangeException : zend_ce_exception;
    }
    if (g_ascii_strcasecmp(normalized, "UnderflowException") == 0) {
        return spl_ce_UnderflowException != NULL ? spl_ce_UnderflowException : zend_ce_exception;
    }

    return zend_ce_exception;
}

static gboolean
phpef_runtime_embed_phpef_throwable_to_zend_exception(PhpefRuntimeCorePhpThrowableObject *throwable, zval *return_value)
{
    if (throwable == NULL) {
        return FALSE;
    }

    gchar *class_name = phpef_runtime_core_php_throwable_object_php_class_name(throwable);
    zend_class_entry *exception_ce = phpef_runtime_embed_zend_exception_class_for_php_class(class_name);

    if (exception_ce == NULL || object_init_ex(return_value, exception_ce) != SUCCESS) {
        g_free(class_name);
        return FALSE;
    }

    zend_class_entry *property_scope = zend_get_exception_base(Z_OBJ_P(return_value));
    if (property_scope == NULL) {
        property_scope = exception_ce;
    }

    PhpefRuntimeCoreZval *message = phpef_runtime_core_php_throwable_object_get_php_message(throwable);
    gchar *message_string = message != NULL ? phpef_runtime_core_zval_to_php_string(message) : NULL;
    zend_update_property_string(property_scope, Z_OBJ_P(return_value), "message", sizeof("message") - 1, message_string != NULL ? message_string : "");

    PhpefRuntimeCoreZval *code = phpef_runtime_core_php_throwable_object_get_php_code(throwable);
    zend_long code_long = 0;
    if (code != NULL && phpef_runtime_core_zval_get_kind(code) == PHPEF_RUNTIME_CORE_VALUE_KIND_INT) {
        code_long = (zend_long) phpef_runtime_core_zval_as_int(code);
    }
    zend_update_property_long(property_scope, Z_OBJ_P(return_value), "code", sizeof("code") - 1, code_long);

    const gchar *file_name = phpef_runtime_core_php_throwable_object_get_file_name(throwable);
    if (file_name != NULL && file_name[0] != '\0') {
        zend_update_property_string(property_scope, Z_OBJ_P(return_value), "file", sizeof("file") - 1, file_name);
    }

    gint line = phpef_runtime_core_php_throwable_object_get_line(throwable);
    if (line > 0) {
        zend_update_property_long(property_scope, Z_OBJ_P(return_value), "line", sizeof("line") - 1, (zend_long) line);
    }

    PhpefRuntimeCoreZval *trace = phpef_runtime_core_php_throwable_object_get_php_trace(throwable);
    if (trace != NULL && phpef_runtime_core_zval_get_kind(trace) == PHPEF_RUNTIME_CORE_VALUE_KIND_ARRAY) {
        zval trace_zend;
        phpef_runtime_embed_phpefzval_to_zendzval(trace, &trace_zend);
        zend_update_property(property_scope, Z_OBJ_P(return_value), "trace", sizeof("trace") - 1, &trace_zend);
        zval_ptr_dtor(&trace_zend);
    }

    PhpefRuntimeCoreZval *previous = phpef_runtime_core_php_throwable_object_get_previous(throwable);
    if (previous != NULL && phpef_runtime_core_zval_get_kind(previous) == PHPEF_RUNTIME_CORE_VALUE_KIND_OBJECT) {
        zval previous_zend;
        phpef_runtime_embed_phpefzval_to_zendzval(previous, &previous_zend);
        if (Z_TYPE(previous_zend) == IS_OBJECT && instanceof_function(Z_OBJCE(previous_zend), zend_ce_throwable)) {
            zend_update_property(property_scope, Z_OBJ_P(return_value), "previous", sizeof("previous") - 1, &previous_zend);
        }
        zval_ptr_dtor(&previous_zend);
    }

    g_free(message_string);
    g_free(class_name);
    return TRUE;
}

void
phpef_runtime_embed_phpefzval_to_zendzval(PhpefRuntimeCoreZval *value, zval *return_value)
{
    if (value == NULL) {
        ZVAL_NULL(return_value);
        return;
    }

    switch (phpef_runtime_core_zval_get_kind(value)) {
    case PHPEF_RUNTIME_CORE_VALUE_KIND_NULL:
    case PHPEF_RUNTIME_CORE_VALUE_KIND_UNDEF:
        ZVAL_NULL(return_value);
        return;
    case PHPEF_RUNTIME_CORE_VALUE_KIND_BOOL:
        ZVAL_BOOL(return_value, phpef_runtime_core_zval_as_bool(value) ? 1 : 0);
        return;
    case PHPEF_RUNTIME_CORE_VALUE_KIND_INT:
        ZVAL_LONG(return_value, (zend_long) phpef_runtime_core_zval_as_int(value));
        return;
    case PHPEF_RUNTIME_CORE_VALUE_KIND_FLOAT:
        ZVAL_DOUBLE(return_value, (double) phpef_runtime_core_zval_as_float(value));
        return;
    case PHPEF_RUNTIME_CORE_VALUE_KIND_STRING: {
        gint bytes_length = 0;
        guint8 *bytes = phpef_runtime_core_zval_to_php_bytes(value, &bytes_length);
        ZVAL_STRINGL(return_value, bytes != NULL ? (const char *) bytes : "", bytes_length > 0 ? (size_t) bytes_length : 0);
        g_free(bytes);
        return;
    }
    case PHPEF_RUNTIME_CORE_VALUE_KIND_ARRAY: {
        gint entries_length = 0;
        PhpefRuntimeCoreZvalArrayEntry **entries = phpef_runtime_core_zval_as_array_entries(value, &entries_length);
        array_init(return_value);
        for (gint i = 0; i < entries_length; i++) {
            PhpefRuntimeCoreZvalArrayEntry *entry = entries[i];
            if (entry == NULL) {
                continue;
            }
            PhpefRuntimeCoreZval *entry_key = phpef_runtime_core_zval_array_entry_get_key(entry);
            PhpefRuntimeCoreZval *entry_value = phpef_runtime_core_zval_array_entry_get_value(entry);
            zval zend_value;
            phpef_runtime_embed_phpefzval_to_zendzval(entry_value, &zend_value);
            if (entry_key != NULL && phpef_runtime_core_zval_get_kind(entry_key) == PHPEF_RUNTIME_CORE_VALUE_KIND_INT) {
                add_index_zval(return_value, (zend_ulong) phpef_runtime_core_zval_as_int(entry_key), &zend_value);
            } else {
                gint key_length = 0;
                guint8 *key_bytes = entry_key != NULL ? phpef_runtime_core_zval_to_php_bytes(entry_key, &key_length) : NULL;
                if (key_bytes != NULL && key_length > 0) {
                    add_assoc_zval_ex(return_value, (const char *) key_bytes, (size_t) key_length, &zend_value);
                } else {
                    add_next_index_zval(return_value, &zend_value);
                }
                g_free(key_bytes);
            }
        }
        g_free(entries);
        return;
    }
    case PHPEF_RUNTIME_CORE_VALUE_KIND_RESOURCE: {
        zend_resource *resource_value = (zend_resource *) phpef_runtime_core_zval_as_resource_pointer(value);
        if (resource_value != NULL) {
            GC_ADDREF(resource_value);
            ZVAL_RES(return_value, resource_value);
            return;
        }
        ZVAL_NULL(return_value);
        return;
    }
    case PHPEF_RUNTIME_CORE_VALUE_KIND_OBJECT: {
        PhpefRuntimeCorePhpObject *object_value = phpef_runtime_core_zval_as_object(value);
        if (object_value != NULL && PHPEF_RUNTIME_CORE_IS_PHP_THROWABLE_OBJECT(object_value)) {
            if (phpef_runtime_embed_phpef_throwable_to_zend_exception(PHPEF_RUNTIME_CORE_PHP_THROWABLE_OBJECT(object_value), return_value)) {
                return;
            }
        }

        gchar *string_value = phpef_runtime_core_zval_to_php_string(value);
        ZVAL_STRING(return_value, string_value != NULL ? string_value : "");
        g_free(string_value);
        return;
    }
    default: {
        gchar *string_value = phpef_runtime_core_zval_to_php_string(value);
        ZVAL_STRING(return_value, string_value != NULL ? string_value : "");
        g_free(string_value);
        return;
    }
    }
}

PhpefRuntimeCoreZval *
phpef_runtime_embed_zendzval_to_phpefzval(zval *value)
{
    if (value == NULL) {
        return phpef_runtime_core_zval_from_null();
    }

    switch (Z_TYPE_P(value)) {
    case IS_NULL:
        return phpef_runtime_core_zval_from_null();
    case IS_FALSE:
        return phpef_runtime_core_zval_from_bool(FALSE);
    case IS_TRUE:
        return phpef_runtime_core_zval_from_bool(TRUE);
    case IS_LONG:
        return phpef_runtime_core_zval_from_int((gint64) Z_LVAL_P(value));
    case IS_DOUBLE:
        return phpef_runtime_core_zval_from_float((gdouble) Z_DVAL_P(value));
    case IS_STRING:
        return phpef_runtime_core_zval_from_string_bytes((guint8 *) Z_STRVAL_P(value), (gint) Z_STRLEN_P(value));
    case IS_RESOURCE: {
        zend_resource *resource_value = Z_RES_P(value);
        if (resource_value != NULL) {
            GC_ADDREF(resource_value);
            return phpef_runtime_core_zval_from_resource_pointer(resource_value);
        }
        return phpef_runtime_core_zval_from_null();
    }
    case IS_ARRAY: {
        HashTable *hash = Z_ARRVAL_P(value);
        GPtrArray *entries = g_ptr_array_new_with_free_func(g_object_unref);
        zend_ulong index = 0;
        zend_string *string_key = NULL;
        zval *entry_value = NULL;

        ZEND_HASH_FOREACH_KEY_VAL(hash, index, string_key, entry_value) {
            PhpefRuntimeCoreZval *key = NULL;
            if (string_key != NULL) {
                key = phpef_runtime_core_zval_from_string_bytes((guint8 *) ZSTR_VAL(string_key), (gint) ZSTR_LEN(string_key));
            } else {
                key = phpef_runtime_core_zval_from_int((gint64) index);
            }
            PhpefRuntimeCoreZval *converted_value = phpef_runtime_embed_zendzval_to_phpefzval(entry_value);
            PhpefRuntimeCoreZvalArrayEntry *entry = phpef_runtime_core_zval_array_entry_new(key, converted_value);
            g_ptr_array_add(entries, entry);
            phpef_runtime_core_zval_unref(key);
            phpef_runtime_core_zval_unref(converted_value);
        } ZEND_HASH_FOREACH_END();

        PhpefRuntimeCoreZval *result = phpef_runtime_core_zval_from_array((PhpefRuntimeCoreZvalArrayEntry **) entries->pdata, (gint) entries->len);
        g_ptr_array_unref(entries);
        return result;
    }
    default: {
        zend_string *string_value = zval_get_string(value);
        PhpefRuntimeCoreZval *result = string_value != NULL
            ? phpef_runtime_core_zval_from_string_bytes((guint8 *) ZSTR_VAL(string_value), (gint) ZSTR_LEN(string_value))
            : phpef_runtime_core_zval_from_string("");
        if (string_value != NULL) {
            zend_string_release(string_value);
        }
        return result;
    }
    }
}

void
phpef_runtime_embed_propagate_zendzval_to_phpefzval(PhpefRuntimeCoreZval *target, zval *value)
{
    if (target == NULL) {
        return;
    }

    PhpefRuntimeCoreZval *converted = phpef_runtime_embed_zendzval_to_phpefzval(value);
    phpef_runtime_core_zval_replace_with(target, converted);
    phpef_runtime_core_zval_unref(converted);
}
