using Gee;

// Stronger optimizer stage for whole-file local functions. It may emit native
// helper functions plus zval wrappers, but it must not specialize based on one
// call site such as benchmark("fibo10"). Instead infer from the function body
// itself: typed declarations, local variable usage and untyped callable params.
// Because this stage builds on local static typing, enabling file-func-vars also
// enables the local-vars optimizer in PhpToValaTranspiler.
// Like local-vars, this stage must treat *_static as a non-zval ABI. If a PHP
// runtime function still needs to return Php.zval, it belongs behind *_zval or
// must be split into more specific static variants.
public class PhpToValaFileFuncVarsOptimizer : Object {
    private PhpToValaTranspiler transpiler;
    private PhpToValaCrossFileFuncVarsOptimizer cross_file;
    private bool emitted_callable_delegate = false;

    public PhpToValaFileFuncVarsOptimizer (PhpToValaTranspiler transpiler) {
        this.transpiler = transpiler;
        this.cross_file = new PhpToValaCrossFileFuncVarsOptimizer (transpiler, this);
    }

    public bool can_emit (LocalFunction local_function) {
        if (body_needs_zval_transpiler (local_function.body)) {
            return false;
        }
        string? return_type = infer_return_type (local_function);
        if (return_type == null) {
            return false;
        }

        foreach (LocalParameter parameter in local_function.parameters) {
            if (infer_parameter_type (local_function, parameter) == null) {
                return false;
            }
        }

        return true;
    }

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

    public void run_cross_file_stage (SourceParts source_parts, int stage_index) {
        cross_file.run_stage (source_parts, stage_index);
    }

    private bool body_needs_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 ("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_native_function_with_zval_wrapper (StringBuilder output, LocalFunction local_function) throws Error {
        append_native_function (output, local_function);
        append_zval_wrapper (output, local_function);
    }

    public string local_function_call_as_native_value (LocalFunction local_function, string args, int php_line_number) throws Error {
        return "%s (%s)".printf (native_function_name (local_function), convert_native_argument_list_for_function (local_function, args, php_line_number));
    }

    public string local_function_statement_as_native_call (LocalFunction local_function, string args, int php_line_number) throws Error {
        return "%s (%s);".printf (native_function_name (local_function), convert_native_argument_list_for_function (local_function, args, php_line_number));
    }

    public bool can_call_external_static (LocalFunction external_function) {
        return cross_file.can_call_external_static (external_function);
    }

    public string external_function_call_as_native_value (LocalFunction external_function, string args, int php_line_number) throws Error {
        return cross_file.external_function_call_as_native_value (external_function, args, php_line_number);
    }

    public string external_function_call_as_zval_value (LocalFunction external_function, string args, int php_line_number) throws Error {
        return cross_file.external_function_call_as_zval_value (external_function, args, php_line_number);
    }

    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 void append_native_function (StringBuilder output, LocalFunction local_function) throws Error {
        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;

        if (needs_callable_delegate (local_function) && !emitted_callable_delegate) {
            output.append ("private delegate void __php2vala_file_func_callable () throws Error;\n\n");
            emitted_callable_delegate = true;
        }

        string return_type = infer_return_type (local_function) ?? "void";
        output.append_printf ("%s static %s %s (", transpiler.optimizes_cross_file_func_vars () ? "public" : "private", return_type, native_function_name (local_function));
        for (int i = 0; i < local_function.parameters.size; i++) {
            LocalParameter parameter = local_function.parameters[i];
            string parameter_type = infer_parameter_type (local_function, parameter) ?? "var";
            if (i > 0) {
                output.append (", ");
            }
            string parameter_storage_name = transpiler.ensure_variable_alias_for_name (parameter.name);
            output.append_printf ("%s %s", parameter_type, parameter_storage_name);
            string? default_value = native_default_value (parameter.default_value, parameter_type);
            if (default_value != null) {
                output.append_printf (" = %s", default_value);
            }
            transpiler.variable_types[parameter_storage_name] = parameter_type;
        }
        output.append (") throws Error {\n");

        var statements = transpiler.ir_statements_for_function (local_function);
        append_callable_delegate_initializers (output, local_function);
        transpiler.append_runtime_out_argument_predeclared_variables_from_ir (output, statements, 1);

        int function_block_level = 0;
        bool last_statement_terminates = false;
        foreach (PhpIrStatement statement in statements) {
            string? vala_line = transpile_native_line (local_function, statement.text, statement.source_line);
            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 && !native_body_has_unconditional_loop_return (local_function)) {
            transpiler.append_indented_line (output, 1, default_native_return (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;
    }

    private void append_zval_wrapper (StringBuilder output, LocalFunction local_function) {
        string return_type = infer_return_type (local_function) ?? "void";
        output.append_printf ("public static Php.zval %s (%s) throws Error {\n", zval_function_name (local_function), transpiler.build_zval_local_function_parameter_list (local_function));
        var args = new StringBuilder ();
        foreach (LocalParameter parameter in local_function.parameters) {
            if (args.len > 0) {
                args.append (", ");
            }
            string parameter_type = infer_parameter_type (local_function, parameter) ?? "var";
            args.append (zval_to_native_argument (parameter.name, parameter_type));
        }
        string native_call = "%s (%s)".printf (native_function_name (local_function), args.str);
        if (return_type == "void") {
            transpiler.append_indented_line (output, 1, "%s;".printf (native_call));
            transpiler.append_indented_line (output, 1, "return Php.zval.null_value ();");
        } else {
            transpiler.append_indented_line (output, 1, "return %s;".printf (native_to_zval_return (native_call, return_type)));
        }
        output.append ("}\n\n");
    }

    private string convert_native_argument_list (string args, int php_line_number) throws Error {
        if (args.strip () == "") {
            return "";
        }

        var converted = new StringBuilder ();
        foreach (string arg in transpiler.split_top_level (args, ',')) {
            if (converted.len > 0) {
                converted.append (", ");
            }
            converted.append (convert_native_expression (arg.strip (), php_line_number, ""));
        }
        return converted.str;
    }

    private string convert_native_argument_list_for_function (LocalFunction local_function, string args, int php_line_number) throws Error {
        if (args.strip () == "") {
            return "";
        }

        var converted = new StringBuilder ();
        var split_args = transpiler.split_top_level (args, ',');
        for (int i = 0; i < split_args.size; i++) {
            if (converted.len > 0) {
                converted.append (", ");
            }
            string expected_type = "";
            if (i < local_function.parameters.size) {
                expected_type = infer_parameter_type (local_function, local_function.parameters[i]) ?? "";
            }
            converted.append (convert_native_expression (split_args[i].strip (), php_line_number, expected_type));
        }
        return converted.str;
    }


    public string native_function_name (LocalFunction local_function) {
        if (transpiler.is_library_output ()) {
            return local_function.name + "_static";
        }
        return "__php2vala_file_func_" + local_function.name;
    }

    public string zval_function_name (LocalFunction local_function) {
        if (transpiler.is_library_output ()) {
            return local_function.name + "_zval";
        }
        return local_function.name;
    }

    public string infer_native_return_type (LocalFunction local_function) {
        return infer_return_type (local_function) ?? "void";
    }

    private string default_native_return (string return_type) {
        if (return_type == "void") {
            return "return;";
        }
        if (return_type == "double") {
            return "return 0.0;";
        }
        if (return_type == "bool") {
            return "return false;";
        }
        if (return_type == "string") {
            return "return \"\";";
        }
        return "return 0;";
    }

    public 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";
        case "void":
            return "void";
        default:
            return null;
        }
    }

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

    private bool function_body_has_return_concat (string body) {
        foreach (string raw_line in 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 (split_top_level_concat_expression (expression).size > 1) {
                return true;
            }
        }
        return false;
    }

    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 = infer_simple_return_expression_type (expression);
            if (type_name == null) {
                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 string? infer_simple_return_expression_type (string expression) {
        string clean = expression.strip ();
        if (clean == "true" || clean == "false") {
            return "bool";
        }
        if ((clean.has_prefix (""") && clean.has_suffix (""")) || (clean.has_prefix ("'") && clean.has_suffix ("'"))) {
            return "string";
        }
        if (PhpToValaGeneratorUtils.is_integer_literal (clean)) {
            return "int";
        }
        if (PhpToValaGeneratorUtils.is_numeric_literal (clean)) {
            return "double";
        }
        return null;
    }

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

    private bool parameter_used_in_string_concat (LocalFunction local_function, LocalParameter parameter) {
        string token = "$" + parameter.name;
        foreach (string raw_line in local_function.body.split ("
")) {
            string line = raw_line.strip ();
            if (line.index_of (token + " .") >= 0
                || line.index_of (token + ".") >= 0
                || line.index_of (". " + token) >= 0
                || line.index_of ("." + token) >= 0) {
                return true;
            }
        }
        return false;
    }

    public string? infer_default_value_type (string? default_value) {
        if (default_value == null) {
            return null;
        }
        string clean = ((string) default_value).strip ().down ();
        if (clean == "true" || clean == "false") {
            return "bool";
        }
        if (clean.has_prefix ("\"") || clean.has_prefix ("'")) {
            return "string";
        }
        if (clean.index_of (".") >= 0) {
            return "double";
        }
        for (int i = 0; i < clean.length; i++) {
            if (!clean[i].isdigit () && !(i == 0 && clean[i] == '-')) {
                return null;
            }
        }
        return clean == "" ? null : "int";
    }

    private string? native_default_value (string? default_value, string parameter_type) {
        if (default_value == null) {
            return null;
        }
        string clean = ((string) default_value).strip ();
        switch (parameter_type) {
        case "bool":
            if (clean.down () == "true" || clean.down () == "false") {
                return clean.down ();
            }
            break;
        case "string":
            if (clean.has_prefix ("\"") || clean.has_prefix ("'")) {
                return transpiler.convert_string_literal_public (clean);
            }
            break;
        case "int":
        case "double":
            return clean;
        }
        return null;
    }

    private bool function_body_has_return (string body) {
        foreach (string raw_line in body.split ("\n")) {
            if (raw_line.strip ().has_prefix ("return")) {
                return true;
            }
        }
        return false;
    }

    private bool native_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)
            && function_body_has_return (local_function.body);
    }

    private bool can_infer_implicit_void_body (LocalFunction local_function) {
        return local_function.body.index_of ("[") < 0
            && local_function.body.index_of ("foreach(") < 0
            && local_function.body.index_of ("foreach (") < 0;
    }

    private bool is_simple_no_return_wrapper (LocalFunction local_function) {
        if (local_function.parameters.size != 0) {
            return false;
        }
        var statements = transpiler.ir_statements_for_function (local_function);
        if (statements.size != 1) {
            return false;
        }
        string line = statements[0].text.strip ();
        if (!line.has_suffix (");")) {
            return false;
        }
        int open_index = line.index_of ("(");
        if (open_index <= 0) {
            return false;
        }
        string function_name = line.substring (0, open_index).strip ();
        return transpiler.local_functions.has_key (function_name);
    }

    private bool needs_callable_delegate (LocalFunction local_function) {
        foreach (LocalParameter parameter in local_function.parameters) {
            if (uses_parameter_as_callable (local_function, parameter) && !assigns_parameter (local_function, parameter)) {
                return true;
            }
        }
        return false;
    }

    private bool uses_parameter_as_callable (LocalFunction local_function, LocalParameter parameter) {
        return local_function.body.index_of ("$" + parameter.name + "();") >= 0
            || local_function.body.index_of ("$" + parameter.name + " ();") >= 0;
    }

    private bool assigns_parameter (LocalFunction local_function, LocalParameter parameter) {
        string name = "$" + parameter.name;
        foreach (string raw_line in local_function.body.split ("\n")) {
            string line = raw_line.strip ();
            if (line.has_prefix (name + " =") || line.has_prefix (name + "=")) {
                return true;
            }
        }
        return false;
    }

    private void append_callable_delegate_initializers (StringBuilder output, LocalFunction local_function) {
        foreach (LocalParameter parameter in local_function.parameters) {
            if (!uses_parameter_as_callable (local_function, parameter) || assigns_parameter (local_function, parameter)) {
                continue;
            }
            string delegate_name = callable_delegate_name (parameter.name);
            transpiler.append_indented_line (output, 1, "__php2vala_file_func_callable? %s = null;".printf (delegate_name));
            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
                    || !can_emit (target)
                    || (infer_return_type (target) ?? "void") != "void") {
                    continue;
                }
                transpiler.append_indented_line (
                    output,
                    1,
                    "%s (%s == \"%s\") { %s = %s; }".printf (
                        first ? "if" : "else if",
                        parameter.name,
                        function_name,
                        delegate_name,
                        native_function_name (target)
                    )
                );
                first = false;
            }
        }
    }

    private string callable_delegate_name (string parameter_name) {
        return "__php2vala_callable_" + parameter_name;
    }

    private string? transpile_native_line (LocalFunction local_function, string line, int php_line_number) throws Error {
        string clean = line.strip ();
        string? dynamic_call = transpile_callable_string_call (local_function, clean, php_line_number);
        if (dynamic_call != null) {
            return dynamic_call;
        }

        string? native_if = transpile_native_if (clean, php_line_number);
        if (native_if != null) {
            return native_if;
        }

        if (clean.has_prefix ("while(") || clean.has_prefix ("while (")) {
            int open_index = clean.index_of ("(");
            int close_index = PhpToValaTranspiler.find_matching_paren (clean, open_index);
            if (open_index >= 0 && close_index > open_index) {
                string condition = convert_native_expression (clean.substring (open_index + 1, close_index - open_index - 1), php_line_number, "bool");
                string rest = clean.substring (close_index + 1).strip ();
                if (rest == "" || rest == "{") {
                    return "while (%s) {".printf (condition);
                }
            }
        }

        string? local_call = transpile_native_local_function_statement (clean, php_line_number);
        if (local_call != null) {
            return local_call;
        }

        string? external_call = transpile_native_external_function_statement (clean, php_line_number);
        if (external_call != null) {
            return external_call;
        }

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

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

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

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

        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 ();
            }
            string return_type = infer_return_type (local_function) ?? "double";
            return "return %s;".printf (convert_native_expression (expression, php_line_number, return_type));
        }

        return transpiler.transpile_line (line, php_line_number);
    }

    private string? transpile_native_if (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("if(") && !line.has_prefix ("if (")) {
            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_native_expression (line.substring (open_index + 1, close_index - open_index - 1), php_line_number, "bool");
        return "if (%s) {".printf (condition);
    }

    private string? transpile_native_local_function_statement (string line, int php_line_number) throws Error {
        if (!line.has_suffix (");")) {
            return null;
        }
        int open_index = line.index_of ("(");
        if (open_index <= 0) {
            return null;
        }
        string function_name = line.substring (0, open_index).strip ();
        if (!transpiler.local_functions.has_key (function_name)) {
            return null;
        }
        int close_index = PhpToValaTranspiler.find_matching_paren (line, open_index);
        if (close_index < open_index || close_index != line.length - 2) {
            return null;
        }
        LocalFunction target = transpiler.local_functions[function_name];
        string inner_args = line.substring (open_index + 1, close_index - open_index - 1);
        if (can_emit (target)) {
            return "%s (%s);".printf (native_function_name (target), convert_native_argument_list (inner_args, php_line_number));
        }
        return null;
    }

    private string? transpile_native_external_function_statement (string line, int php_line_number) throws Error {
        return cross_file.transpile_native_external_function_statement (line, php_line_number);
    }

    private string? transpile_callable_string_call (LocalFunction local_function, string line, int php_line_number) throws Error {
        if (!line.has_prefix ("$") || !line.has_suffix ("();")) {
            return null;
        }
        string parameter_name = line.substring (1, line.length - 1 - "();".length).strip ();
        LocalParameter? parameter = null;
        foreach (LocalParameter candidate in local_function.parameters) {
            if (candidate.name == parameter_name) {
                parameter = candidate;
                break;
            }
        }
        if (parameter == null || infer_parameter_type (local_function, parameter) != "string") {
            return null;
        }

        if (!assigns_parameter (local_function, parameter)) {
            return "%s ();".printf (callable_delegate_name (parameter.name));
        }

        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 || (infer_return_type (target) ?? "void") != "void") {
                continue;
            }
            string call_name = can_emit (target) ? native_function_name (target) : target.name;
            output.append_printf (
                "%s (%s == \"%s\") { %s (); }",
                first ? "if" : "else if",
                parameter_name,
                function_name,
                call_name
            );
            first = false;
        }
        if (first) {
            return null;
        }
        output.append (" else { }");
        return output.str;
    }

    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_native_expression (parts[parts.size - 1].strip (), php_line_number, "");
        string type_name = infer_native_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 (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_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 (name.substring (1).strip (), line.has_suffix ("++;") ? "++" : "--");
    }

    private string? transpile_native_compound_assignment (string line) {
        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;
            }
            string vala_op = op.substring (0, 1) + "=";
            return "%s %s %s;".printf (lhs.substring (1).strip (), vala_op, rhs.has_prefix ("$") ? rhs.substring (1).strip () : rhs);
        }
        return null;
    }

    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 storage_name = transpiler.ensure_variable_alias_for_name (name);
        string expression = convert_native_expression (parts[1].strip (), php_line_number, "");
        string type_name = infer_native_expression_type (expression);
        if (!transpiler.variable_types.has_key (storage_name)) {
            transpiler.variable_types[storage_name] = type_name;
            return "%s %s = %s;".printf (type_name, storage_name, expression);
        }
        return "%s = %s;".printf (storage_name, expression);
    }

    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? 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;", target.substring (1).strip (), convert_native_expression (values[i].strip (), php_line_number, ""));
        }
        return output.str;
    }

    public string convert_native_expression (string expression, int php_line_number, string expected_type) throws Error {
        string clean = expression.strip ();
        if (clean == "true") {
            return "true";
        }
        if (clean == "false") {
            return "false";
        }
        if (expected_type == "string") {
            string? concat = convert_native_string_concat_expression (clean, php_line_number);
            if (concat != null) {
                return concat;
            }
            if (clean.index_of ("->") >= 0) {
                string? object_method = convert_native_object_method_string_expression (clean, php_line_number);
                if (object_method != null) {
                    return object_method;
                }
                return "Php.zval_to_string (%s)".printf (transpiler.convert_zval_expression (clean, php_line_number));
            }
            if ((clean.has_prefix ("\"") && clean.has_suffix ("\"")) || (clean.has_prefix ("'") && clean.has_suffix ("'"))) {
                return transpiler.convert_string_literal_public (clean);
            }
            if (PhpToValaGeneratorUtils.is_numeric_literal (clean)) {
                return "\"%s\"".printf (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) {
            if (expected_type == "bool") {
                return native_truthy_expression (runtime_static_call);
            }
            return runtime_static_call;
        }

        string? native_division = convert_native_division_expression (clean, php_line_number);
        if (native_division != null) {
            return native_division;
        }

        string? native_comparison = convert_native_comparison_expression (clean, php_line_number);
        if (native_comparison != null) {
            return native_comparison;
        }

        if (clean.has_prefix ("$")) {
            string variable_name = clean.substring (1).strip ();
            string storage_name = transpiler.variable_aliases.has_key (variable_name) ? transpiler.variable_aliases[variable_name] : variable_name;
            if (transpiler.variable_types.has_key (storage_name)) {
                string storage_type = transpiler.variable_types[storage_name];
                if (expected_type == "int" && storage_type == "double") {
                    return "(int) %s".printf (storage_name);
                }
                if (expected_type == "string" && storage_type != "string") {
                    if (storage_type == "Php.zval") {
                        return "Php.zval_to_string (%s)".printf (storage_name);
                    }
                    return "%s.to_string ()".printf (storage_name);
                }
                return storage_name;
            }
        }

        foreach (string function_name in transpiler.local_functions.keys) {
            LocalFunction? target = transpiler.local_functions[function_name];
            if (target == null || !can_emit (target)) {
                continue;
            }
            clean = replace_inline_local_function_call (clean, target, php_line_number);
        }

        foreach (string function_name in transpiler.local_functions.keys) {
            LocalFunction? target = transpiler.local_functions[function_name];
            if (target == null || can_emit (target)) {
                continue;
            }
            clean = replace_inline_local_function_zval_call (clean, target, php_line_number);
        }

        if (transpiler.optimizes_cross_file_func_vars ()) {
            foreach (string function_name in transpiler.external_functions.keys) {
                LocalFunction? target = transpiler.external_functions[function_name];
                if (target == null || !can_call_external_static (target)) {
                    continue;
                }
                clean = replace_inline_external_function_call (clean, target, php_line_number);
            }
        }

        string converted = transpiler.convert_expression (clean, php_line_number);
        if (expected_type == "double" && converted.index_of ("/") >= 0) {
            return converted;
        }
        if (expected_type == "int" && infer_native_expression_type (converted) == "double") {
            return "(int) (%s)".printf (converted);
        }
        if (expected_type == "string" && clean.index_of ("$") >= 0 && (converted.index_of ("->") >= 0 || converted.index_of ("- >") >= 0)) {
            return "Php.zval_to_string (%s)".printf (transpiler.convert_zval_expression (clean, php_line_number));
        }
        if (expected_type == "string" && clean.index_of (".") < 0 && converted.index_of ("+") >= 0) {
            if (clean.index_of ("$") >= 0) {
                return "Php.zval_to_string (%s)".printf (transpiler.convert_zval_expression (clean, php_line_number));
            }
            return "(%s).to_string ()".printf (converted);
        }
        if (expected_type == "string" && infer_native_expression_type (converted) != "string") {
            if (PhpToValaGeneratorUtils.is_numeric_literal (clean)) {
                return "\"%s\"".printf (clean);
            }
            if (clean.index_of ("->") >= 0) {
                string? object_method = transpiler.convert_zval_object_method_expression (clean, php_line_number);
                if (object_method != null) {
                    return "Php.zval_to_string (%s)".printf (object_method);
                }
            }
            if (clean.index_of ("$") >= 0 || converted.index_of ("Php.zval") >= 0 || converted.index_of ("current_zval") >= 0) {
                return "Php.zval_to_string (%s)".printf (transpiler.convert_zval_expression (clean, php_line_number));
            }
            return "(%s).to_string ()".printf (converted);
        }
        return converted;
    }

    private string? convert_native_object_method_string_expression (string expression, int php_line_number) throws Error {
        int arrow_index = expression.index_of ("->");
        if (!expression.has_prefix ("$") || arrow_index <= 0) {
            return null;
        }
        string object_name = expression.substring (1, arrow_index - 1).strip ();
        if (!PhpToValaTranspiler.is_identifier (object_name)) {
            return null;
        }
        string call = expression.substring (arrow_index + 2).strip ();
        int open_index = call.index_of ("(");
        if (open_index < 0 || !call.has_suffix (")") || PhpToValaTranspiler.find_matching_paren (call, open_index) != call.length - 1) {
            return null;
        }
        string method_name = call.substring (0, open_index).strip ();
        if (!PhpToValaTranspiler.is_identifier (method_name)) {
            return null;
        }
        string args = call.substring (open_index + 1, call.length - open_index - 2).strip ();
        string target = transpiler.variable_aliases.has_key (object_name) ? transpiler.variable_aliases[object_name] : object_name;
        return "Php.zval_to_string (%s.%s (%s))".printf (target, method_name, transpiler.convert_zval_argument_list_public (args, php_line_number));
    }

    private string? convert_native_string_concat_expression (string expression, int php_line_number) throws Error {
        var parts = split_top_level_concat_expression (expression);
        if (parts.size < 2) {
            return null;
        }
        var output = new StringBuilder ();
        foreach (string part in parts) {
            if (output.len > 0) {
                output.append (" + ");
            }
            output.append (convert_native_expression (part.strip (), php_line_number, "string"));
        }
        return output.str;
    }

    private ArrayList<string> split_top_level_concat_expression (string expression) {
        var parts = new ArrayList<string> ();
        int depth = 0;
        bool in_single_string = false;
        bool in_double_string = false;
        int start = 0;
        for (int i = 0; i < expression.length; i++) {
            char c = expression[i];
            if (in_single_string) {
                if (c == '\'' && (i == 0 || expression[i - 1] != '\\')) {
                    in_single_string = false;
                }
                continue;
            }
            if (in_double_string) {
                if (c == '"' && (i == 0 || expression[i - 1] != '\\')) {
                    in_double_string = false;
                }
                continue;
            }
            if (c == '\'') {
                in_single_string = true;
                continue;
            }
            if (c == '"') {
                in_double_string = true;
                continue;
            }
            if (c == '(' || c == '[' || c == '{') {
                depth++;
                continue;
            }
            if (c == ')' || c == ']' || c == '}') {
                depth--;
                continue;
            }
            if (depth == 0 && c == '.') {
                bool numeric_dot = i > 0 && i + 1 < expression.length && expression[i - 1].isdigit () && expression[i + 1].isdigit ();
                if (!numeric_dot) {
                    parts.add (expression.substring (start, i - start));
                    start = i + 1;
                }
            }
        }
        if (parts.size > 0) {
            parts.add (expression.substring (start));
        }
        return parts;
    }

    private string native_truthy_expression (string expression) {
        string type_name = infer_native_expression_type (expression);
        if (type_name == "bool") {
            return expression;
        }
        if (type_name == "string") {
            return "%s != \"\"".printf (expression);
        }
        if (type_name == "double") {
            return "%s != 0.0".printf (expression);
        }
        return "%s != 0".printf (expression);
    }

    private string replace_inline_local_function_zval_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 {
                        string zval_call = "%s (%s)".printf (
                            local_function.name,
                            transpiler.convert_zval_argument_list_public (inner_args, php_line_number)
                        );
                        string return_type = infer_return_type (local_function) ?? "void";
                        output.append (zval_to_native_argument (zval_call, return_type));
                        i = close_index + 1;
                        continue;
                    } catch (Error e) {
                    }
                }
            }
            output.append_c (expression[i]);
            i++;
        }
        return output.str;
    }

    private string replace_inline_external_function_call (string expression, LocalFunction external_function, int php_line_number) {
        return cross_file.replace_inline_external_function_call (expression, external_function, php_line_number);
    }

    private string? convert_native_comparison_expression (string expression, int php_line_number) throws Error {
        string[] operators = {"!==", "===", ">=", "<=", "!=", "==", ">", "<"};
        foreach (string op in operators) {
            int index = find_top_level_operator (expression, op);
            if (index <= 0) {
                continue;
            }
            string left = expression.substring (0, index).strip ();
            string right = expression.substring (index + op.length).strip ();
            string vala_op = op == "===" ? "==" : (op == "!==" ? "!=" : op);
            return "%s %s %s".printf (
                convert_native_expression (left, php_line_number, ""),
                vala_op,
                convert_native_expression (right, php_line_number, "")
            );
        }
        return null;
    }

    private string? convert_native_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_native_expression (left, php_line_number, "double"),
            convert_native_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_native_expression_type (string expression) {
        string clean = expression.strip ();
        if (is_vala_string_literal (clean)) {
            return "string";
        }
        if (transpiler.variable_types.has_key (clean)) {
            return transpiler.variable_types[clean];
        }
        if (expression == "true" || expression == "false") {
            return "bool";
        }
        if (clean.has_prefix ("Php.div_static(") || clean.has_prefix ("Php.div_static (")) {
            return "double";
        }
        if (clean.index_of ("\"") >= 0 && clean.index_of ("+") >= 0) {
            return "string";
        }
        string? bound_return_type = transpiler.infer_bound_call_return_type (clean);
        if (bound_return_type != null && bound_return_type != "var") {
            return bound_return_type;
        }
        string? local_return_type = infer_native_local_function_return_type (clean);
        if (local_return_type != null) {
            return local_return_type;
        }
        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 ("/") >= 0) {
            return "double";
        }
        return "int";
    }

    private string? infer_native_local_function_return_type (string expression) {
        string prefix = "__php2vala_file_func_";
        if (!expression.has_prefix (prefix)) {
            return null;
        }
        int open_index = expression.index_of ("(");
        if (open_index <= prefix.length) {
            return null;
        }
        string function_name = expression.substring (prefix.length, open_index - prefix.length).strip ();
        LocalFunction? local_function = transpiler.local_functions.get (function_name);
        if (local_function == null) {
            return null;
        }
        return infer_return_type (local_function);
    }

    private static bool is_vala_string_literal (string expression) {
        return expression.length >= 2
            && expression.has_prefix ("\"")
            && expression.has_suffix ("\"");
    }

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

    public 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 ()";
        }
    }
}
