// =====================================================================================================================
//  fennec, a free and open source game engine
//  Copyright © 2025 - 2026  Medusa Slockbower
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <https://www.gnu.org/licenses/>.
// =====================================================================================================================

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

#include <fennec/filesystem/file.h>
#include <fennec/filesystem/path.h>


#ifdef FENNEC_COMPILER_MSVC



#else

#include <sys/file.h>
#include <bits/posix2_lim.h>

#endif

namespace fennec
{

constexpr cstring fmode_translate(uint8_t mode) {
	if (not file::is_valid(mode)) {
		return "";
	}

	mode &= ~fmode_wide; // Ignore wide bit

	switch (mode) {
		case fmode_read:  return "r";
		case fmode_write: return "a";

// I chose r+ because for most read/write assets in the context of the game engine will be setting files, save files,
// and editor assets, which require a complete read of the file before any writing is done
		case fmode_read | fmode_write: return "r+";

		case              fmode_write | fmode_trunc: return "w";
		case fmode_read | fmode_write | fmode_trunc: return "w+";

		case              fmode_write | fmode_trunc | fmode_exclusive: return "wx";
		case fmode_read | fmode_write | fmode_trunc | fmode_exclusive: return "wx+";

// Binary Files
		case fmode_binary | fmode_read:  return "rb";
		case fmode_binary | fmode_write: return "ab";

		case fmode_binary | fmode_read | fmode_write: return "rb+";

		case fmode_binary |               fmode_write | fmode_trunc: return "wb";
		case fmode_binary | fmode_read  | fmode_write | fmode_trunc: return "wb+";

		case fmode_binary |               fmode_write | fmode_trunc | fmode_exclusive: return "wxb";
		case fmode_binary | fmode_read  | fmode_write | fmode_trunc | fmode_exclusive: return "wxb+";

		default: return "";
	}
}

file& file::cout() {
	static constexpr char path[] = "stdout";
	static file out = []() -> file {
		file res;
		res._mode   = fmode_write;
		res._handle = stdout;
		res._path   = string(path);
		return res;
	}();
	return out;
}

file& file::cin() {
	static constexpr char path[] = "stdin";
	static file out = []() -> file {
		file res;
		res._mode   = fmode_read;
		res._handle = stdin;
		res._path   = string(path);
		return res;
	}();
	return out;
}

file& file::cerr() {
	static constexpr char path[] = "stderr";
	static file out = []() -> file {
		file res;
		res._mode   = fmode_write;
		res._handle = stderr;
		res._path   = string(path);
		return res;
	}();
	return out;
}

file::file()
	: _handle(nullptr)
	, _path("")
	, _mode(0)
	, _error(nullptr) {

}

file::~file() {
	if (is_open()) {
		close();
	}
}

file::file(file&& file) noexcept
: _handle(file._handle), _path(file._path), _mode(file._mode), _error(file._error) {
	file._handle = nullptr;
	file._error  = nullptr;
}

file& file::operator=(file&& file) noexcept {
	assert(_error == nullptr, "Attempted Operation on a File in an Errored State");

	// Swap, if file is temporary it'll close the file for us
	swap(_handle, file._handle);
	swap(_path,   file._path);
	swap(_mode,   file._mode);
	swap(_error,  file._error);

	return *this;
}

bool file::open(const cstring& p, uint8_t mode) {
	assert(_error == nullptr, "Attempted Operation on a File in an Errored State");

	// Ensure validity of the mode
	if (not is_valid(mode)) {
		return true;
	}

	// Close the file if already open
	if (_handle) {
		close();
	}

	// Attempt to open the file
	_handle = fopen(p, fmode_translate(mode));

	if (_handle == nullptr) {
		_error = strerror(errno);
		return true;
	}

	// Attempt to lock the file
	if (flock(fileno(_handle), LOCK_EX)) {
		_error = strerror(errno);
		fclose(_handle);
		_handle = nullptr;
		return true;
	}

	_mode = mode;
	_path = p;
	_path = _path.absolute();
	return false;
}

bool file::open(const string& p, uint8_t mode) {
	assert(_error == nullptr, "Attempted Operation on a File in an Errored State");

	// Ensure validity of the mode
	if (not is_valid(mode)) {
		return true;
	}

	// Close the file if already open
	if (_handle) {
		close();
	}

	// Attempt to open the file
	_handle = fopen(p.cstr(), fmode_translate(mode));

	// Validate the file
	if (_handle == nullptr) {
		_error = strerror(errno);
		return true;
	}

	// Attempt to lock the file
	if (flock(fileno(_handle), LOCK_EX)) {
		_error = strerror(errno);
		fclose(_handle);
		_handle = nullptr;
		return true;
	}

	// Set the orientation
	fwide(_handle, mode & fmode_wide ? 1 : -1);

	_mode = mode;
	_path = p;
	_path = _path.absolute();

	return false;
}

bool file::open(const path& p, uint8_t mode) {
	assert(_error == nullptr, "Attempted Operation on a File in an Errored State");

	// Ensure validity of the mode
	if (not is_valid(mode)) {
		return true;
	}

	// Close the file if already open
	if (_handle) {
		close();
	}

	// Attempt to open the file
	_handle = fopen(p.cstr(), fmode_translate(mode));

	// Validate the file
	if (_handle == nullptr) {
		_error = strerror(errno);
		return true;
	}

	// Attempt to lock the file
	if (flock(fileno(_handle), LOCK_EX)) {
		_error = strerror(errno);
		fclose(_handle);
		_handle = nullptr;
		return true;
	}

	// Set the orientation
	fwide(_handle, mode & fmode_wide ? 1 : -1);

	_mode = mode;
	_path = p.absolute();

	return false;
}

bool file::close() {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return false;
	}

	// Attempt to unlock the file
	if (flock(fileno(_handle), LOCK_UN)) {
		_error = strerror(errno);
		return true;
	}

	// Close the file and reset variables
	fclose(_handle);
	_mode = 0;
	_handle = nullptr;
	_path = "";

	return false;
}

bool file::commit() {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return false;
	}

	// Attempt to flush
	if (fflush(_handle)) {
		_error = strerror(errno);
		return true;
	}

	return false;
}

bool file::erase() {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return false;
	}

	// Close the file
	const path path = move(_path);
	if (close()) {
		return true;
	}

	// Erase the file
	remove(path.cstr());

	return false;
}

bool file::rename(const cstring& str) {
	static const size_t page_size = pagesize();
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return false;
	}

	// Validate path
	path fpath = str;
	fpath = fpath.absolute();
	if (_path == fpath) {
		return false;
	}

	// Attempt to open the new file
	FILE* fnew = fopen(fpath.cstr(), "wx");

	// Check for open failure
	if (fnew == nullptr) {
		_error = strerror(errno);
		return true;
	}

	// Reopen this file as read
	_handle = freopen(nullptr, "r", _handle);

	// Check if it failed to reopen
	if (_handle == nullptr) {
		_error = strerror(errno);
		return true;
	}

	// Initialize buffer
	void* buffer = operator new(page_size, static_cast<align_t>(page_size));

	// Copy contents
	size_t read = 0;
	while ((read = fread(buffer, 1, page_size, _handle)) == page_size) {
		if (fwrite(buffer, 1, page_size, fnew) != page_size) {
			break;
		}
	}

	// Handle eof
	if (feof(_handle)) {
		fwrite(buffer, 1, read, fnew);
	}


	// Check new file for errors
	if (ferror(fnew)) {
		_error = strerror(errno);
		fclose(fnew);
		_mode = fmode_read;
		return true;
	}

	// Check the original file for errors
	if (ferror(_handle)) {
		_error = strerror(errno);
		fclose(fnew);
		fclose(_handle);
		_handle = nullptr;
		_mode = 0;
		_path = "";
		return true;
	}

	// Cleanup the buffer
	operator delete(buffer, page_size, static_cast<align_t>(page_size));

	// Close old file
	fclose(_handle);

	// Reopen the new file
	_handle = freopen(fpath.cstr(), fmode_translate(_mode), fnew);

	// Check for open failure
	if (_handle == nullptr) {
		_error = strerror(errno);
		return true;
	}

	// Erase the old file
	remove(_path.cstr());

	// Set the new path
	_path = fpath;

	return false;
}

bool file::rename(const string& str) {
	static const size_t page_size = pagesize();
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return false;
	}

	// Validate path
	path fpath = str;
	fpath = fpath.absolute();
	if (_path == fpath) {
		return false;
	}

	// Attempt to open the new file
	FILE* fnew = fopen(fpath.cstr(), "wx");

	// Check for open failure
	if (fnew == nullptr) {
		_error = strerror(errno);
		return true;
	}

	// Reopen this file as read
	_handle = freopen(nullptr, "r", _handle);

	// Check if it failed to reopen
	if (_handle == nullptr) {
		_error = strerror(errno);
		return true;
	}

	// Initialize buffer
	void* buffer = operator new(page_size, static_cast<align_t>(page_size));

	// Copy contents
	size_t read = 0;
	while ((read = fread(buffer, 1, page_size, _handle)) == page_size) {
		if (fwrite(buffer, 1, page_size, fnew) != page_size) {
			break;
		}
	}

	// Handle eof
	if (feof(_handle)) {
		fwrite(buffer, 1, read, fnew);
	}

	// Check new file for errors
	if (ferror(fnew)) {
		_error = strerror(errno);
		fclose(fnew);
		_mode = fmode_read;
		return true;
	}

	// Check the original file for errors
	if (ferror(_handle)) {
		_error = strerror(errno);
		fclose(fnew);
		fclose(_handle);
		_handle = nullptr;
		_mode = 0;
		_path = "";
		return true;
	}

	// Cleanup the buffer
	operator delete(buffer, page_size, static_cast<align_t>(page_size));

	// Close old file
	fclose(_handle);

	// Reopen the new file
	_handle = freopen(_path.cstr(), fmode_translate(_mode), fnew);

	// Check for open failure
	if (_handle == nullptr) {
		_error = strerror(errno);
		return true;
	}

	// Set the new path
	_path = fpath;

	return false;
}

bool file::rename(const path& p) {
	static const size_t page_size = pagesize();
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return false;
	}

	// Validate path
	const path fpath = p.absolute();
	if (_path == fpath) {
		return false;
	}

	// Attempt to open the new file
	FILE* fnew = fopen(fpath.cstr(), "wx");

	// Check for open failure
	if (fnew == nullptr) {
		_error = strerror(errno);
		return true;
	}

	// Reopen this file as read
	_handle = freopen(nullptr, "r", _handle);

	// Check if it failed to reopen
	if (_handle == nullptr) {
		_error = strerror(errno);
		return true;
	}

	// Initialize buffer
	void* buffer = operator new(page_size, static_cast<align_t>(page_size));

	// Copy contents
	size_t read = 0;
	while ((read = fread(buffer, 1, page_size, _handle)) == page_size) {
		if (fwrite(buffer, 1, page_size, fnew) != page_size) {
			break;
		}
	}

	// Handle eof
	if (feof(_handle)) {
		fwrite(buffer, 1, read, fnew);
	}

	// Check new file for errors
	if (ferror(fnew)) {
		_error = strerror(errno);
		fclose(fnew);
		_mode = fmode_read;
		return true;
	}

	// Check the original file for errors
	if (ferror(_handle)) {
		_error = strerror(errno);
		fclose(fnew);
		fclose(_handle);
		_handle = nullptr;
		_mode = 0;
		_path = "";
		return true;
	}

	// Cleanup the buffer
	operator delete(buffer, page_size, static_cast<align_t>(page_size));

	// Close old file
	fclose(_handle);

	// Reopen the new file
	_handle = freopen(_path.cstr(), fmode_translate(_mode), fnew);

	// Check for open failure
	if (_handle == nullptr) {
		_error = strerror(errno);
		return true;
	}

	// Set the new path
	_path = fpath;

	return false;
}

file file::copy(const cstring& str) {
	static const size_t page_size = pagesize();
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return file();
	}

	// Validate path
	path fpath = str;
	fpath = fpath.absolute();
	if (_path == fpath) {
		return file();
	}

	// Attempt to open the new file
	FILE* fnew = fopen(fpath.cstr(), "wx");

	// Check for open failure
	if (fnew == nullptr) {
		_error = strerror(errno);
		return file();
	}

	// Reopen this file as read
	_handle = freopen(nullptr, "r", _handle);

	// Check if it failed to reopen
	if (_handle == nullptr) {
		_error = strerror(errno);
		return file();
	}

	// Initialize buffer
	void* buffer = operator new(page_size, static_cast<align_t>(page_size));

	// Copy contents
	size_t read = 0;
	while ((read = fread(buffer, 1, page_size, _handle)) == page_size) {
		if (fwrite(buffer, 1, page_size, fnew) != page_size) {
			break;
		}
	}

	// Handle eof
	if (feof(_handle)) {
		fwrite(buffer, 1, read, fnew);
	}

	// Check new file for errors
	if (ferror(fnew)) {
		_error = strerror(errno);
		fclose(fnew);
		_mode = fmode_read;
		return file();
	}

	// Check the original file for errors
	if (ferror(_handle)) {
		_error = strerror(errno);
		fclose(fnew);
		fclose(_handle);
		_handle = nullptr;
		_mode = 0;
		_path = "";
		return file();
	}

	// Cleanup the buffer
	operator delete(buffer, page_size, static_cast<align_t>(page_size));

	// Reopen the new file
	_handle = freopen(nullptr, fmode_translate(_mode), _handle);

	// Check for open failure
	if (_handle == nullptr) {
		_error = strerror(errno);
		return file();
	}

	// Set the new path
	file res;
	res._handle = fnew;
	res._path   = fpath;
	res._mode   = fmode_write;

	return res;
}

file file::copy(const string& str) {
	static const size_t page_size = pagesize();
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return file();
	}

	// Validate path
	path fpath = str;
	fpath = fpath.absolute();
	if (_path == fpath) {
		return file();
	}

	// Attempt to open the new file
	FILE* fnew = fopen(fpath.cstr(), "wx");

	// Check for open failure
	if (fnew == nullptr) {
		_error = strerror(errno);
		return file();
	}

	// Reopen this file as read
	_handle = freopen(nullptr, "r", _handle);

	// Check if it failed to reopen
	if (_handle == nullptr) {
		_error = strerror(errno);
		return file();
	}

	// Initialize buffer
	void* buffer = operator new(page_size, static_cast<align_t>(page_size));

	// Copy contents
	size_t read = 0;
	while ((read = fread(buffer, 1, page_size, _handle)) == page_size) {
		if (fwrite(buffer, 1, page_size, fnew) != page_size) {
			break;
		}
	}

	// Handle eof
	if (feof(_handle)) {
		fwrite(buffer, 1, read, fnew);
	}

	// Check new file for errors
	if (ferror(fnew)) {
		operator delete(buffer, page_size, static_cast<align_t>(page_size));
		_error = strerror(errno);
		fclose(fnew);
		_mode = fmode_read;
		return file();
	}

	// Check the original file for errors
	if (ferror(_handle)) {
		operator delete(buffer, page_size, static_cast<align_t>(page_size));
		_error = strerror(errno);
		fclose(fnew);
		fclose(_handle);
		_handle = nullptr;
		_mode = 0;
		_path = "";
		return file();
	}

	// Cleanup the buffer
	operator delete(buffer, page_size, static_cast<align_t>(page_size));

	// Reopen the new file
	_handle = freopen(nullptr, fmode_translate(_mode), _handle);

	// Check for open failure
	if (_handle == nullptr) {
		_error = strerror(errno);
		return file();
	}

	// Set the new path
	file res;
	res._handle = fnew;
	res._path   = fpath;
	res._mode   = fmode_write;

	return res;
}

file file::copy(const path& p) {
	static const size_t page_size = pagesize();
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return file();
	}

	// Validate path
	const path fpath = p.absolute();
	if (_path == fpath) {
		return file();
	}

	// Attempt to open the new file
	FILE* fnew = fopen(fpath.cstr(), "wx");

	// Check for open failure
	if (fnew == nullptr) {
		_error = strerror(errno);
		return file();
	}

	// Reopen this file as read
	_handle = freopen(nullptr, "r", _handle);

	// Check if it failed to reopen
	if (_handle == nullptr) {
		_error = strerror(errno);
		return file();
	}

	// Initialize buffer
	void* buffer = operator new(page_size, static_cast<align_t>(page_size));

	// Copy contents
	size_t read = 0;
	while ((read = fread(buffer, 1, page_size, _handle)) == page_size) {
		if (fwrite(buffer, 1, page_size, fnew) != page_size) {
			break;
		}
	}

	// Handle eof
	if (feof(_handle)) {
		fwrite(buffer, 1, read, fnew);
	}

	// Check new file for errors
	if (ferror(fnew)) {
		operator delete(buffer, page_size, static_cast<align_t>(page_size));
		_error = strerror(errno);
		fclose(fnew);
		_mode = fmode_read;
		return file();
	}

	// Check the original file for errors
	if (ferror(_handle)) {
		operator delete(buffer, page_size, static_cast<align_t>(page_size));
		_error = strerror(errno);
		fclose(fnew);
		fclose(_handle);
		_handle = nullptr;
		_mode = 0;
		_path = "";
		return file();
	}

	// Cleanup the buffer
	operator delete(buffer, page_size, static_cast<align_t>(page_size));

	// Reopen the new file
	_handle = freopen(nullptr, fmode_translate(_mode), _handle);

	// Check for open failure
	if (_handle == nullptr) {
		_error = strerror(errno);
		return file();
	}

	// Set the new path
	file res;
	res._handle = fnew;
	res._path   = fpath;
	res._mode   = fmode_write;

	return res;
}

size_t file::get_pos() const {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return npos;
	}

	return ftell(_handle);
}

bool file::set_pos(size_t i) {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return false;
	}

	if (fseek(_handle, i, SEEK_SET)) {
		_error = strerror(errno);
		return true;
	}

	return false;
}

bool file::rewind() {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return false;
	}

	::rewind(_handle);
	if (ferror(_handle)) {
		_error = strerror(errno);
		return true;
	}

	return false;
}

bool file::eof() const {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return false;
	}

	return feof(_handle);
}

char file::getc() {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");
	assert(not(_mode & fmode_wide), "Attempted Wide Operation on Byte File");

	// Check if there is a file
	if (_handle == nullptr) {
		return 0;
	}

	return fgetc(_handle);
}

wchar_t file::getwc() {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");
	assert(_mode & fmode_wide, "Attempted Byte Operation on Wide File");

	// Check if there is a file
	if (_handle == nullptr) {
		return 0;
	}

	return fgetwc(_handle);
}

size_t file::read(void* data, size_t size, size_t n) {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return 0;
	}

	const size_t read = fread(data, size, n, _handle);
	if (read != size && ferror(_handle)) {
		_error = strerror(errno);
	}

	return read;
}

size_t file::write(const void* data, size_t size, size_t n) {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");

	// Check if there is a file
	if (_handle == nullptr) {
		return 0;
	}

	const size_t r = fwrite(data, size, n, _handle);

	if (r != size && ferror(_handle)) {
		_error = strerror(errno);
		return 0;
	}

	return r;
}

string file::getline() {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");
	assert(not(_mode & fmode_wide), "Attempted Wide Operation on Byte File");

	// Check if there is a file
	if (_handle == nullptr) {
		return string{ "" };
	}

	// Read the next line;
	char arr[LINE_MAX + 2] = { L'\0' };
	cstring buff = arr;
	string res{""};

	// read until first newline or end of file
	while (fgets(arr, LINE_MAX + 2, _handle)) {
		res += cstring(arr, buff.length());
		if (buff.length() < LINE_MAX || buff[LINE_MAX] == L'\n') {
			return res;
		}
	}

	_error = strerror(errno);
	return string("");
}

wstring file::getwline() {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");
	assert(_mode & fmode_wide, "Attempted Byte Operation on Wide File");

	// Check if there is a file
	if (_handle == nullptr) {
		return _wstring{ L"" };
	}

	// Read the next line;
	wchar_t arr[LINE_MAX + 2] = { L'\0' };
	wcstring buff = arr;
	wstring res{L""};

	// read until first newline or end of file
	while (fgetws(arr, LINE_MAX + 2, _handle)) {
		res += wcstring(arr, buff.length());
		if (buff.length() < LINE_MAX || buff[LINE_MAX] == L'\n') {
			return res;
		}
	}

	_error = strerror(errno);
	return wstring(L"");
}

void file::print(const cstring& str) {
	write(str.data(), str.length());
}

void file::print(const string& str) {
	write(str.data(), str.length());
}

void file::println(const cstring& str) {
	write(str.data(), str.length());
	putc('\n');
}

void file::println(const string& str) {
	write(str.data(), str.length());
	putc('\n');
}

bool file::putc(char c) {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");
	assert(not(_mode & fmode_wide), "Attempted Wide Operation on Byte File");

	// Check if there is a file
	if (_handle == nullptr) {
		return false;
	}

	int res;
	if (char(res = fputc(c, _handle)) != c && res != EOF) {
		_error = strerror(errno);
		return true;
	}

	return false;
}

bool file::putwc(wchar_t c) {
	assert(_error == nullptr, "Attempted an Operation on a File in an Errored State");
	assert(_mode & fmode_wide, "Attempted Byte Operation on Wide File");

	// Check if there is a file
	if (_handle == nullptr) {
		return false;
	}

	int res;
	if (wchar_t(res = fputwc(c, _handle)) != c && res != EOF) {
		_error = strerror(errno);
		return true;
	}

	return false;
}

}
