/* GPLv2 or OpenIB.org BSD (MIT) See COPYING file */

#include <errno.h>
#include <pthread.h>
#include <stdlib.h>

#include <ccan/list.h>
#include <util/interval_set.h>
#include <util/util.h>

struct iset {
	struct list_head head;
	pthread_mutex_t lock;
};

struct iset_range {
	struct list_node entry;
	uint64_t start;
	uint64_t length;
};

struct iset *iset_create(void)
{
	struct iset *iset;

	iset = calloc(1, sizeof(*iset));
	if (!iset) {
		errno = ENOMEM;
		return NULL;
	}

	pthread_mutex_init(&iset->lock, NULL);
	list_head_init(&iset->head);
	return iset;
}

void iset_destroy(struct iset *iset)
{
	struct iset_range *range, *tmp;

	list_for_each_safe(&iset->head, range, tmp, entry)
		free(range);

	free(iset);
}

static int
range_overlap(uint64_t s1, uint64_t len1, uint64_t s2, uint64_t len2)
{
	if (((s1 < s2) && (s1 + len1 - 1 < s2)) ||
	    ((s1 > s2) && (s1 > s2 + len2 - 1)))
		return 0;

	return 1;
}

static struct iset_range *create_range(uint64_t start, uint64_t length)
{
	struct iset_range *range;

	range = calloc(1, sizeof(*range));
	if (!range) {
		errno = ENOMEM;
		return NULL;
	}

	range->start = start;
	range->length = length;
	return range;
}

static void delete_range(struct iset_range *r)
{
	list_del(&r->entry);
	free(r);
}

static bool check_do_combine(struct iset *iset,
			     struct iset_range *p, struct iset_range *n,
			     uint64_t start, uint64_t length)
{
	bool combined2prev = false, combined2next = false;

	if (p && (p->start + p->length == start)) {
		p->length += length;
		combined2prev = true;
	}

	if (n && (start + length == n->start)) {
		if (combined2prev) {
			p->length += n->length;
			delete_range(n);
		} else {
			n->start = start;
			n->length += length;
		}
		combined2next = true;
	}

	return combined2prev || combined2next;
}

int iset_insert_range(struct iset *iset, uint64_t start, uint64_t length)
{
	struct iset_range *prev = NULL, *r, *rnew;
	bool found = false, combined;
	int ret = 0;

	if (!length || (start + length - 1 < start)) {
		errno = EINVAL;
		return errno;
	}

	pthread_mutex_lock(&iset->lock);
	list_for_each(&iset->head, r, entry) {
		if (range_overlap(r->start, r->length, start, length)) {
			errno = EINVAL;
			ret = errno;
			goto out;
		}

		if (r->start > start) {
			found = true;
			break;
		}

		prev = r;
	}

	combined = check_do_combine(iset, prev, found ? r : NULL,
				    start, length);
	if (!combined) {
		rnew = create_range(start, length);
		if (!rnew) {
			ret = errno;
			goto out;
		}

		if (!found)
			list_add_tail(&iset->head, &rnew->entry);
		else
			list_add_before(&iset->head, &r->entry, &rnew->entry);
	}

out:
	pthread_mutex_unlock(&iset->lock);
	return ret;
}

static int power_of_two(uint64_t x)
{
	return ((x != 0) && !(x & (x - 1)));
}

int iset_alloc_range(struct iset *iset, uint64_t length,
		     uint64_t *start, uint64_t alignment)
{
	struct iset_range *r, *rnew;
	uint64_t astart, rend;
	bool found = false;
	int ret = 0;

	if (!power_of_two(alignment)) {
		errno = EINVAL;
		return errno;
	}

	pthread_mutex_lock(&iset->lock);
	list_for_each(&iset->head, r, entry) {
		astart = align(r->start, alignment);
		/* Check for wrap around */
		if ((astart + length - 1 >= astart) &&
		    (astart + length - 1 <= r->start + r->length - 1)) {
			found = true;
			break;
		}
	}
	if (!found) {
		errno = ENOSPC;
		ret = errno;
		goto out;
	}

	if (r->start == astart) {
		if (r->length == length) { /* Case #1 */
			delete_range(r);
		} else {	/* Case #2 */
			r->start += length;
			r->length -= length;
		}
	} else {
		rend = r->start + r->length;
		if (astart + length != rend) { /* Case #4 */
			rnew = create_range(astart + length,
					    rend - astart - length);
			if (!rnew) {
				ret = errno;
				goto out;
			}
			list_add_after(&iset->head, &r->entry, &rnew->entry);
		}
		r->length = astart - r->start; /* Case #3 & #4 */
	}

	*start = astart;
out:
	pthread_mutex_unlock(&iset->lock);
	return ret;
}
