#include "obfuscate_file.h"
#include <stdlib.h>

#include <stdio.h>
#include <string.h>

static uint32_t dummy_u32(uint64_t seed, uint32_t salt)
{
	uint64_t x = seed ^ ((uint64_t)salt * 0x9E3779B97F4A7C15ULL);

	x ^= x >> 33;
	x *= 0xFF51AFD7ED558CCDULL;
	x ^= x >> 33;
	return (uint32_t)(x ^ (x >> 32));
}

static void put_u32_le(uint8_t *dst, uint32_t v)
{
	dst[0] = (uint8_t)(v);
	dst[1] = (uint8_t)(v >> 8);
	dst[2] = (uint8_t)(v >> 16);
	dst[3] = (uint8_t)(v >> 24);
}

static void put_u64_le(uint8_t *dst, uint64_t v)
{
	unsigned int i;

	for (i = 0; i < 8u; i++)
		dst[i] = (uint8_t)(v >> (8u * i));
}

static uint32_t get_u32_le(const uint8_t *src)
{
	return (uint32_t)src[0] | ((uint32_t)src[1] << 8) | ((uint32_t)src[2] << 16) |
	       ((uint32_t)src[3] << 24);
}

static uint64_t get_u64_le(const uint8_t *src)
{
	uint64_t v = 0;
	unsigned int i;

	for (i = 0; i < 8u; i++)
		v |= (uint64_t)src[i] << (8u * i);
	return v;
}

int32_t obf_file_payload_count(long file_size, size_t *count_out)
{
	long payload_bytes;

	if (file_size < 0)
		return OBF_ERR_IO;
	if ((unsigned long)file_size < OBF_FILE_OVERHEAD + sizeof(uint64_t))
		return OBF_ERR_FORMAT;

	payload_bytes = file_size - (long)OBF_FILE_OVERHEAD;
	if (payload_bytes % (long)sizeof(uint64_t) != 0)
		return OBF_ERR_FORMAT;

	*count_out = (size_t)payload_bytes / sizeof(uint64_t);
	return OBF_OK;
}

static int32_t write_encoded_file(const char *path, const uint64_t *encoded, size_t count,
				  uint64_t seed)
{
	FILE *fp;
	uint8_t hdr[OBF_FILE_HDR_SIZE];
	uint8_t tail[OBF_FILE_TAIL_SIZE];
	uint8_t block[sizeof(uint64_t)];
	size_t i;

	if (path == NULL || encoded == NULL || count == 0)
		return OBF_ERR_NULL;

	fp = fopen(path, "wb");
	if (fp == NULL)
		return OBF_ERR_IO;

	put_u32_le(hdr, dummy_u32(seed, 0xA11CEu));
	if (fwrite(hdr, 1, sizeof(hdr), fp) != sizeof(hdr)) {
		fclose(fp);
		return OBF_ERR_IO;
	}

	for (i = 0; i < count; i++) {
		put_u64_le(block, encoded[i]);
		if (fwrite(block, 1, sizeof(block), fp) != sizeof(block)) {
			fclose(fp);
			return OBF_ERR_IO;
		}
	}

	put_u32_le(tail, dummy_u32(seed, 0x7A1Lu));
	if (fwrite(tail, 1, sizeof(tail), fp) != sizeof(tail)) {
		fclose(fp);
		return OBF_ERR_IO;
	}

	if (fclose(fp) != 0)
		return OBF_ERR_IO;
	return OBF_OK;
}

int32_t write_many_to(const char *path, const uint64_t *vals, size_t count, uint64_t seed)
{
	uint64_t *encoded;
	size_t i;
	int32_t rv;

	if (path == NULL || vals == NULL || count == 0)
		return OBF_ERR_NULL;

	encoded = (uint64_t *)malloc(count * sizeof(uint64_t));
	if (encoded == NULL)
		return OBF_ERR_IO;

	for (i = 0; i < count; i++) {
		rv = encode(vals[i], seed, &encoded[i]);
		if (rv != OBF_OK) {
			free(encoded);
			return rv;
		}
	}

	rv = write_encoded_file(path, encoded, count, seed);
	free(encoded);
	return rv;
}

int32_t write_to(const char *path, uint64_t val, uint64_t seed)
{
	return write_many_to(path, &val, 1, seed);
}

int32_t read_many_from(const char *path, uint64_t seed, uint64_t *out_vals, size_t out_cap,
		       size_t *out_count)
{
	FILE *fp;
	long file_size;
	size_t count;
	size_t i;
	uint8_t hdr[OBF_FILE_HDR_SIZE];
	uint8_t tail[OBF_FILE_TAIL_SIZE];
	uint8_t block[sizeof(uint64_t)];
	uint64_t encoded;
	uint64_t decoded;
	int32_t rv;

	if (path == NULL || out_vals == NULL || out_count == NULL)
		return OBF_ERR_NULL;

	fp = fopen(path, "rb");
	if (fp == NULL)
		return OBF_ERR_IO;

	if (fseek(fp, 0, SEEK_END) != 0) {
		fclose(fp);
		return OBF_ERR_IO;
	}
	file_size = ftell(fp);
	if (file_size < 0 || fseek(fp, 0, SEEK_SET) != 0) {
		fclose(fp);
		return OBF_ERR_IO;
	}

	rv = obf_file_payload_count(file_size, &count);
	if (rv != OBF_OK) {
		fclose(fp);
		return rv;
	}
	if (count > out_cap) {
		fclose(fp);
		return OBF_ERR_OVERFLOW;
	}

	if (fread(hdr, 1, sizeof(hdr), fp) != sizeof(hdr)) {
		fclose(fp);
		return OBF_ERR_IO;
	}
	(void)get_u32_le(hdr);

	for (i = 0; i < count; i++) {
		if (fread(block, 1, sizeof(block), fp) != sizeof(block)) {
			fclose(fp);
			return OBF_ERR_IO;
		}
		encoded = get_u64_le(block);
		rv = decode(encoded, seed, &decoded);
		if (rv != OBF_OK) {
			fclose(fp);
			return rv;
		}
		out_vals[i] = decoded;
	}

	if (fread(tail, 1, sizeof(tail), fp) != sizeof(tail)) {
		fclose(fp);
		return OBF_ERR_IO;
	}
	(void)get_u32_le(tail);

	fclose(fp);
	*out_count = count;
	return OBF_OK;
}

int32_t read_from(const char *path, uint64_t seed, uint64_t *out)
{
	size_t count = 0;

	return read_many_from(path, seed, out, 1, &count);
}
