#include "iocache.h"
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

struct iocache {
	char *ioc_buf; /* the data */
	unsigned long ioc_offset; /* where we're reading in the buffer */
	unsigned long ioc_buflen; /* the amount of data read into the buffer */
	unsigned long ioc_bufsize; /* size of the buffer */
};

void iocache_destroy(iocache *ioc)
{
	if (!ioc)
		return;

	if (ioc->ioc_buf)
		free(ioc->ioc_buf);

	free(ioc);
}

/**
 * Attempts to move data from the end to the beginning
 * of the ioc_buf, expelling old data to make more room
 * for new reads.
 */
static inline void iocache_move_data(iocache *ioc)
{
	unsigned long available;

	if (!ioc->ioc_offset)
		return; /* nothing to do */

	/* if we're fully read, we only have to reset the counters */
	if (ioc->ioc_buflen <= ioc->ioc_offset) {
		iocache_reset(ioc);
		return;
	}
	available = ioc->ioc_buflen - ioc->ioc_offset;
	memmove(ioc->ioc_buf, ioc->ioc_buf + ioc->ioc_offset, available);
	ioc->ioc_offset = 0;
	ioc->ioc_buflen = available;
}

void iocache_reset(iocache *ioc)
{
	if (ioc)
		ioc->ioc_offset = ioc->ioc_buflen = 0;
}

int iocache_resize(iocache *ioc, unsigned long new_size)
{
	char *buf;

	if (!ioc)
		return -1;

	iocache_move_data(ioc);

	buf = realloc(ioc->ioc_buf, new_size);
	if (!buf)
		return -1;
	ioc->ioc_buf = buf;
	ioc->ioc_bufsize = new_size;
	return 0;
}

int iocache_grow(iocache *ioc, unsigned long increment)
{
	return iocache_resize(ioc, iocache_size(ioc) + increment);
}

unsigned long iocache_size(iocache *ioc)
{
	return ioc ? ioc->ioc_bufsize : 0;
}

unsigned long iocache_capacity(iocache *ioc)
{
	if (!ioc)
		return -1;

	if (!ioc->ioc_buf)
		return -2;

	if (!ioc->ioc_bufsize)
		return -3;

	iocache_move_data(ioc);

	return ioc->ioc_bufsize - ioc->ioc_buflen;
}

unsigned long iocache_available(iocache *ioc)
{
	if (!ioc || !ioc->ioc_buf || !ioc->ioc_bufsize || !ioc->ioc_buflen)
		return 0;

	return ioc->ioc_buflen - ioc->ioc_offset;
}

char *iocache_use_size(iocache *ioc, unsigned long size)
{
	char *ret;

	if (!ioc || !ioc->ioc_buf)
		return NULL;
	if (ioc->ioc_bufsize < size || iocache_available(ioc) < size)
		return NULL;

	ret = ioc->ioc_buf + ioc->ioc_offset;
	ioc->ioc_offset += size;

	return ret;
}

int iocache_unuse_size(iocache *ioc, unsigned long size)
{
	if (!ioc || !ioc->ioc_buf)
		return -1;
	if (size > ioc->ioc_offset)
		return -1;

	ioc->ioc_offset -= size;

	return 0;
}


char *iocache_use_delim(iocache *ioc, const char *delim, size_t delim_len, unsigned long *size)
{
	char *ptr = NULL;
	char *buf;
	unsigned long remains;

	if (!ioc || !ioc->ioc_buf || !ioc->ioc_bufsize || !ioc->ioc_buflen)
		return NULL;

	*size = 0;
	if (ioc->ioc_offset >= ioc->ioc_buflen) {
		iocache_move_data(ioc);
		return NULL;
	}

	buf = &ioc->ioc_buf[ioc->ioc_offset];
	remains = iocache_available(ioc);
	while (remains >= delim_len) {
		unsigned long jump;
		ptr = memchr(buf, *delim, remains - (delim_len - 1));
		if (!ptr) {
			return NULL;
		}
		if (delim_len == 1 || !memcmp(ptr, delim, delim_len)) {
			unsigned long ioc_start;

			ioc_start = (unsigned long)ioc->ioc_buf + ioc->ioc_offset;
			*size = (unsigned long)ptr - ioc_start;

			/* make sure we use up all of the delimiter as well */
			return iocache_use_size(ioc, delim_len + *size);
		}
		jump = 1 + (unsigned long)ptr - (unsigned long)buf;
		remains -= jump;
		buf += jump;
	}
	return NULL;
}

iocache *iocache_create(unsigned long size)
{
	iocache *ioc;

	ioc = calloc(1, sizeof(*ioc));
	if (ioc && size) {
		ioc->ioc_buf = calloc(1, size);
		if (!ioc->ioc_buf) {
			free(ioc);
			return NULL;
		}
		ioc->ioc_bufsize = size;
	}

	return ioc;
}

int iocache_read(iocache *ioc, int fd)
{
	int to_read, bytes_read, ret;

	if (!ioc || !ioc->ioc_buf || fd < 0)
		return -1;

	/* we make sure we've got as much room as possible */
	iocache_move_data(ioc);

	/* if we've maxed out our buflen, grow by 2x to be safe */
	if (ioc->ioc_buflen >= ioc->ioc_bufsize) {

		to_read = ioc->ioc_buflen + ioc->ioc_bufsize;
		ret = iocache_grow(ioc, ioc->ioc_bufsize);
		if (ret == -1) {
			to_read = ioc->ioc_buflen;
		}

	} else {

		/* calculate the size we should read */
		to_read = ioc->ioc_bufsize - ioc->ioc_buflen;
	}

	bytes_read = read(fd, ioc->ioc_buf + ioc->ioc_buflen, to_read);
	if (bytes_read > 0) {
		ioc->ioc_buflen += bytes_read;
	}

	return bytes_read;
}


int iocache_add(iocache *ioc, char *buf, unsigned int len)
{
	if (!ioc || iocache_capacity(ioc) < len)
		return -1;

	memcpy(ioc->ioc_buf + ioc->ioc_offset, buf, len);
	ioc->ioc_buflen += len;
	return ioc->ioc_buflen - ioc->ioc_offset;
}

/*
 * Three cases to handle:
 *  - buf has data, iocache doesn't.
 *  - iocache has data, buf doesn't.
 *  - both buf and iocache has data.
 */
int iocache_sendto(iocache *ioc, int fd, char *buf, unsigned int len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)
{
	int sent;

	errno = 0;
	if (!ioc)
		return -1;

	if (!ioc->ioc_buflen && !len)
		return 0;

	if (ioc->ioc_buf && iocache_available(ioc)) {
		if (buf && len) {
			/* copy buf and len to iocache buffer to use just one write */
			if (iocache_capacity(ioc) < len) {
				if (iocache_grow(ioc, iocache_size(ioc)) < 0)
					return -1;
			}
			if (iocache_add(ioc, buf, len) < 0)
				return -1;
		}
		buf = ioc->ioc_buf;
		len = iocache_available(ioc);
	}

	sent = sendto(fd, buf, len, flags, dest_addr, addrlen);
	if (sent < 1)
		return -errno;

	if (iocache_available(ioc))
		iocache_use_size(ioc, sent);

	return sent;
}
