#include <ctype.h>
#include <stdio.h>
#include <string.h>

#include <glib.h>
#include <sapi/embed/php_embed.h>

static gboolean
is_vala_identifier(const char *name)
{
    if (name == NULL || name[0] == '\0')
        return FALSE;

    if (!(g_ascii_isalpha(name[0]) || name[0] == '_'))
        return FALSE;

    for (const char *p = name + 1; *p != '\0'; p++) {
        if (!(g_ascii_isalnum(*p) || *p == '_'))
            return FALSE;
    }

    return TRUE;
}

static void
scan_explicit_wrappers(GHashTable *wrappers, const char *path)
{
    gchar *contents = NULL;
    gsize length = 0;
    if (!g_file_get_contents(path, &contents, &length, NULL))
        return;

    const char *needle = "public Php.zval ";
    const char *cursor = contents;
    while ((cursor = strstr(cursor, needle)) != NULL) {
        cursor += strlen(needle);

        const char *start = cursor;
        while (g_ascii_isalnum(*cursor) || *cursor == '_')
            cursor++;

        if (cursor > start) {
            gchar *name = g_strndup(start, cursor - start);
            if (g_str_has_suffix(name, "_zval")) {
                g_hash_table_add(wrappers, name);
            } else {
                g_free(name);
            }
        }
    }

    g_free(contents);
}

static gint
compare_strings(gconstpointer a, gconstpointer b)
{
    const char *left = *(const char * const *) a;
    const char *right = *(const char * const *) b;
    return g_strcmp0(left, right);
}

static gboolean
is_accepted_php_module(GHashTable *accepted_modules, const char *module_name)
{
    if (module_name == NULL)
        return FALSE;

    return g_hash_table_contains(accepted_modules, module_name);
}

static GHashTable *
create_baseline_php_modules(void)
{
    static const char *baseline_modules[] = {
        "Core",
        "Reflection",
        "SPL",
        "date",
        "hash",
        "json",
        "pcre",
        "random",
        "standard",
    };

    GHashTable *modules = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);

    for (guint i = 0; i < G_N_ELEMENTS(baseline_modules); i++)
        g_hash_table_add(modules, g_strdup(baseline_modules[i]));

    return modules;
}

static gboolean
add_internal_function_names(GPtrArray *functions, GHashTable *accepted_modules)
{
    zend_string *function_name;
    zend_function *function;

    ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(CG(function_table), function_name, function) {
        if (function_name == NULL || function == NULL)
            continue;
        if (function->type != ZEND_INTERNAL_FUNCTION)
            continue;

        zend_module_entry *module = function->internal_function.module;
        if (module == NULL || !is_accepted_php_module(accepted_modules, module->name))
            continue;

        const char *name = ZSTR_VAL(function_name);
        if (!is_vala_identifier(name))
            continue;
        g_ptr_array_add(functions, g_strdup(name));
    } ZEND_HASH_FOREACH_END();

    return TRUE;
}

int
main(int argc, char **argv)
{
    if (argc < 2) {
        g_printerr("usage: %s OUTPUT [EXPLICIT_VALA...]\n", argv[0]);
        return 2;
    }

    const char *output_path = argv[1];
    GHashTable *explicit_wrappers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
    GHashTable *accepted_modules = create_baseline_php_modules();
    for (int i = 2; i < argc; i++) {
        if (g_strcmp0(argv[i], "--php-module") == 0 && i + 1 < argc) {
            g_hash_table_add(accepted_modules, g_strdup(argv[++i]));
            continue;
        }
        if (g_str_has_prefix(argv[i], "--php-module=")) {
            g_hash_table_add(accepted_modules, g_strdup(argv[i] + strlen("--php-module=")));
            continue;
        }
        scan_explicit_wrappers(explicit_wrappers, argv[i]);
    }

    php_embed_module.phpinfo_as_text = 1;
    char *php_argv[] = { "valaphp-embed-zval-generator", NULL };
    if (php_embed_init(1, php_argv) != SUCCESS) {
        g_printerr("failed to initialize embedded PHP\n");
        g_hash_table_unref(accepted_modules);
        g_hash_table_unref(explicit_wrappers);
        return 1;
    }

    GPtrArray *functions = g_ptr_array_new_with_free_func(g_free);
    gboolean ok = add_internal_function_names(functions, accepted_modules);
    php_embed_shutdown();

    if (!ok) {
        g_printerr("failed to read PHP internal function table through libphp\n");
        g_ptr_array_unref(functions);
        g_hash_table_unref(accepted_modules);
        g_hash_table_unref(explicit_wrappers);
        return 1;
    }

    g_ptr_array_sort(functions, compare_strings);

    FILE *out = fopen(output_path, "w");
    if (out == NULL) {
        g_printerr("failed to open %s for writing\n", output_path);
        g_ptr_array_unref(functions);
        g_hash_table_unref(accepted_modules);
        g_hash_table_unref(explicit_wrappers);
        return 1;
    }

    fprintf(out, "/* Generated by tools/generate_embed_zval_wrappers.c through libphp internal function table. */\n");
    fprintf(out, "namespace PhpEmbed {\n");

    const char *previous = NULL;
    for (guint i = 0; i < functions->len; i++) {
        const char *function = g_ptr_array_index(functions, i);
        if (previous != NULL && strcmp(previous, function) == 0)
            continue;
        previous = function;

        gchar *wrapper = g_strdup_printf("%s_zval", function);
        if (g_hash_table_contains(explicit_wrappers, wrapper)) {
            g_free(wrapper);
            continue;
        }

        fprintf(out, "    public Php.zval %s (string php_file, int php_line, params Php.zval[] values) throws Error {\n", wrapper);
        fprintf(out, "        return call_zval_values (\"%s\", php_file, php_line, values);\n", function);
        fprintf(out, "    }\n\n");
        g_free(wrapper);
    }

    fprintf(out, "}\n");
    fclose(out);

    g_ptr_array_unref(functions);
    g_hash_table_unref(accepted_modules);
    g_hash_table_unref(explicit_wrappers);
    return 0;
}
