#include "php_embed_runtime.h"

#include <Zend/zend_exceptions.h>
#include <Zend/zend_compile.h>
#include <Zend/zend_stream.h>
#include <main/php_main.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;

#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_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 *
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 || valaphp_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 = 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 {
            php_zval_append(result, child);
        }
        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_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 (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);

    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;
}

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_count(value);

        for (int i = 0; i < count; i++) {
            Phpzval *child = php_zval_get_index(value, i);
            zval php_child;
            valaphp_embed_zval_to_zend_zval(child, &php_child);
            add_next_index_zval(return_value, &php_child);
            if (child != NULL)
                php_zval_unref(child);
        }
        break;
    }

    case PHP_VALUE_TYPE_OBJECT: {
        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;
    }
    }
}
