namespace PhpEmbed {
    errordomain EmbedRuntimeError {
        RUNTIME
    }

    [CCode (cname = "valaphp_embed_call_no_args_to_string", cheader_filename = "call/static_call.h")]
    private extern static string? call_no_args_to_string (string function_name, out string? error_message);

    [CCode (cname = "valaphp_embed_call_no_args_output_to_string_and_print", cheader_filename = "call/static_call.h")]
    private extern static string? call_no_args_output_to_string_and_print (string function_name, out string? error_message);

    [CCode (cname = "valaphp_embed_call_no_args_to_bool", cheader_filename = "call/static_call.h")]
    private extern static bool call_no_args_to_bool (string function_name, out string? error_message);

    [CCode (cname = "valaphp_embed_call_bool_to_string", cheader_filename = "call/static_call.h")]
    private extern static string? call_bool_to_string (string function_name, bool value, out string? error_message);

    [CCode (cname = "valaphp_embed_call_bool_to_double", cheader_filename = "call/static_call.h")]
    private extern static double call_bool_to_double (string function_name, bool value, out string? error_message);

    [CCode (cname = "valaphp_embed_call_int_to_string", cheader_filename = "call/static_call.h")]
    private extern static string? call_int_to_string (string function_name, int64 value, out string? error_message);

    [CCode (cname = "valaphp_embed_call_int_to_int", cheader_filename = "call/static_call.h")]
    private extern static int call_int_to_int (string function_name, int64 value, out string? error_message);

    [CCode (cname = "valaphp_embed_call_int_to_bool", cheader_filename = "call/static_call.h")]
    private extern static bool call_int_to_bool (string function_name, int64 value, out string? error_message);

    [CCode (cname = "valaphp_embed_call_double_to_double", cheader_filename = "call/static_call.h")]
    private extern static double call_double_to_double (string function_name, double value, out string? error_message);

    [CCode (cname = "valaphp_embed_call_double_int_int_to_double", cheader_filename = "call/static_call.h")]
    private extern static double call_double_int_int_to_double (string function_name, double value, int precision, int mode, out string? error_message);

    [CCode (cname = "valaphp_embed_call_string_to_string", cheader_filename = "call/static_call.h")]
    private extern static string? call_string_to_string (string function_name, string value, out string? error_message);

    [CCode (cname = "valaphp_embed_call_string_to_int", cheader_filename = "call/static_call.h")]
    private extern static int call_string_to_int (string function_name, string value, out string? error_message);

    [CCode (cname = "valaphp_embed_call_string_to_bool", cheader_filename = "call/static_call.h")]
    private extern static bool call_string_to_bool (string function_name, string value, out string? error_message);

    [CCode (cname = "valaphp_embed_call_string_int_int_to_string", cheader_filename = "call/static_call.h")]
    private extern static string? call_string_int_int_to_string (string function_name, string value, int offset, int length, out string? error_message);

    [CCode (cname = "valaphp_embed_call_double_int_string_string_to_string", cheader_filename = "call/static_call.h")]
    private extern static string? call_double_int_string_string_to_string (string function_name, double value, int decimals, string dec_point, string thousands_sep, out string? error_message);

    [CCode (cname = "valaphp_embed_call_string_string_to_string", cheader_filename = "call/static_call.h")]
    private extern static string? call_string_string_to_string (string function_name, string first, string second, out string? error_message);

    [CCode (cname = "valaphp_embed_call_string_string_int_to_string", cheader_filename = "call/static_call.h")]
    private extern static string? call_string_string_int_to_string (string function_name, string first, string second, int third, out string? error_message);

    [CCode (cname = "valaphp_embed_call_string_string_string_to_string", cheader_filename = "call/static_call.h")]
    private extern static string? call_string_string_string_to_string (string function_name, string first, string second, string third, out string? error_message);

    [CCode (cname = "valaphp_embed_call_string_string_to_int_or_minus_one", cheader_filename = "call/static_call.h")]
    private extern static int call_string_string_to_int_or_minus_one (string function_name, string first, string second, out string? error_message);

    [CCode (cname = "valaphp_embed_call_string_string_to_bool", cheader_filename = "call/static_call.h")]
    private extern static bool call_string_string_to_bool (string function_name, string first, string second, out string? error_message);

    [CCode (cname = "valaphp_embed_call_string_bool_to_bool", cheader_filename = "call/static_call.h")]
    private extern static bool call_string_bool_to_bool (string function_name, string value, bool flag, out string? error_message);

    private void throw_static_call_error (string function_name, string php_file, int php_line, string? error_message) throws Error {
        throw new EmbedRuntimeError.RUNTIME ("%s:%d: PHP %s failed: %s", php_file, php_line, function_name, error_message ?? "unknown error");
    }

    private string checked_static_string (string function_name, string php_file, int php_line, string? value, string? error_message) throws Error {
        if (error_message != null || value == null)
            throw_static_call_error (function_name, php_file, php_line, error_message);
        return value;
    }

    public string json_last_error_msg_static (string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("json_last_error_msg", php_file, php_line, call_no_args_to_string ("json_last_error_msg", out error_message), error_message);
    }

    public string token_name_static (int token_id, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("token_name", php_file, php_line, call_int_to_string ("token_name", token_id, out error_message), error_message);
    }

    public string phpinfo_static (string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("phpinfo", php_file, php_line, call_no_args_output_to_string_and_print ("phpinfo", out error_message), error_message);
    }

    public string zend_version_static (string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("zend_version", php_file, php_line, call_no_args_to_string ("zend_version", out error_message), error_message);
    }

    public double round_static (double value, int precision, RoundMode mode, string php_file, int php_line) throws Error {
        string? error_message = null;
        double result = call_double_int_int_to_double ("round", value, precision, (int) mode, out error_message);
        if (error_message != null)
            throw_static_call_error ("round", php_file, php_line, error_message);
        return result;
    }

    public double microtime_float_static (string php_file, int php_line) throws Error {
        string? error_message = null;
        double result = call_bool_to_double ("microtime", true, out error_message);
        if (error_message != null)
            throw_static_call_error ("microtime", php_file, php_line, error_message);
        return result;
    }

    public string microtime_string_static (string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("microtime", php_file, php_line, call_bool_to_string ("microtime", false, out error_message), error_message);
    }

    public int sleep_static (int seconds, string php_file, int php_line) throws Error {
        string? error_message = null;
        int result = call_int_to_int ("sleep", seconds, out error_message);
        if (error_message != null)
            throw_static_call_error ("sleep", php_file, php_line, error_message);
        return result;
    }

    public bool usleep_static (int microseconds, string php_file, int php_line) throws Error {
        string? error_message = null;
        bool result = call_int_to_bool ("usleep", microseconds, out error_message);
        if (error_message != null)
            throw_static_call_error ("usleep", php_file, php_line, error_message);
        return result;
    }

    public double floor_static (double value, string php_file, int php_line) throws Error {
        string? error_message = null;
        double result = call_double_to_double ("floor", value, out error_message);
        if (error_message != null)
            throw_static_call_error ("floor", php_file, php_line, error_message);
        return result;
    }

    public int strlen_static (string value, string php_file, int php_line) throws Error {
        string? error_message = null;
        int result = call_string_to_int ("strlen", value, out error_message);
        if (error_message != null)
            throw_static_call_error ("strlen", php_file, php_line, error_message);
        return result;
    }

    public string substr_static (string value, int offset, int length, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("substr", php_file, php_line, call_string_int_int_to_string ("substr", value, offset, length, out error_message), error_message);
    }

    public string exec_static (string command, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("exec", php_file, php_line, call_string_to_string ("exec", command, out error_message), error_message);
    }

    public string exec_output_status_static (string command, out Php.zval output, out int status, string php_file, int php_line) throws Error {
        output = Php.array_new ();
        status = 0;

        string standard_output;
        string standard_error;
        int wait_status;
        try {
            string[] argv = { "/bin/sh", "-c", command };
            Process.spawn_sync (null, argv, null, SpawnFlags.SEARCH_PATH, null, out standard_output, out standard_error, out wait_status);
        } catch (SpawnError e) {
            status = 1;
            Php.array_append (output, Php.zval.string_value (e.message));
            return "";
        }

        status = wait_status;
        string last_line = "";
        foreach (string raw_line in standard_output.split ("\n")) {
            if (raw_line == "") {
                continue;
            }
            last_line = raw_line;
            Php.array_append (output, Php.zval.string_value (raw_line));
        }

        return last_line;
    }

    public string shell_exec_static (string command, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("shell_exec", php_file, php_line, call_string_to_string ("shell_exec", command, out error_message), error_message);
    }

    public string file_get_contents_static (string path, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("file_get_contents", php_file, php_line, call_string_to_string ("file_get_contents", path, out error_message), error_message);
    }

    public string date_static (string format, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("date", php_file, php_line, call_string_to_string ("date", format, out error_message), error_message);
    }

    public string escapeshellarg_static (string value, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("escapeshellarg", php_file, php_line, call_string_to_string ("escapeshellarg", value, out error_message), error_message);
    }

    public bool is_numeric_static (string value, string php_file, int php_line) throws Error {
        string? error_message = null;
        bool result = call_string_to_bool ("is_numeric", value, out error_message);
        if (error_message != null)
            throw_static_call_error ("is_numeric", php_file, php_line, error_message);
        return result;
    }

    public string trim_static (string value, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("trim", php_file, php_line, call_string_to_string ("trim", value, out error_message), error_message);
    }

    public string ltrim_static (string value, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("ltrim", php_file, php_line, call_string_to_string ("ltrim", value, out error_message), error_message);
    }

    public string rtrim_static (string value, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("rtrim", php_file, php_line, call_string_to_string ("rtrim", value, out error_message), error_message);
    }

    public string rtrim_chars_static (string value, string characters, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("rtrim", php_file, php_line, call_string_string_to_string ("rtrim", value, characters, out error_message), error_message);
    }

    public string number_format_static (double value, int decimals, string dec_point, string thousands_sep, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("number_format", php_file, php_line, call_double_int_string_string_to_string ("number_format", value, decimals, dec_point, thousands_sep, out error_message), error_message);
    }

    public string str_replace_static (string search, string replacement, string value, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("str_replace", php_file, php_line, call_string_string_string_to_string ("str_replace", search, replacement, value, out error_message), error_message);
    }

    public string preg_replace_static (string pattern, string replacement, string value, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("preg_replace", php_file, php_line, call_string_string_string_to_string ("preg_replace", pattern, replacement, value, out error_message), error_message);
    }

    public string pack_string_static (string format, string value, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("pack", php_file, php_line, call_string_string_to_string ("pack", format, value, out error_message), error_message);
    }

    public string sprintf_string_int_static (string format, string value, int number, string php_file, int php_line) throws Error {
        string? error_message = null;
        return checked_static_string ("sprintf", php_file, php_line, call_string_string_int_to_string ("sprintf", format, value, number, out error_message), error_message);
    }

    public bool preg_match_bool_static (string pattern, string value, string php_file, int php_line) throws Error {
        string? error_message = null;
        int result = call_string_string_to_int_or_minus_one ("preg_match", pattern, value, out error_message);
        if (error_message != null)
            throw_static_call_error ("preg_match", php_file, php_line, error_message);
        return result > 0;
    }

    public int preg_match_static (string pattern, string value, out Php.zval matches, string php_file, int php_line) throws Error {
        matches = Php.array_new ();
        try {
            Regex regex = compile_php_regex (pattern);
            MatchInfo match_info;
            if (!regex.match (value, 0, out match_info)) {
                return 0;
            }

            int count = match_info.get_match_count ();
            for (int i = 0; i < count; i++) {
                string? part = match_info.fetch (i);
                Php.array_append (matches, Php.zval.string_value (part ?? ""));
            }
            return 1;
        } catch (RegexError e) {
            throw_static_call_error ("preg_match", php_file, php_line, e.message);
        }
        return 0;
    }

    public int preg_match_all_set_order_static (string pattern, string value, out Php.zval matches, string php_file, int php_line) throws Error {
        matches = Php.array_new ();
        try {
            Regex regex = compile_php_regex (pattern);
            MatchInfo match_info;
            if (!regex.match (value, 0, out match_info)) {
                return 0;
            }

            int count = 0;
            while (match_info.matches ()) {
                Php.zval row = Php.array_new ();
                int capture_count = match_info.get_match_count ();
                for (int i = 0; i < capture_count; i++) {
                    string? part = match_info.fetch (i);
                    Php.array_append (row, Php.zval.string_value (part ?? ""));
                }
                Php.array_append (matches, row);
                count++;
                if (!match_info.next ()) {
                    break;
                }
            }
            return count;
        } catch (RegexError e) {
            throw_static_call_error ("preg_match_all", php_file, php_line, e.message);
        }
        return 0;
    }

    public int preg_match_all_static (string pattern, string value, out Php.zval matches, int flags, string php_file, int php_line) throws Error {
        return preg_match_all_set_order_static (pattern, value, out matches, php_file, php_line);
    }

    private Regex compile_php_regex (string pattern) throws RegexError {
        string expression = pattern;
        string flags = "";
        if (pattern.length >= 2) {
            char delimiter = pattern[0];
            if (!delimiter.isalnum () && delimiter != '\\' && !delimiter.isspace ()) {
                int end = find_php_regex_delimiter (pattern, delimiter);
                if (end > 0) {
                    expression = pattern.substring (1, end - 1).replace ("\\" + delimiter.to_string (), delimiter.to_string ());
                    flags = pattern.substring (end + 1);
                }
            }
        }

        RegexCompileFlags compile_flags = RegexCompileFlags.OPTIMIZE;
        for (int i = 0; i < flags.length; i++) {
            switch (flags[i]) {
            case 'i':
                compile_flags |= RegexCompileFlags.CASELESS;
                break;
            case 'm':
                compile_flags |= RegexCompileFlags.MULTILINE;
                break;
            case 's':
                compile_flags |= RegexCompileFlags.DOTALL;
                break;
            case 'x':
                compile_flags |= RegexCompileFlags.EXTENDED;
                break;
            default:
                break;
            }
        }

        return new Regex (expression, compile_flags);
    }

    private int find_php_regex_delimiter (string pattern, char delimiter) {
        bool escaped = false;
        for (int i = pattern.length - 1; i > 0; i--) {
            char c = pattern[i];
            if (c != delimiter) {
                continue;
            }

            escaped = false;
            for (int j = i - 1; j >= 0 && pattern[j] == '\\'; j--) {
                escaped = !escaped;
            }
            if (!escaped) {
                return i;
            }
        }
        return -1;
    }

    public int stripos_static (string haystack, string needle, string php_file, int php_line) throws Error {
        string? error_message = null;
        int result = call_string_string_to_int_or_minus_one ("stripos", haystack, needle, out error_message);
        if (error_message != null)
            throw_static_call_error ("stripos", php_file, php_line, error_message);
        return result;
    }

    public bool flush_static (string php_file, int php_line) throws Error {
        string? error_message = null;
        bool result = call_no_args_to_bool ("flush", out error_message);
        if (error_message != null)
            throw_static_call_error ("flush", php_file, php_line, error_message);
        return result;
    }

    public bool class_exists_static (string class_name, bool autoload, string php_file, int php_line) throws Error {
        string? error_message = null;
        bool result = call_string_bool_to_bool ("class_exists", class_name, autoload, out error_message);
        if (error_message != null)
            throw_static_call_error ("class_exists", php_file, php_line, error_message);
        return result;
    }

    public bool str_starts_with_static (string haystack, string needle, string php_file, int php_line) throws Error {
        string? error_message = null;
        bool result = call_string_string_to_bool ("str_starts_with", haystack, needle, out error_message);
        if (error_message != null)
            throw_static_call_error ("str_starts_with", php_file, php_line, error_message);
        return result;
    }

    public int count_static (Php.zval value, string php_file, int php_line) throws Error {
        return value.count ();
    }

    public string json_encode_static (Php.zval value, int flags, string php_file, int php_line) throws Error {
        bool pretty = (flags & 128) != 0;
        return value.to_json (pretty);
    }

    public string implode_static (string separator, Php.zval values, string php_file, int php_line) throws Error {
        var builder = new StringBuilder ();
        bool first = true;
        foreach (Php.zval value in Php.array_values (values)) {
            if (!first)
                builder.append (separator);
            builder.append (Php.zval_to_string (value));
            first = false;
        }
        return builder.str;
    }

    private string regex_pattern_from_php_pattern (string pattern) {
        if (pattern.length < 2 || pattern[0] != '/')
            return pattern;

        int end = pattern.last_index_of_char ('/');
        if (end <= 0)
            return pattern;

        return pattern.substring (1, end - 1);
    }

    public Php.zval preg_split_static (string pattern, string value, string php_file, int php_line) throws Error {
        Php.zval result = Php.zval.array ();
        try {
            Regex regex = new Regex (regex_pattern_from_php_pattern (pattern));
            foreach (string part in regex.split (value))
                Php.array_append (result, Php.zval.string_value (part));
        } catch (RegexError e) {
            throw_static_call_error ("preg_split", php_file, php_line, e.message);
        }
        return result;
    }

    public Gee.ArrayList<string> preg_split_string_list_static (string pattern, string value, string php_file, int php_line) throws Error {
        var result = new Gee.ArrayList<string> ();
        try {
            Regex regex = new Regex (regex_pattern_from_php_pattern (pattern));
            foreach (string part in regex.split (value))
                result.add (part);
        } catch (RegexError e) {
            throw_static_call_error ("preg_split", php_file, php_line, e.message);
        }
        return result;
    }

    public Php.zval array_slice_static (Php.zval values, int offset, string php_file, int php_line) throws Error {
        Php.zval result = Php.zval.array ();
        Gee.ArrayList<Php.zval> array_values = Php.array_values (values);
        int start = offset >= 0 ? offset : array_values.size + offset;
        if (start < 0)
            start = 0;

        for (int i = start; i < array_values.size; i++)
            Php.array_append (result, array_values[i]);

        return result;
    }

    public Gee.ArrayList<string> array_slice_string_list_static (Gee.ArrayList<string> values, int offset, string php_file, int php_line) throws Error {
        var result = new Gee.ArrayList<string> ();
        int start = offset >= 0 ? offset : values.size + offset;
        if (start < 0)
            start = 0;

        for (int i = start; i < values.size; i++)
            result.add (values[i]);

        return result;
    }

    private int compare_zvals (Php.zval left, Php.zval right) {
        double left_number = Php.zval_to_double (left);
        double right_number = Php.zval_to_double (right);
        if (left_number < right_number)
            return -1;
        if (left_number > right_number)
            return 1;
        return 0;
    }

    public Php.zval min_static (Php.zval values, string php_file, int php_line) throws Error {
        Gee.ArrayList<Php.zval> array_values = Php.array_values (values);
        if (array_values.size == 0)
            return Php.zval.null_value ();

        Php.zval result = array_values[0];
        for (int i = 1; i < array_values.size; i++) {
            if (compare_zvals (array_values[i], result) < 0)
                result = array_values[i];
        }
        return result;
    }

    public Php.zval max_static (Php.zval values, string php_file, int php_line) throws Error {
        Gee.ArrayList<Php.zval> array_values = Php.array_values (values);
        if (array_values.size == 0)
            return Php.zval.null_value ();

        Php.zval result = array_values[0];
        for (int i = 1; i < array_values.size; i++) {
            if (compare_zvals (array_values[i], result) > 0)
                result = array_values[i];
        }
        return result;
    }

}
