#ifndef FILE_H
#define FILE_H

#include <stddef.h>
#include <stdint.h>

#define CONCAT(a, b) CONCAT_INNER(a, b)
#define CONCAT_INNER(a, b) a ## b

#define UNIQUE_NAME(base) CONCAT(base, __COUNTER__)

enum file_err {
	FILE_ERR_OK,
	FILE_ERR_FAIL_OPEN,
	FILE_ERR_FAIL_SEEK,
	FILE_ERR_FAIL_READ,
	FILE_ERR_FAIL_WRITE,
	FILE_ERR_FAIL_CALLOC,
	FILE_ERR_FAIL_CLOSE,
	FILE_ERR_FILE_EMPTY,
	FILE_ERR_FAIL_WROTE_MORE,
	FILE_ERR_EMPTY
};

uint8_t *file_read_all(const char *filepath, size_t *ret_size,
		       enum file_err *ret_err);
void file_save(const char *filepath, const char *str, size_t str_size,
	       enum file_err *ret_err);

void file_err_set(enum file_err *err, enum file_err err_);
const char *file_err_to_cstr(enum file_err err);

#if defined(BMP_IMP) || defined(IMP)

#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

uint8_t *
file_read_all(const char *filepath, size_t *ret_size, enum file_err *ret_err)
{
	int32_t fd = -1;
	off_t file_size = -1;
	size_t buf_size = 0;
	uint8_t *buf = NULL;

	fd = open(filepath, O_RDONLY);
	if ( fd < 0 ) {
		file_err_set(ret_err, FILE_ERR_FAIL_OPEN);
		return NULL;
	}

	file_size = lseek(fd, 0, SEEK_END);
	if ( file_size < 0 ) {
		file_err_set(ret_err, FILE_ERR_FAIL_SEEK);
		close(fd);
		return NULL;
	}
	lseek(fd, 0, SEEK_SET);

	if ( file_size == 0 ) {
		file_err_set(ret_err, FILE_ERR_EMPTY);
		close(fd);
		return NULL;
	}

	buf_size = ((size_t)file_size) + 1;
	buf = calloc(buf_size, sizeof(uint8_t));
	if ( buf == NULL ) {
		file_err_set(ret_err, FILE_ERR_FAIL_CALLOC);
		close(fd);
		return NULL;
	}

	{
		ssize_t rd = read(fd, buf, (size_t)file_size);
		if ( rd < 0 ) {
			file_err_set(ret_err, FILE_ERR_FAIL_READ);
			close(fd);
			free(buf);
			return NULL;
		}
		if ( rd == 0 ) {
			file_err_set(ret_err, FILE_ERR_FILE_EMPTY);
			close(fd);
			free(buf);
			return NULL;
		}
	}

	if ( close(fd) != 0 ) {
		/* It should be possible to handle EIO */
		file_err_set(ret_err, FILE_ERR_FAIL_CLOSE);
		close(fd);
		return NULL;
	}

	if ( ret_size != NULL ) {
		*ret_size = buf_size;
	}
	file_err_set(ret_err, FILE_ERR_OK);
	return buf;
}

void
file_save(const char *filepath, const char *str, size_t str_size,
	  enum file_err *ret_err)
{
	ssize_t wrote = -1;
	int32_t fd = open(filepath,
			  O_WRONLY | O_CREAT | O_TRUNC,
			  S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
	if ( fd < 0 ) {
		file_err_set(ret_err, FILE_ERR_FAIL_OPEN);
		return;
	}

	wrote = write(fd, str, str_size);
	if ( wrote == -1 ) {
		file_err_set(ret_err, FILE_ERR_FAIL_WRITE);
		return;
	}

	if ( ((size_t) wrote) != str_size ) {
		file_err_set(ret_err, FILE_ERR_FAIL_WROTE_MORE);
		return;
	}

	if ( close(fd) != 0 ) {
		/* It should be possible to handle EIO */
		file_err_set(ret_err, FILE_ERR_FAIL_CLOSE);
		close(fd);
		return;
	}

	file_err_set(ret_err, FILE_ERR_OK);
}

void
file_err_set(enum file_err *err, enum file_err err_)
{
	if ( err != NULL ) {
		*err = err_;
	}
}

const char *
file_err_to_cstr(enum file_err err)
{
	switch ( err ) {
	case FILE_ERR_OK: 		return "FILE_ERR_OK"; break;
	case FILE_ERR_FAIL_OPEN:	return "FILE_ERR_FAIL_OPEN"; break;
	case FILE_ERR_FAIL_SEEK:	return "FILE_ERR_FAIL_SEEK"; break;
	case FILE_ERR_FAIL_READ:	return "FILE_ERR_FAIL_READ"; break;
	case FILE_ERR_FAIL_WRITE:	return "FILE_ERR_FAIL_WRITE"; break;
	case FILE_ERR_FAIL_CALLOC:	return "FILE_ERR_FAIL_CALLOC"; break;
	case FILE_ERR_FAIL_CLOSE:	return "FILE_ERR_FAIL_CLOSE"; break;
	case FILE_ERR_FILE_EMPTY:	return "FILE_ERR_FILE_EMPTY"; break;
	case FILE_ERR_FAIL_WROTE_MORE:	return "FILE_ERR_FAIL_WROTE_MORE"; break;
	case FILE_ERR_EMPTY:		return "FILE_ERR_EMPTY"; break;
	}
	return "FILE ERR INVALID";
}

#endif
#endif
