#include "spress.h"

static spress_header_t spress_hdr = {
	.magic = SPRESS_HEADER_MAGIC,
	.major_version = 0,
	.minor_version = 1,
	.file_hdr_sz = sizeof(spress_header_t),
	.chunk_hdr_sz = sizeof(chunk_header_t),
	.chunk_sz = 0,
	.compress_sz = 0,
	.decompress_sz = 0,
};

size_t spr_size_decompressed(const char *source)
{
	spress_header_t* hdr = (spress_header_t*)source;
	return hdr->decompress_sz;
}

size_t spr_size_compressed(const char *source)
{
	spress_header_t* hdr = (spress_header_t*)source;
	return hdr->compress_sz;
}

static char* spr_split_next(const char* src, size_t size, uint32_t* praw) {
	uint32_t raw = 0;
	uint32_t cnt = 0;
	uint32_t* ptr = (uint32_t*)src;
	uint32_t val = *ptr;
	uint32_t tmp;

	if (size > SPRESS_BLK_MAX * SPRESS_BLK_SIZE) {
		size = SPRESS_BLK_MAX * SPRESS_BLK_SIZE;
	}

	while ((char*)ptr < (src + size)) {
		tmp = *ptr++;
		if (tmp == val) {
			cnt++;
			if (raw && (cnt >= SPRESS_BLK_MIN)) {
				ptr -= cnt;
				break;
			}
			if (!raw && (cnt >= SPRESS_BLK_MAX)) {
				break;
			}
		} else {
			if (cnt >= SPRESS_BLK_MIN) {
				raw = 0;
				ptr --;
				break;
			}
			raw = 1;
			cnt = 1;
			val = tmp;
		}
	}

	*praw = raw;
	return (char*)ptr;
}

static void spr_copy_data(uint32_t* dst, uint32_t* src, uint32_t cnt) {
	while (cnt-- > 0) {
		*dst++ = *src++;
	}
}

static void spr_fill_data(uint32_t* dst, uint32_t dat, uint32_t cnt) {
	while (cnt-- > 0) {
		*dst++ = dat;
	}
}

size_t spr_compress_index(const char* source, char* destination, size_t size)
{
	size_t rsize = size;
	chunk_header_t* hdr = (chunk_header_t*)(destination + sizeof(spress_header_t));
	char* src = (char*)source;
	char* next;
	uint32_t raw, len;

	// write chunk
	while (rsize > 0) {
		// split chunk
		next = spr_split_next(src, rsize, &raw);
		len = next - src;
		if (raw) {
			// raw
			hdr->data_sz = (len >> 1) | CHUNK_TYPE_RAW;
		} else {
			// fill
			hdr->data_sz = (len >> 1) | CHUNK_TYPE_FILL;
		}
		hdr++;
		src = next;
		rsize -= len;
	}

	// algin process
	hdr = (chunk_header_t*)((char*)hdr + ((SPRESS_BLK_SIZE - ((uintptr_t)hdr & 0x3)) & 0x3));

	// write header
	spress_hdr.chunk_sz = (uint16_t)((char*)hdr - destination - sizeof(spress_header_t));

	return spress_hdr.chunk_sz;
}

size_t spr_compress_data(const char* source, char* destination, size_t size)
{
	size_t rsize = size;
	chunk_header_t* hdr = (chunk_header_t*)(destination + sizeof(spress_header_t));
	char* src = (char*)source;
	char* dst = (char*)hdr + spress_hdr.chunk_sz;
	uint32_t raw, len;

	// write data
	while (rsize > 0) {
		raw = ((hdr->data_sz & CHUNK_TYPE_MASK) == CHUNK_TYPE_RAW);
		len = (hdr->data_sz & ~CHUNK_TYPE_MASK) << 1;
		if (len == 0) {
			len = SPRESS_BLK_MAX * SPRESS_BLK_SIZE;
		}
		if (raw) {
			// raw
			spr_copy_data((uint32_t*)dst, (uint32_t*)src, len / SPRESS_BLK_SIZE);
			dst += len;
		} else {
			// fill
			spr_fill_data((uint32_t*)dst, *(uint32_t*)src, 1);
			dst += SPRESS_BLK_SIZE;
		}
		hdr++;
		src += len;
		rsize -= len;
	}

	// write header
	spress_hdr.compress_sz = (uint32_t)(dst - destination);
	spress_hdr.decompress_sz = size;
	memcpy(destination, &spress_hdr, sizeof(spress_header_t));

	return spress_hdr.compress_sz;
}

size_t spr_compress(const char* source, char* destination, size_t size)
{
	size_t rsize = size;
	chunk_header_t* hdr = (chunk_header_t*)(destination + sizeof(spress_header_t));
	char* src = (char*)source;
	char* dst;
	char* next;
	uint32_t raw, len;

	// write chunk
	while (rsize > 0) {
		// split chunk
		next = spr_split_next(src, rsize, &raw);
		len = next - src;
		if (raw) {
			// raw
			hdr->data_sz = (len >> 1) | CHUNK_TYPE_RAW;
		} else {
			// fill
			hdr->data_sz = (len >> 1) | CHUNK_TYPE_FILL;
		}
		hdr++;
		src = next;
		rsize -= len;
	}

	// algin process
	hdr = (chunk_header_t*)((char*)hdr + ((SPRESS_BLK_SIZE - ((uintptr_t)hdr & 0x3)) & 0x3));

	// write header
	spress_hdr.chunk_sz = (uint16_t)((char*)hdr - destination - sizeof(spress_header_t));

	// write data
	rsize = size;
	src = (char*)source;
	dst = (char*)hdr;
	hdr = (chunk_header_t*)(destination + sizeof(spress_header_t));
	while (rsize > 0) {
		raw = ((hdr->data_sz & CHUNK_TYPE_MASK) == CHUNK_TYPE_RAW);
		len = (hdr->data_sz & ~CHUNK_TYPE_MASK) << 1;
		if (len == 0) {
			len = SPRESS_BLK_MAX * SPRESS_BLK_SIZE;
		}
		if (raw) {
			// raw
			spr_copy_data((uint32_t*)dst, (uint32_t*)src, len / SPRESS_BLK_SIZE);
			dst += len;
		} else {
			// fill
			spr_fill_data((uint32_t*)dst, *(uint32_t*)src, 1);
			dst += SPRESS_BLK_SIZE;
		}
		hdr++;
		src += len;
		rsize -= len;
	}

	// write header
	spress_hdr.compress_sz = (uint32_t)(dst - destination);
	spress_hdr.decompress_sz = size;
	memcpy(destination, &spress_hdr, sizeof(spress_header_t));

	return spress_hdr.compress_sz;
}

size_t spr_decompress(const char* source, char* destination)
{
	spress_header_t* shdr = (spress_header_t*)source;
	size_t rsize = shdr->compress_sz - sizeof(spress_header_t) - shdr->chunk_sz;
	chunk_header_t* hdr = (chunk_header_t*)(source + sizeof(spress_header_t));
	char* src = (char*)source + sizeof(spress_header_t) + shdr->chunk_sz;
	char* dst = destination;
	uint16_t raw, len;

	// write chunk
	while (rsize > 0) {
		// parse chunk
		raw = ((hdr->data_sz & CHUNK_TYPE_MASK) == CHUNK_TYPE_RAW);
		len = (hdr->data_sz & ~CHUNK_TYPE_MASK) << 1;
		if (len == 0) {
			len = SPRESS_BLK_MAX * SPRESS_BLK_SIZE;
		}
		if (raw) {
			// raw
			spr_copy_data((uint32_t*)dst, (uint32_t*)src, len / SPRESS_BLK_SIZE);
			dst += len;
		} else {
			// fill
			spr_fill_data((uint32_t*)dst, *(uint32_t*)src, len / SPRESS_BLK_SIZE);
			dst += len;
			len = SPRESS_BLK_SIZE;
		}
		hdr++;
		src += len;
		rsize -= len;
	}

	return shdr->decompress_sz;
}

