#include <bits/printf.h>
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <sys/syscall.h>
#include <unistd.h>

struct FILE
{
	int fd			{ -1 };
	bool eof		{ false };
	bool error		{ false };

	unsigned char buffer[BUFSIZ] {};
	uint32_t buffer_index { 0 };
};

static FILE s_files[FOPEN_MAX] {
	{ .fd = STDIN_FILENO  },
	{ .fd = STDOUT_FILENO },
	{ .fd = STDERR_FILENO },
};

FILE* stdin  = &s_files[0];
FILE* stdout = &s_files[1];
FILE* stderr = &s_files[2];

void clearerr(FILE* file)
{
	file->eof = false;
	file->error = false;
}

char* ctermid(char* buffer)
{
	static char s_buffer[L_ctermid];
	char* target = buffer ? buffer : s_buffer;
	syscall(SYS_TERMID, target);
	return target;
}

int fclose(FILE* file)
{
	if (close(file->fd) == -1)
		return EOF;
	file->fd = -1;
	return 0;
}

FILE* fdopen(int fd, const char* mode)
{
	// FIXME
	(void)mode;

	for (int i = 0; i < FOPEN_MAX; i++)
	{
		if (s_files[i].fd == -1)
		{
			s_files[i] = { .fd = fd };
			return &s_files[i];
		}
	}

	errno = EMFILE;
	return nullptr;
}

int feof(FILE* file)
{
	return file->eof;
}

int ferror(FILE* file)
{
	return file->error;
}

int fflush(FILE* file)
{
	if (file == nullptr)
	{
		for (int i = 0; i < FOPEN_MAX; i++)
			if (s_files[i].fd != -1)
				if (int ret = fflush(&s_files[i]); ret != 0)
					return ret;
		return 0;
	}

	if (file->buffer_index == 0)
		return 0;

	if (syscall(SYS_WRITE, file->fd, file->buffer, file->buffer_index) < 0)
	{
		file->error = true;
		return EOF;
	}

	file->buffer_index = 0;
	return 0;
}

int fgetc(FILE* file)
{
	if (file->eof)
		return EOF;

	unsigned char c;
	long ret = syscall(SYS_READ, file->fd, &c, 1);

	if (ret < 0)
	{
		file->error = true;
		return EOF;
	}

	if (ret == 0)
	{
		file->eof = true;
		return EOF;
	}

	return c;
}

int fgetpos(FILE* file, fpos_t* pos)
{
	off_t offset = ftello(file);
	if (offset == -1)
		return -1;
	*pos = offset;
	return 0;
}

char* fgets(char* str, int size, FILE* file)
{
	if (size == 0)
		return nullptr;
	int i = 0;
	for (; i < size - 1; i++)
	{
		char c = fgetc(file);
		if (c == EOF)
		{
			if (i == 0)
				return nullptr;
			break;
		}
		str[i] = c;
		if (c == '\n')
		{
			i++;
			break;
		}
	}
	str[i] = '\0';
	return str;
}

int fileno(FILE* fp)
{
	if (fp == nullptr)
		return EBADF;
	return fp->fd;
}

// TODO
void flockfile(FILE*);

FILE* fopen(const char* pathname, const char* mode)
{
	uint8_t flags = 0;
	if (mode[0] == 'r')
		flags |= O_RDONLY;
	else if (mode[0] == 'w')
		flags |= O_WRONLY | O_CREAT | O_TRUNC;
	else if (mode[0] == 'a')
		flags |= O_WRONLY | O_CREAT | O_APPEND;
	else
	{
		errno = EINVAL;
		return nullptr;
	}

	if (mode[1] && mode[2] && mode[1] == mode[2])
	{
		errno = EINVAL;
		return nullptr;
	}

	for (int i = 1; i <= 2; i++)
	{
		if (mode[i] == 0)
			break;
		else if (mode[i] == '+')
			flags |= O_RDWR;
		else if (mode[i] == 'b')
			continue;
		else
		{
			errno = EINVAL;
			return nullptr;
		}
	}

	int fd = open(pathname, flags);
	if (fd == -1)
		return nullptr;

	for (int i = 0; i < FOPEN_MAX; i++)
	{
		if (s_files[i].fd == -1)
		{
			s_files[i] = { .fd = fd };
			return &s_files[i];
		}
	}

	errno = EMFILE;
	return nullptr;
}

int fprintf(FILE* file, const char* format, ...)
{
	va_list arguments;
	va_start(arguments, format);
	int ret = vfprintf(file, format, arguments);
	va_end(arguments);
	return ret;
}

int fputc(int c, FILE* file)
{
	file->buffer[file->buffer_index++] = c;
	if (c == '\n' || file->buffer_index == sizeof(file->buffer))
		if (fflush(file) == EOF)
			return EOF;
	return (unsigned char)c;
}

int fputs(const char* str, FILE* file)
{
	while (*str)
	{
		if (fputc(*str, file) == EOF)
			return EOF;
		str++;
	}
	return 0;
}

size_t fread(void* buffer, size_t size, size_t nitems, FILE* file)
{
	if (file->eof || nitems * size == 0)
		return 0;

	size_t target = size * nitems;
	size_t nread = 0;

	while (nread < target)
	{
		ssize_t ret = syscall(SYS_READ, file->fd, (uint8_t*)buffer + nread, target - nread);

		if (ret < 0)
			file->error = true;
		else if (ret == 0)
			file->eof = true;

		if (ret <= 0)
			return nread;

		nread += ret;
	}

	return nread / size;
}

// TODO
FILE* freopen(const char*, const char*, FILE*);

// TODO
int fscanf(FILE*, const char*, ...);

int fseek(FILE* file, long offset, int whence)
{
	return fseeko(file, offset, whence);
}

int fseeko(FILE* file, off_t offset, int whence)
{
	long ret = syscall(SYS_SEEK, file->fd, offset, whence);
	if (ret < 0)
		return -1;

	file->eof = false;
	return 0;
}

int fsetpos(FILE* file, const fpos_t* pos)
{
	return fseek(file, *pos, SEEK_SET);
}

long ftell(FILE* file)
{
	return ftello(file);
}

off_t ftello(FILE* file)
{
	long ret = syscall(SYS_TELL, file->fd);
	if (ret < 0)
		return -1;
	return ret;
}

// TODO
int ftrylockfile(FILE*);

// TODO
void funlockfile(FILE*);

size_t fwrite(const void* buffer, size_t size, size_t nitems, FILE* file)
{
	unsigned char* ubuffer = (unsigned char*)buffer;
	for (size_t byte = 0; byte < nitems * size; byte++)
		if (fputc(ubuffer[byte], file) == EOF)
			return byte / size;
	return nitems;
}

int getc(FILE* file)
{
	return fgetc(file);
}

int getchar(void)
{
	return getc(stdin);
}

// TODO
int getc_unlocked(FILE*);

// TODO
int getchar_unlocked(void);

char* gets(char* buffer)
{
	if (stdin->eof)
		return nullptr;

	unsigned char* ubuffer = (unsigned char*)buffer;

	int first = fgetc(stdin);
	if (first == EOF)
		return nullptr;
	*ubuffer++ = first;

	for (;;)
	{
		int c = fgetc(stdin);
		if (c == '\n' || c == EOF)
		{
			*ubuffer++ = '\0';
			return buffer;
		}
		*ubuffer++ = c;
	}
}

// TODO
int pclose(FILE*);

void perror(const char* string)
{
	if (string && *string)
	{
		fputs(string, stderr);
		fputs(": ", stderr);
	}
	fputs(strerror(errno), stderr);
	fputc('\n', stderr);
	stderr->error = true;
}

// TODO
FILE* popen(const char*, const char*);

int printf(const char* format, ...)
{
	va_list arguments;
	va_start(arguments, format);
	int ret = vfprintf(stdout, format, arguments);
	va_end(arguments);
	return ret;
}

int putc(int c, FILE* file)
{
	return fputc(c, file);
}

int putchar(int c)
{
	return putc(c, stdout);
}

// TODO
int putc_unlocked(int, FILE*);

// TODO
int putchar_unlocked(int);

int puts(const char* string)
{
	if (fputs(string, stdout) == EOF)
		return EOF;
	if (fputc('\n', stdout) == EOF)
		return EOF;
	return 0;
}

int remove(const char* path)
{
	struct stat st;
	if (stat(path, &st) == -1)
		return -1;
	if (S_ISDIR(st.st_mode))
		return rmdir(path);
	return unlink(path);
}

// TODO
int rename(const char*, const char*);

// TODO
void rewind(FILE*);

// TODO
int scanf(const char*, ...);

// TODO
void setbuf(FILE*, char*);

// TODO
int setvbuf(FILE*, char*, int, size_t);

int snprintf(char* buffer, size_t max_size, const char* format, ...)
{
	va_list arguments;
	va_start(arguments, format);
	int ret = vsnprintf(buffer, max_size, format, arguments);
	va_end(arguments);
	return ret;
}

int sprintf(char* buffer, const char* format, ...)
{
	va_list arguments;
	va_start(arguments, format);
	int ret = vsprintf(buffer, format, arguments);
	va_end(arguments);
	return ret;
}

// TODO
int sscanf(const char*, const char*, ...);

// TODO
char* tempnam(const char*, const char*);

// TODO
FILE* tmpfile(void);

// TODO
char* tmpnam(char*);

// TODO
int ungetc(int, FILE*);

int vfprintf(FILE* file, const char* format, va_list arguments)
{
	return printf_impl(format, arguments, [](int c, void* file) { return fputc(c, (FILE*)file); }, file);
}

// TODO
int vfscanf(FILE*, const char*, va_list);

int vprintf(const char* format, va_list arguments)
{
	return vfprintf(stdout, format, arguments);
}

// TODO
int vscanf(const char*, va_list);

int vsnprintf(char* buffer, size_t max_size, const char* format, va_list arguments)
{
	if (buffer == nullptr)
		return printf_impl(format, arguments, [](int, void*) { return 0; }, nullptr);

	struct print_info
	{
		char* buffer;
		size_t remaining;
	};
	print_info info { buffer, max_size };

	int ret = printf_impl(format, arguments,
		[](int c, void* _info)
		{
			print_info* info = (print_info*)_info;
			if (info->remaining == 1)
			{
				*info->buffer = '\0';
				info->remaining = 0;
			}
			else if (info->remaining)
			{
				*info->buffer = c;
				info->buffer++;
				info->remaining--;
			}
			return 0;
		}, &info
	);

	*info.buffer = '\0';
	return ret;
}

int vsprintf(char* buffer, const char* format, va_list arguments)
{
	if (buffer == nullptr)
		return printf_impl(format, arguments, [](int, void*) { return 0; }, nullptr);

	int ret = printf_impl(format, arguments,
		[](int c, void* _buffer)
		{
			*(*(char**)_buffer)++ = c;
			return 0;
		}, &buffer
	);

	*buffer = '\0';
	return ret;
}

// TODO
int vsscanf(const char*, const char*, va_list);
