#include "./fs_const_archiver.hpp"

#include <cstring>

#include <mm/logger.hpp>
#define LOG_CRIT(...)		__LOG_CRIT(	"Filesystem", __VA_ARGS__)
#define LOG_ERROR(...)		__LOG_ERROR("Filesystem", __VA_ARGS__)
#define LOG_WARN(...)		__LOG_WARN(	"Filesystem", __VA_ARGS__)
#define LOG_INFO(...)		__LOG_INFO(	"Filesystem", __VA_ARGS__)
#define LOG_DEBUG(...)		__LOG_DEBUG("Filesystem", __VA_ARGS__)
#define LOG_TRACE(...)		__LOG_TRACE("Filesystem", __VA_ARGS__)

namespace MM {

std::unordered_map<std::string, std::pair<uint8_t*, size_t>> FSConstArchiver::_storage {};

void* FSConstArchiver::PH_openArchive(PHYSFS_Io* io, const char* name, int forWrite, int* claimed) {
	(void)io;
	(void)name;
	// TODO: test name

	if (forWrite) {
		//PHYSFS_setErrorCode(PHYSFS_ERR_READ_ONLY);
		*claimed = 0;
		return nullptr;
	}

	*claimed = 1;

	return (void*)1; // non zero
}

PHYSFS_EnumerateCallbackResult FSConstArchiver::PH_enumerate(void* opaque, const char* dirname, PHYSFS_EnumerateCallback cb, const char* origdir, void* callbackdata) {
	(void)opaque;
	(void)dirname;
	(void)cb;
	(void)origdir;
	(void)callbackdata;

	// "files" just not enumeratable
	//return PHYSFS_ENUM_STOP;
	return PHYSFS_ENUM_OK;
}

PHYSFS_Io* FSConstArchiver::PH_openRead(void* opaque, const char* fnm) {
	(void)opaque;

	if (!_storage.count(fnm)) {
		PHYSFS_setErrorCode(PHYSFS_ERR_NOT_FOUND);
		return nullptr;
	}

	return createIO(fnm);
}

PHYSFS_Io* FSConstArchiver::PH_openWrite(void* opaque, const char* filename) {
	(void)opaque;
	(void)filename;
	return nullptr;
}

PHYSFS_Io* FSConstArchiver::PH_openAppend(void* opaque, const char* filename) {
	(void)opaque;
	(void)filename;
	return nullptr;
}

int FSConstArchiver::PH_remove(void* opaque, const char* filename) {
	(void)opaque;
	(void)filename;

	// TODO: support this?

	PHYSFS_setErrorCode(PHYSFS_ERR_READ_ONLY);

	return 0;
}

int FSConstArchiver::PH_mkdir(void* opaque, const char* filename) {
	(void)opaque;
	(void)filename;

	PHYSFS_setErrorCode(PHYSFS_ERR_READ_ONLY);

	return 0;
}

int FSConstArchiver::PH_stat(void* opaque, const char* fn, PHYSFS_Stat* stat) {
	(void)opaque;

	if (pathIsDir(fn)) {
		stat->filesize = 0;
		stat->filetype = PHYSFS_FILETYPE_DIRECTORY;
	} else if (_storage.count(fn)) {
		stat->filesize = _storage[fn].second;
		stat->filetype = PHYSFS_FILETYPE_REGULAR;
	} else {
		return 0;
	}

	stat->accesstime = -1;
	stat->createtime = -1;
	stat->modtime = -1;
	stat->readonly = 1;

	return 1;
}

void FSConstArchiver::PH_closeArchive(void* opaque) {
	(void)opaque;
}

PHYSFS_Io* FSConstArchiver::createIO(const char* filename, PHYSFS_uint64 pos) {
	if (!_storage.count(filename)) {
		//LOGCA(std::string("error: path '") + filename + "' alredy used!");
		LOG_ERROR("path '{}' already used!", filename);
		return nullptr;
	}

	//LOGCA(std::string("###creating io for ") + filename);
	LOG_TRACE("creating io for {}", filename);

	struct io_internal {
		std::string filename;
		PHYSFS_uint64 pos = 0;
		uint8_t* data = nullptr;
		size_t data_size = 0;
	};

	auto new_io = new PHYSFS_Io;


	new_io->version = 0;
	new_io->write = nullptr;
	new_io->flush = nullptr;

	new_io->opaque = new io_internal{
		filename,
		pos,
		_storage[filename].first,
		_storage[filename].second
	};
	new_io->destroy = [](PHYSFS_Io* io) {
		delete (io_internal*)io->opaque;
		delete io;
	};

	new_io->tell = [](PHYSFS_Io* io) -> PHYSFS_sint64 { return ((io_internal*)io->opaque)->pos; };
	new_io->seek = [](PHYSFS_Io* io, PHYSFS_uint64 offset) -> int {
		auto* inter = (io_internal*)io->opaque;
		if (offset > inter->data_size)
			return 0; // error, past end
		inter->pos = offset;
		return 1;
	};
	new_io->length = [](PHYSFS_Io* io) -> PHYSFS_sint64 { return ((io_internal*)io->opaque)->data_size; };
	new_io->duplicate = [](PHYSFS_Io* io) -> PHYSFS_Io* {
		if (!io)
			return nullptr;

		auto* inter = (io_internal*)io->opaque;
		auto* dup = createIO(inter->filename.c_str(), inter->pos);
		return dup;
	};
	new_io->read = [](PHYSFS_Io* io, void* buf, PHYSFS_uint64 len) -> PHYSFS_sint64 {
		if (!io)
			return -1;

		auto* inter = (io_internal*)io->opaque;
		if (inter->data_size == inter->pos)
			return 0; // EOF

		PHYSFS_sint64 bytes_to_read = 0;
		if (inter->pos + len >= inter->data_size) {
			bytes_to_read = inter->data_size - inter->pos; // remaining data
		} else {
			bytes_to_read = len;
		}

		memcpy(buf, inter->data + inter->pos, bytes_to_read);
		inter->pos += bytes_to_read;
		return bytes_to_read;
	};

	return new_io;
}

bool FSConstArchiver::pathIsDir(const char* path) {
	// TODO: fix leading '/'

	std::string_view pstr {path};
	//for (auto&[str, data] : _storage) {
	for (auto& it : _storage) {
		// stats_with c++20 <.<

		if (it.first.compare(0, pstr.size(), pstr) == 0 && it.first.size() != pstr.size()) {
			return true;
		}
	}

	return false;
}

const PHYSFS_Archiver* FSConstArchiver::getArchiverStruct(void) {
	static const PHYSFS_Archiver a {
		0, // version
		{ // info
			"MEM", // ext
			"in const memory 'archiver'", // desc
			"Green", // author
			"", // url
			0 // sym
		},
		&PH_openArchive,
		&PH_enumerate,
		&PH_openRead,
		&PH_openWrite,
		&PH_openAppend,
		&PH_remove,
		&PH_mkdir,
		&PH_stat,
		&PH_closeArchive
	};

	return &a;
}

void FSConstArchiver::addFile(const char* path, uint8_t* data, size_t data_size) {
	if (!path)
		return;

	// remove leading / es
	while (path[0] == '/') {
		path++;

		if (path[0] == '\0')
			return;
	}

	if (_storage.count(path)) {
		//LOGCA(std::string("error: '") + path + "' already in path");
		LOG_TRACE("'{}' already in path, overriding...", path);
	}

	_storage[path] = {data, data_size};
}

} // MM

