#include "FileSystem.h"
#include "FileStream.h"
#include "FileSystemType.h"

#ifdef HAS_RNS

#include <Log.h>

#if FS_TYPE == FS_TYPE_INTERNALFS

inline int _countLfsBlock(void *p, lfs_block_t block) {
	lfs_size_t *size = (lfs_size_t*) p;
	*size += 1;
	return 0;
}

lfs_ssize_t usedBlocks() {
    lfs_size_t size = 0;
    lfs_traverse(FS._getFS(), _countLfsBlock, &size);
    return size;
}

size_t usedBytes() {
	const lfs_config* config = FS._getFS()->cfg;
	const size_t usedBlockCount = usedBlocks();
	return config->block_size * usedBlockCount;
}

size_t totalBytes() {
	const lfs_config* config = FS._getFS()->cfg;
	return config->block_size * config->block_count;
}

#elif FS_TYPE == FS_TYPE_FLASHFS

Adafruit_FlashTransport_SPI g_flashTransport(SS, SPI);

//Flash definition structure for GD25Q16C Flash (RAK15001)
Cached_SPIFlash g_flash(&g_flashTransport);
SPIFlash_Device_t g_RAK15001 {
	.total_size = (1UL << 21),
	.start_up_time_us = 5000,
	.manufacturer_id = 0xc8,
	.memory_type = 0x40,
	.capacity = 0x15,
	.max_clock_speed_mhz = 15,
	.quad_enable_bit_mask = 0x00,
	.has_sector_protection = false,
	.supports_fast_read = true,
	.supports_qspi = false,
	.supports_qspi_writes = false,
	.write_status_register_split = false,
	.single_status_byte = true,
};

#endif


bool FileSystem::init() {
	TRACE("Initializing filesystem...");
	try {
#if FS_TYPE == FS_TYPE_SPIFFS
		// Initialize SPIFFS
		INFO("SPIFFS mounting filesystem");
		if (!SPIFFS.begin(true, "")) {
			ERROR("SPIFFS filesystem mount failed");
			return false;
		}
		INFO("SPIFFS filesystem is ready");
#elif FS_TYPE == FS_TYPE_LITTLEFS
		// Initialize LittleFS
		INFO("LittleFS mounting filesystem");
		if (!LittleFS.begin(true, "")) {
			ERROR("LittleFS filesystem mount failed");
			return false;
		}
		DEBUG("LittleFS filesystem is ready");
#elif FS_TYPE == FS_TYPE_INTERNALFS
		// Initialize InternalFileSystem
		INFO("InternalFS mounting filesystem");
		if (!InternalFS.begin()) {
			ERROR("InternalFS filesystem mount failed");
			return false;
		}
		INFO("InternalFS filesystem is ready");
#elif FS_TYPE == FS_TYPE_FLASHFS
		// Initialize FlashFileSystem
		INFO("FlashFS mounting filesystem");
		if (!g_flash.begin(&g_RAK15001)) {
			ERROR("FlashFS failed to initialize");
			return false;
		}
		if (!FlashFS.begin(&g_flash)) {
			ERROR("FlashFS filesystem mount failed");
			return false;
		}
#endif
		// Ensure filesystem is writable and reformat if not
		RNS::Bytes test("test");
		if (write_file("/test", test) < 4) {
			HEAD("Failed to write test file, filesystem is being reformatted...", RNS::LOG_CRITICAL);
			//FS.format();
			reformat();
		}
		else {
			remove_file("/test");
		}
	}
	catch (std::exception& e) {
		//ERROR("FileSystem init Exception: " + std::string(e.what()));
		return false;
	}
	TRACE("Finished initializing");
	return true;
}

bool FileSystem::format() {
	INFO("Formatting filesystem...");
	try {
		if (!FS.format()) {
			ERROR("Format failed!");
			return false;
		}
		return true;
	}
	catch (std::exception& e) {
		ERROR("FileSystem reformat Exception: " + std::string(e.what()));
	}
	return false;
}

bool FileSystem::reformat() {
	INFO("Reformatting filesystem...");
	try {
		RNS::Bytes eeprom;
		read_file("/eeprom", eeprom);
		RNS::Bytes transport_identity;
		read_file("/transport_identity", transport_identity);
		//RNS::Bytes time_offset;
		//read_file("/time_offset", time_offset);
		if (!FS.format()) {
			ERROR("Format failed!");
			return false;
		}
		if (eeprom) {
			write_file("/eeprom", eeprom);
		}
		if (transport_identity) {
			write_file("/transport_identity", transport_identity);
		}
		//if (time_offset) {
		//	write_file("/time_offset", time_offset);
		//}
		return true;
	}
	catch (std::exception& e) {
		ERROR("FileSystem reformat Exception: " + std::string(e.what()));
	}
	return false;
}

#ifndef NDEBUG

void FileSystem::listDir(const char* dir, const char* prefix /*= ""*/) {
	Serial.print(prefix);
	std::string full_dir(dir);
	if (full_dir.compare("/") != 0) {
		full_dir += "/";
	}
	Serial.println(full_dir.c_str());
	std::string pre(prefix);
	pre.append("  ");
	try {
		File root = FS.open(dir);
		if (!root) {
			Serial.print(pre.c_str());
			Serial.println("(failed to open directory)");
			return;
		}
		File file = root.openNextFile();
		while (file) {
			char* name = (char*)file.name();
			std::string recurse_dir(full_dir);
			if (file.isDirectory()) {
				recurse_dir += name;
				listDir(recurse_dir.c_str(), pre.c_str());
			}
			else {
				Serial.print(pre.c_str());
				//Serial.print("FILE: ");
				Serial.print(name);
				Serial.print(" (");
				Serial.print(file.size());
				Serial.println(" bytes)");
			}
			file.close();
			file = root.openNextFile();
		}
		root.close();
	}
	catch (std::exception& e) {
		Serial.print("listDir Exception: ");
		Serial.println(e.what());
	}
}

void FileSystem::dumpDir(const char* dir) {
	Serial.print("DIR: ");
	std::string full_dir(dir);
	if (full_dir.compare("/") != 0) {
		full_dir += "/";
	}
	Serial.println(full_dir.c_str());
	try {
		File root = FS.open(dir);
		if (!root) {
			Serial.println("(failed to open directory)");
			return;
		}
		File file = root.openNextFile();
		while (file) {
			char* name = (char*)file.name();
			if (file.isDirectory()) {
				std::string recurse_dir(full_dir);
				recurse_dir += name;
				dumpDir(recurse_dir.c_str());
			}
			else {
				Serial.print("\nFILE: ");
				Serial.print(name);
				Serial.print(" (");
				Serial.print(file.size());
				Serial.println(" bytes)");
				char data[4096];
				size_t size = file.size();
				size_t read = file.readBytes(data, (size < sizeof(data)) ? size : sizeof(data));
				Serial.write(data, read);
				Serial.println("");
			}
			file.close();
			file = root.openNextFile();
		}
		root.close();
	}
	catch (std::exception& e) {
		Serial.print("dumpDir Exception: ");
		Serial.println(e.what());
	}
}

#endif


/*virtua*/ bool FileSystem::file_exists(const char* file_path) {
	TRACEF("file_exists: checking for existence of file %s", file_path);
/*
#if FS_TYPE == FS_TYPE_INTERNALFS || FS_TYPE == FS_TYPE_FLASHFS
	File file(FS);
	if (file.open(file_path, FILE_O_READ)) {
#else
	File file = FS.open(file_path, FILE_READ);
	if (file) {
#endif
		bool is_directory = file.isDirectory();
		file.close();
		return !is_directory;
	}
	return false;
*/
	return FS.exists(file_path);
}

/*virtua*/ size_t FileSystem::read_file(const char* file_path, RNS::Bytes& data) {
	TRACEF("read_file: reading from file %s", file_path);
	size_t read = 0;
#if FS_TYPE == FS_TYPE_INTERNALFS || FS_TYPE == FS_TYPE_FLASHFS
	File file(FS);
	if (file.open(file_path, FILE_O_READ)) {
#else
	File file = FS.open(file_path, FILE_READ);
	if (file) {
#endif
		size_t size = file.size();
		read = file.readBytes((char*)data.writable(size), size);
		TRACEF("read_file: read %u bytes from file %s", read, file_path);
		if (read != size) {
			ERRORF("read_file: failed to read file %s", file_path);
            data.resize(read);
		}
		//TRACE("read_file: closing input file");
		file.close();
	}
	else {
		ERRORF("read_file: failed to open input file %s", file_path);
	}
    return read;
}

/*virtua*/ size_t FileSystem::write_file(const char* file_path, const RNS::Bytes& data) {
	TRACEF("write_file: writing to file %s", file_path);
	// CBA TODO Replace remove with working truncation
	if (FS.exists(file_path)) {
		FS.remove(file_path);
	}
	size_t wrote = 0;
#if FS_TYPE == FS_TYPE_INTERNALFS || FS_TYPE == FS_TYPE_FLASHFS
	File file(FS);
	if (file.open(file_path, FILE_O_WRITE)) {
#else
	File file = FS.open(file_path, FILE_WRITE);
	if (file) {
#endif
		// Seek to beginning to overwrite
		//file.seek(0);
		//file.truncate(0);
		wrote = file.write(data.data(), data.size());
        TRACEF("write_file: wrote %u bytes to file %s", wrote, file_path);
        if (wrote < data.size()) {
			WARNINGF("write_file: not all data was written to file %s", file_path);
		}
		//TRACE("write_file: closing output file");
		file.close();
	}
	else {
		ERRORF("write_file: failed to open output file %s", file_path);
	}
    return wrote;
}

/*virtual*/ RNS::FileStream FileSystem::open_file(const char* file_path, RNS::FileStream::MODE file_mode) {
	TRACEF("open_file: opening file %s", file_path);
#if FS_TYPE == FS_TYPE_INTERNALFS || FS_TYPE == FS_TYPE_FLASHFS
	int mode;
	if (file_mode == RNS::FileStream::MODE_READ) {
		mode = FILE_O_READ;
	}
	else if (file_mode == RNS::FileStream::MODE_WRITE) {
		mode = FILE_O_WRITE;
		// CBA TODO Replace remove with working truncation
		if (FS.exists(file_path)) {
			FS.remove(file_path);
		}
	}
	else if (file_mode == RNS::FileStream::MODE_APPEND) {
		// CBA This is the default write mode for nrf52 littlefs
		mode = FILE_O_WRITE;
	}
	else {
		ERRORF("open_file: unsupported mode %d", file_mode);
		return {RNS::Type::NONE};
	}
	File* file = new File(FS);
	if (!file->open(file_path, mode)) {
		ERRORF("open_file: failed to open output file %s", file_path);
		return {RNS::Type::NONE};
	}
	// Seek to beginning to overwrite (this is failing on nrf52)
	//if (file_mode == RNS::FileStream::MODE_WRITE) {
	//	file->seek(0);
	//	file->truncate(0);
	//}
	TRACEF("open_file: successfully opened file %s", file_path);
	return RNS::FileStream(new FileStream(file));
#else
	const char* mode;
	if (file_mode == RNS::FileStream::MODE_READ) {
		mode = FILE_READ;
	}
	else if (file_mode == RNS::FileStream::MODE_WRITE) {
		mode = FILE_WRITE;
	}
	else if (file_mode == RNS::FileStream::MODE_APPEND) {
		mode = FILE_APPEND;
	}
	else {
		ERRORF("open_file: unsupported mode %d", file_mode);
		return {RNS::Type::NONE};
	}
	TRACEF("open_file: opening file %s in mode %s", file_path, mode);
	// CBA Using copy constructor to obtain File*
	File* file = new File(FS.open(file_path, mode));
	if (file == nullptr || !(*file)) {
		ERRORF("open_file: failed to open output file %s", file_path);
		return {RNS::Type::NONE};
	}
	TRACEF("open_file: successfully opened file %s", file_path);
	return RNS::FileStream(new FileStream(file));
#endif
}

/*virtua*/ bool FileSystem::remove_file(const char* file_path) {
	TRACEF("remove_file: removing file %s", file_path);
	return FS.remove(file_path);
}

/*virtua*/ bool FileSystem::rename_file(const char* from_file_path, const char* to_file_path) {
	TRACEF("rename_file: renaming file %s to %s", from_file_path, to_file_path);
	return FS.rename(from_file_path, to_file_path);
}

/*virtua*/ bool FileSystem::directory_exists(const char* directory_path) {
	TRACEF("directory_exists: checking for existence of directory %s", directory_path);
#if FS_TYPE == FS_TYPE_INTERNALFS || FS_TYPE == FS_TYPE_FLASHFS
	File file(FS);
	if (file.open(directory_path, FILE_O_READ)) {
#else
	File file = FS.open(directory_path, FILE_READ);
	if (file) {
#endif
		bool is_directory = file.isDirectory();
		file.close();
		return is_directory;
	}
	return false;
}

/*virtua*/ bool FileSystem::create_directory(const char* directory_path) {
	TRACEF("create_directory: creating directory %s", directory_path);
	if (!FS.mkdir(directory_path)) {
		ERROR("create_directory: failed to create directory " + std::string(directory_path));
		return false;
	}
	return true;
}

/*virtua*/ bool FileSystem::remove_directory(const char* directory_path) {
	TRACEF("remove_directory: removing directory %s", directory_path);
#if FS_TYPE == FS_TYPE_INTERNALFS || FS_TYPE == FS_TYPE_FLASHFS
	if (!FS.rmdir_r(directory_path)) {
#else
	if (!FS.rmdir(directory_path)) {
#endif
		ERROR("remove_directory: failed to remove directory " + std::string(directory_path));
		return false;
	}
	return true;
}

/*virtua*/ std::list<std::string> FileSystem::list_directory(const char* directory_path) {
	TRACEF("list_directory: listing directory %s", directory_path);
	std::list<std::string> files;
	File root = FS.open(directory_path);
	if (!root) {
		ERROR("list_directory: failed to open directory " + std::string(directory_path));
		return files;
	}
	File file = root.openNextFile();
	while (file) {
		if (!file.isDirectory()) {
			char* name = (char*)file.name();
			files.push_back(name);
		}
		// CBA Following close required to avoid leaking memory
		file.close();
		file = root.openNextFile();
	}
	root.close();
	TRACE("list_directory: returning directory listing");
	return files;
}

/*virtual*/ size_t FileSystem::storage_size() {
#if FS_TYPE == FS_TYPE_INTERNALFS
	return totalBytes();
#else
	return FS.totalBytes();
#endif
}

/*virtual*/ size_t FileSystem::storage_available() {
#if FS_TYPE == FS_TYPE_INTERNALFS
	return (totalBytes() - usedBytes());
#else
	return (FS.totalBytes() - FS.usedBytes());
#endif
}

#endif
