#include <sys/socket.h>

#include <assert.h>
#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>

#include "events.h"
#include "mpool.h"

#include "network.h"

struct network_read_cookie {
	int (* callback)(void *, ssize_t);
	void * cookie;
	int fd;
	uint8_t * buf;
	size_t buflen;
	size_t minlen;
	size_t bufpos;
};

MPOOL(network_read_cookie, struct network_read_cookie, 16);

/* Invoke the callback, clean up, and return the callback's status. */
static int
docallback(struct network_read_cookie * C, ssize_t nbytes)
{
	int rc;

	/* Invoke the callback. */
	rc = (C->callback)(C->cookie, nbytes);

	/* Clean up. */
	mpool_network_read_cookie_free(C);

	/* Return the callback's status. */
	return (rc);
}

/* The socket is ready for reading/writing. */
static int
callback_buf(void * cookie)
{
	struct network_read_cookie * C = cookie;
	size_t oplen;
	ssize_t len;

	/* Attempt to read/write data to/from the buffer. */
	oplen = C->buflen - C->bufpos;
	len = recv(C->fd, C->buf + C->bufpos, oplen, 0);

	/* Failure? */
	if (len == -1) {
		/* Was it really an error, or just a try-again? */
		if ((errno == EAGAIN) ||
#if EAGAIN != EWOULDBLOCK
		    (errno == EWOULDBLOCK) ||
#endif
		    (errno == EINTR))
			goto tryagain;

		/* Something went wrong. */
		goto failed;
	} else if (len == 0) {
		/* The socket was shut down by the remote host. */
		goto eof;
	}

	/* We processed some data. */
	C->bufpos += (size_t)len;

	/* Do we need to keep going? */
	if (C->bufpos < C->minlen)
		goto tryagain;

	/* Sanity-check: buffer position must fit into a ssize_t. */
	assert(C->bufpos <= SSIZE_MAX);

	/* Invoke the callback and return. */
	return (docallback(C, (ssize_t)C->bufpos));

tryagain:
	/* Reset the event. */
	if (events_network_register(callback_buf, C, C->fd,
	    EVENTS_NETWORK_OP_READ))
		goto failed;

	/* Callback was reset. */
	return (0);

eof:
	/* Invoke the callback with an EOF status and return. */
	return (docallback(C, 0));

failed:
	/* Invoke the callback with a failure status and return. */
	return (docallback(C, -1));
}

/**
 * network_read(fd, buf, buflen, minread, callback, cookie):
 * Asynchronously read up to ${buflen} bytes of data from ${fd} into ${buf}.
 * When at least ${minread} bytes have been read or on error, invoke
 * ${callback}(${cookie}, lenread), where lenread is 0 on EOF or -1 on error,
 * and the number of bytes read (between ${minread} and ${buflen} inclusive)
 * otherwise.  Return a cookie which can be passed to network_read_cancel() in
 * order to cancel the read.
 */
void *
network_read(int fd, uint8_t * buf, size_t buflen, size_t minread,
    int (* callback)(void *, ssize_t), void * cookie)
{
	struct network_read_cookie * C;

	/* Make sure buflen is non-zero. */
	assert(buflen != 0);

	/* Sanity-check: # bytes must fit into a ssize_t. */
	assert(buflen <= SSIZE_MAX);

	/* Bake a cookie. */
	if ((C = mpool_network_read_cookie_malloc()) == NULL)
		goto err0;
	C->callback = callback;
	C->cookie = cookie;
	C->fd = fd;
	C->buf = buf;
	C->buflen = buflen;
	C->minlen = minread;
	C->bufpos = 0;

	/* Register a callback for network readiness. */
	if (events_network_register(callback_buf, C, C->fd,
	    EVENTS_NETWORK_OP_READ))
		goto err1;

	/* Success! */
	return (C);

err1:
	mpool_network_read_cookie_free(C);
err0:
	/* Failure! */
	return (NULL);
}

/**
 * network_read_cancel(cookie):
 * Cancel the buffer read for which the cookie ${cookie} was returned by
 * network_read().  Do not invoke the callback associated with the read.
 */
void
network_read_cancel(void * cookie)
{
	struct network_read_cookie * C = cookie;

	/* Kill the network event. */
	events_network_cancel(C->fd, EVENTS_NETWORK_OP_READ);

	/* Free the cookie. */
	mpool_network_read_cookie_free(C);
}
