using Gee;

public class PhpToValaRuntimeCallResolver : Object {
    private PhpToValaTranspiler transpiler;
    private HashMap<string, bool> warned_missing_embed_static = new HashMap<string, bool> ();
    private HashMap<string, bool> informed_missing_native_runtime = new HashMap<string, bool> ();

    public PhpToValaRuntimeCallResolver (PhpToValaTranspiler transpiler) {
        this.transpiler = transpiler;
    }

    public void reset_transpile_state () {
        warned_missing_embed_static.clear ();
        informed_missing_native_runtime.clear ();
    }

    private bool is_user_defined_function (string function_name) {
        return transpiler.local_functions.has_key (function_name)
            || transpiler.external_functions.has_key (function_name)
            || transpiler.local_closure_variables.has_key (function_name);
    }

    public string map_php_function_name (string function_name, int php_line_number) throws Error {
        FunctionBinding? binding = transpiler.lookup_function_binding (function_name);
        if (binding == null) {
            throw transpiler.unsupported_function (php_line_number, function_name);
        }

        if (is_embed_runtime_binding (binding)) {
            info_missing_native_runtime_once (function_name, binding.mapped_function_name ?? function_name);
        }
        transpiler.mark_function_used (function_name);
        return binding.call_name (function_name);
    }

    public string map_php_runtime_function_name (string php_function_name, string runtime_function_name, int php_line_number) throws Error {
        FunctionBinding? binding = transpiler.lookup_function_binding (runtime_function_name);
        if (binding != null) {
            if (is_embed_runtime_binding (binding)) {
                info_missing_native_runtime_once (php_function_name, runtime_function_name);
            }
            transpiler.mark_function_used (runtime_function_name);
            return binding.call_name (runtime_function_name);
        }

        FunctionBinding? php_binding = transpiler.lookup_function_binding (php_function_name);
        if (php_binding != null && is_embed_runtime_binding (php_binding)) {
            info_missing_native_runtime_once (php_function_name, runtime_function_name);
            transpiler.mark_function_used (runtime_function_name);
            return php_binding.namespace_name + "." + runtime_function_name;
        }

        throw transpiler.unsupported_function (php_line_number, php_function_name);
    }

    public void note_embed_zval_fallback (string function_name, string runtime_function_name) {
        if (!has_native_runtime_candidate (function_name)) {
            info_missing_native_runtime_once (function_name, runtime_function_name);
        }
    }

    public string? resolve_static_call_name (string function_name) {
        if (function_name == "next") {
            return null;
        }
        string runtime_function_name = function_name + "_static";

        if (transpiler.get_runtime_name () == "native-runtime") {
            FunctionBinding? native_static = find_native_runtime_binding (runtime_function_name);
            if (native_static != null) {
                return native_static.call_name (runtime_function_name);
            }
            info_resolver_step_once (
                function_name,
                "native-static",
                "info: TODO in php2vala: static implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying native zval".printf (function_name)
            );
            return null;
        }

        if (transpiler.get_runtime_name () == "embed-runtime") {
            FunctionBinding? embed_static = find_embed_runtime_binding (runtime_function_name);
            if (embed_static != null) {
                return embed_static.call_name (runtime_function_name);
            }
            warn_missing_embed_static_once (function_name);
            return null;
        }

        if (transpiler.get_runtime_name () == "both-runtimes") {
            FunctionBinding? native_static = find_native_runtime_binding (runtime_function_name);
            if (native_static != null) {
                return native_static.call_name (runtime_function_name);
            }

            info_resolver_step_once (
                function_name,
                "native-static",
                "info: TODO in php2vala: static implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying native zval".printf (function_name)
            );

            if (find_native_runtime_binding (function_name + "_zval") != null) {
                return null;
            }

            info_resolver_step_once (
                function_name,
                "native-zval",
                "info: TODO in php2vala: zval implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying embed static".printf (function_name)
            );

            FunctionBinding? embed_static = find_embed_runtime_binding (runtime_function_name);
            if (embed_static != null) {
                return embed_static.call_name (runtime_function_name);
            }

            warn_missing_embed_static_once (function_name);
            return null;
        }

        return null;
    }

    public FunctionBinding? resolve_dynamic_static_binding_from_vapi (
        string function_name,
        int arg_count,
        out string runtime_function_name
    ) {
        runtime_function_name = "";
        if (is_php_construct_runtime_pseudo_function (function_name)) {
            return null;
        }

        if (transpiler.get_runtime_name () == "native-runtime") {
            FunctionBinding? native_static = find_runtime_static_binding_for_arg_count (
                transpiler.native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (native_static != null) {
                return native_static;
            }
            info_resolver_step_once (
                function_name,
                "native-static",
                "info: TODO in php2vala: static implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying native zval".printf (function_name)
            );
            return null;
        }

        if (transpiler.get_runtime_name () == "embed-runtime") {
            FunctionBinding? embed_static = find_runtime_static_binding_for_arg_count (
                transpiler.embed_function_bindings,
                "PhpEmbed",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (embed_static != null) {
                return embed_static;
            }
            warn_missing_embed_static_once (function_name);
            return null;
        }

        if (transpiler.get_runtime_name () == "both-runtimes") {
            FunctionBinding? native_static = find_runtime_static_binding_for_arg_count (
                transpiler.native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (native_static != null) {
                return native_static;
            }

            info_resolver_step_once (
                function_name,
                "native-static",
                "info: TODO in php2vala: static implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying native zval".printf (function_name)
            );

            if (find_native_runtime_binding (function_name + "_zval") != null) {
                return null;
            }

            info_resolver_step_once (
                function_name,
                "native-zval",
                "info: TODO in php2vala: zval implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying embed static".printf (function_name)
            );

            FunctionBinding? embed_static = find_runtime_static_binding_for_arg_count (
                transpiler.embed_function_bindings,
                "PhpEmbed",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (embed_static != null) {
                return embed_static;
            }

            warn_missing_embed_static_once (function_name);
            return null;
        }

        return null;
    }

    public FunctionBinding? resolve_dynamic_static_binding_from_vapi_for_args (
        string function_name,
        ArrayList<string> zargs,
        out string runtime_function_name
    ) {
        runtime_function_name = "";
        if (is_php_construct_runtime_pseudo_function (function_name)) {
            return null;
        }

        if (transpiler.get_runtime_name () == "native-runtime") {
            FunctionBinding? native_static = find_runtime_static_binding_for_args (
                transpiler.native_function_bindings,
                "PhpNative",
                function_name,
                zargs,
                out runtime_function_name
            );
            if (native_static != null) {
                return native_static;
            }
            info_resolver_step_once (
                function_name,
                "native-static",
                "info: TODO in php2vala: static implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying native zval".printf (function_name)
            );
            return null;
        }

        if (transpiler.get_runtime_name () == "embed-runtime") {
            FunctionBinding? embed_static = find_runtime_static_binding_for_args (
                transpiler.embed_function_bindings,
                "PhpEmbed",
                function_name,
                zargs,
                out runtime_function_name
            );
            if (embed_static != null) {
                return embed_static;
            }
            warn_missing_embed_static_once (function_name);
            return null;
        }

        if (transpiler.get_runtime_name () == "both-runtimes") {
            FunctionBinding? native_static = find_runtime_static_binding_for_args (
                transpiler.native_function_bindings,
                "PhpNative",
                function_name,
                zargs,
                out runtime_function_name
            );
            if (native_static != null) {
                return native_static;
            }

            info_resolver_step_once (
                function_name,
                "native-static",
                "info: TODO in php2vala: static implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying native zval".printf (function_name)
            );

            if (find_native_runtime_binding (function_name + "_zval") != null) {
                return null;
            }

            info_resolver_step_once (
                function_name,
                "native-zval",
                "info: TODO in php2vala: zval implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying embed static".printf (function_name)
            );

            FunctionBinding? embed_static = find_runtime_static_binding_for_args (
                transpiler.embed_function_bindings,
                "PhpEmbed",
                function_name,
                zargs,
                out runtime_function_name
            );
            if (embed_static != null) {
                return embed_static;
            }

            warn_missing_embed_static_once (function_name);
            return null;
        }

        return null;
    }

    public FunctionBinding? resolve_dynamic_static_binding_for_native_local (
        string function_name,
        int arg_count,
        out string runtime_function_name
    ) {
        runtime_function_name = "";
        if (is_php_construct_runtime_pseudo_function (function_name)) {
            return null;
        }

        if (transpiler.get_runtime_name () == "native-runtime") {
            return find_runtime_static_binding_for_arg_count_native_local (
                transpiler.native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
        }

        if (transpiler.get_runtime_name () == "embed-runtime") {
            return find_runtime_static_binding_for_arg_count_native_local (
                transpiler.embed_function_bindings,
                "PhpEmbed",
                function_name,
                arg_count,
                out runtime_function_name
            );
        }

        if (transpiler.get_runtime_name () == "both-runtimes") {
            FunctionBinding? native_static = find_runtime_static_binding_for_arg_count_native_local (
                transpiler.native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (native_static != null) {
                return native_static;
            }

            return find_runtime_static_binding_for_arg_count_native_local (
                transpiler.embed_function_bindings,
                "PhpEmbed",
                function_name,
                arg_count,
                out runtime_function_name
            );
        }

        return null;
    }

    public FunctionBinding? resolve_dynamic_static_binding_for_native_local_args (
        string function_name,
        ArrayList<string> zargs,
        out string runtime_function_name
    ) {
        runtime_function_name = "";
        if (is_php_construct_runtime_pseudo_function (function_name)) {
            return null;
        }

        if (transpiler.get_runtime_name () == "native-runtime") {
            return find_runtime_static_binding_for_args (
                transpiler.native_function_bindings,
                "PhpNative",
                function_name,
                zargs,
                out runtime_function_name,
                true
            );
        }

        if (transpiler.get_runtime_name () == "embed-runtime") {
            return find_runtime_static_binding_for_args (
                transpiler.embed_function_bindings,
                "PhpEmbed",
                function_name,
                zargs,
                out runtime_function_name,
                true
            );
        }

        if (transpiler.get_runtime_name () == "both-runtimes") {
            FunctionBinding? native_static = find_runtime_static_binding_for_args (
                transpiler.native_function_bindings,
                "PhpNative",
                function_name,
                zargs,
                out runtime_function_name,
                true
            );
            if (native_static != null) {
                return native_static;
            }

            return find_runtime_static_binding_for_args (
                transpiler.embed_function_bindings,
                "PhpEmbed",
                function_name,
                zargs,
                out runtime_function_name,
                true
            );
        }

        return null;
    }

    private FunctionBinding? find_runtime_static_binding_for_arg_count (
        HashMap<string, FunctionBinding> bindings,
        string namespace_name,
        string function_name,
        int arg_count,
        out string runtime_function_name
    ) {
        runtime_function_name = "";
        FunctionBinding? base_binding = bindings.get (function_name);
        if (base_binding != null
            && base_binding.namespace_name == namespace_name
            && base_binding.mapped_function_name != null
            && base_binding.mapped_function_name.has_suffix ("_static")
            && base_binding.accepts_runtime_arg_count (arg_count)
            && can_convert_runtime_static_binding (base_binding)) {
            runtime_function_name = base_binding.mapped_function_name;
            return base_binding;
        }
        FunctionBinding? exact = bindings.get (function_name + "_static");
        if (exact != null
            && exact.namespace_name == namespace_name
            && exact.accepts_runtime_arg_count (arg_count)
            && can_convert_runtime_static_binding (exact)) {
            runtime_function_name = function_name + "_static";
            return exact;
        }

        foreach (string candidate_name in bindings.keys) {
            if (!PhpToValaTranspiler.is_static_variant_for_php_function (function_name, candidate_name)) {
                continue;
            }
            FunctionBinding? candidate = bindings.get (candidate_name);
            if (candidate == null
                || candidate.namespace_name != namespace_name
                || !candidate.accepts_runtime_arg_count (arg_count)
                || !can_convert_runtime_static_binding (candidate)) {
                continue;
            }

            runtime_function_name = candidate_name;
            return candidate;
        }

        return null;
    }

    private FunctionBinding? find_runtime_static_binding_for_args (
        HashMap<string, FunctionBinding> bindings,
        string namespace_name,
        string function_name,
        ArrayList<string> zargs,
        out string runtime_function_name,
        bool require_native_local_return = false
    ) {
        runtime_function_name = "";
        FunctionBinding? best_binding = null;
        string best_runtime_function_name = "";
        int best_score = -2147483647;
        int best_order = 2147483647;
        int order = 0;

        FunctionBinding? base_binding = bindings.get (function_name);
        if (base_binding != null
            && base_binding.namespace_name == namespace_name
            && base_binding.mapped_function_name != null
            && base_binding.mapped_function_name.has_suffix ("_static")) {
            consider_runtime_static_binding_for_args (
                base_binding,
                base_binding.mapped_function_name,
                zargs,
                require_native_local_return,
                order++,
                ref best_binding,
                ref best_runtime_function_name,
                ref best_score,
                ref best_order
            );
        }

        FunctionBinding? exact = bindings.get (function_name + "_static");
        if (exact != null && exact.namespace_name == namespace_name) {
            consider_runtime_static_binding_for_args (
                exact,
                function_name + "_static",
                zargs,
                require_native_local_return,
                order++,
                ref best_binding,
                ref best_runtime_function_name,
                ref best_score,
                ref best_order
            );
        }

        foreach (string candidate_name in bindings.keys) {
            if (!PhpToValaTranspiler.is_static_variant_for_php_function (function_name, candidate_name)) {
                continue;
            }
            FunctionBinding? candidate = bindings.get (candidate_name);
            if (candidate == null || candidate.namespace_name != namespace_name) {
                continue;
            }
            consider_runtime_static_binding_for_args (
                candidate,
                candidate_name,
                zargs,
                require_native_local_return,
                order++,
                ref best_binding,
                ref best_runtime_function_name,
                ref best_score,
                ref best_order
            );
        }

        if (best_binding == null) {
            return null;
        }

        runtime_function_name = best_runtime_function_name;
        return best_binding;
    }

    private void consider_runtime_static_binding_for_args (
        FunctionBinding binding,
        string candidate_name,
        ArrayList<string> zargs,
        bool require_native_local_return,
        int order,
        ref FunctionBinding? best_binding,
        ref string best_runtime_function_name,
        ref int best_score,
        ref int best_order
    ) {
        if (!binding.accepts_runtime_arg_count (zargs.size)
            || !can_convert_runtime_static_binding (binding)
            || !can_convert_runtime_static_arguments (binding, zargs)
            || (require_native_local_return && !transpiler.can_use_runtime_static_return_as_native_local (binding.return_type))) {
            return;
        }

        int score = runtime_static_argument_specificity_score (binding, zargs);
        if (best_binding == null || score > best_score || (score == best_score && order < best_order)) {
            best_binding = binding;
            best_runtime_function_name = candidate_name;
            best_score = score;
            best_order = order;
        }
    }

    private int runtime_static_argument_specificity_score (FunctionBinding binding, ArrayList<string> zargs) {
        int score = 0;
        for (int i = 0; i < zargs.size; i++) {
            string parameter_type = PhpToValaTranspiler.normalize_runtime_type (binding.parameter_types[binding.runtime_parameter_index (i)]);
            string arg_type = PhpToValaTranspiler.normalize_runtime_type (transpiler.infer_expression_type (zargs[i]));
            if (parameter_type == "Php.zval" || parameter_type == "Php.zval[]") {
                continue;
            }
            if (parameter_type == arg_type) {
                score += 40;
            } else if (PhpToValaTranspiler.is_runtime_object_type (parameter_type)) {
                score += 30;
            } else {
                score += 10;
            }
        }
        return score;
    }

    private static bool is_php_construct_runtime_pseudo_function (string function_name) {
        return PhpToValaGeneratorUtils.is_php_construct_runtime_pseudo_function (function_name);
    }

    private FunctionBinding? find_runtime_static_binding_for_arg_count_native_local (
        HashMap<string, FunctionBinding> bindings,
        string namespace_name,
        string function_name,
        int arg_count,
        out string runtime_function_name
    ) {
        runtime_function_name = "";
        FunctionBinding? base_binding = bindings.get (function_name);
        if (base_binding != null
            && base_binding.namespace_name == namespace_name
            && base_binding.mapped_function_name != null
            && base_binding.mapped_function_name.has_suffix ("_static")
            && base_binding.accepts_runtime_arg_count (arg_count)
            && can_convert_runtime_static_binding (base_binding)
            && transpiler.can_use_runtime_static_return_as_native_local (base_binding.return_type)) {
            runtime_function_name = base_binding.mapped_function_name;
            return base_binding;
        }
        foreach (string candidate_name in bindings.keys) {
            if (!PhpToValaTranspiler.is_static_variant_for_php_function (function_name, candidate_name)) {
                continue;
            }
            FunctionBinding? candidate = bindings.get (candidate_name);
            if (candidate == null
                || candidate.namespace_name != namespace_name
                || !candidate.accepts_runtime_arg_count (arg_count)
                || !can_convert_runtime_static_binding (candidate)
                || !transpiler.can_use_runtime_static_return_as_native_local (candidate.return_type)) {
                continue;
            }

            runtime_function_name = candidate_name;
            return candidate;
        }

        FunctionBinding? exact = bindings.get (function_name + "_static");
        if (exact != null
            && exact.namespace_name == namespace_name
            && exact.accepts_runtime_arg_count (arg_count)
            && can_convert_runtime_static_binding (exact)
            && transpiler.can_use_runtime_static_return_as_native_local (exact.return_type)) {
            runtime_function_name = function_name + "_static";
            return exact;
        }

        return null;
    }

    private bool can_convert_runtime_static_binding (FunctionBinding binding) {
        if (!can_convert_runtime_static_return_type (binding.return_type)) {
            return false;
        }
        for (int i = 0; i < binding.runtime_parameter_count (); i++) {
            string parameter_type = binding.parameter_types[binding.runtime_parameter_index (i)];
            if (!can_convert_runtime_static_parameter_type (parameter_type)) {
                return false;
            }
        }
        return true;
    }

    public bool can_convert_runtime_static_return_type (string type_name) {
        switch (PhpToValaTranspiler.normalize_runtime_type (type_name)) {
        case "bool":
        case "boolean":
        case "int":
        case "int64":
        case "long":
        case "double":
        case "float":
        case "string":
        case "Php.zval":
            return true;
        default:
            if (PhpToValaTranspiler.normalize_runtime_type (type_name).has_suffix (".RoundMode")) {
                return true;
            }
            return PhpToValaTranspiler.is_runtime_object_type (type_name);
        }
    }

    private bool can_convert_runtime_static_parameter_type (string type_name) {
        switch (PhpToValaTranspiler.normalize_runtime_type (type_name)) {
        case "bool":
        case "boolean":
        case "int":
        case "int64":
        case "long":
        case "double":
        case "float":
        case "string":
        case "Php.zval":
            return true;
        default:
            if (PhpToValaTranspiler.normalize_runtime_type (type_name).has_suffix (".RoundMode")) {
                return true;
            }
            return PhpToValaTranspiler.is_runtime_object_type (type_name);
        }
    }

    public bool can_convert_runtime_static_arguments (FunctionBinding binding, ArrayList<string> zargs) {
        if (!binding.accepts_runtime_arg_count (zargs.size)) {
            return false;
        }
        for (int i = 0; i < zargs.size; i++) {
            string parameter_type = transpiler.runtime_parameter_type_for_arg (binding, i);
            if (PhpToValaTranspiler.is_out_runtime_parameter_type (parameter_type)) {
                string? out_name = PhpToValaTranspiler.zval_wrapped_identifier (zargs[i]);
                if (out_name == null || !PhpToValaTranspiler.is_identifier (out_name)) {
                    return false;
                }
            }
            if (PhpToValaTranspiler.is_runtime_object_type (parameter_type)
                && PhpToValaTranspiler.normalize_runtime_type (parameter_type) != PhpToValaTranspiler.normalize_runtime_type (transpiler.infer_expression_type (zargs[i]))) {
                return false;
            }
        }
        return true;
    }

    private bool runtime_binding_accepts_arg_count (FunctionBinding binding, int arg_count) {
        int offset = binding.runtime_parameter_offset ();
        int limit = binding.runtime_parameter_limit ();
        if (limit < offset) {
            return false;
        }
        return binding.accepts_runtime_arg_count (arg_count);
    }

    private bool can_use_runtime_zval_binding (FunctionBinding binding) {
        if (PhpToValaTranspiler.normalize_runtime_type (binding.return_type) != "Php.zval") {
            return false;
        }
        int offset = binding.runtime_parameter_offset ();
        int limit = binding.runtime_parameter_limit ();
        for (int i = offset; i < limit; i++) {
            string parameter_type = PhpToValaTranspiler.normalize_runtime_type (binding.parameter_types[i]);
            if (parameter_type == "Php.zval" || parameter_type == "Php.zval[]") {
                continue;
            }
            if (PhpToValaTranspiler.is_out_runtime_parameter_type (binding.parameter_types[i])
                && PhpToValaTranspiler.normalize_runtime_type (binding.parameter_types[i]) == "Php.zval") {
                continue;
            }
            return false;
        }
        return true;
    }

    private FunctionBinding? find_runtime_zval_binding_for_arg_count (
        HashMap<string, FunctionBinding> bindings,
        string namespace_name,
        string function_name,
        int arg_count,
        out string runtime_function_name
    ) {
        runtime_function_name = "";
        FunctionBinding? exact = bindings.get (function_name + "_zval");
        if (exact != null
            && exact.namespace_name == namespace_name
            && runtime_binding_accepts_arg_count (exact, arg_count)
            && can_use_runtime_zval_binding (exact)) {
            runtime_function_name = function_name + "_zval";
            return exact;
        }

        foreach (string candidate_name in bindings.keys) {
            if (!PhpToValaGeneratorUtils.is_zval_variant_for_php_function (function_name, candidate_name)) {
                continue;
            }
            FunctionBinding? candidate = bindings.get (candidate_name);
            if (candidate == null
                || candidate.namespace_name != namespace_name
                || !runtime_binding_accepts_arg_count (candidate, arg_count)
                || !can_use_runtime_zval_binding (candidate)) {
                continue;
            }

            runtime_function_name = candidate_name;
            return candidate;
        }

        return null;
    }

    public FunctionBinding? resolve_zval_runtime_binding_for_arg_count (string function_name, int arg_count, out string runtime_function_name) {
        runtime_function_name = "";

        if (transpiler.get_runtime_name () == "native-runtime") {
            FunctionBinding? native_zval = find_runtime_zval_binding_for_arg_count (
                transpiler.native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (native_zval != null) {
                return native_zval;
            }
            return null;
        }

        if (transpiler.get_runtime_name () == "both-runtimes") {
            FunctionBinding? native_zval = find_runtime_zval_binding_for_arg_count (
                transpiler.native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (native_zval != null) {
                return native_zval;
            }
            info_resolver_step_once (
                function_name,
                "native-zval",
                "info: TODO in php2vala: zval implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying embed zval".printf (function_name)
            );
        }

        FunctionBinding? embed_zval = find_runtime_zval_binding_for_arg_count (
            transpiler.embed_function_bindings,
            "PhpEmbed",
            function_name,
            arg_count,
            out runtime_function_name
        );
        if (embed_zval != null) {
            return embed_zval;
        }

        return null;
    }

    public FunctionBinding? resolve_zval_runtime_binding_for_arg_count_silent (string function_name, int arg_count, out string runtime_function_name) {
        runtime_function_name = "";

        if (transpiler.get_runtime_name () == "native-runtime") {
            return find_runtime_zval_binding_for_arg_count (
                transpiler.native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
        }

        if (transpiler.get_runtime_name () == "both-runtimes") {
            FunctionBinding? native_zval = find_runtime_zval_binding_for_arg_count (
                transpiler.native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (native_zval != null) {
                return native_zval;
            }
        }

        return find_runtime_zval_binding_for_arg_count (
            transpiler.embed_function_bindings,
            "PhpEmbed",
            function_name,
            arg_count,
            out runtime_function_name
        );
    }

    public FunctionBinding? resolve_zval_runtime_binding (string function_name, string runtime_function_name) {
        if (transpiler.get_runtime_name () == "both-runtimes") {
            if (transpiler.optimizes_local_vars () || transpiler.optimizes_file_func_vars ()) {
                if (find_native_runtime_binding (function_name + "_static") == null) {
                    info_resolver_step_once (
                        function_name,
                        "native-static",
                        "info: TODO in php2vala: static implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying native zval".printf (function_name)
                    );
                }
            }

            FunctionBinding? native_zval = find_native_runtime_binding (runtime_function_name);
            if (native_zval != null) {
                return native_zval;
            }

            info_resolver_step_once (
                function_name,
                "native-zval",
                "info: TODO in php2vala: zval implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying embed zval".printf (function_name)
            );

            FunctionBinding? embed_zval = find_embed_runtime_binding (runtime_function_name);
            if (embed_zval != null) {
                return embed_zval;
            }
            return null;
        }

        if (transpiler.get_runtime_name () == "native-runtime") {
            if (transpiler.optimizes_local_vars () || transpiler.optimizes_file_func_vars ()) {
                if (find_native_runtime_binding (function_name + "_static") == null) {
                    info_resolver_step_once (
                        function_name,
                        "native-static",
                        "info: TODO in php2vala: static implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying native zval".printf (function_name)
                    );
                }
            }
            return find_native_runtime_binding (runtime_function_name);
        }

        if (transpiler.optimizes_local_vars () || transpiler.optimizes_file_func_vars ()) {
            if (runtime_function_name.has_suffix ("_static")) {
                warn_missing_embed_static_once (function_name);
            }
        }
        FunctionBinding? embed_binding = find_embed_runtime_binding (runtime_function_name);
        if (embed_binding != null) {
            return embed_binding;
        }
        return null;
    }

    public void info_resolver_step_once (string function_name, string step_name, string message) {
        if (function_name == "next") {
            return;
        }
        if (is_user_defined_function (function_name)
            || (!has_native_runtime_candidate (function_name) && !has_embed_runtime_candidate (function_name))) {
            return;
        }
        string key = function_name + "|" + step_name;
        if (informed_missing_native_runtime.has_key (key)) {
            return;
        }

        informed_missing_native_runtime[key] = true;
        stderr.printf ("%s\n", message);
    }

    public void warn_missing_embed_static_once (string function_name) {
        if (is_user_defined_function (function_name)) {
            return;
        }
        if (!(transpiler.optimizes_local_vars () || transpiler.optimizes_file_func_vars ())) {
            return;
        }
        if (has_embed_static_binding (function_name)) {
            return;
        }
        if (warned_missing_embed_static.has_key (function_name)) {
            return;
        }

        warned_missing_embed_static[function_name] = true;
        stderr.printf (
            "info: TODO in php2vala: static implementation in valaphp-runtime-embed missing for PHP function '%s'; falling back to zval\n",
            function_name
        );
    }

    private void info_missing_native_runtime_once (string function_name, string runtime_function_name) {
        if (is_user_defined_function (function_name)) {
            return;
        }
        if (transpiler.get_runtime_name () != "both-runtimes") {
            return;
        }
        string key = function_name + "|" + runtime_function_name;
        if (informed_missing_native_runtime.has_key (key)) {
            return;
        }

        informed_missing_native_runtime[key] = true;
        string embed_fallback_kind = "embed fallback";
        if (runtime_function_name.has_suffix ("_static")) {
            embed_fallback_kind = "embed static fallback";
        } else if (runtime_function_name.has_suffix ("_zval")) {
            embed_fallback_kind = "embed zval fallback";
        }
        stderr.printf (
            "info: TODO in php2vala: native implementation in valaphp-runtime-* missing for PHP function '%s' via '%s'; using %s\n",
            function_name,
            runtime_function_name,
            embed_fallback_kind
        );
    }

    private bool has_embed_static_binding (string function_name) {
        FunctionBinding? binding = find_embed_runtime_binding (function_name + "_static");
        return binding != null && is_embed_runtime_binding (binding);
    }

    public bool has_embed_runtime_candidate (string function_name) {
        FunctionBinding? direct = find_embed_runtime_binding (function_name);
        if (direct != null && is_embed_runtime_binding (direct)) {
            return true;
        }
        FunctionBinding? static_binding = find_embed_runtime_binding (function_name + "_static");
        if (static_binding != null && is_embed_runtime_binding (static_binding)) {
            return true;
        }
        FunctionBinding? zval_binding = find_embed_runtime_binding (function_name + "_zval");
        return zval_binding != null && is_embed_runtime_binding (zval_binding);
    }

    private bool has_native_runtime_binding (string runtime_function_name) {
        return find_native_runtime_binding (runtime_function_name) != null;
    }

    public bool has_native_runtime_candidate (string function_name) {
        if (has_native_runtime_binding (function_name)
            || has_native_runtime_binding (function_name + "_static")
            || has_native_runtime_binding (function_name + "_zval")) {
            return true;
        }

        foreach (string runtime_function_name in transpiler.native_function_bindings.keys) {
            if (PhpToValaTranspiler.is_static_variant_for_php_function (function_name, runtime_function_name)
                || PhpToValaTranspiler.is_zval_variant_for_php_function (function_name, runtime_function_name)) {
                FunctionBinding? binding = find_native_runtime_binding (runtime_function_name);
                if (binding != null) {
                    return true;
                }
            }
        }

        return false;
    }

    public FunctionBinding? find_native_runtime_binding (string runtime_function_name) {
        FunctionBinding? binding = transpiler.native_function_bindings.get (runtime_function_name);
        if (binding == null || binding.namespace_name != "PhpNative") {
            return null;
        }
        return binding;
    }

    private FunctionBinding? find_embed_runtime_binding (string runtime_function_name) {
        FunctionBinding? binding = transpiler.embed_function_bindings.get (runtime_function_name);
        if (binding == null || !is_embed_runtime_binding (binding)) {
            return null;
        }
        return binding;
    }

    private bool is_embed_runtime_binding (FunctionBinding binding) {
        return binding.namespace_name == "PhpEmbed"
            && PhpToValaRuntimeBindings.is_runtime_package (binding.package_name, "valaphp-runtime-embed");
    }

    public string? convert_function_args_from_binding (FunctionBinding? binding, string converted_args, int php_line_number) {
        if (binding == null) {
            return null;
        }

        var args = new ArrayList<string> ();
        if (converted_args.strip () != "") {
            foreach (string arg in PhpToValaGeneratorUtils.split_top_level (converted_args, ',')) {
                args.add (arg.strip ());
            }
        }

        int runtime_parameter_count = binding.runtime_parameter_count ();
        while (args.size < runtime_parameter_count) {
            int parameter_index = binding.runtime_parameter_index (args.size);
            if (parameter_index >= binding.parameter_defaults.size || binding.parameter_defaults[parameter_index] == null) {
                return null;
            }
            args.add (transpiler.runtime_parameter_default_value (binding, parameter_index, php_line_number));
        }

        var output = new StringBuilder ();
        if (binding.source_location_args_first ()) {
            output.append (transpiler.source_location_args (php_line_number).substring (2));
        }
        for (int i = 0; i < args.size; i++) {
            if (output.len > 0) {
                output.append (", ");
            }
            string parameter_type = transpiler.runtime_parameter_type_for_arg (binding, i);
            if (PhpToValaTranspiler.is_out_runtime_parameter_type (parameter_type)) {
                string? out_name = PhpToValaTranspiler.zval_wrapped_identifier (args[i]);
                output.append ("out " + (out_name ?? args[i]));
            } else {
                output.append (args[i]);
            }
        }

        if (binding.source_location_args && !binding.source_location_args_first ()) {
            if (output.len == 0) {
                output.append (transpiler.source_location_args (php_line_number).substring (2));
            } else {
                output.append (transpiler.source_location_args (php_line_number));
            }
        }

        return output.str;
    }

    public string build_zval_runtime_call (FunctionBinding binding, string runtime_function_name, ArrayList<string> zargs, int php_line_number) {
        string call_name = binding.call_name (runtime_function_name);
        string argument_list = build_zval_runtime_argument_list (binding, zargs, php_line_number);
        return "%s (%s)".printf (call_name, argument_list);
    }

    private string build_zval_runtime_argument_list (FunctionBinding binding, ArrayList<string> zargs, int php_line_number) {
        var output = new StringBuilder ();
        if (binding.source_location_args_first ()) {
            output.append (transpiler.source_location_args (php_line_number).substring (2));
        }

        int call_arg_count = zargs.size > binding.runtime_parameter_count () ? zargs.size : binding.runtime_parameter_count ();
        for (int i = 0; i < call_arg_count; i++) {
            if (output.len > 0) {
                output.append (", ");
            }
            string parameter_type = transpiler.runtime_parameter_type_for_arg (binding, i);
            if (i >= zargs.size) {
                int parameter_index = binding.runtime_parameter_index (i);
                output.append (transpiler.runtime_parameter_default_value (binding, parameter_index, php_line_number));
                continue;
            }
            if (PhpToValaTranspiler.is_out_runtime_parameter_type (parameter_type)) {
                string zarg = zargs[i].strip ();
                string out_name = PhpToValaTranspiler.zval_wrapped_identifier (zarg) ?? zarg;
                output.append ("out " + out_name);
            } else {
                output.append (zargs[i]);
            }
        }

        if (binding.source_location_args && !binding.source_location_args_first ()) {
            if (output.len > 0) {
                output.append (", ");
            }
            output.append (transpiler.source_location_args (php_line_number).substring (2));
        }
        return output.str;
    }

    public string build_dynamic_static_argument_list (FunctionBinding binding, ArrayList<string> zargs, int php_line_number) {
        var output = new StringBuilder ();
        if (!binding.accepts_runtime_arg_count (zargs.size)) {
            return "";
        }
        if (binding.source_location_args_first ()) {
            output.append (transpiler.source_location_args (php_line_number).substring (2));
        }

        int call_arg_count = zargs.size > binding.runtime_parameter_count () ? zargs.size : binding.runtime_parameter_count ();
        for (int i = 0; i < call_arg_count; i++) {
            if (output.len > 0) {
                output.append (", ");
            }
            string parameter_type = transpiler.runtime_parameter_type_for_arg (binding, i);
            if (i >= zargs.size) {
                int parameter_index = binding.runtime_parameter_index (i);
                output.append (transpiler.runtime_parameter_default_value (binding, parameter_index, php_line_number));
                continue;
            }
            if (PhpToValaTranspiler.is_out_runtime_parameter_type (parameter_type)) {
                string? out_name = PhpToValaTranspiler.zval_wrapped_identifier (zargs[i]);
                output.append ("out " + (out_name ?? zargs[i]));
            } else {
                output.append (convert_zval_arg_for_runtime_static_type (zargs[i], parameter_type));
            }
        }

        if (binding.source_location_args && !binding.source_location_args_first ()) {
            if (output.len == 0) {
                output.append (transpiler.source_location_args (php_line_number).substring (2));
            } else {
                output.append (transpiler.source_location_args (php_line_number));
            }
        }

        return output.str;
    }

    public string convert_zval_arg_for_runtime_static_type (string zarg, string type_name) {
        switch (PhpToValaTranspiler.normalize_runtime_type (type_name)) {
        case "bool":
        case "boolean":
            {
                string? inner = PhpToValaGeneratorUtils.unwrap_zval_constructor_arg (zarg, "Php.zval.boolean (");
                if (inner != null) {
                    return inner;
                }
            }
            return "Php.truthy (%s)".printf (zarg);
        case "int":
        case "int64":
        case "long":
            {
                string? inner = PhpToValaGeneratorUtils.unwrap_zval_constructor_arg (zarg, "Php.zval.integer (");
                if (inner != null) {
                    return inner;
                }
            }
            return "Php.zval_to_int (%s)".printf (zarg);
        case "double":
        case "float":
            {
                string? inner = PhpToValaGeneratorUtils.unwrap_zval_constructor_arg (zarg, "Php.zval.double_number (");
                if (inner != null) {
                    return inner;
                }
                inner = PhpToValaGeneratorUtils.unwrap_zval_constructor_arg (zarg, "Php.zval.integer (");
                if (inner != null) {
                    return inner;
                }
            }
            return "Php.zval_to_double (%s)".printf (zarg);
        case "string":
            {
                string? string_concat = convert_concat_many_to_native_string (zarg);
                if (string_concat != null) {
                    return string_concat;
                }
                string_concat = convert_concat_to_native_string (zarg);
                if (string_concat != null) {
                    return string_concat;
                }
                string? inner = PhpToValaGeneratorUtils.unwrap_zval_constructor_arg (zarg, "Php.zval.string_value (");
                if (inner != null && transpiler.can_pass_unwrapped_string_to_static_arg (inner)) {
                    return inner;
                }
            }
            return "Php.zval_to_string (%s)".printf (zarg);
        case "Php.zval":
            if (PhpToValaTranspiler.normalize_runtime_type (transpiler.infer_expression_type (zarg)) == "Gee.ArrayList<string>") {
                return "__php2vala_string_list_to_zval (%s)".printf (zarg);
            }
            return zarg;
        default:
            return zarg;
        }
    }

    private string? convert_concat_many_to_native_string (string expression) {
        string clean = expression.strip ();
        string prefix = "Php.concat_many (";
        if (!clean.has_prefix (prefix) || !clean.has_suffix (")")) {
            return null;
        }
        int open_index = prefix.length - 1;
        int close_index = PhpToValaGeneratorUtils.find_matching_paren (clean, open_index);
        if (close_index != clean.length - 1) {
            return null;
        }

        var native_parts = new ArrayList<string> ();
        string args = clean.substring (open_index + 1, close_index - open_index - 1);
        foreach (string raw_arg in PhpToValaGeneratorUtils.split_top_level (args, ',')) {
            string arg = raw_arg.strip ();
            if (arg == "") {
                continue;
            }
            string? native_string_part = convert_zval_constructor_to_native_string_part (arg);
            if (native_string_part == null) {
                return null;
            }
            native_parts.add (native_string_part);
        }

        if (native_parts.size == 0) {
            return "\"\"";
        }
        return PhpToValaGeneratorUtils.join_strings (native_parts, " + ");
    }

    private string? convert_concat_to_native_string (string expression) {
        string clean = expression.strip ();
        string prefix = "Php.concat (";
        if (!clean.has_prefix (prefix) || !clean.has_suffix (")")) {
            return null;
        }
        int open_index = prefix.length - 1;
        int close_index = PhpToValaGeneratorUtils.find_matching_paren (clean, open_index);
        if (close_index != clean.length - 1) {
            return null;
        }
        var args = PhpToValaGeneratorUtils.split_top_level (clean.substring (open_index + 1, close_index - open_index - 1), ',');
        if (args.size != 2) {
            return null;
        }
        var native_parts = new ArrayList<string> ();
        foreach (string raw_arg in args) {
            string? native_string_part = convert_zval_constructor_to_native_string_part (raw_arg.strip ());
            if (native_string_part == null) {
                return null;
            }
            native_parts.add (native_string_part);
        }
        return PhpToValaGeneratorUtils.join_strings (native_parts, " + ");
    }

    private string? convert_zval_constructor_to_native_string_part (string expression) {
        string clean = expression.strip ();
        string? inner = PhpToValaGeneratorUtils.unwrap_zval_constructor_arg (clean, "Php.zval.string_value (");
        if (inner != null) {
            return transpiler.can_pass_unwrapped_string_to_static_arg (inner) ? inner : null;
        }

        inner = PhpToValaGeneratorUtils.unwrap_zval_constructor_arg (clean, "Php.zval.integer (");
        if (inner != null) {
            return "%s.to_string ()".printf (inner);
        }

        inner = PhpToValaGeneratorUtils.unwrap_zval_constructor_arg (clean, "Php.zval.double_number (");
        if (inner != null) {
            return "%s.to_string ()".printf (inner);
        }

        inner = PhpToValaGeneratorUtils.unwrap_zval_constructor_arg (clean, "Php.zval.boolean (");
        if (inner != null) {
            return "%s ? \"1\" : \"\"".printf (inner);
        }

        if (clean == "Php.zval.null_value ()") {
            return "\"\"";
        }
        return null;
    }

    public string build_dynamic_static_call_with_zval_outs (
        FunctionBinding binding,
        string runtime_function_name,
        ArrayList<string> zargs,
        int php_line_number,
        out string prelude,
        out string postlude
    ) {
        prelude = "";
        postlude = "";
        if (!binding.accepts_runtime_arg_count (zargs.size)) {
            return "";
        }
        var output = new StringBuilder ();
        if (binding.source_location_args_first ()) {
            output.append (transpiler.source_location_args (php_line_number).substring (2));
        }

        int call_arg_count = zargs.size > binding.runtime_parameter_count () ? zargs.size : binding.runtime_parameter_count ();
        for (int i = 0; i < call_arg_count; i++) {
            if (output.len > 0) {
                output.append (", ");
            }

            string parameter_type = transpiler.runtime_parameter_type_for_arg (binding, i);
            if (i >= zargs.size) {
                int parameter_index = binding.runtime_parameter_index (i);
                output.append (transpiler.runtime_parameter_default_value (binding, parameter_index, php_line_number));
                continue;
            }
            if (PhpToValaTranspiler.is_out_runtime_parameter_type (parameter_type)) {
                string temp_name = transpiler.next_temp_variable_name ("__php2vala_out");
                string value_type = PhpToValaTranspiler.normalize_runtime_type (parameter_type);
                string zarg = zargs[i].strip ();
                string out_name = PhpToValaTranspiler.zval_wrapped_identifier (zarg) ?? zarg;
                string temp_type = value_type == "string" ? "string" : value_type;
                prelude += "%s %s = %s;\n".printf (temp_type, temp_name, convert_zval_arg_for_runtime_static_type (zarg, value_type));
                output.append ("out " + temp_name);
                if (transpiler.variable_types.has_key (out_name) && PhpToValaTranspiler.normalize_runtime_type (transpiler.variable_types[out_name]) != "Php.zval") {
                    postlude += "\n%s = %s;".printf (out_name, temp_name);
                } else {
                    postlude += "\n%s = %s;".printf (out_name, convert_runtime_static_out_arg_to_zval (temp_name, value_type));
                }
                continue;
            }

            output.append (convert_zval_arg_for_runtime_static_type (zargs[i], parameter_type));
        }

        if (binding.source_location_args && !binding.source_location_args_first ()) {
            if (output.len == 0) {
                output.append (transpiler.source_location_args (php_line_number).substring (2));
            } else {
                output.append (transpiler.source_location_args (php_line_number));
            }
        }

        return "%s (%s)".printf (binding.call_name (runtime_function_name), output.str);
    }

    private string convert_runtime_static_out_arg_to_zval (string expression, string type_name) {
        switch (PhpToValaTranspiler.normalize_runtime_type (type_name)) {
        case "bool":
        case "boolean":
            return "Php.zval.boolean (%s)".printf (expression);
        case "int":
        case "int64":
        case "long":
            return "Php.zval.integer (%s)".printf (expression);
        case "double":
        case "float":
            return "Php.zval.double_number (%s)".printf (expression);
        case "string":
            return "Php.zval.string_value (%s)".printf (expression);
        default:
            return expression;
        }
    }

    public string? build_dynamic_static_zval_call (
        FunctionBinding binding,
        string runtime_function_name,
        ArrayList<string> zargs,
        int php_line_number
    ) {
        if (!can_convert_runtime_static_arguments (binding, zargs)) {
            return null;
        }

        string function_call = "%s (%s)".printf (
            binding.call_name (runtime_function_name),
            build_dynamic_static_argument_list (binding, zargs, php_line_number)
        );

        switch (PhpToValaTranspiler.normalize_runtime_type (binding.return_type)) {
        case "bool":
        case "boolean":
            return "Php.zval.boolean (%s)".printf (function_call);
        case "int":
        case "int64":
        case "long":
            return "Php.zval.integer (%s)".printf (function_call);
        case "double":
        case "float":
            return "Php.zval.double_number (%s)".printf (function_call);
        case "string":
            return "Php.zval.string_value (%s)".printf (function_call);
        case "Php.zval":
            return function_call;
        default:
            return null;
        }
    }

}
