using Gee;

// Optimizes only inside one PHP function body. The public generated function
// signature, parameters and return value stay Php.zval for compatibility.
// Untyped parameters may still become static locals when their use is clear
// inside the body, e.g. a PHP callable variable call means the parameter is a
// callable-name string locally.
// Optimized calls must use runtime *_static functions only when those functions
// return real Vala types. A *_static function returning Php.zval is a runtime
// naming bug and should be split into concrete static variants plus *_zval.
public class PhpToValaLocalVarsOptimizer : Object {
    private PhpToValaTranspiler transpiler;
    private HashSet<string> active_can_emit_checks = new HashSet<string> ();
    private HashSet<string> known_can_emit = new HashSet<string> ();
    private HashSet<string> known_cannot_emit = new HashSet<string> ();

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

    public bool can_emit (LocalFunction local_function) {
        if (known_can_emit.contains (local_function.name)) {
            return true;
        }
        if (known_cannot_emit.contains (local_function.name)) {
            return false;
        }
        if (Environment.get_variable ("PHP2VALA_DEBUG_PHASES") == "1") {
            stderr.printf ("phase: local-vars can_emit %s
", local_function.name);
        }
        if (active_can_emit_checks.contains (local_function.name)) {
            if (Environment.get_variable ("PHP2VALA_DEBUG_PHASES") == "1") {
                stderr.printf ("phase: local-vars skip recursive check %s
", local_function.name);
            }
            return false;
        }

        active_can_emit_checks.add (local_function.name);
        if (!has_basic_static_signature (local_function)) {
            active_can_emit_checks.remove (local_function.name);
            known_cannot_emit.add (local_function.name);
            if (Environment.get_variable ("PHP2VALA_DEBUG_PHASES") == "1") {
                stderr.printf ("phase: local-vars skip signature %s
", local_function.name);
            }
            return false;
        }

        bool safe = can_transpile_body_safely (local_function);
        active_can_emit_checks.remove (local_function.name);
        if (safe) {
            known_can_emit.add (local_function.name);
        } else {
            known_cannot_emit.add (local_function.name);
        }
        if (Environment.get_variable ("PHP2VALA_DEBUG_PHASES") == "1") {
            stderr.printf ("phase: local-vars can_emit %s => %s
", local_function.name, safe ? "yes" : "no");
        }
        return safe;
    }

    public void run_stage (SourceParts source_parts, int stage_index) {
        if (stage_index < 1) {
            return;
        }
        active_can_emit_checks.clear ();
        known_can_emit.clear ();
        known_cannot_emit.clear ();
        foreach (LocalFunction local_function in source_parts.functions) {
            can_emit (local_function);
        }
    }

    private bool has_basic_static_signature (LocalFunction local_function) {
        if (body_needs_plain_zval_transpiler (local_function.body)) {
            return false;
        }
        if (local_function.body.index_of (local_function.name + "(") >= 0
            || local_function.body.index_of (local_function.name + " (") >= 0) {
            return false;
        }
        if (local_function.parameters.size == 0) {
            return false;
        }
        foreach (LocalParameter parameter in local_function.parameters) {
            if (local_function.body.index_of ("$" + parameter.name + "();") >= 0
                || local_function.body.index_of ("$" + parameter.name + " ();") >= 0) {
                return false;
            }
        }
        if (infer_return_type (local_function) == null) {
            return false;
        }
        foreach (LocalParameter parameter in local_function.parameters) {
            if (infer_parameter_type (local_function, parameter) == null) {
                return false;
            }
        }
        return true;
    }

    private bool can_transpile_body_safely (LocalFunction local_function) {
        HashMap<string, string> previous_variable_types = transpiler.variable_types;
        HashMap<string, string> previous_variable_aliases = transpiler.variable_aliases;
        bool previous_transpiling_local_function = transpiler.transpiling_local_function;
        string previous_local_function_name = transpiler.current_local_function_name;

        transpiler.variable_types = new HashMap<string, string> ();
        transpiler.variable_aliases = new HashMap<string, string> ();
        transpiler.transpiling_local_function = true;
        transpiler.current_local_function_name = local_function.name;

        foreach (LocalParameter parameter in local_function.parameters) {
            string parameter_type = infer_parameter_type (local_function, parameter) ?? "var";
            string local_name = "__php2vala_local_" + parameter.name;
            transpiler.variable_aliases[parameter.name] = local_name;
            transpiler.variable_types[local_name] = parameter_type;
        }

        bool safe = true;
        try {
            string return_type = infer_return_type (local_function) ?? "void";
            var statements = transpiler.ir_statements_for_function (local_function);
            foreach (PhpIrStatement statement in statements) {
                string? vala_line = transpile_zval_local_line (statement.text, statement.source_line, return_type);
                if (vala_line == null) {
                    continue;
                }
                transpiler.validate_no_untranslated_php (statement.text, statement.source_line, statement.source_column, vala_line);
                if (line_needs_zval_fallback (vala_line)) {
                    safe = false;
                    break;
                }
            }
        } catch (Error e) {
            safe = false;
        }

        transpiler.variable_types = previous_variable_types;
        transpiler.variable_aliases = previous_variable_aliases;
        transpiler.transpiling_local_function = previous_transpiling_local_function;
        transpiler.current_local_function_name = previous_local_function_name;
        return safe;
    }

    private bool line_needs_zval_fallback (string vala_line) {
        foreach (string raw_line in vala_line.split ("\n")) {
            string line = raw_line.strip ();
            if (line == "") {
                continue;
            }

            if (native_declaration_assigns_null (line)
                || native_assignment_uses_zval_value (line)
                || line.index_of ("Php.add_zval (") >= 0
                || line.index_of ("Php.sub_zval (") >= 0
                || line.index_of ("Php.concat (") >= 0
                || line.index_of ("Php.concat_assign (") >= 0
                || unsafe_unoptimized_local_function_call (line)) {
                return true;
            }
        }
        return false;
    }

    private bool native_declaration_assigns_null (string line) {
        return (line.has_prefix ("int ")
            || line.has_prefix ("double ")
            || line.has_prefix ("bool ")
            || line.has_prefix ("string "))
            && line.index_of (" = null") >= 0;
    }

    private bool native_assignment_uses_zval_value (string line) {
        if (line.index_of (" = Php.zval.") < 0) {
            return false;
        }
        return line.has_prefix ("int ")
            || line.has_prefix ("double ")
            || line.has_prefix ("bool ")
            || line.has_prefix ("string ")
            || line.index_of (" = ") > 0;
    }

    private bool unsafe_unoptimized_local_function_call (string line) {
        foreach (string function_name in transpiler.local_functions.keys) {
            LocalFunction? target = transpiler.local_functions[function_name];
            if (target == null || has_basic_static_signature (target)) {
                continue;
            }
            string zval_name = transpiler.zval_local_function_name (target);
            if (line.index_of (" = " + function_name + " (") >= 0
                || line.index_of (" = " + zval_name + " (") >= 0
                || line.index_of ("return Php.zval.integer (" + function_name + " (") >= 0
                || line.index_of ("return Php.zval.double_number (" + function_name + " (") >= 0
                || line.index_of ("return Php.zval.string_value (" + function_name + " (") >= 0
                || line.index_of ("return Php.zval.boolean (" + function_name + " (") >= 0
                || line.index_of ("return Php.zval.integer (" + zval_name + " (") >= 0
                || line.index_of ("return Php.zval.double_number (" + zval_name + " (") >= 0
                || line.index_of ("return Php.zval.string_value (" + zval_name + " (") >= 0
                || line.index_of ("return Php.zval.boolean (" + zval_name + " (") >= 0) {
                return true;
            }
        }
        return false;
    }

    private bool body_needs_plain_zval_transpiler (string body) {
        foreach (string raw_line in body.split ("\n")) {
            string line = raw_line.strip ();
            if (line.has_prefix ("throw ")
                || line.has_prefix ("try")
                || line.has_prefix ("finally")
                || line.has_prefix ("switch")
                || line.has_prefix ("for")
                || line.has_prefix ("case ")
                || line.has_prefix ("default:")
                || line.index_of ("= function") >= 0
                || line.index_of ("$this->") >= 0
                || line.index_of ("=>") >= 0
                || line.index_of ("+=") >= 0
                || line.index_of ("$out") >= 0
                || line.index_of ("exec(") >= 0
                || line.index_of ("exec (") >= 0
                || line.index_of ("pack(") >= 0
                || line.index_of ("pack (") >= 0
                || line.index_of ("preg_replace(") >= 0
                || line.index_of ("preg_replace (") >= 0) {
                return true;
            }
        }
        return false;
    }

    public void append_zval_local_function (StringBuilder output, LocalFunction local_function) throws Error {
        if (Environment.get_variable ("PHP2VALA_DEBUG_PHASES") == "1") {
            stderr.printf ("phase: local-vars emit %s
", local_function.name);
        }
        HashMap<string, string> previous_variable_types = transpiler.variable_types;
        HashMap<string, string> previous_variable_aliases = transpiler.variable_aliases;
        bool previous_transpiling_local_function = transpiler.transpiling_local_function;
        string previous_local_function_name = transpiler.current_local_function_name;

        transpiler.variable_types = new HashMap<string, string> ();
        transpiler.variable_aliases = new HashMap<string, string> ();
        transpiler.transpiling_local_function = true;
        transpiler.current_local_function_name = local_function.name;

        string return_type = infer_return_type (local_function) ?? "void";
        output.append_printf ("public static Php.zval %s (%s) throws Error {\n", transpiler.zval_local_function_name (local_function), transpiler.build_zval_local_function_parameter_list (local_function));
        foreach (LocalParameter parameter in local_function.parameters) {
            string parameter_type = infer_parameter_type (local_function, parameter) ?? "var";
            string local_name = "__php2vala_local_" + parameter.name;
            transpiler.variable_aliases[parameter.name] = local_name;
            transpiler.variable_types[local_name] = parameter_type;
            transpiler.append_indented_line (output, 1, "%s %s = %s;".printf (parameter_type, local_name, zval_to_native_argument (parameter.name, parameter_type)));
        }

        var statements = transpiler.ir_statements_for_function (local_function);
        int function_block_level = 0;
        bool last_statement_terminates = false;
        foreach (PhpIrStatement statement in statements) {
            string? vala_line = transpile_zval_local_line (statement.text, statement.source_line, return_type);
            if (vala_line == null) {
                continue;
            }
            transpiler.validate_no_untranslated_php (statement.text, statement.source_line, statement.source_column, vala_line);
            function_block_level = transpiler.append_generated_block (output, 1, vala_line, function_block_level);
            last_statement_terminates = PhpToValaTranspiler.zval_statement_terminates_function (vala_line);
        }
        if (!last_statement_terminates && !body_has_unconditional_loop_return (local_function)) {
            transpiler.append_indented_line (output, 1, "return %s;".printf (native_to_zval_return (return_type == "double" ? "0.0" : return_type == "bool" ? "false" : return_type == "string" ? "\"\"" : "0", return_type)));
        }
        output.append ("}\n\n");

        transpiler.variable_types = previous_variable_types;
        transpiler.variable_aliases = previous_variable_aliases;
        transpiler.transpiling_local_function = previous_transpiling_local_function;
        transpiler.current_local_function_name = previous_local_function_name;
    }

    public string local_function_call_as_native_value (LocalFunction local_function, string args, int php_line_number) throws Error {
        string native_type = infer_return_type (local_function) ?? "var";
        return zval_to_native_argument (
            "%s (%s)".printf (transpiler.zval_local_function_name (local_function), convert_native_argument_list_to_zval_args (local_function, args, php_line_number)),
            native_type
        );
    }

    public string replace_inline_local_function_call (string expression, LocalFunction local_function, int php_line_number) {
        string function_name = local_function.name;
        var output = new StringBuilder ();
        int i = 0;
        while (i < expression.length) {
            if (expression.substring (i).has_prefix (function_name + "(")
                && (i == 0 || (!PhpToValaTranspiler.is_identifier_char (expression[i - 1]) && expression[i - 1] != '.'))) {
                int open_index = i + function_name.length;
                int close_index = PhpToValaTranspiler.find_matching_paren (expression, open_index);
                if (close_index > open_index) {
                    string inner_args = expression.substring (open_index + 1, close_index - open_index - 1);
                    try {
                        output.append (local_function_call_as_native_value (local_function, inner_args, php_line_number));
                        i = close_index + 1;
                        continue;
                    } catch (Error e) {
                    }
                }
            }
            output.append_c (expression[i]);
            i++;
        }
        return output.str;
    }

    private string? transpile_zval_local_line (string line, int php_line_number, string return_type) throws Error {
        string clean = line.strip ();
        string? callable_call = transpile_callable_string_call (clean);
        if (callable_call != null) {
            return callable_call;
        }

        string? while_line = transpile_control_condition ("while", clean, php_line_number);
        if (while_line != null) {
            return while_line;
        }

        string? if_line = transpile_control_condition ("if", clean, php_line_number);
        if (if_line != null) {
            return if_line;
        }

        string? chained_assignment = transpile_native_chained_assignment (clean, php_line_number);
        if (chained_assignment != null) {
            return chained_assignment;
        }

        string? assignment = transpile_native_assignment (clean, php_line_number);
        if (assignment != null) {
            return assignment;
        }

        string? increment = transpile_native_increment (clean);
        if (increment != null) {
            return increment;
        }

        string? compound = transpile_native_compound_assignment (clean, php_line_number);
        if (compound != null) {
            return compound;
        }

        string? list_assignment = transpile_native_list_assignment (clean, php_line_number);
        if (list_assignment != null) {
            return list_assignment;
        }

        if (clean.has_prefix ("return")) {
            string expression = PhpToValaTranspiler.trim_trailing_semicolon (clean.substring ("return".length).strip ());
            if (expression.has_prefix ("(") && expression.has_suffix (")")) {
                expression = expression.substring (1, expression.length - 2).strip ();
            }
            if (expression == "") {
                return "return Php.zval.null_value ();";
            }
            return "return %s;".printf (native_to_zval_return (convert_local_expression (expression, php_line_number, return_type), return_type));
        }

        return transpiler.transpile_line (line, php_line_number);
    }

    private string? transpile_control_condition (string keyword, string line, int php_line_number) throws Error {
        if (!line.has_prefix (keyword + "(") && !line.has_prefix (keyword + " (")) {
            return null;
        }
        int open_index = line.index_of ("(");
        int close_index = PhpToValaTranspiler.find_matching_paren (line, open_index);
        if (open_index < 0 || close_index <= open_index) {
            return null;
        }
        string rest = line.substring (close_index + 1).strip ();
        if (rest != "" && rest != "{") {
            return null;
        }
        string condition = convert_local_expression (line.substring (open_index + 1, close_index - open_index - 1), php_line_number, "bool");
        return "%s (%s) {".printf (keyword, condition);
    }

    private string? transpile_native_chained_assignment (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("$") || !line.has_suffix (";")) {
            return null;
        }
        var parts = transpiler.split_top_level (PhpToValaTranspiler.trim_trailing_semicolon (line), '=');
        if (parts.size <= 2) {
            return null;
        }
        string expression = convert_local_expression (parts[parts.size - 1].strip (), php_line_number, "");
        string type_name = infer_local_expression_type (expression);
        var output = new StringBuilder ();
        for (int i = parts.size - 2; i >= 0; i--) {
            string name = parts[i].strip ();
            if (!name.has_prefix ("$")) {
                return null;
            }
            name = name.substring (1).strip ();
            if (!is_plain_identifier (name)) {
                return null;
            }
            if (output.len > 0) {
                output.append ("\n");
            }
            if (!transpiler.variable_types.has_key (name)) {
                transpiler.variable_types[name] = type_name;
                output.append_printf ("%s %s = %s;", type_name, name, expression);
            } else {
                output.append_printf ("%s = %s;", name, expression);
            }
            expression = name;
        }
        return output.str;
    }

    private string? transpile_native_assignment (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("$") || !line.has_suffix (";")) {
            return null;
        }
        string clean = PhpToValaTranspiler.trim_trailing_semicolon (line);
        var parts = transpiler.split_top_level (clean, '=');
        if (parts.size != 2) {
            return null;
        }
        string lhs = parts[0].strip ();
        if (!lhs.has_prefix ("$")) {
            return null;
        }
        string name = lhs.substring (1).strip ();
        if (!is_plain_identifier (name)) {
            return null;
        }
        string expression = convert_local_expression (parts[1].strip (), php_line_number, "");
        string type_name = infer_local_expression_type (expression);
        if (!transpiler.variable_types.has_key (name)) {
            transpiler.variable_types[name] = type_name;
            return "%s %s = %s;".printf (type_name, name, expression);
        }
        return "%s = %s;".printf (name, expression);
    }

    private string? transpile_native_increment (string line) {
        if (!line.has_suffix ("++;") && !line.has_suffix ("--;")) {
            return null;
        }
        string name = line.substring (0, line.length - 3).strip ();
        if (!name.has_prefix ("$")) {
            return null;
        }
        return "%s%s;".printf (native_variable_name (name.substring (1).strip ()), line.has_suffix ("++;") ? "++" : "--");
    }

    private string? transpile_native_compound_assignment (string line, int php_line_number) throws Error {
        string clean = PhpToValaTranspiler.trim_trailing_semicolon (line);
        string[] operators = {"+=", "-=", "*=", "/="};
        foreach (string op in operators) {
            int index = clean.index_of (op);
            if (index <= 0) {
                continue;
            }
            string lhs = clean.substring (0, index).strip ();
            string rhs = clean.substring (index + op.length).strip ();
            if (!lhs.has_prefix ("$")) {
                return null;
            }
            return "%s %s %s;".printf (
                native_variable_name (lhs.substring (1).strip ()),
                op,
                convert_local_expression (rhs, php_line_number, "")
            );
        }
        return null;
    }

    private string? transpile_native_list_assignment (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("list(") && !line.has_prefix ("list (")) {
            return null;
        }
        int close_index = line.index_of (")");
        if (close_index < 0) {
            return null;
        }
        string rest = PhpToValaTranspiler.trim_trailing_semicolon (line.substring (close_index + 1).strip ());
        if (!rest.has_prefix ("= array(") || !rest.has_suffix (")")) {
            return null;
        }
        var targets = transpiler.split_top_level (line.substring (line.index_of ("(") + 1, close_index - line.index_of ("(") - 1), ',');
        var values = transpiler.split_top_level (rest.substring ("= array(".length, rest.length - "= array(".length - 1), ',');
        var output = new StringBuilder ();
        for (int i = 0; i < targets.size && i < values.size; i++) {
            string target = targets[i].strip ();
            if (!target.has_prefix ("$")) {
                continue;
            }
            if (output.len > 0) {
                output.append ("\n");
            }
            output.append_printf ("%s = %s;", native_variable_name (target.substring (1).strip ()), convert_local_expression (values[i].strip (), php_line_number, ""));
        }
        return output.str;
    }

    private string convert_local_expression (string expression, int php_line_number, string expected_type) throws Error {
        string clean = expression.strip ();
        if (clean == "true" || clean == "false") {
            return clean;
        }
        if (clean == "microtime(true)" || clean == "microtime (true)") {
            return "%s (%s)".printf (
                transpiler.map_php_runtime_function_name ("microtime", "microtime_float_static", php_line_number),
                transpiler.source_location_args (php_line_number).substring (2)
            );
        }
        string? runtime_static_call = transpiler.convert_zval_function_expression_to_native_local (clean, php_line_number);
        if (runtime_static_call != null) {
            return runtime_static_call;
        }
        string? division = convert_local_division_expression (clean, php_line_number);
        if (division != null) {
            return division;
        }
        if (clean.has_prefix ("$")) {
            string variable_name = clean.substring (1).strip ();
            if (is_plain_identifier (variable_name)) {
                return native_variable_name (variable_name);
            }
        }
        return transpiler.convert_expression (clean, php_line_number);
    }

    private string? convert_local_division_expression (string expression, int php_line_number) throws Error {
        int index = find_top_level_operator (expression, "/");
        if (index <= 0) {
            return null;
        }
        string left = expression.substring (0, index).strip ();
        string right = expression.substring (index + 1).strip ();
        return "Php.div_static (%s, %s%s)".printf (
            convert_local_expression (left, php_line_number, "double"),
            convert_local_expression (right, php_line_number, "double"),
            transpiler.source_location_args (php_line_number)
        );
    }

    private int find_top_level_operator (string expression, string op) {
        int depth = 0;
        bool in_string = false;
        char string_quote = '\0';
        for (int i = 0; i <= expression.length - op.length; i++) {
            char c = expression[i];
            if (in_string) {
                if (c == string_quote && (i == 0 || expression[i - 1] != '\\')) {
                    in_string = false;
                }
                continue;
            }
            if (c == '\'' || c == '"') {
                in_string = true;
                string_quote = c;
                continue;
            }
            if (c == '(' || c == '[' || c == '{') {
                depth++;
                continue;
            }
            if (c == ')' || c == ']' || c == '}') {
                depth--;
                continue;
            }
            if (depth == 0 && expression.substring (i).has_prefix (op)) {
                return i;
            }
        }
        return -1;
    }

    private string infer_local_expression_type (string expression) {
        if (expression == "true" || expression == "false") {
            return "bool";
        }
        if (expression.index_of ("Php.zval_to_double") >= 0
            || expression.index_of ("microtime_float_static") >= 0
            || expression.index_of ("round_static") >= 0
            || expression.index_of ("div_static") >= 0
            || expression.index_of ("/") >= 0) {
            return "double";
        }
        return "int";
    }

    private bool is_plain_identifier (string name) {
        if (name.length == 0 || name[0].isdigit ()) {
            return false;
        }
        for (int i = 0; i < name.length; i++) {
            if (!PhpToValaTranspiler.is_identifier_char (name[i])) {
                return false;
            }
        }
        return true;
    }

    private string native_variable_name (string php_name) {
        string? alias = transpiler.variable_aliases.get (php_name);
        return alias ?? php_name;
    }

    private string? transpile_callable_string_call (string line) {
        if (!line.has_prefix ("$") || !line.has_suffix ("();")) {
            return null;
        }
        string parameter_name = line.substring (1, line.length - 1 - "();".length).strip ();
        string? alias = transpiler.variable_aliases.get (parameter_name);
        if (alias == null || !transpiler.variable_types.has_key (alias) || transpiler.variable_types[alias] != "string") {
            return null;
        }

        var output = new StringBuilder ();
        bool first = true;
        foreach (string function_name in transpiler.local_functions.keys) {
            LocalFunction? target = transpiler.local_functions[function_name];
            if (target == null || target.parameters.size != 0) {
                continue;
            }
            output.append_printf (
                "%s (%s == \"%s\") { %s (); }",
                first ? "if" : "else if",
                alias,
                function_name,
                function_name
            );
            first = false;
        }
        if (first) {
            return null;
        }
        output.append (" else { }");
        return output.str;
    }

    private string convert_native_argument_list_to_zval_args (LocalFunction local_function, string args, int php_line_number) throws Error {
        var converted = new StringBuilder ();
        var raw_args = transpiler.split_top_level (args, ',');
        for (int i = 0; i < raw_args.size; i++) {
            if (converted.len > 0) {
                converted.append (", ");
            }
            string native_type = i < local_function.parameters.size ? infer_parameter_type (local_function, local_function.parameters[i]) ?? "var" : "var";
            converted.append (native_to_zval_return (transpiler.convert_expression (raw_args[i].strip (), php_line_number), native_type));
        }
        return converted.str;
    }

    private string? infer_return_type (LocalFunction local_function) {
        string? declared = native_scalar_type (local_function.return_type);
        if (declared != null) {
            return declared;
        }
        string? runtime_return_type = infer_runtime_return_type (local_function);
        if (runtime_return_type != null) {
            return runtime_return_type;
        }
        if (local_function.body.index_of ("return $") >= 0) {
            return "double";
        }
        return null;
    }

    private string? infer_runtime_return_type (LocalFunction local_function) {
        string? inferred_type = null;
        foreach (string raw_line in local_function.body.split ("\n")) {
            string line = raw_line.strip ();
            if (!line.has_prefix ("return ")) {
                continue;
            }
            string expression = PhpToValaTranspiler.trim_trailing_semicolon (line.substring ("return ".length).strip ());
            if (expression == "null") {
                return null;
            }
            string? type_name = transpiler.infer_native_static_function_expression_return_type (expression);
            if (type_name == null) {
                return null;
            }
            if (inferred_type != null && inferred_type != type_name) {
                return null;
            }
            inferred_type = type_name;
        }
        return inferred_type;
    }

    private bool body_has_unconditional_loop_return (LocalFunction local_function) {
        return (local_function.body.index_of ("while (true)") >= 0 || local_function.body.index_of ("while(true)") >= 0)
            && (local_function.body.index_of ("return ") >= 0 || local_function.body.index_of ("return(") >= 0);
    }

    private string? infer_parameter_type (LocalFunction local_function, LocalParameter parameter) {
        string? declared = native_scalar_type (parameter.type_name);
        if (declared != null) {
            return declared;
        }
        if (local_function.body.index_of ("$" + parameter.name + "();") >= 0
            || local_function.body.index_of ("$" + parameter.name + " ();") >= 0) {
            return "string";
        }
        return null;
    }

    private string? native_scalar_type (string? php_type) {
        if (php_type == null) {
            return null;
        }
        switch (php_type.down ()) {
        case "int":
        case "integer":
            return "int";
        case "float":
        case "double":
            return "double";
        case "string":
            return "string";
        case "bool":
        case "boolean":
            return "bool";
        default:
            return null;
        }
    }

    private string zval_to_native_argument (string parameter_name, string native_type) {
        switch (native_type) {
        case "int":
            return "Php.zval_to_int (%s)".printf (parameter_name);
        case "double":
            return "Php.zval_to_double (%s)".printf (parameter_name);
        case "string":
            return "Php.zval_to_string (%s)".printf (parameter_name);
        case "bool":
            return "Php.truthy (%s)".printf (parameter_name);
        default:
            return parameter_name;
        }
    }

    private string native_to_zval_return (string expression, string native_type) {
        switch (native_type) {
        case "int":
            return "Php.zval.integer (%s)".printf (expression);
        case "double":
            return "Php.zval.double_number (%s)".printf (expression);
        case "string":
            return "Php.zval.string_value (%s)".printf (expression);
        case "bool":
            return "Php.zval.boolean (%s)".printf (expression);
        default:
            return "Php.zval.null_value ()";
        }
    }
}
