using Gee;

public class PhpToValaZvalStructureGenerator : Object {
    private PhpToValaTranspiler transpiler;

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

    public void append_local_functions (StringBuilder output, PhpIrProgram ir_program) throws Error {
        foreach (LocalFunction local_function in ir_program.functions) {
            HashMap<string, string> previous_variable_types = transpiler.variable_types;
            HashMap<string, string> previous_parameter_php_types = transpiler.current_parameter_php_types;
            HashMap<string, string> previous_object_class_names = transpiler.object_class_names;
            HashMap<string, bool> previous_local_closure_variables = transpiler.local_closure_variables;
            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.current_parameter_php_types = new HashMap<string, string> ();
            transpiler.object_class_names = new HashMap<string, string> ();
            transpiler.local_closure_variables = new HashMap<string, bool> ();
            transpiler.transpiling_local_function = true;
            transpiler.current_local_function_name = local_function.name;
            foreach (LocalParameter parameter in local_function.parameters) {
                transpiler.variable_types[parameter.name] = "Php.zval";
                if (parameter.type_name != null && parameter.type_name != "") {
                    transpiler.current_parameter_php_types[parameter.name] = parameter.type_name;
                }
            }

            if (transpiler.should_emit_file_func_vars (local_function)) {
                transpiler.append_file_func_native_function_with_zval_wrapper (output, local_function);
                restore_local_context (
                    previous_variable_types,
                    previous_parameter_php_types,
                    previous_object_class_names,
                    previous_local_closure_variables,
                    previous_transpiling_local_function,
                    previous_local_function_name
                );
                continue;
            }

            if (transpiler.should_emit_local_vars (local_function)) {
                transpiler.append_local_vars_zval_function (output, local_function);
                restore_local_context (
                    previous_variable_types,
                    previous_parameter_php_types,
                    previous_object_class_names,
                    previous_local_closure_variables,
                    previous_transpiling_local_function,
                    previous_local_function_name
                );
                continue;
            }

            var ir_statements = ir_program.statements_for_function (local_function);
            bool emits_void_exit_function = transpiler.optimizes_cross_file_func_vars () && transpiler.ir_statements_last_statement_exits (ir_statements);
            output.append_printf (
                "public static %s %s (%s) throws Error {\n",
                emits_void_exit_function ? "void" : "Php.zval",
                transpiler.zval_local_function_name (local_function),
                transpiler.build_zval_local_function_parameter_list (local_function)
            );
            transpiler.append_zval_predeclared_variables_from_ir (output, ir_statements, 1);
            bool last_statement_terminates = false;
            transpiler.append_zval_ir_statements (output, ir_statements, 1, out last_statement_terminates);
            if (!emits_void_exit_function && !last_statement_terminates) {
                transpiler.append_indented_line (output, 1, "return Php.zval.null_value ();");
            }
            output.append ("}\n\n");

            restore_local_context (
                previous_variable_types,
                previous_parameter_php_types,
                previous_object_class_names,
                previous_local_closure_variables,
                previous_transpiling_local_function,
                previous_local_function_name
            );
        }
    }

    public void append_classes (StringBuilder output, PhpIrProgram ir_program) throws Error {
        foreach (PhpClass php_class in ir_program.classes) {
            string? vala_backing_class = doc_value (php_class.doc_comment, "@vala");
            if (vala_backing_class != null && vala_backing_class != "") {
                append_vala_backed_class (output, php_class, vala_backing_class);
                continue;
            }

            HashMap<string, string> typed_properties = collect_typed_class_properties (php_class, ir_program);
            string vala_namespace = php_namespace_to_vala_namespace (php_class.namespace_name);
            if (vala_namespace != "") {
                output.append_printf ("namespace %s {\n", vala_namespace);
            }
            output.append_printf ("public class %s : Object {\n", php_class.name);
            transpiler.append_indented_line (output, 1, "public Php.zval __php2vala_props = Php.array_new ();");
            foreach (string property_name in typed_properties.keys) {
                transpiler.append_indented_line (output, 1, "private %s %s = null;".printf (typed_properties[property_name], property_name));
            }
            output.append ("\n");

            foreach (LocalFunction method in php_class.methods) {
                HashMap<string, string> previous_variable_types = transpiler.variable_types;
                HashMap<string, string> previous_parameter_php_types = transpiler.current_parameter_php_types;
                HashMap<string, string> previous_object_class_names = transpiler.object_class_names;
                HashMap<string, bool> previous_local_closure_variables = transpiler.local_closure_variables;
                HashMap<string, string> previous_class_typed_properties = transpiler.current_class_typed_properties;
                HashMap<string, string> previous_stream_select_read_resources = transpiler.stream_select_read_resources;
                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.current_parameter_php_types = new HashMap<string, string> ();
                transpiler.object_class_names = new HashMap<string, string> ();
                transpiler.local_closure_variables = new HashMap<string, bool> ();
                transpiler.current_class_typed_properties = typed_properties;
                transpiler.stream_select_read_resources = new HashMap<string, string> ();
                transpiler.transpiling_local_function = true;
                transpiler.current_local_function_name = php_class.name + "::" + method.name;
                foreach (string property_name in typed_properties.keys) {
                    transpiler.variable_types["this." + property_name] = typed_properties[property_name];
                }
                foreach (LocalParameter parameter in method.parameters) {
                    transpiler.variable_types[parameter.name] = "Php.zval";
                    if (parameter.type_name != null && parameter.type_name != "") {
                        transpiler.current_parameter_php_types[parameter.name] = parameter.type_name;
                    }
                }

                output.append_printf (
                    "    public %sPhp.zval %s (%s) throws Error {\n",
                    method.is_static ? "static " : "",
                    method.name,
                    transpiler.build_zval_local_function_parameter_list (method)
                );

                if (method.name == "__construct") {
                    foreach (LocalParameter parameter in method.parameters) {
                        if (parameter.is_promoted_property) {
                            transpiler.append_indented_line (
                                output,
                                2,
                                "Php.array_set (this.__php2vala_props, Php.zval.string_value (\"%s\"), %s);".printf (parameter.name, parameter.name)
                            );
                        }
                    }
                }

                var ir_statements = ir_program.statements_for_method (php_class, method);
                transpiler.append_zval_predeclared_variables_from_ir (output, ir_statements, 2);
                bool last_statement_terminates = false;
                transpiler.append_zval_ir_statements (output, ir_statements, 2, out last_statement_terminates);
                if (!last_statement_terminates) {
                    transpiler.append_indented_line (output, 2, "return Php.zval.null_value ();");
                }
                output.append ("    }\n\n");

                transpiler.variable_types = previous_variable_types;
                transpiler.current_parameter_php_types = previous_parameter_php_types;
                transpiler.object_class_names = previous_object_class_names;
                transpiler.local_closure_variables = previous_local_closure_variables;
                transpiler.current_class_typed_properties = previous_class_typed_properties;
                transpiler.stream_select_read_resources = previous_stream_select_read_resources;
                transpiler.transpiling_local_function = previous_transpiling_local_function;
                transpiler.current_local_function_name = previous_local_function_name;
            }

            output.append ("}\n\n");
            if (vala_namespace != "") {
                output.append ("}\n\n");
            }
        }
    }

    private void append_vala_backed_class (StringBuilder output, PhpClass php_class, string vala_backing_class) throws Error {
        string vala_namespace = php_namespace_to_vala_namespace (php_class.namespace_name);
        if (vala_namespace != "") {
            output.append_printf ("namespace %s {\n", vala_namespace);
        }
        output.append_printf ("public class %s : Object {\n", php_class.name);
        transpiler.append_indented_line (output, 1, "public Php.zval __php2vala_props = Php.array_new ();");
        string qualified_vala_backing_class = vala_backing_class.index_of (".") >= 0
            ? "global::" + vala_backing_class
            : vala_backing_class;
        transpiler.append_indented_line (output, 1, "private %s __php2vala_inner;".printf (qualified_vala_backing_class));
        output.append ("\n");
        transpiler.append_indented_line (output, 1, "public %s () {".printf (php_class.name));
        transpiler.append_indented_line (output, 2, build_vala_backed_default_constructor_assignment (php_class, qualified_vala_backing_class));
        transpiler.append_indented_line (output, 1, "}");
        output.append ("\n");

        foreach (LocalFunction method in php_class.methods) {
            string? constructor_marker = doc_value (method.doc_comment, "@vala-constructor");
            string? method_marker = doc_value (method.doc_comment, "@vala-method");
            if (constructor_marker != null) {
                append_vala_backed_constructor_method (output, method, qualified_vala_backing_class);
                continue;
            }
            if (method_marker != null && method_marker != "") {
                append_vala_backed_forward_method (output, method, method_marker);
                continue;
            }
            append_unsupported_vala_backed_method (output, method);
        }

        output.append ("}\n\n");
        if (vala_namespace != "") {
            output.append ("}\n\n");
        }
    }

    private string build_vala_backed_default_constructor_assignment (PhpClass php_class, string vala_backing_class) {
        LocalFunction? constructor = null;
        foreach (LocalFunction method in php_class.methods) {
            if (doc_value (method.doc_comment, "@vala-constructor") != null) {
                constructor = method;
                break;
            }
        }
        string args = "";
        if (constructor != null) {
            args = build_vala_backed_default_constructor_arguments ((LocalFunction) constructor);
        }
        return "this.__php2vala_inner = new %s (%s);".printf (vala_backing_class, args);
    }

    private string build_vala_backed_default_constructor_arguments (LocalFunction constructor) {
        var args = new StringBuilder ();
        foreach (LocalParameter parameter in constructor.parameters) {
            string? default_value = parameter.default_value;
            if (default_value == null) {
                return "";
            }
            if (args.len > 0) {
                args.append (", ");
            }
            args.append (vala_backed_default_argument_expression (parameter, (string) default_value));
        }
        return args.str;
    }

    private string vala_backed_default_argument_expression (LocalParameter parameter, string default_value) {
        string php_type = (parameter.type_name ?? "").strip ();
        if (php_type.has_prefix ("?")) {
            php_type = php_type.substring (1).strip ();
        }
        string clean = default_value.strip ();
        switch (php_type) {
        case "int":
        case "float":
        case "double":
            return clean;
        case "bool":
            return clean == "true" || clean == "1" ? "true" : "false";
        case "string":
            return clean == "null" ? "null" : clean;
        default:
            return clean;
        }
    }

    private void append_vala_backed_constructor_method (StringBuilder output, LocalFunction method, string vala_backing_class) {
        output.append_printf (
            "    public Php.zval %s (%s) throws Error {\n",
            method.name,
            transpiler.build_zval_local_function_parameter_list (method)
        );
        transpiler.append_indented_line (
            output,
            2,
            "this.__php2vala_inner = new %s (%s);".printf (
                vala_backing_class,
                build_vala_backed_call_arguments (method, vala_backing_class, true)
            )
        );
        transpiler.append_indented_line (output, 2, "return Php.zval.null_value ();");
        output.append ("    }\n\n");
    }

    private void append_vala_backed_forward_method (StringBuilder output, LocalFunction method, string vala_method_name) {
        output.append_printf (
            "    public Php.zval %s (%s) throws Error {\n",
            method.name,
            transpiler.build_zval_local_function_parameter_list (method)
        );
        string call = "this.__php2vala_inner.%s (%s)".printf (
            vala_method_name,
            build_vala_backed_call_arguments (method, "", false)
        );
        append_vala_backed_return (output, method.return_type, call);
        output.append ("    }\n\n");
    }

    private void append_unsupported_vala_backed_method (StringBuilder output, LocalFunction method) {
        output.append_printf (
            "    public Php.zval %s (%s) throws Error {\n",
            method.name,
            transpiler.build_zval_local_function_parameter_list (method)
        );
        transpiler.append_indented_line (
            output,
            2,
            "throw new IOError.FAILED (\"Vala-backed method %s has no @vala-method metadata\");".printf (method.name)
        );
        output.append ("    }\n\n");
    }

    private string build_vala_backed_call_arguments (LocalFunction method, string vala_backing_class, bool is_constructor) {
        var args = new StringBuilder ();
        foreach (LocalParameter parameter in method.parameters) {
            if (args.len > 0) {
                args.append (", ");
            }
            args.append (vala_backed_argument_expression (parameter, vala_backing_class, is_constructor));
        }
        return args.str;
    }

    private string vala_backed_argument_expression (LocalParameter parameter, string vala_backing_class, bool is_constructor) {
        string php_type = (parameter.type_name ?? "").strip ();
        if (php_type.has_prefix ("?")) {
            php_type = php_type.substring (1).strip ();
        }
        bool nullable = (parameter.type_name ?? "").strip ().has_prefix ("?")
            || (parameter.default_value != null && parameter.default_value.strip () == "null");

        switch (php_type) {
        case "int":
            return "Php.zval_to_int (%s)".printf (parameter.name);
        case "float":
        case "double":
            return "Php.zval_to_double (%s)".printf (parameter.name);
        case "bool":
            return "Php.truthy (%s)".printf (parameter.name);
        case "string":
            if (nullable) {
                return "%s.is_null () ? null : Php.zval_to_string (%s)".printf (parameter.name, parameter.name);
            }
            return "Php.zval_to_string (%s)".printf (parameter.name);
        default:
            return parameter.name;
        }
    }

    private void append_vala_backed_return (StringBuilder output, string? php_return_type, string call) {
        string return_type = (php_return_type ?? "").strip ();
        if (return_type.has_prefix (":")) {
            return_type = return_type.substring (1).strip ();
        }
        if (return_type.has_prefix ("?")) {
            return_type = return_type.substring (1).strip ();
        }

        switch (return_type) {
        case "int":
            transpiler.append_indented_line (output, 2, "return Php.zval.integer (%s);".printf (call));
            break;
        case "float":
        case "double":
            transpiler.append_indented_line (output, 2, "return Php.zval.double_number (%s);".printf (call));
            break;
        case "bool":
            transpiler.append_indented_line (output, 2, "return Php.zval.boolean (%s);".printf (call));
            break;
        case "string":
            transpiler.append_indented_line (output, 2, "string? __php2vala_result = %s;".printf (call));
            transpiler.append_indented_line (output, 2, "return __php2vala_result == null ? Php.zval.null_value () : Php.zval.string_value (__php2vala_result);");
            break;
        case "void":
        case "":
            transpiler.append_indented_line (output, 2, "%s;".printf (call));
            transpiler.append_indented_line (output, 2, "return Php.zval.null_value ();");
            break;
        default:
            transpiler.append_indented_line (output, 2, "return %s;".printf (call));
            break;
        }
    }

    private static string? doc_value (string doc_comment, string key) {
        return PhpToValaGeneratorUtils.php2vala_doc_value (doc_comment, key);
    }

    private static string php_namespace_to_vala_namespace (string php_namespace) {
        if (php_namespace == "") {
            return "";
        }
        return php_namespace.replace ("\\", ".");
    }

    private void restore_local_context (
        HashMap<string, string> previous_variable_types,
        HashMap<string, string> previous_parameter_php_types,
        HashMap<string, string> previous_object_class_names,
        HashMap<string, bool> previous_local_closure_variables,
        bool previous_transpiling_local_function,
        string previous_local_function_name
    ) {
        transpiler.variable_types = previous_variable_types;
        transpiler.current_parameter_php_types = previous_parameter_php_types;
        transpiler.object_class_names = previous_object_class_names;
        transpiler.local_closure_variables = previous_local_closure_variables;
        transpiler.transpiling_local_function = previous_transpiling_local_function;
        transpiler.current_local_function_name = previous_local_function_name;
    }

    private HashMap<string, string> collect_typed_class_properties (PhpClass php_class, PhpIrProgram ir_program) throws Error {
        var properties = new HashMap<string, string> ();
        if (!(transpiler.optimizes_local_vars () || transpiler.optimizes_file_func_vars ())) {
            return properties;
        }
        foreach (LocalFunction method in php_class.methods) {
            var ir_statements = ir_program.statements_for_method (php_class, method);
            foreach (PhpIrStatement ir_statement in ir_statements) {
                string statement = ir_statement.text.strip ();
                if (!statement.has_prefix ("$this->") || !statement.has_suffix (";")) {
                    continue;
                }
                int equals_index = PhpToValaTranspiler.find_top_level_assignment_index (statement);
                if (equals_index < 0) {
                    continue;
                }
                string property_access = statement.substring (0, equals_index).strip ();
                string property_name = property_access.substring ("$this->".length).strip ();
                if (!PhpToValaTranspiler.is_identifier (property_name)) {
                    continue;
                }
                string raw_expression = PhpToValaTranspiler.trim_trailing_semicolon (statement.substring (equals_index + 1).strip ());
                string? type_name = transpiler.typed_static_assignment_return_type (raw_expression);
                if (type_name == null) {
                    continue;
                }
                if (properties.has_key (property_name) && properties[property_name] != type_name) {
                    continue;
                }
                properties[property_name] = type_name;
            }
        }
        return properties;
    }
}
