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

#include "str8.h"
#include "../../array/array.h"
#include "../../mem/allocator/mem_allocator.h"
#include "../../../common/aliases/aliases.h"
#include "../../../common/assert/assert.h"
#include <ctype.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>

#define STR8_BUF_ALLOC_SIZE(CAPACITY) (sizeof(WpStr8) + sizeof(c8) * CAPACITY)

WpStr8 *wpStr8AllocBuf(const WpAllocator *allocator, u64 capacity) {
	wpDebugAssert(allocator != NULL, "`allocator` should not be NULL");

	WpStr8 *str = wpMemAllocatorAlloc(allocator, STR8_BUF_ALLOC_SIZE(capacity));
	if (!str) {
		goto RETURN_STR8;
	}

	str->buf      = (u8 *)str + sizeof(WpStr8);
	str->size     = 0;
	str->capacity = capacity;

RETURN_STR8:
	return str;
}

WpStr8 *wpStr8AllocAndFillBuf(const WpAllocator *allocator, u64 capacity) {
	WpStr8 *out = wpStr8AllocBuf(allocator, capacity);
	if (out) {
		memset(out->buf, 0, capacity);
		out->size = capacity;
	}
	return out;
}

WpStr8 *wpStr8AllocCstr(const WpAllocator *allocator, const char *str) {
	wpDebugAssert(allocator != NULL && str != NULL, "`allocator` and `str` should not be NULL");

	u64 length    = strlen(str);
	WpStr8 *output  = wpStr8AllocBuf(allocator, length * 2);
	if (!output) {
		goto RETURN_ALLOC_CSTR;
	}

	output->size = length;
	memcpy(output->buf, str, length);

RETURN_ALLOC_CSTR:
	return output;
}

WpStr8 *wpStr8AllocStr8(const WpAllocator *allocator, WpStr8RO *str) {
	wpDebugAssert(allocator != NULL && str != NULL, "`allocator` and `str` should not be NULL");

	WpStr8 *output = wpStr8AllocBuf(allocator, str->capacity);
	if (!output) {
		goto RETURN_ALLOC_STR8;
	}

	output->size = str->size;
	memcpy(output->buf, str->buf, str->size);

RETURN_ALLOC_STR8:
	return output;
}

WpStr8 *wpStr8AllocSubstr(const WpAllocator *allocator, WpStr8RO *str, u64 start, u64 end) {
	wpDebugAssert(allocator != NULL && str != NULL, "`allocator` and `str` should not be NULL");

	WpStr8 *output = NULL;

	if (start >= str->size || start >= end) {
		goto RETURN_ALLOC_SUBSTR;
	}

	if (end > str->size) {
		end = str->size;
	}

	output = wpStr8AllocBuf(allocator, str->capacity);
	if (!output) {
		goto RETURN_ALLOC_SUBSTR;
	}

	output->size = end - start;
	memcpy(output->buf, str->buf + start, output->size);

RETURN_ALLOC_SUBSTR:
	return output;
}

void wpStr8DeallocBuf(const WpAllocator *allocator, WpStr8 **str) {
	wpDebugAssert(allocator != NULL && str != NULL && (*str) != NULL, "Either `allocator` is NULL or `str` is an invalid double pointer");
	wpMemAllocatorFree(allocator, (void **)str, STR8_BUF_ALLOC_SIZE((*str)->capacity));
}

c8 wpStr8Get(const WpStr8 *str, u64 index) {
	if (index >= str->size) {
		return '\0';
	}

	return str->buf[index];
}

void wpStr8Set(WpStr8 *str, u64 index, c8 c) {
	if (index >= str->size) {
		return;
	}

	str->buf[index] = c;
}

void wpStr8PushBack(WpStr8 *str, c8 c) {
	if (!(str->size < str->capacity)) {
		return;
	}

	u64 index = (str->size)++;
	wpStr8Set(str, index, c);
}

b8 wpStr8Equal(WpStr8RO *s1, WpStr8RO *s2) {
	if (s1->size != s2->size) {
		return false;
	}

	return wpStr8EqualToCount(s1, s2, s1->size);
}

b8 wpStr8EqualToCount(WpStr8RO* s1, WpStr8RO* s2, u64 count) {
	if (!s1 || !s2) {
		return false;
	}

	return memcmp(s1->buf, s2->buf, count) == 0;
}

WpStr8 wpStr8Slice(WpStr8RO *str, u64 start, u64 end) {
	if (start >= str->size || start >= end) {
		start  = str->size;
		end    = str->size;
	}

	if (end > str->size) {
		end = str->size;
	}

	return (WpStr8RO){
		.capacity = end - start,
		.size     = end - start,
		.buf      = str->buf + start,
	};
}

WpStr8 *wpStr8AllocConcat(const WpAllocator *allocator, WpStr8 *dst, WpStr8RO *src) {
	wpDebugAssert(allocator != NULL && dst != NULL && src != NULL, "`allocator`, `dst` and `src` should not be NULL");

	WpStr8 *output  = NULL;
	u64 remaining = dst->capacity - dst->size;
	if (src->size <= remaining) {
		output = dst;
		goto SOURCE_STRING_STR8_CONCAT;
	}

	u64 capacity = dst->capacity + src->size;

	output = wpStr8AllocBuf(allocator, capacity);
	if (!output) {
		goto RETURN_STR8_CONCAT;
	}

	wpStr8ConcatCapped(output, dst);

SOURCE_STRING_STR8_CONCAT:
	wpStr8ConcatCapped(output, src);

RETURN_STR8_CONCAT:
	return output;
}

void wpStr8ConcatCapped(WpStr8 *dst, WpStr8RO *src) {
	wpDebugAssert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL");

	u64 remaining = dst->capacity - dst->size;
	u64 to_copy   = remaining < src->size ? remaining : src->size;

	memcpy(dst->buf + dst->size, src->buf, to_copy);
	dst->size += to_copy;
}

void wpStr8CopyCstrCapped(WpStr8 *dst, const char *src) {
	wpDebugAssert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL");

	u64 length  = strlen(src);
	u64 to_copy = length <= dst->capacity ? length : dst->capacity;

	memset(dst->buf, 0, dst->size);
	memcpy(dst->buf, src, to_copy);
	dst->size = to_copy;
}

void wpStr8CopyStr8Capped(WpStr8 *dst, WpStr8RO *src) {
	wpDebugAssert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL");

	u64 to_copy = src->size <= dst->capacity ? src->size : dst->capacity;

	memset(dst->buf, 0, dst->size);
	memcpy(dst->buf, src->buf, to_copy);
	dst->size = to_copy;
}

void wpStr8CopyToCstr(char *dst, WpStr8RO *src, u64 dst_capacity) {
	wpDebugAssert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL");

	u64 to_copy = src->size < dst_capacity ? src->size : dst_capacity - 1;

	memset(dst, 0, dst_capacity);
	memcpy(dst, src->buf, to_copy);
}

void wpStr8Format(WpStr8 *dst, const char *format, ...) {
	wpDebugAssert(dst != NULL && format != NULL, "`dst` and `format` should not be NULL");

	va_list args1;
	va_list args2;

	va_start(args1, format);
	va_copy(args2, args1);

	u64 total_size = vsnprintf(NULL, 0, format, args1);
	dst->size      = total_size <= dst->capacity ? total_size : dst->capacity;

	vsnprintf((char *)(dst->buf), dst->capacity, format, args2);

	va_end(args1);
	va_end(args2);
}

void wpStr8ToLower(WpStr8 *dst, WpStr8RO *src) {
	wpDebugAssert(src != NULL && dst != NULL, "`dst` and `src` should not be NULL");
	wpDebugAssert(dst->capacity >= src->capacity, "`dst` does not have enough capacity");

	dst->size = src->size;

	// NOTE (Abdelrahman): Uses a while loop instead of a for loop to get rid of
	// MSVC Spectre mitigation warnings
	u64 index  = 0;
	b8 running = true;
	while (running) {
		wpStr8Set(dst, index, (u8)tolower(wpStr8Get(src, index)));
		++index;
		running = index < src->size;
	}
}

void wpStr8ToUpper(WpStr8 *dst, WpStr8RO *src) {
	wpDebugAssert(src != NULL && dst != NULL, "`dst` and `src` should not be NULL");
	wpDebugAssert(dst->capacity >= src->capacity, "`dst` does not have enough capacity");

	dst->size = src->size;

	// NOTE (Abdelrahman): Uses a while loop instead of a for loop to get rid of
	// MSVC Spectre mitigation warnings
	u64 index  = 0;
	b8 running = true;
	while (running) {
		wpStr8Set(dst, index, (u8)toupper(wpStr8Get(src, index)));
		++index;
		running = index < src->size;
	}
}

void wpStr8FromBytes(WpStr8 *dst, const WpU8Array src) {
	wpDebugAssert(src != NULL && dst != NULL, "`dst` and `src` should not be NULL");

	u64 size = wpArrayCount(src) * wpArrayItemSize(src);

	wpDebugAssert(dst->capacity >= size, "`dst` does not have enough capacity");

	dst->size = size;
	memcpy(dst->buf, src, size);
}

i64 wpStr8Find(WpStr8RO *str, WpStr8RO substr) {
	if (!str || substr.size > str->size) {
		return -1;
	}

	// NOTE (Abdelrahman): Uses a while loop instead of a for loop to get rid of
	// MSVC Spectre mitigation warnings
	u64 char_index  = 0;
	b8 running      = char_index < str->size;
	while (running) {
		const c8 *sub = str->buf + char_index;
		if (memcmp(sub, substr.buf, substr.size) == 0) {
			return char_index;
		}

		++char_index;
		running = char_index < str->size;
	}

	return -1;
}

i64 wpStr8Rfind(WpStr8RO *str, WpStr8RO substr) {
	if (!str || substr.size > str->size) {
		return -1;
	}

	// NOTE (Abdelrahman): Uses a while loop instead of a for loop to get rid of
	// MSVC Spectre mitigation warnings
	i64 char_index = str->size - substr.size;
	b8 running     = char_index >= 0;
	while (running) {
		const c8 *sub = str->buf + char_index;
		if (memcmp(sub, substr.buf, substr.size) == 0) {
			return char_index;
		}

		--char_index;
		running = char_index >= 0;
	}

	return -1;
}

WpStr8List *wpStr8SplitWithMax(const WpAllocator *allocator, WpStr8RO *str, WpStr8RO *delimiter, i64 max_splits) {
	wpDebugAssert(allocator != NULL && str != NULL && delimiter != NULL, "`allocator`, `str` and `delimiter` should not be NULL");

	WpStr8List *output = wpDblListAlloc(WpStr8, allocator);

	if (delimiter->size > str->size) {
		WpStr8 *full = wpStr8AllocStr8(allocator, str);
		if (full) {
			wpDblListPushBackAlloc(WpStr8, allocator, output, full);
		}

		goto RETURN_STR8_SPLIT;
	}

	i64 start  = 0;
	i64 end    = 0;
	i64 splits = 0;
	WpStr8 *rest = wpStr8AllocStr8(allocator, str);
	WpStr8 *before_str;

	while ((end = wpStr8Find(rest, *delimiter)) != -1) {
		if (max_splits > 0 && splits >= max_splits) {
			break;
		}

		before_str = wpStr8AllocSubstr(allocator, str, start, start + end);
		if (before_str) {
			wpDblListPushBackAlloc(WpStr8, allocator, output, before_str);
		}

		wpMemAllocatorFree(allocator, (void **)&rest, sizeof(WpStr8));
		rest   = wpStr8AllocSubstr(allocator, str, start + end + delimiter->size, str->size);
		start += end + delimiter->size;

		++splits;
	}

	// Ensure the last part of the string after the delimiter is added to the list
	rest = wpStr8AllocSubstr(allocator, str, start, str->size);
	if (rest) {
		wpDblListPushBackAlloc(WpStr8, allocator, output, rest);
	}

RETURN_STR8_SPLIT:
	return output;
}

WpStr8List *wpStr8RsplitWithMax(const WpAllocator *allocator, WpStr8RO *str, WpStr8RO *delimiter, i64 max_splits) {
	wpDebugAssert(allocator != NULL && str != NULL && delimiter != NULL, "`allocator`, `str` and `delimiter` should not be NULL");

	WpStr8List *output = wpDblListAlloc(WpStr8, allocator);

	if (delimiter->size > str->size) {
		WpStr8 *full = wpStr8AllocStr8(allocator, str);
		if (full) {
			wpDblListPushBackAlloc(WpStr8, allocator, output, full);
		}

		goto RETURN_STR8_SPLIT;
	}

	i64 end    = 0;
	i64 splits = 0;
	WpStr8 *rest = wpStr8AllocStr8(allocator, str);
	WpStr8 *after_str;

	while ((end = wpStr8Rfind(rest, *delimiter)) != -1) {
		if (max_splits > 0 && splits >= max_splits) {
			break;
		}

		after_str = wpStr8AllocSubstr(allocator, rest, end + delimiter->size, str->size);
		if (after_str) {
			wpDblListPushFrontAlloc(WpStr8, allocator, output, after_str);
		}

		wpMemAllocatorFree(allocator, (void **)&rest, sizeof(WpStr8));
		rest = wpStr8AllocSubstr(allocator, rest, 0, end);

		++splits;
	}

	rest = wpStr8AllocSubstr(allocator, str, 0, rest->size);
	if (rest) {
		wpDblListPushFrontAlloc(WpStr8, allocator, output, rest);
	}

RETURN_STR8_SPLIT:
	return output;
}

WpStr8 *wpStr8Join(const WpAllocator *allocator, const WpStr8List *list, WpStr8RO *delimiter) {
	wpDebugAssert(allocator != NULL && list != NULL && delimiter != NULL, "`allocator`, `list` and `delimiter` should not be NULL");

	u64 capacity = wpStr8ListTotalSize(list) + (delimiter->size * (list->node_count - 1));
	WpStr8 *output = wpStr8AllocBuf(allocator, capacity * 2);

	// NOTE (Abdelrahman): Uses a while loop instead of a for loop to get rid of
	// MSVC Spectre mitigation warnings
	WpStr8 *node;
	u64 node_index = 0;
	b8 running     = node_index < list->node_count;
	while (running) {
		node = wpDblListGet(WpStr8, list, node_index);
		if (!node) {
			break;
		}

		wpStr8ConcatCapped(output, node);

		// NOTE (Abdelrahman): Comparison extracted to variable to silence
		// MSVC Spectre mitigation warnings
		b8 not_last = node_index + 1 < list->node_count;
		if (not_last) {
			wpStr8ConcatCapped(output, delimiter);
		}

		++node_index;
		running = node_index < list->node_count;
	}

	return output;
}

u64 wpStr8ListTotalSize(const WpStr8List *list) {
	if (!list) {
		return 0;
	}

	// NOTE (Abdelrahman): Uses a while loop instead of a for loop to get rid of
	// MSVC Spectre mitigation warnings
	WpStr8 *node;
	u64 node_index = 0;
	u64 output     = 0;
	b8 running     = node_index < list->node_count;
	while (running) {
		node = wpDblListGet(WpStr8, list, node_index);
		if (!node) {
				break;
		}

		output += node->size;
		++node_index;
		running = node_index < list->node_count;
	}

	return output;
}
