// vim:fileencoding=utf-8:foldmethod=marker

#include "./array.h"
#include "../../common/assert/assert.h"
#include "../mem/allocator/mem_allocator.h"
#include "../../common/misc/misc_utils.h"
#include "../../common/aliases/aliases.h"
#include <stddef.h>

#define _arrayHeader(ARRAY) (WpArrayHeader *)(wpMiscUtilsOffsetPointer(ARRAY, (i64)sizeof(WpArrayHeader) * -1))

wp_persist inline void _arrayValidate(const WpArray array, u64 item_size);

u64 _arrayCount(WpArray array) {
	wpDebugAssert(array != NULL, "`array` should not be NULL");

	WpArrayHeader *header = _arrayHeader(array);
	wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array");

	return header->count;
}

u64 _arrayCapacity(WpArray array) {
	wpDebugAssert(array != NULL, "`array` should not be NULL");

	WpArrayHeader *header = _arrayHeader(array);
	wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array");

	return header->capacity;
}

u64 _arrayItemSize(WpArray array) {
	wpDebugAssert(array != NULL, "`array` should not be NULL");

	WpArrayHeader *header = _arrayHeader(array);
	wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array");

	return header->item_size;
}

void _arraySetCount(WpArray array, u64 count) {
	wpDebugAssert(array != NULL, "`array` should not be NULL");

	WpArrayHeader *header = _arrayHeader(array);
	wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array");

	header->count = count;
}

void *_arrayGet(WpArray array, u64 index, u64 item_size) {
	wpRuntimeAssert(array != NULL, "`array` should not be NULL");
	_arrayValidate(array, item_size);

	WpArrayHeader *header = _arrayHeader(array);
	wpRuntimeAssert(index < header->count, "`index` is out of bounds");

	return wpMiscUtilsOffsetPointer(array, header->item_size * index);
}

void _arraySet(WpArray array, u64 index, void *value, u64 item_size) {
	void *item = _arrayGet(array, index, item_size);

	WpArrayHeader *header = _arrayHeader(array);
	memcpy(item, value, header->item_size);
}

void _arrayAppendCapped(WpArray array, void *value, u64 item_size) {
	wpRuntimeAssert(array != NULL, "`array` should not be NULL");
	_arrayValidate(array, item_size);

	WpArrayHeader *header = _arrayHeader(array);
	if (header->count >= header->capacity) { return; }

	u64 index = (header->count)++;
	_arraySet(array, index, value, item_size);
}

void _arrayExtendCappend(WpArray dst, const WpArray src, u64 item_size) {
	wpRuntimeAssert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL");
	_arrayValidate(dst, item_size);
	_arrayValidate(src, item_size);

	WpArrayHeader *src_header = _arrayHeader(src);
	WpArrayHeader *dst_header = _arrayHeader(dst);
	u64 remaining_capacity = dst_header->capacity - dst_header->count;

	u64 copy_count = src_header->count < remaining_capacity ? src_header->count : remaining_capacity;
	void *dst_ptr  = wpMiscUtilsOffsetPointer(dst, dst_header->count * dst_header->item_size);
	memcpy(dst_ptr, src, copy_count * src_header->item_size);
	dst_header->count += copy_count;
}

void _arrayCopyCapped(WpArray dst, const WpArray src, u64 item_size) {
	wpRuntimeAssert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL");
	_arrayValidate(dst, item_size);
	_arrayValidate(src, item_size);

	_arrayClear(dst, item_size);

	WpArrayHeader *src_header = _arrayHeader(src);
	WpArrayHeader *dst_header = _arrayHeader(dst);
	u64 copy_count = src_header->count < dst_header->capacity ? src_header->count : dst_header->capacity;
	memcpy((void *)dst, (void *)src, copy_count * src_header->item_size);
	dst_header->count = copy_count;
}

WpArray _arrayAppendAlloc(const WpAllocator *allocator, WpArray array, void *value,
                                 WpArrayInitFlags flags, u64 item_size) {
	wpRuntimeAssert(allocator != NULL && array != NULL, "`allocator` and `array` should not be NULL");
	_arrayValidate(array, item_size);

	WpArray output = array;

	WpArrayHeader *header = _arrayHeader(array);
	if (header->count >= header->capacity) {
		u64 new_capacity = wpMiscUtilsU64RoundUpPow2(header->capacity * 2);
		output           = (WpArray )_arrayAllocCapacity(allocator, new_capacity, flags,
		                                                        header->item_size);
		if (!output) {
			output = array;
			goto RETURN_ARRAY_APPEND_ALLOC;
		}
		_arrayCopyCapped(output, array, item_size);
	}

	_arrayAppendCapped(output, value, item_size);

	if ((flags & WP_ARRAY_INIT_FILLED) == WP_ARRAY_INIT_FILLED) {
		_arraySetCount(output, _arrayCapacity(output));
	}

RETURN_ARRAY_APPEND_ALLOC:
	return output;
}

WpArray _arrayExtendAlloc(const WpAllocator *allocator, WpArray dst, const WpArray src,
                                 WpArrayInitFlags flags, u64 item_size) {
	wpRuntimeAssert(allocator != NULL && dst != NULL && src != NULL, "`allocator`, `dst` and `src` should not be NULL");
	_arrayValidate(dst, item_size);
	_arrayValidate(src, item_size);

	WpArray output = dst;

	WpArrayHeader *src_header = _arrayHeader(src);
	WpArrayHeader *dst_header = _arrayHeader(dst);
	u64 remaining_capacity = dst_header->capacity - dst_header->count;
	if (src_header->count >= remaining_capacity) {
		u64 new_capacity  = wpMiscUtilsU64RoundUpPow2(dst_header->capacity * 2);
		output            = (WpArray )_arrayAllocCapacity(allocator, new_capacity,
		                                                         flags, dst_header->item_size);
		if (!output) {
			output = dst;
			goto RETURN_ARRAY_EXTEND_ALLOC;
		}
		_arrayCopyCapped(output, dst, item_size);
	}

	_arrayExtendCappend(output, src, item_size);

	if ((flags & WP_ARRAY_INIT_FILLED) == WP_ARRAY_INIT_FILLED) {
		_arraySetCount(output, _arrayCapacity(output));
	}

RETURN_ARRAY_EXTEND_ALLOC:
	return output;
}

WpArray _arrayCopyAlloc(const WpAllocator *allocator, WpArray dst, const WpArray src,
                               WpArrayInitFlags flags, u64 item_size) {
	wpRuntimeAssert(allocator != NULL && dst != NULL && src != NULL, "`allocator`, `dst` and `src` should not be NULL");
	_arrayValidate(dst, item_size);
	_arrayValidate(src, item_size);

	WpArray output = dst;

	WpArrayHeader *src_header = _arrayHeader(src);
	WpArrayHeader *dst_header = _arrayHeader(dst);
	if (src_header->count >= dst_header->capacity) {
		u64 new_capacity  = wpMiscUtilsU64RoundUpPow2(dst_header->capacity * 2);
		output            = (WpArray )_arrayAllocCapacity(allocator, new_capacity,
		                                                         flags, src_header->item_size);
		if (!output) {
			output = dst;
			goto RETURN_ARRAY_COPY_ALLOC;
		}
	}

	_arrayCopyCapped(output, src, item_size);

	if ((flags & WP_ARRAY_INIT_FILLED) == WP_ARRAY_INIT_FILLED) {
		_arraySetCount(output, _arrayCapacity(output));
	}

RETURN_ARRAY_COPY_ALLOC:
	return output;
}

void *_arrayPop(WpArray array, u64 item_size) {
	wpRuntimeAssert(array != NULL, "`array` should not be NULL");
	_arrayValidate(array, item_size);

	WpArrayHeader *header = _arrayHeader(array);
	if (header->count == 0) { return NULL; }

	u64 index = header->count - 1;
	void *out = _arrayGet(array, index, item_size);
	--(header->count);
	return out;
}

void _arrayClear(WpArray array, u64 item_size) {
	wpRuntimeAssert(array != NULL, "`array` should not be NULL");
	_arrayValidate(array, item_size);

	WpArrayHeader *header = _arrayHeader(array);
	header->count = 0;
}

u64 _arrayCalcAllocSize(u64 capacity, u64 item_size) {
	return sizeof(WpArrayHeader) + item_size * capacity;
}

WpArray _arrayAllocCapacity(const WpAllocator *allocator, u64 capacity, WpArrayInitFlags flags,
                                   u64 item_size) {
	wpRuntimeAssert(allocator != NULL, "`allocator` should not be NULL");

	WpArray output = NULL;

	u64 allocation_size = _arrayCalcAllocSize(capacity, item_size);
	void *buffer        = wpMemAllocatorAlloc(allocator, allocation_size);
	if (!buffer) {
		goto RETURN_ARRAY_ALLOC;
	}

	output = _arrayFromPreallocatedBuffer(buffer, allocation_size, flags, item_size);

RETURN_ARRAY_ALLOC:
	return output;
}


WpArray _arrayFromPreallocatedBuffer(void *buffer, u64 buffer_size, WpArrayInitFlags flags,
                                             u64 item_size) {
	wpRuntimeAssert(buffer != NULL, "`buffer` should not be NULL");

	i64 data_buffer_size = (i64)buffer_size - (i64)(sizeof(WpArrayHeader));
	if (data_buffer_size <= 0) {
		return NULL;
	}

	u64 item_capacity   = (u64)data_buffer_size / item_size;
	WpArrayHeader *header = (WpArrayHeader *)buffer;
	WpArray output = (u8 *)(header + 1);
	header->magic       = WP_ARRAY_MAGIC;
	header->count       = flags & WP_ARRAY_INIT_FILLED ? item_capacity : 0;
	header->capacity    = item_capacity;
	header->item_size   = item_size;

	return output;
}

void _arrayDealloc(const WpAllocator *allocator, WpArray *array, u64 item_size) {
	wpRuntimeAssert(allocator != NULL, "`allocator` should not be NULL");

	u64 capacity          = wpArrayCapacity(*array);
	u64 allocation_size   = _arrayCalcAllocSize(capacity, item_size);
	void *header          = (void *)_arrayHeader(*array);
	wpMemAllocatorFree(allocator, &header, allocation_size);
	*array = NULL;
}

wp_persist inline void _arrayValidate(const WpArray array, u64 item_size) {
	WpArrayHeader *header = _arrayHeader(array);
	wpRuntimeAssert(WP_ARRAY_MAGIC == header->magic, "`array` is not a valid wapp array");
	wpRuntimeAssert(item_size == header->item_size, "Invalid item type provided");
}
