using Gee;

public errordomain PhpToValaError {
    UNSUPPORTED
}

public class FunctionBinding : Object {
    public string namespace_name;
    public string package_name;
    public string return_type;
    public bool source_location_args;
    public bool source_location_args_at_start;
    public string? mapped_function_name;
    public ArrayList<string> parameter_types;
    public ArrayList<string?> parameter_defaults;
    public bool native_runtime_only;

    public FunctionBinding (
        string namespace_name,
        string package_name,
        string return_type,
        bool source_location_args,
        string? mapped_function_name = null,
        ArrayList<string>? parameter_types = null,
        ArrayList<string?>? parameter_defaults = null,
        bool native_runtime_only = false,
        bool source_location_args_at_start = false
    ) {
        this.namespace_name = namespace_name;
        this.package_name = package_name;
        this.return_type = return_type;
        this.source_location_args = source_location_args;
        this.source_location_args_at_start = source_location_args_at_start;
        this.mapped_function_name = mapped_function_name;
        this.parameter_types = parameter_types ?? new ArrayList<string> ();
        this.parameter_defaults = parameter_defaults ?? new ArrayList<string?> ();
        this.native_runtime_only = native_runtime_only;
    }

    public string call_name (string function_name) {
        return namespace_name + "." + (mapped_function_name ?? function_name);
    }

    public int runtime_parameter_count () {
        int count = runtime_parameter_limit () - runtime_parameter_offset ();
        if (count > 0 && parameter_types[runtime_parameter_limit () - 1].has_suffix ("[]")) {
            return count - 1;
        }
        return count;
    }

    public int runtime_parameter_min_count () {
        int offset = runtime_parameter_offset ();
        int limit = runtime_parameter_limit ();
        if (limit < offset) {
            return 0;
        }

        bool has_params_array = limit > offset && parameter_types[limit - 1].has_suffix ("[]");
        int fixed_limit = limit - (has_params_array ? 1 : 0);
        int count = 0;
        for (int i = offset; i < fixed_limit; i++) {
            if (i >= parameter_defaults.size || parameter_defaults[i] == null) {
                count++;
            }
        }
        return count;
    }

    public bool accepts_runtime_arg_count (int arg_count) {
        int offset = runtime_parameter_offset ();
        int limit = runtime_parameter_limit ();
        if (limit < offset) {
            return false;
        }

        bool has_params_array = limit > offset && parameter_types[limit - 1].has_suffix ("[]");
        int max_count = runtime_parameter_count ();
        int min_count = runtime_parameter_min_count ();
        return has_params_array
            ? arg_count >= min_count
            : arg_count >= min_count && arg_count <= max_count;
    }

    public int runtime_parameter_offset () {
        if (source_location_args_at_start) {
            return 2;
        }
        return 0;
    }

    public int runtime_parameter_limit () {
        if (source_location_args
            && parameter_types.size >= 2
            && !source_location_args_at_start) {
            return parameter_types.size - 2;
        }
        return parameter_types.size;
    }

    public int runtime_parameter_index (int arg_index) {
        return runtime_parameter_offset () + arg_index;
    }

    public bool source_location_args_first () {
        return source_location_args && source_location_args_at_start;
    }
}

public class LocalParameter : Object {
    public string name;
    public string? type_name;
    public string? default_value;
    public bool is_promoted_property;

    public LocalParameter (string name, string? type_name = null, string? default_value = null, bool is_promoted_property = false) {
        this.name = name;
        this.type_name = type_name;
        this.default_value = default_value;
        this.is_promoted_property = is_promoted_property;
    }
}

public class LocalFunction : Object {
    public string name;
    public ArrayList<LocalParameter> parameters = new ArrayList<LocalParameter> ();
    public string? return_type;
    public string body;
    public int line;
    public bool is_static;
    public string doc_comment;
    public string? static_call_name;
    public string? static_return_type;
    public string? zval_call_name;
    public string? stub_package_name;

    public LocalFunction (string name, ArrayList<LocalParameter> parameters, string? return_type, string body, int line, bool is_static = true, string doc_comment = "") {
        this.name = name;
        this.parameters = parameters;
        this.return_type = return_type;
        this.body = body;
        this.line = line;
        this.is_static = is_static;
        this.doc_comment = doc_comment;
        this.static_call_name = null;
        this.static_return_type = null;
        this.zval_call_name = null;
        this.stub_package_name = null;
    }
}

public class PhpClassConstant : Object {
    public string name;
    public string value;
    public int line;

    public PhpClassConstant (string name, string value, int line) {
        this.name = name;
        this.value = value;
        this.line = line;
    }
}

public class PhpClass : Object {
    public string name;
    public string namespace_name;
    public int line;
    public string doc_comment;
    public string extends_name;
    public ArrayList<LocalFunction> methods = new ArrayList<LocalFunction> ();
    public ArrayList<PhpClassConstant> constants = new ArrayList<PhpClassConstant> ();
    public string? pwphpbytecode_loader_symbol;
    public string? pwphpbytecode_stub_source;

    public PhpClass (string name, int line, string doc_comment = "", string extends_name = "", string namespace_name = "") {
        this.name = name;
        this.namespace_name = namespace_name;
        this.line = line;
        this.doc_comment = doc_comment;
        this.extends_name = extends_name;
        this.pwphpbytecode_loader_symbol = null;
        this.pwphpbytecode_stub_source = null;
    }
}

public class SourceParts : Object {
    public string main_source = "";
    public string namespace_name = "";
    public ArrayList<LocalFunction> functions = new ArrayList<LocalFunction> ();
    public ArrayList<PhpClass> classes = new ArrayList<PhpClass> ();
}

public enum PhpAstStatementKind {
    COMMENT,
    EXPRESSION,
    CONTROL,
    DECLARATION,
    UNKNOWN
}

public class PhpAstStatement : Object {
    public string text;
    public int line;
    public int column;
    public PhpAstStatementKind kind;

    public PhpAstStatement (string text, int line, int column, PhpAstStatementKind kind = PhpAstStatementKind.UNKNOWN) {
        this.text = text;
        this.line = line;
        this.column = column;
        this.kind = kind;
    }
}

public class PhpAstProgram : Object {
    public ArrayList<PhpAstStatement> main_statements = new ArrayList<PhpAstStatement> ();
    public ArrayList<LocalFunction> functions = new ArrayList<LocalFunction> ();
    public ArrayList<PhpClass> classes = new ArrayList<PhpClass> ();
    public HashMap<string, ArrayList<PhpAstStatement>> function_statements = new HashMap<string, ArrayList<PhpAstStatement>> ();
    public HashMap<string, ArrayList<PhpAstStatement>> method_statements = new HashMap<string, ArrayList<PhpAstStatement>> ();

    public PhpAstProgram (ArrayList<PhpAstStatement>? main_statements = null, ArrayList<LocalFunction>? functions = null, ArrayList<PhpClass>? classes = null) {
        if (main_statements != null) {
            foreach (PhpAstStatement statement in main_statements) {
                this.main_statements.add (statement);
            }
        }
        if (functions != null) {
            foreach (LocalFunction local_function in functions) {
                this.functions.add (local_function);
            }
        }
        if (classes != null) {
            foreach (PhpClass php_class in classes) {
                this.classes.add (php_class);
            }
        }
    }

    public static string method_statement_key (string class_name, string method_name) {
        return class_name + "::" + method_name;
    }

    public ArrayList<PhpAstStatement> statements_for_function (LocalFunction local_function) {
        ArrayList<PhpAstStatement>? statements = function_statements[local_function.name];
        return statements ?? new ArrayList<PhpAstStatement> ();
    }

    public ArrayList<PhpAstStatement> statements_for_method (PhpClass php_class, LocalFunction method) {
        ArrayList<PhpAstStatement>? statements = method_statements[method_statement_key (php_class.name, method.name)];
        return statements ?? new ArrayList<PhpAstStatement> ();
    }
}

public enum PhpIrStatementKind {
    COMMENT,
    EXPRESSION,
    CONTROL,
    DECLARATION,
    UNKNOWN
}

public class PhpIrStatement : Object {
    public string text;
    public int source_line;
    public int source_column;
    public PhpIrStatementKind kind;
    public string? assignment_name;
    public string? assignment_expression;
    public string? control_keyword;
    public string? return_expression;
    public PhpAstStatement? ast_statement;

    public PhpIrStatement (
        string text,
        int source_line,
        int source_column,
        PhpIrStatementKind kind = PhpIrStatementKind.UNKNOWN,
        PhpAstStatement? ast_statement = null,
        string? assignment_name = null,
        string? assignment_expression = null,
        string? control_keyword = null,
        string? return_expression = null
    ) {
        this.text = text;
        this.source_line = source_line;
        this.source_column = source_column;
        this.kind = kind;
        this.ast_statement = ast_statement;
        this.assignment_name = assignment_name;
        this.assignment_expression = assignment_expression;
        this.control_keyword = control_keyword;
        this.return_expression = return_expression;
    }
}

public class PhpIrProgram : Object {
    public ArrayList<PhpIrStatement> main_statements = new ArrayList<PhpIrStatement> ();
    public ArrayList<LocalFunction> functions = new ArrayList<LocalFunction> ();
    public ArrayList<PhpClass> classes = new ArrayList<PhpClass> ();
    public HashMap<string, ArrayList<PhpIrStatement>> function_statements = new HashMap<string, ArrayList<PhpIrStatement>> ();
    public HashMap<string, ArrayList<PhpIrStatement>> method_statements = new HashMap<string, ArrayList<PhpIrStatement>> ();

    public PhpIrProgram (ArrayList<PhpIrStatement>? main_statements = null, ArrayList<LocalFunction>? functions = null, ArrayList<PhpClass>? classes = null) {
        if (main_statements != null) {
            foreach (PhpIrStatement statement in main_statements) {
                this.main_statements.add (statement);
            }
        }
        if (functions != null) {
            foreach (LocalFunction local_function in functions) {
                this.functions.add (local_function);
            }
        }
        if (classes != null) {
            foreach (PhpClass php_class in classes) {
                this.classes.add (php_class);
            }
        }
    }

    public static string method_statement_key (string class_name, string method_name) {
        return class_name + "::" + method_name;
    }

    public ArrayList<PhpIrStatement> statements_for_function (LocalFunction local_function) {
        ArrayList<PhpIrStatement>? statements = function_statements[local_function.name];
        return statements ?? new ArrayList<PhpIrStatement> ();
    }

    public ArrayList<PhpIrStatement> statements_for_method (PhpClass php_class, LocalFunction method) {
        ArrayList<PhpIrStatement>? statements = method_statements[method_statement_key (php_class.name, method.name)];
        return statements ?? new ArrayList<PhpIrStatement> ();
    }
}

public class ResolvedSource : Object {
    public string source = "";
    public ArrayList<string> files = new ArrayList<string> ();
    public ArrayList<int> lines = new ArrayList<int> ();
}

public class Statements : Object {
    public ArrayList<string> texts = new ArrayList<string> ();
    public ArrayList<int> lines = new ArrayList<int> ();
    public ArrayList<int> columns = new ArrayList<int> ();
}
