#include <ds9.h>
#include <assert.h>

typedef void (*copy_header_func_t)(VALUE, nghttp2_nv *, size_t);

VALUE mDS9;
VALUE cDS9Session;
VALUE cDS9Client;
VALUE cDS9Server;
VALUE cDS9Callbacks;
VALUE eDS9Exception;
VALUE eDS9UninitializedException;

ID id_on_header,
   id_on_begin_headers,
   id_on_frame_not_send,
   id_on_begin_frame,
   id_on_invalid_frame_recv,
   id_send_event,
   id_recv_event,
   id_on_frame_send,
   id_on_frame_recv,
   id_on_stream_close,
   id_on_data_chunk_recv,
   id_on_data_source_read,
   id_before_frame_send;

#define CheckSelf(ptr) \
    if (NULL == (ptr)) \
      rb_raise(eDS9UninitializedException, "not initialized, call `super` from `initialize`");

static void * wrap_xmalloc(size_t size, void *mem_user_data)
{
    return xmalloc(size);
}

static void wrap_xfree(void *ptr, void *mem_user_data)
{
    xfree(ptr);
}

static void *wrap_xcalloc(size_t nmemb, size_t size, void *mem_user_data)
{
    return xcalloc(nmemb, size);
}

static void *wrap_xrealloc(void *ptr, size_t size, void *mem_user_data)
{
    return xrealloc(ptr, size);
}

static VALUE explode(int code) {
    return rb_funcall(eDS9Exception, rb_intern("abort"), 1, INT2NUM(code));
}

static int before_frame_send_callback(nghttp2_session *session,
				      const nghttp2_frame *frame,
				      void *user_data)
{
    VALUE self = (VALUE)user_data;
    VALUE ret;

    ret = rb_funcall(self, id_before_frame_send, 1, WrapDS9Frame(frame));

    if (ret == Qfalse) return 1;

    return 0;
}

static ssize_t send_callback(nghttp2_session * session,
			     const uint8_t *data,
			     size_t length,
			     int flags, void *user_data)
{
    VALUE self = (VALUE)user_data;
    VALUE ret;

    ret = rb_funcall(self, id_send_event, 1, rb_str_new((const char *)data, length));

    return NUM2INT(ret);
}

static int on_frame_recv_callback(nghttp2_session *session,
				  const nghttp2_frame *frame,
				  void *user_data)
{
    VALUE self = (VALUE)user_data;
    VALUE ret;

    ret = rb_funcall(self, id_on_frame_recv, 1, WrapDS9Frame(frame));

    if (ret == Qfalse) {
	return 1;
    }

    return 0;
}

static int on_stream_close_callback(nghttp2_session *session,
				    int32_t stream_id,
				    uint32_t error_code,
				    void *user_data)
{
    VALUE self = (VALUE)user_data;

    VALUE ret = rb_funcall(self, id_on_stream_close, 2, INT2NUM(stream_id), INT2NUM(error_code));

    if (ret == Qfalse) {
	return 1;
    }

    return 0;
}

static int on_header_callback(nghttp2_session *session,
			      const nghttp2_frame *frame,
			      const uint8_t *name, size_t namelen,
			      const uint8_t *value, size_t valuelen,
			      uint8_t flags, void *user_data)
{
    VALUE self = (VALUE)user_data;

    VALUE ret = rb_funcall(self, id_on_header, 4,
	    rb_usascii_str_new((const char *)name, namelen),
	    rb_usascii_str_new((const char *)value, valuelen),
	    WrapDS9Frame(frame),
	    INT2NUM(flags));

    if (ret == Qfalse) {
	return 1;
    }

    return 0;
}

static int on_begin_headers_callback(nghttp2_session *session,
			             const nghttp2_frame *frame,
			             void *user_data)
{
    VALUE self = (VALUE)user_data;

    VALUE ret = rb_funcall(self, id_on_begin_headers, 1,
	    WrapDS9Frame(frame));

    if (ret == Qfalse) {
	return 1;
    }

    return 0;
}

static ssize_t recv_callback(nghttp2_session *session, uint8_t *buf,
			     size_t length, int flags,
			     void *user_data)
{
    VALUE self = (VALUE)user_data;
    VALUE ret;
    ssize_t len;

    ret = rb_funcall(self, id_recv_event, 1, INT2NUM(length));

    if (FIXNUM_P(ret)) {
	return NUM2INT(ret);
    }

    Check_Type(ret, T_STRING);
    len = RSTRING_LEN(ret);

    memcpy(buf, StringValuePtr(ret), len);

    return len;
}

static int on_begin_frame_callback(nghttp2_session *session,
				   const nghttp2_frame_hd *hd,
				   void *user_data)
{
    VALUE self = (VALUE)user_data;
    VALUE ret;

    ret = rb_funcall(self, id_on_begin_frame, 1, WrapDS9FrameHeader(hd));

    if (ret == Qfalse) {
	return 1;
    }

    return 0;
}

static int on_data_chunk_recv_callback(nghttp2_session *session,
				       uint8_t flags,
				       int32_t stream_id,
				       const uint8_t *data,
				       size_t len, void *user_data)
{
    VALUE self = (VALUE)user_data;
    VALUE ret;

    ret = rb_funcall(self, id_on_data_chunk_recv, 3,
	    INT2NUM(stream_id),
	    rb_str_new((const char *)data, len),
	    INT2NUM(flags));

    if (ret == Qfalse) {
	return 1;
    }

    return 0;
}

static int on_invalid_frame_recv_callback(nghttp2_session *session,
					  const nghttp2_frame *frame, int lib_error_code,
					  void *user_data)
{
    VALUE self = (VALUE)user_data;
    VALUE ret;

    ret = rb_funcall(self, id_on_invalid_frame_recv, 2,
	    WrapDS9Frame(frame),
	    INT2NUM(lib_error_code));

    if (ret == Qfalse) {
	return 1;
    }

    return 0;
}

static int on_frame_send_callback(nghttp2_session *session,
				  const nghttp2_frame *frame,
				  void *user_data)
{
    VALUE self = (VALUE)user_data;
    VALUE ret;

    ret = rb_funcall(self, id_on_frame_send, 1, WrapDS9Frame(frame));

    if (ret == Qfalse) {
	return 1;
    }

    return 0;
}

static int on_frame_not_send_callback(nghttp2_session *session,
				      const nghttp2_frame *frame,
				      int lib_error_code,
				      void *user_data)
{
    VALUE self = (VALUE)user_data;
    VALUE reason = rb_str_new2(nghttp2_strerror(lib_error_code));
    VALUE ret;

    ret = rb_funcall(self, id_on_frame_not_send, 2, WrapDS9Frame(frame), reason);

    if (ret == Qfalse) {
	return 1;
    }

    return 0;
}

static ssize_t rb_data_read_callback(nghttp2_session *session,
				     int32_t stream_id, uint8_t *buf,
				     size_t length, uint32_t *data_flags,
				     nghttp2_data_source *source, void *user_data)
{
    VALUE self = (VALUE)user_data;
    VALUE ret;
    ssize_t len;

    ret = rb_funcall(self, id_on_data_source_read, 2, INT2NUM(stream_id),
	    INT2NUM(length));

    if (NIL_P(ret)) {
	*data_flags |= NGHTTP2_DATA_FLAG_EOF;
	return 0;
    }

    if (ret == Qfalse) {
	*data_flags |= NGHTTP2_DATA_FLAG_EOF;
	*data_flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM;
	return 0;
    }

    Check_Type(ret, T_STRING);
    len = RSTRING_LEN(ret);
    memcpy(buf, StringValuePtr(ret), len);

    return len;
}

static void copy_list_to_nv(VALUE list, nghttp2_nv * head, size_t niv)
{
    size_t i;

    for(i = 0; i < niv; i++, head++) {
	VALUE tuple = rb_ary_entry(list, (long)i);
	VALUE name = rb_ary_entry(tuple, 0);
	VALUE value = rb_ary_entry(tuple, 1);

	head->name = (uint8_t *)StringValuePtr(name);
	head->namelen = RSTRING_LEN(name);

	head->value = (uint8_t *)StringValuePtr(value);
	head->valuelen = RSTRING_LEN(value);
	head->flags = NGHTTP2_NV_FLAG_NONE;
    }
}

struct hash_copy_ctx {
    nghttp2_nv * head;
};

static int
hash_copy_i(VALUE name, VALUE value, struct hash_copy_ctx * ctx)
{
    nghttp2_nv * head = ctx->head;

    head->name = (uint8_t *)StringValuePtr(name);
    head->namelen = RSTRING_LEN(name);

    head->value = (uint8_t *)StringValuePtr(value);
    head->valuelen = RSTRING_LEN(value);
    head->flags = NGHTTP2_NV_FLAG_NONE;

    ctx->head = head + 1;

    return ST_CONTINUE;
}

static void copy_hash_to_nv(VALUE hash, nghttp2_nv * head, size_t niv)
{
    struct hash_copy_ctx copy_ctx;
    copy_ctx.head = head;

    rb_hash_foreach(hash, hash_copy_i, &copy_ctx);
}

static VALUE allocate_session(VALUE klass)
{
    return TypedData_Wrap_Struct(klass, &ds9_session_type, 0);
}

static VALUE client_init_internals(VALUE self, VALUE cb)
{
    nghttp2_session_callbacks *callbacks;
    nghttp2_session *session;
    nghttp2_mem mem = {
	NULL,
	wrap_xmalloc,
	wrap_xfree,
	wrap_xcalloc,
	wrap_xrealloc
    };

    TypedData_Get_Struct(cb, nghttp2_session_callbacks, &ds9_callbacks_type, callbacks);

    nghttp2_session_client_new3(&session, callbacks, (void *)self, NULL, &mem);
    DATA_PTR(self) = session;

    return self;
}

static VALUE server_init_internals(VALUE self, VALUE cb)
{
    nghttp2_session_callbacks *callbacks;
    nghttp2_session *session;
    nghttp2_mem mem = {
	NULL,
	wrap_xmalloc,
	wrap_xfree,
	wrap_xcalloc,
	wrap_xrealloc
    };

    TypedData_Get_Struct(cb, nghttp2_session_callbacks, &ds9_callbacks_type, callbacks);

    nghttp2_session_server_new3(&session, callbacks, (void *)self, NULL, &mem);
    DATA_PTR(self) = session;

    return self;
}

static VALUE session_want_write_p(VALUE self)
{
    nghttp2_session *session;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    if (nghttp2_session_want_write(session) == 0) {
	return Qfalse;
    }

    return Qtrue;
}

static VALUE session_want_read_p(VALUE self)
{
    nghttp2_session *session;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    if (nghttp2_session_want_read(session) == 0) {
	return Qfalse;
    }

    return Qtrue;
}

static VALUE session_submit_ping(VALUE self)
{
    nghttp2_session *session;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    if (nghttp2_submit_ping(session, NGHTTP2_FLAG_NONE, NULL) == 0)
	return Qtrue;

    return Qfalse;
}

static VALUE session_submit_settings(VALUE self, VALUE settings)
{
    size_t niv, i;
    nghttp2_settings_entry *iv, *head;
    nghttp2_session *session;
    int rv;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    niv = RARRAY_LEN(settings);
    iv = xcalloc(niv, sizeof(nghttp2_settings_entry));
    head = iv;

    for(i = 0; i < niv; i++, head++) {
	VALUE tuple = rb_ary_entry(settings, (long)i);
	head->settings_id = NUM2INT(rb_ary_entry(tuple, 0));
	head->value = NUM2INT(rb_ary_entry(tuple, 1));
    }

    rv = nghttp2_submit_settings(session, NGHTTP2_FLAG_NONE, iv, niv);

    xfree(iv);

    if (0 != rv) {
	explode(rv);
    }

    return self;
}

static VALUE session_submit_trailer(VALUE self, VALUE stream_id, VALUE trailers)
{
    size_t niv;
    nghttp2_nv *nva;
    nghttp2_session *session;
    int rv;
    int32_t s_id;
    copy_header_func_t copy_func;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    s_id = NUM2INT(stream_id);

    switch(TYPE(trailers))
    {
	case T_ARRAY:
	    niv = RARRAY_LEN(trailers);
	    copy_func = copy_list_to_nv;
	    break;
	case T_HASH:
	    niv = RHASH_SIZE(trailers);
	    copy_func = copy_hash_to_nv;
	    break;
	default:
	    Check_Type(trailers, T_ARRAY);
    }

    nva = xcalloc(niv, sizeof(nghttp2_nv));
    copy_func(trailers, nva, niv);

    rv = nghttp2_submit_trailer(session, s_id, nva, niv);

    xfree(nva);

    if (0 != rv) {
	explode(rv);
    }

    return self;
}

static VALUE session_send(VALUE self)
{
    int rv;
    nghttp2_session *session;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    rv = nghttp2_session_send(session);
    if (rv != 0) {
	explode(rv);
    }

    return self;
}

static VALUE session_receive(VALUE self)
{
    int rv;
    nghttp2_session *session;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    assert(session);
    rv = nghttp2_session_recv(session);
    if (rv != 0) {
	explode(rv);
    }

    return self;
}

static VALUE session_mem_receive(VALUE self, VALUE buf)
{
    int rv;
    nghttp2_session *session;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    rv = nghttp2_session_mem_recv(session, (const uint8_t *)StringValuePtr(buf), RSTRING_LEN(buf));
    if (rv < 0) {
	explode(rv);
    }

    return INT2NUM(rv);
}

static VALUE session_mem_send(VALUE self)
{
    ssize_t rv;
    const uint8_t *data;
    nghttp2_session *session;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    rv = nghttp2_session_mem_send(session, &data);

    if (rv == 0) {
	return Qfalse;
    }

    if (rv < 0) {
	explode(rv);
    }

    return rb_str_new((const char *)data, rv);
}

static VALUE session_outbound_queue_size(VALUE self)
{
    nghttp2_session *session;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    return INT2NUM(nghttp2_session_get_outbound_queue_size(session));
}

static ssize_t
ruby_read(nghttp2_session *session, int32_t stream_id, uint8_t *buf, size_t length,
    uint32_t *data_flags, nghttp2_data_source *source, void *user_data)
{
    VALUE ret = rb_funcall(source->ptr, rb_intern("read"), 1, INT2NUM(length));

    if (NIL_P(ret)) {
	VALUE self = (VALUE)user_data;
	rb_funcall(self, rb_intern("remove_post_buffer"), 1, INT2NUM(stream_id));
	*data_flags |= NGHTTP2_DATA_FLAG_EOF;
	return 0;
    } else {
	memcpy(buf, RSTRING_PTR(ret), RSTRING_LEN(ret));
	return RSTRING_LEN(ret);
    }
}

static ssize_t
file_read(nghttp2_session *session, int32_t stream_id, uint8_t *buf, size_t length,
    uint32_t *data_flags, nghttp2_data_source *source, void *user_data)
{
    ssize_t nread;
    rb_io_t * fptr;

    fptr = (rb_io_t *)source->ptr;
    rb_io_check_readable(fptr);

    nread = read(fptr->fd, buf, length);

    if (nread == -1) {
	return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
    }

    if (nread == 0) {
	*data_flags |= NGHTTP2_DATA_FLAG_EOF;
	VALUE self = (VALUE)user_data;
	rb_funcall(self, rb_intern("remove_post_buffer"), 1, INT2NUM(stream_id));
    }
    return nread;
}

static VALUE session_submit_request(VALUE self, VALUE settings, VALUE body)
{
    size_t niv, i;
    nghttp2_nv *nva, *head;
    nghttp2_session *session;
    nghttp2_data_provider provider;
    int rv;
    copy_header_func_t copy_func;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    switch(TYPE(settings))
    {
	case T_ARRAY:
	    niv = RARRAY_LEN(settings);
	    copy_func = copy_list_to_nv;
	    break;
	case T_HASH:
	    niv = RHASH_SIZE(settings);
	    copy_func = copy_hash_to_nv;
	    break;
	default:
	    Check_Type(settings, T_ARRAY);
    }

    nva = xcalloc(niv, sizeof(nghttp2_nv));

    copy_func(settings, nva, niv);

    if (NIL_P(body)) {
	rv = nghttp2_submit_request(session, NULL, nva, niv, NULL, NULL);
    } else {
	if (TYPE(body) == T_FILE) {
	    rb_io_t * rb_file;
	    GetOpenFile(body, rb_file);
	    /* Treat as a file descriptor */
	    provider.source.ptr = rb_file;
	    provider.read_callback = file_read;
	} else {
	    provider.source.ptr = body;
	    provider.read_callback = ruby_read;
	}

	rv = nghttp2_submit_request(session, NULL, nva, niv, &provider, NULL);
    }

    xfree(nva);

    if (rv < 0) {
	explode(rv);
    }

    if(!NIL_P(body)) {
	rb_funcall(self, rb_intern("save_post_buffer"), 2, INT2NUM(rv), body);
    }

    return INT2NUM(rv);
}

static VALUE session_submit_shutdown(VALUE self)
{
    int rv;
    nghttp2_session *session;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    rv = nghttp2_submit_shutdown_notice(session);

    if (rv == 0)
	return Qtrue;

    return explode(rv);
}

static VALUE session_submit_goaway(VALUE self, VALUE last_stream_id, VALUE err)
{
    int rv;
    nghttp2_session *session;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    rv = nghttp2_submit_goaway(session, NGHTTP2_FLAG_NONE,
	    NUM2INT(last_stream_id), NUM2INT(err), NULL, 0);

    if (rv == 0)
	return Qtrue;

    return explode(rv);
}

static VALUE session_stream_local_closed_p(VALUE self, VALUE streamid)
{
    nghttp2_session *session;
    int stream_id;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    stream_id = NUM2INT(streamid);

    switch(nghttp2_session_get_stream_local_close(session, stream_id))
    {
	case 1:
	    return Qtrue;
	    break;
	case 0:
	    return Qfalse;
	    break;
	default:
	    rb_raise(rb_eStandardError, "no such stream: %d", stream_id);
    }

    return Qfalse;
}

static VALUE session_stream_remote_closed_p(VALUE self, VALUE streamid)
{
    nghttp2_session *session;
    int stream_id;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    stream_id = NUM2INT(streamid);

    switch(nghttp2_session_get_stream_remote_close(session, stream_id))
    {
	case 1:
	    return Qtrue;
	    break;
	case 0:
	    return Qfalse;
	    break;
	default:
	    rb_raise(rb_eStandardError, "no such stream: %d", stream_id);
    }

    return Qfalse;
}

static VALUE session_terminate_session(VALUE self, VALUE err)
{
    int rv;
    nghttp2_session *session;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    rv = nghttp2_session_terminate_session(session, NUM2INT(err));

    if (rv != 0) {
	explode(rv);
    }

    return self;
}

static VALUE server_submit_response(VALUE self, VALUE stream_id, VALUE headers)
{
    nghttp2_session *session;
    size_t niv;
    nghttp2_nv *nva, *head;
    nghttp2_data_provider provider;
    int rv;
    copy_header_func_t copy_func;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    switch(TYPE(headers))
    {
	case T_ARRAY:
	    niv = RARRAY_LEN(headers);
	    copy_func = copy_list_to_nv;
	    break;
	case T_HASH:
	    niv = RHASH_SIZE(headers);
	    copy_func = copy_hash_to_nv;
	    break;
	default:
	    Check_Type(headers, T_ARRAY);
    }

    nva = xcalloc(niv, sizeof(nghttp2_nv));

    copy_func(headers, nva, niv);

    provider.read_callback = rb_data_read_callback;

    rv = nghttp2_submit_response(session, NUM2INT(stream_id), nva, niv, &provider);

    xfree(nva);

    if (0 != rv) {
	explode(rv);
    }

    return self;
}

static VALUE make_callbacks(VALUE self)
{
    nghttp2_session_callbacks *callbacks;
    nghttp2_session_callbacks_new(&callbacks);

    if (rb_obj_respond_to(self, id_on_header, 1))
	nghttp2_session_callbacks_set_on_header_callback(callbacks, on_header_callback);

    if (rb_obj_respond_to(self, id_on_begin_headers, 1))
	nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks, on_begin_headers_callback);

    if (rb_obj_respond_to(self, id_on_frame_not_send, 1))
	nghttp2_session_callbacks_set_on_frame_not_send_callback(callbacks, on_frame_not_send_callback);

    if (rb_obj_respond_to(self, id_on_begin_frame, 1))
	nghttp2_session_callbacks_set_on_begin_frame_callback(callbacks, on_begin_frame_callback);

    if (rb_obj_respond_to(self, id_on_invalid_frame_recv, 1))
	nghttp2_session_callbacks_set_on_invalid_frame_recv_callback(callbacks, on_invalid_frame_recv_callback);

    if (rb_obj_respond_to(self, id_send_event, 1))
	nghttp2_session_callbacks_set_send_callback(callbacks, send_callback);

    if (rb_obj_respond_to(self, id_recv_event, 1))
	nghttp2_session_callbacks_set_recv_callback(callbacks, recv_callback);

    if (rb_obj_respond_to(self, id_on_frame_send, 1))
	nghttp2_session_callbacks_set_on_frame_send_callback(callbacks, on_frame_send_callback);

    if (rb_obj_respond_to(self, id_on_frame_recv, 1))
	nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, on_frame_recv_callback);

    if (rb_obj_respond_to(self, id_on_stream_close, 1))
	nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, on_stream_close_callback);

    if (rb_obj_respond_to(self, id_on_data_chunk_recv, 1))
	nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, on_data_chunk_recv_callback);

    if (rb_obj_respond_to(self, id_before_frame_send, 1))
	nghttp2_session_callbacks_set_before_frame_send_callback(callbacks, before_frame_send_callback);

    return TypedData_Wrap_Struct(cDS9Callbacks, &ds9_callbacks_type, callbacks);
}

static VALUE server_submit_push_promise(VALUE self, VALUE stream_id, VALUE headers)
{
    nghttp2_session *session;
    nghttp2_nv *nva, *head;
    size_t niv, i;
    int rv;

    TypedData_Get_Struct(self, nghttp2_session, &ds9_session_type, session);
    CheckSelf(session);

    Check_Type(headers, T_ARRAY);
    niv = RARRAY_LEN(headers);
    nva = xcalloc(niv, sizeof(nghttp2_nv));
    head = nva;

    for(i = 0; i < niv; i++, head++) {
	VALUE tuple = rb_ary_entry(headers, (long)i);
	VALUE name = rb_ary_entry(tuple, 0);
	VALUE value = rb_ary_entry(tuple, 1);

	head->name = (uint8_t *)StringValuePtr(name);
	head->namelen = RSTRING_LEN(name);

	head->value = (uint8_t *)StringValuePtr(value);
	head->valuelen = RSTRING_LEN(value);
	head->flags = NGHTTP2_NV_FLAG_NONE;
    }

    rv = nghttp2_submit_push_promise(session, 0, NUM2INT(stream_id), nva, niv, NULL);

    xfree(nva);

    switch(rv) {
	case NGHTTP2_ERR_NOMEM:
	case NGHTTP2_ERR_PROTO:
	case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
	case NGHTTP2_ERR_INVALID_ARGUMENT:
	    return explode(rv);
	    break;
	default:
	    return INT2NUM(rv);
    }
}

static VALUE rb_nghttp_version(VALUE klass)
{
    nghttp2_info * info = nghttp2_version(0);

    return rb_usascii_str_new2(info->version_str);
}

static VALUE errors_to_string(VALUE mod, VALUE err)
{
    return rb_usascii_str_new2(nghttp2_strerror(NUM2INT(err)));
}

void Init_ds9(void)
{
    mDS9 = rb_define_module("DS9");

    Init_ds9_frames(mDS9);

    rb_define_const(mDS9, "PROTO_VERSION_ID", rb_str_new(NGHTTP2_PROTO_VERSION_ID, NGHTTP2_PROTO_VERSION_ID_LEN));

    eDS9Exception = rb_define_class_under(mDS9, "Exception", rb_eStandardError);
    eDS9UninitializedException = rb_define_class_under(mDS9, "UninitializedException", rb_eStandardError);

    VALUE mDS9Settings = rb_define_module_under(mDS9, "Settings");
    rb_define_const(mDS9Settings, "MAX_CONCURRENT_STREAMS", INT2NUM(NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS));
    rb_define_const(mDS9Settings, "INITIAL_WINDOW_SIZE", INT2NUM(NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE));
    rb_define_const(mDS9Settings, "HEADER_TABLE_SIZE", INT2NUM(NGHTTP2_SETTINGS_HEADER_TABLE_SIZE));
    rb_define_const(mDS9Settings, "ENABLE_PUSH", INT2NUM(NGHTTP2_SETTINGS_ENABLE_PUSH));
    rb_define_const(mDS9Settings, "MAX_FRAME_SIZE", INT2NUM(NGHTTP2_SETTINGS_MAX_FRAME_SIZE));
    rb_define_const(mDS9Settings, "MAX_HEADER_LIST_SIZE", INT2NUM(NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE));

    rb_define_const(mDS9, "ERR_WOULDBLOCK", INT2NUM(NGHTTP2_ERR_WOULDBLOCK));
    rb_define_const(mDS9, "ERR_EOF", INT2NUM(NGHTTP2_ERR_EOF));
    rb_define_const(mDS9, "NO_ERROR", INT2NUM(NGHTTP2_NO_ERROR));
    rb_define_const(mDS9, "DEFAULT_WEIGHT", INT2NUM(NGHTTP2_DEFAULT_WEIGHT));
    rb_define_const(mDS9, "MAX_WEIGHT", INT2NUM(NGHTTP2_MAX_WEIGHT));
    rb_define_const(mDS9, "MIN_WEIGHT", INT2NUM(NGHTTP2_MIN_WEIGHT));
    rb_define_const(mDS9, "MAX_WINDOW_SIZE", INT2NUM(NGHTTP2_MAX_WINDOW_SIZE));
    rb_define_const(mDS9, "INITIAL_WINDOW_SIZE", INT2NUM(NGHTTP2_INITIAL_WINDOW_SIZE));
    rb_define_const(mDS9, "INITIAL_CONNECTION_WINDOW_SIZE", INT2NUM(NGHTTP2_INITIAL_CONNECTION_WINDOW_SIZE));
    rb_define_const(mDS9, "DEFAULT_HEADER_TABLE_SIZE", INT2NUM(NGHTTP2_DEFAULT_HEADER_TABLE_SIZE));
    rb_define_const(mDS9, "CLIENT_MAGIC", rb_str_new(NGHTTP2_CLIENT_MAGIC, NGHTTP2_CLIENT_MAGIC_LEN));

    rb_define_singleton_method(mDS9, "nghttp_version", rb_nghttp_version, 0);

    cDS9Callbacks = rb_define_class_under(mDS9, "Callbacks", rb_cData);

    cDS9Session = rb_define_class_under(mDS9, "Session", rb_cObject);
    cDS9Client = rb_define_class_under(mDS9, "Client", cDS9Session);
    cDS9Server = rb_define_class_under(mDS9, "Server", cDS9Session);

    rb_define_alloc_func(cDS9Session, allocate_session);

    rb_define_method(cDS9Session, "want_write?", session_want_write_p, 0);
    rb_define_method(cDS9Session, "want_read?", session_want_read_p, 0);
    rb_define_method(cDS9Session, "submit_settings", session_submit_settings, 1);
    rb_define_method(cDS9Session, "submit_ping", session_submit_ping, 0);
    rb_define_method(cDS9Session, "submit_goaway", session_submit_goaway, 2);
    rb_define_method(cDS9Session, "send", session_send, 0);
    rb_define_method(cDS9Session, "receive", session_receive, 0);
    rb_define_method(cDS9Session, "mem_receive", session_mem_receive, 1);
    rb_define_method(cDS9Session, "mem_send", session_mem_send, 0);
    rb_define_method(cDS9Session, "outbound_queue_size", session_outbound_queue_size, 0);
    rb_define_method(cDS9Session, "terminate_session", session_terminate_session, 1);
    rb_define_method(cDS9Session, "stream_local_closed?", session_stream_local_closed_p, 1);
    rb_define_method(cDS9Session, "stream_remote_closed?", session_stream_remote_closed_p, 1);

    rb_define_private_method(cDS9Session, "submit_request", session_submit_request, 2);
    rb_define_private_method(cDS9Session, "make_callbacks", make_callbacks, 0);
    rb_define_private_method(cDS9Client, "init_internals", client_init_internals, 1);
    rb_define_private_method(cDS9Server, "init_internals", server_init_internals, 1);

    rb_define_method(cDS9Server, "submit_response", server_submit_response, 2);
    rb_define_method(cDS9Server, "submit_push_promise", server_submit_push_promise, 2);
    rb_define_method(cDS9Server, "submit_shutdown", session_submit_shutdown, 0);
    rb_define_method(cDS9Server, "submit_trailer", session_submit_trailer, 2);

    rb_define_singleton_method(eDS9Exception, "to_string", errors_to_string, 1);

    id_on_header             = rb_intern("on_header");
    id_on_begin_headers      = rb_intern("on_begin_headers");
    id_on_frame_not_send     = rb_intern("on_frame_not_send");
    id_on_begin_frame        = rb_intern("on_begin_frame");
    id_on_invalid_frame_recv = rb_intern("on_invalid_frame_recv");
    id_send_event            = rb_intern("send_event");
    id_recv_event            = rb_intern("recv_event");
    id_on_frame_send         = rb_intern("on_frame_send");
    id_on_frame_recv         = rb_intern("on_frame_recv");
    id_on_stream_close       = rb_intern("on_stream_close");
    id_on_data_chunk_recv    = rb_intern("on_data_chunk_recv");
    id_before_frame_send     = rb_intern("before_frame_send");
    id_on_data_source_read   = rb_intern("on_data_source_read");
}

/* vim: set noet sws=4 sw=4: */