using Gee;

public class RuntimePackageMetadata : Object {
    private HashSet<string> native_runtime_only_functions = new HashSet<string> ();

    public static RuntimePackageMetadata load_for_vapi (string vapi_path) {
        var metadata = new RuntimePackageMetadata ();
        string metadata_path = vapi_path.substring (0, vapi_path.length - ".vapi".length) + ".php2vala-runtime";
        string contents;
        try {
            FileUtils.get_contents (metadata_path, out contents);
        } catch (Error e) {
            return metadata;
        }

        foreach (string raw_line in contents.split ("\n")) {
            string line = raw_line.strip ();
            if (line == "" || line.has_prefix ("#")) {
                continue;
            }

            int equals_index = line.index_of ("=");
            if (equals_index <= 0) {
                continue;
            }

            string key = line.substring (0, equals_index).strip ();
            string value = line.substring (equals_index + 1).strip ();
            if (key == "native-runtime-only") {
                foreach (string function_name in value.split (",")) {
                    string clean_name = function_name.strip ();
                    if (clean_name != "") {
                        metadata.native_runtime_only_functions.add (clean_name);
                    }
                }
            }
        }

        return metadata;
    }

    public bool is_native_runtime_only (string function_name) {
        return native_runtime_only_functions.contains (function_name);
    }
}

public class PhpToValaRuntimeBindings : Object {
    public static bool has_embed_runtime_binding (string function_name) {
        FunctionBinding? binding = load ("embed-runtime").get (function_name);
        return binding != null
            && binding.namespace_name == "PhpEmbed"
            && is_runtime_package (binding.package_name, "valaphp-runtime-embed");
    }

    public static bool is_runtime_package (string package_name, string module_name) {
        return package_name == module_name || package_name.has_prefix (module_name + "-");
    }

    public static HashMap<string, FunctionBinding> load (string runtime_name) {
        var bindings = new HashMap<string, FunctionBinding> ();
        bool allow_native = runtime_name == "native-runtime" || runtime_name == "both-runtimes";
        bool allow_embed = runtime_name == "embed-runtime" || runtime_name == "both-runtimes";
        bool include_native_runtime_only = runtime_name == "native-runtime";

        // Runtime naming contract:
        // - *_zval is the PHP-compatible dynamic ABI and must return Php.zval.
        // - *_static is the optimizer ABI and must not return Php.zval; it must
        //   return a concrete Vala type such as int, double, string, bool, void
        //   or a typed collection.
        // - PHP functions with input-dependent return types need separate typed
        //   static variants, while the mixed case stays in *_zval. Example:
        //   microtime(true) should map to a double-returning static variant;
        //   generic microtime remains zval.
        string? core_vapi_path = find_preferred_vapi_for_runtime_package ("valaphp-runtime-core");
        if (core_vapi_path != null) {
            load_vapi_functions (bindings, core_vapi_path, "Php", package_name_from_vapi_path (core_vapi_path), false);
        }
        add_core_static_aliases (bindings);

        if (allow_native) {
            var loaded_native_packages = new HashSet<string> ();
            foreach (string vapi_path in find_valaphp_vapis ()) {
                string basename = Path.get_basename (vapi_path);
                string package_name = package_name_from_vapi_path (vapi_path);
                if (!basename.has_prefix ("valaphp-runtime-")
                    || is_runtime_package (package_name, "valaphp-runtime-core")
                    || is_runtime_package (package_name, "valaphp-runtime-embed")) {
                    continue;
                }
                if (loaded_native_packages.contains (package_name)) {
                    continue;
                }
                loaded_native_packages.add (package_name);

                var metadata = RuntimePackageMetadata.load_for_vapi (vapi_path);
                load_vapi_functions (bindings, vapi_path, "PhpNative", package_name, false, metadata, include_native_runtime_only);
            }

            add_suffix_aliases (bindings, "PhpNative");
            add_native_static_aliases (bindings);
        }

        if (allow_embed) {
            string? embed_vapi_path = find_preferred_vapi_for_runtime_package ("valaphp-runtime-embed");
            if (embed_vapi_path != null) {
                load_vapi_functions (bindings, embed_vapi_path, "PhpEmbed", package_name_from_vapi_path (embed_vapi_path), false);
            }

            add_suffix_aliases (bindings, "PhpEmbed");
            add_embed_fallback_mapped (bindings, "json_decode", "json_decode_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "token_get_all", "token_get_all_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "token_name", "token_name_static", "string");
            add_embed_fallback_mapped (bindings, "json_encode", "json_encode_zval", "Php.zval");
            add_embed_fallback (bindings, "json_last_error_msg");
            add_embed_fallback (bindings, "phpinfo");
            add_embed_fallback_mapped (bindings, "print_r", "print_r_zval", "Php.zval");
            add_embed_fallback (bindings, "round");
            add_embed_fallback_mapped (bindings, "serialize", "serialize_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "var_dump", "var_dump_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "var_export", "var_export_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "unserialize", "unserialize_zval", "Php.zval");
            add_embed_fallback (bindings, "strlen");
            add_embed_fallback (bindings, "trim");
            add_embed_fallback (bindings, "ltrim");
            add_embed_fallback (bindings, "rtrim");
            add_embed_fallback_mapped (bindings, "strval", "strval_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "intval", "intval_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "boolval", "boolval_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "count", "count_zval", "Php.zval");
            add_embed_fallback (bindings, "flush");
            add_embed_fallback (bindings, "number_format");
            add_embed_fallback (bindings, "explode");
            add_embed_fallback (bindings, "str_replace");
            add_embed_fallback (bindings, "stripos");
            add_embed_fallback (bindings, "class_exists");
            add_embed_fallback_mapped (bindings, "gettype", "gettype_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "is_array", "is_array_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "is_object", "is_object_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "is_null", "is_null_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "is_string", "is_string_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "is_int", "is_int_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "is_integer", "is_integer_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "is_long", "is_long_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "is_float", "is_float_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "is_double", "is_double_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "is_real", "is_real_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "is_bool", "is_bool_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "is_numeric", "is_numeric_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "echo", "echo_zval", "Php.zval");
            add_embed_fallback (bindings, "substr");
            add_embed_fallback (bindings, "exec");
            add_embed_fallback (bindings, "shell_exec");
            add_embed_fallback (bindings, "file_get_contents");
            add_embed_fallback_mapped (bindings, "microtime", "microtime_zval", "Php.zval");
            add_embed_fallback (bindings, "floor");
            add_embed_fallback (bindings, "div");
            add_embed_fallback_mapped (bindings, "exit", "exit_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "die", "die_zval", "Php.zval");
            add_embed_fallback_mapped (bindings, "exit_no_args", "exit_no_args_static", "int");
            add_embed_fallback_mapped (bindings, "exit_int", "exit_int_static", "int");
            add_embed_fallback_mapped (bindings, "exit_string", "exit_string_static", "int");
            add_embed_fallback_mapped (bindings, "die_no_args", "die_no_args_static", "int");
            add_embed_fallback_mapped (bindings, "die_int", "die_int_static", "int");
            add_embed_fallback_mapped (bindings, "die_string", "die_string_static", "int");
        }

        return bindings;
    }

    private static void add_core_static_aliases (HashMap<string, FunctionBinding> bindings) {
        add_core_alias (bindings, "isset", "isset_value_static", "bool");
        add_core_alias (bindings, "empty", "empty_value_static", "bool");
        add_core_alias (bindings, "div", "div_static", "double");
    }

    private static void add_core_alias (HashMap<string, FunctionBinding> bindings, string function_name, string mapped_function_name, string return_type) {
        FunctionBinding? mapped = bindings.get (mapped_function_name);
        if (mapped == null || mapped.namespace_name != "Php") {
            bindings[function_name] = new FunctionBinding (
                "Php",
                "valaphp-runtime-core",
                return_type,
                true,
                mapped_function_name
            );
            return;
        }

        bindings[function_name] = new FunctionBinding (
            mapped.namespace_name,
            mapped.package_name,
            mapped.return_type,
            mapped.source_location_args,
            mapped_function_name,
            mapped.parameter_types,
            mapped.parameter_defaults,
            mapped.native_runtime_only
        );
    }

    private static void add_embed_fallback (HashMap<string, FunctionBinding> bindings, string function_name) {
        if (should_set_embed_fallback (bindings, function_name)) {
            string return_type = "var";
            switch (function_name) {
            case "json_decode":
            case "unserialize":
            case "microtime":
                return_type = "Php.zval";
                break;
            case "json_last_error_msg":
            case "json_encode":
            case "phpinfo":
            case "print_r":
            case "serialize":
            case "var_dump":
            case "var_export":
            case "gettype":
            case "trim":
            case "substr":
            case "exec":
            case "shell_exec":
            case "file_get_contents":
            case "strval":
            case "ltrim":
            case "rtrim":
            case "number_format":
            case "str_replace":
                return_type = "string";
                break;
            case "explode":
                return_type = "Gee.ArrayList<string>";
                break;
            case "strlen":
            case "count":
            case "intval":
                return_type = "int";
                break;
            case "boolval":
            case "is_array":
            case "is_object":
            case "is_null":
            case "is_string":
            case "is_int":
            case "is_integer":
            case "is_long":
            case "is_float":
            case "is_double":
            case "is_real":
            case "is_bool":
            case "is_numeric":
            case "flush":
            case "class_exists":
                return_type = "bool";
                break;
            case "round":
            case "div":
            case "floor":
                return_type = "double";
                break;
            }
            FunctionBinding? mapped = bindings.get (function_name + "_static");
            if (mapped != null && mapped.namespace_name == "PhpEmbed") {
                bindings[function_name] = binding_alias (mapped, function_name + "_static", php_parameter_defaults_for (function_name, mapped.parameter_defaults));
            } else {
                bindings[function_name] = new FunctionBinding ("PhpEmbed", "valaphp-runtime-embed", return_type, true, function_name + "_static");
            }
        }
    }

    private static void add_embed_fallback_mapped (HashMap<string, FunctionBinding> bindings, string function_name, string mapped_function_name, string return_type) {
        if (should_set_embed_fallback (bindings, function_name)) {
            FunctionBinding? mapped = bindings.get (mapped_function_name);
            if (mapped != null && mapped.namespace_name == "PhpEmbed") {
                bindings[function_name] = binding_alias (mapped, mapped_function_name, php_parameter_defaults_for (function_name, mapped.parameter_defaults));
            } else {
                bindings[function_name] = new FunctionBinding ("PhpEmbed", "valaphp-runtime-embed", return_type, true, mapped_function_name);
            }
        }
    }

    private static void add_native_static_aliases (HashMap<string, FunctionBinding> bindings) {
        // Native runtime modules expose the ABI contract directly:
        // - base PHP names map to the concrete *_static optimizer ABI
        // - *_zval remains available for dynamic zval code paths
        add_native_alias (bindings, "echo", "echo_static");
        add_native_alias (bindings, "number_format", "number_format_static");
        add_native_alias (bindings, "var_dump", "var_dump_static");
        add_native_alias (bindings, "flush", "flush_static");
        add_native_alias (bindings, "round", "round_static");
        add_native_alias (bindings, "floor", "floor_static");
        add_native_alias (bindings, "div", "div_static");
        add_native_alias (bindings, "microtime", "microtime_zval");
        add_native_alias (bindings, "exit", "exit_zval");
        add_native_alias (bindings, "die", "die_zval");
        add_native_alias (bindings, "exit_no_args", "exit_no_args_static");
        add_native_alias (bindings, "exit_int", "exit_int_static");
        add_native_alias (bindings, "exit_string", "exit_string_static");
        add_native_alias (bindings, "die_no_args", "die_no_args_static");
        add_native_alias (bindings, "die_int", "die_int_static");
        add_native_alias (bindings, "die_string", "die_string_static");
    }

    private static void add_suffix_aliases (HashMap<string, FunctionBinding> bindings, string namespace_name) {
        var function_names = new ArrayList<string> ();
        foreach (string function_name in bindings.keys) {
            function_names.add (function_name);
        }

        foreach (string suffix in new string[] {"_static", "_zval"}) {
            foreach (string function_name in function_names) {
                if (!function_name.has_suffix (suffix)) {
                    continue;
                }
                FunctionBinding? mapped = bindings.get (function_name);
                if (mapped == null || mapped.namespace_name != namespace_name) {
                    continue;
                }

                string php_function_name = function_name.substring (0, function_name.length - suffix.length);
                if (bindings.has_key (php_function_name)) {
                    continue;
                }

                bindings[php_function_name] = binding_alias (
                    mapped,
                    function_name,
                    php_parameter_defaults_for (php_function_name, mapped.parameter_defaults)
                );
            }
        }
    }

    private static void add_native_alias (HashMap<string, FunctionBinding> bindings, string function_name, string mapped_function_name) {
        FunctionBinding? mapped = bindings.get (mapped_function_name);
        if (mapped == null || mapped.namespace_name != "PhpNative") {
            return;
        }

        bindings[function_name] = new FunctionBinding (
            mapped.namespace_name,
            mapped.package_name,
            mapped.return_type,
            mapped.source_location_args,
            mapped_function_name,
            mapped.parameter_types,
            php_parameter_defaults_for (function_name, mapped.parameter_defaults)
        );
    }

    private static FunctionBinding binding_alias (FunctionBinding mapped, string mapped_function_name, ArrayList<string?>? parameter_defaults = null) {
        return new FunctionBinding (
            mapped.namespace_name,
            mapped.package_name,
            mapped.return_type,
            mapped.source_location_args,
            mapped_function_name,
            mapped.parameter_types,
            parameter_defaults ?? mapped.parameter_defaults,
            mapped.native_runtime_only
        );
    }

    private static ArrayList<string?> php_parameter_defaults_for (string function_name, ArrayList<string?> existing_defaults) {
        var defaults = new ArrayList<string?> ();
        for (int i = 0; i < existing_defaults.size; i++) {
            defaults.add (existing_defaults[i]);
        }

        if (function_name == "number_format" && defaults.size >= 4) {
            defaults[1] = "0";
            defaults[2] = "\".\"";
            defaults[3] = "\",\"";
        }
        if (function_name == "substr" && defaults.size >= 3) {
            defaults[2] = "2147483647";
        }

        return defaults;
    }

    private static bool should_set_embed_fallback (HashMap<string, FunctionBinding> bindings, string function_name) {
        FunctionBinding? binding = bindings.get (function_name);
        if (binding == null) {
            return true;
        }

        return binding.namespace_name == "PhpEmbed"
            && is_runtime_package (binding.package_name, "valaphp-runtime-embed")
            && binding.mapped_function_name == null
            && !function_name.has_suffix ("_static")
            && !function_name.has_suffix ("_zval");
    }

    private static void load_vapi_functions (
        HashMap<string, FunctionBinding> bindings,
        string vapi_path,
        string expected_namespace,
        string package_name,
        bool overwrite_existing,
        RuntimePackageMetadata? metadata = null,
        bool include_native_runtime_only = true
    ) {
        string contents;
        try {
            FileUtils.get_contents (vapi_path, out contents);
        } catch (Error e) {
            return;
        }

        string current_namespace = "";
        foreach (string raw_line in contents.split ("\n")) {
            string line = raw_line.strip ();

            if (line.has_prefix ("namespace ")) {
                string[] parts = line.split (" ");
                current_namespace = parts.length >= 2 ? parts[1] : "";
                continue;
            }

            if (current_namespace != expected_namespace || !line.has_prefix ("public static ")) {
                continue;
            }

            int open_paren = line.index_of ("(");
            if (open_paren < 0) {
                continue;
            }

            string before_args = line.substring (0, open_paren).strip ();
            string[] tokens = before_args.split (" ");
            if (tokens.length == 0) {
                continue;
            }

            string function_name = tokens[tokens.length - 1];
            string return_type = tokens.length >= 2 ? tokens[tokens.length - 2] : "var";
            if (function_name == "" || function_name.has_prefix ("@")) {
                continue;
            }

            string php_function_name = php_function_name_from_runtime_function_name (function_name);
            bool native_runtime_only = metadata != null && metadata.is_native_runtime_only (php_function_name);
            if (native_runtime_only && !include_native_runtime_only) {
                continue;
            }

            if (!overwrite_existing && bindings.has_key (function_name)) {
                continue;
            }

            string args = line.substring (open_paren + 1, line.index_of (")") - open_paren - 1);
            ArrayList<string> parameter_types;
            ArrayList<string?> parameter_defaults;
            parse_vapi_parameters (args, out parameter_types, out parameter_defaults);

            bool has_source_location = line.index_of ("string php_file") >= 0
                && line.index_of ("int php_line") >= 0;
            bindings[function_name] = new FunctionBinding (
                expected_namespace,
                package_name,
                return_type,
                has_source_location,
                null,
                parameter_types,
                parameter_defaults,
                native_runtime_only
            );
        }
    }

    private static string php_function_name_from_runtime_function_name (string function_name) {
        if (function_name.has_suffix ("_static")) {
            return function_name.substring (0, function_name.length - "_static".length);
        }
        if (function_name.has_suffix ("_zval")) {
            return function_name.substring (0, function_name.length - "_zval".length);
        }
        return function_name;
    }

    private static void parse_vapi_parameters (
        string args,
        out ArrayList<string> parameter_types,
        out ArrayList<string?> parameter_defaults
    ) {
        parameter_types = new ArrayList<string> ();
        parameter_defaults = new ArrayList<string?> ();

        if (args.strip () == "") {
            return;
        }

        foreach (string raw_parameter in split_vapi_parameters (args)) {
            string parameter = raw_parameter.strip ();
            int equals_index = parameter.index_of ("=");
            string default_value = "";
            if (equals_index >= 0) {
                default_value = parameter.substring (equals_index + 1).strip ();
                parameter = parameter.substring (0, equals_index).strip ();
            }

            if (parameter.has_prefix ("params ")) {
                parameter = parameter.substring ("params ".length).strip ();
            }

            int last_space = parameter.last_index_of (" ");
            if (last_space <= 0) {
                continue;
            }

            parameter_types.add (parameter.substring (0, last_space).strip ());
            parameter_defaults.add (default_value == "" ? null : default_value);
        }
    }

    private static ArrayList<string> split_vapi_parameters (string args) {
        var parts = new ArrayList<string> ();
        var current = new StringBuilder ();
        bool in_string = false;
        char string_quote = '\0';
        int depth = 0;

        for (int i = 0; i < args.length; i++) {
            char c = args[i];
            if (in_string) {
                current.append_c (c);
                if (c == string_quote && (i == 0 || args[i - 1] != '\\')) {
                    in_string = false;
                }
                continue;
            }

            if (c == '\'' || c == '"') {
                in_string = true;
                string_quote = c;
                current.append_c (c);
                continue;
            }

            if (c == '<' || c == '(' || c == '[') {
                depth++;
            } else if (c == '>' || c == ')' || c == ']') {
                depth--;
            } else if (c == ',' && depth == 0) {
                parts.add (current.str);
                current.truncate (0);
                continue;
            }

            current.append_c (c);
        }

        parts.add (current.str);
        return parts;
    }

    private static string package_name_from_vapi_path (string vapi_path) {
        string basename = Path.get_basename (vapi_path);
        if (basename.has_suffix (".vapi")) {
            return basename.substring (0, basename.length - ".vapi".length);
        }
        return basename;
    }

    private static ArrayList<string> find_valaphp_vapis () {
        var paths = new ArrayList<string> ();
        add_vapis_from_vala_flags (paths);
        add_vapis_from_pkg_config_path (paths);
        add_vapis_from_dir (paths, "/usr/local/share/vala/vapi");
        add_vapis_from_dir (paths, "/usr/share/vala/vapi");
        return paths;
    }

    private static string? find_preferred_vapi_for_runtime_package (string module_name) {
        string? fallback = null;
        foreach (string vapi_path in find_valaphp_vapis ()) {
            string package_name = package_name_from_vapi_path (vapi_path);
            if (!is_runtime_package (package_name, module_name)) {
                continue;
            }
            if (package_name == module_name) {
                return vapi_path;
            }
            if (fallback == null) {
                fallback = vapi_path;
            }
        }
        return fallback;
    }

    private static void add_vapis_from_vala_flags (ArrayList<string> paths) {
        string? vala_flags = Environment.get_variable ("VALAFLAGS");
        if (vala_flags == null || vala_flags == "") {
            return;
        }

        foreach (string token in vala_flags.split (" ")) {
            if (token.has_prefix ("--vapidir=")) {
                add_vapis_from_dir (paths, token.substring ("--vapidir=".length));
            }
        }
    }

    private static void add_vapis_from_pkg_config_path (ArrayList<string> paths) {
        string? pkg_config_path = Environment.get_variable ("PKG_CONFIG_PATH");
        if (pkg_config_path == null || pkg_config_path == "") {
            return;
        }

        foreach (string dir_path in pkg_config_path.split (":")) {
            if (dir_path == "") {
                continue;
            }

            string suffix = "/pkgconfig";
            if (!dir_path.has_suffix (suffix)) {
                continue;
            }

            string libdir = dir_path.substring (0, dir_path.length - suffix.length);
            string prefix = Path.get_dirname (libdir);
            add_vapis_from_dir (paths, Path.build_filename (prefix, "share", "vala", "vapi"));
        }
    }

    private static void add_vapis_from_dir (ArrayList<string> paths, string dir_path) {
        try {
            Dir dir = Dir.open (dir_path);
            string? name;
            while ((name = dir.read_name ()) != null) {
                if (name.has_prefix ("valaphp-runtime-") && name.has_suffix (".vapi")) {
                    string path = Path.build_filename (dir_path, name);
                    if (!paths.contains (path)) {
                        paths.add (path);
                    }
                }
            }
        } catch (Error e) {
        }
    }
}
