namespace PhpEmbed {
    [CCode (cname = "valaphp_embed_stream_fopen", cheader_filename = "call/stream_resource.h")]
    private extern static uint call_stream_fopen (string path, string mode, out string? error_message);

    [CCode (cname = "valaphp_embed_stream_fclose", cheader_filename = "call/stream_resource.h")]
    private extern static bool call_stream_fclose (uint resource_id, out string? error_message);

    [CCode (cname = "valaphp_embed_stream_socket_client", cheader_filename = "call/stream_resource.h")]
    private extern static uint call_stream_socket_client (string target, out int err_no, out string? err_str, double timeout, int flags, out string? error_message);

    [CCode (cname = "valaphp_embed_stream_fread", cheader_filename = "call/stream_resource.h")]
    private extern static string? call_stream_fread (uint resource_id, int length, out string? error_message);

    [CCode (cname = "valaphp_embed_stream_fwrite", cheader_filename = "call/stream_resource.h")]
    private extern static int call_stream_fwrite (uint resource_id, string data, out string? error_message);

    [CCode (cname = "valaphp_embed_stream_fflush", cheader_filename = "call/stream_resource.h")]
    private extern static bool call_stream_fflush (uint resource_id, out string? error_message);

    [CCode (cname = "valaphp_embed_stream_set_blocking", cheader_filename = "call/stream_resource.h")]
    private extern static bool call_stream_set_blocking (uint resource_id, bool enable, out string? error_message);

    [CCode (cname = "valaphp_embed_stream_select_read", cheader_filename = "call/stream_resource.h")]
    private extern static int call_stream_select_read (uint resource_id, int seconds, int microseconds, out string? error_message);

    [CCode (cname = "valaphp_embed_stream_is_resource", cheader_filename = "call/stream_resource.h")]
    private extern static bool call_stream_is_resource (uint resource_id);

    [CCode (cname = "valaphp_embed_stream_resource_release", cheader_filename = "call/stream_resource.h")]
    private extern static void call_stream_resource_release (uint resource_id);

    public class StreamResource : Object {
        private uint resource_id;
        private bool closed;

        internal StreamResource (uint resource_id) {
            this.resource_id = resource_id;
            this.closed = resource_id == 0;
        }

        ~StreamResource () {
            close ();
        }

        internal uint get_resource_id () {
            return closed ? 0 : resource_id;
        }

        internal void mark_closed () {
            closed = true;
            resource_id = 0;
        }

        public bool is_closed () {
            return closed;
        }

        public bool is_resource () {
            return !closed && call_stream_is_resource (resource_id);
        }

        public void close () {
            if (closed)
                return;
            call_stream_resource_release (resource_id);
            mark_closed ();
        }
    }

    private void throw_stream_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");
    }

    public StreamResource fopen_static (string path, string mode, string php_file, int php_line) throws Error {
        string? error_message = null;
        uint resource_id = call_stream_fopen (path, mode, out error_message);
        if (resource_id == 0)
            throw_stream_call_error ("fopen", php_file, php_line, error_message);
        return new StreamResource (resource_id);
    }

    public StreamResource stream_socket_client_static (string target, out int err_no, out string? err_str, double timeout, int flags, string php_file, int php_line) throws Error {
        string? error_message = null;
        uint resource_id = call_stream_socket_client (target, out err_no, out err_str, timeout, flags, out error_message);
        if (resource_id == 0)
            throw_stream_call_error ("stream_socket_client", php_file, php_line, error_message ?? err_str);
        return new StreamResource (resource_id);
    }

    public bool fclose_static (StreamResource stream, string php_file, int php_line) throws Error {
        if (stream == null || stream.is_closed ())
            return true;

        string? error_message = null;
        bool result = call_stream_fclose (stream.get_resource_id (), out error_message);
        if (error_message != null)
            throw_stream_call_error ("fclose", php_file, php_line, error_message);
        if (result)
            stream.mark_closed ();
        return result;
    }

    public bool stream_set_blocking_static (StreamResource stream, bool enable, string php_file, int php_line) throws Error {
        string? error_message = null;
        bool result = call_stream_set_blocking (stream != null ? stream.get_resource_id () : 0, enable, out error_message);
        if (error_message != null)
            throw_stream_call_error ("stream_set_blocking", php_file, php_line, error_message);
        return result;
    }

    public int stream_select_read_static (StreamResource stream, int seconds, int microseconds, string php_file, int php_line) throws Error {
        string? error_message = null;
        int result = call_stream_select_read (stream != null ? stream.get_resource_id () : 0, seconds, microseconds, out error_message);
        if (error_message != null)
            throw_stream_call_error ("stream_select", php_file, php_line, error_message);
        return result;
    }

    public string fread_static (StreamResource stream, int length, string php_file, int php_line) throws Error {
        string? error_message = null;
        string? result = call_stream_fread (stream != null ? stream.get_resource_id () : 0, length, out error_message);
        if (error_message != null || result == null)
            throw_stream_call_error ("fread", php_file, php_line, error_message);
        return result;
    }

    public int fwrite_static (StreamResource stream, string data, string php_file, int php_line) throws Error {
        string? error_message = null;
        int result = call_stream_fwrite (stream != null ? stream.get_resource_id () : 0, data, out error_message);
        if (error_message != null)
            throw_stream_call_error ("fwrite", php_file, php_line, error_message);
        return result;
    }

    public bool fflush_static (StreamResource stream, string php_file, int php_line) throws Error {
        string? error_message = null;
        bool result = call_stream_fflush (stream != null ? stream.get_resource_id () : 0, out error_message);
        if (error_message != null)
            throw_stream_call_error ("fflush", php_file, php_line, error_message);
        return result;
    }

    public bool is_resource_static (StreamResource stream, string php_file, int php_line) throws Error {
        return stream != null && stream.is_resource ();
    }
}
