/*
 * \brief  FatFS VFS plugin
 * \author Christian Prochaska
 * \author Emery Hemingway
 * \date   2016-05-22
 *
 * See http://www.elm-chan.org/fsw/ff/00index_e.html
 * or documents/00index_e.html in the FatFS source.
 */

/*
 * Copyright (C) 2016-2017 Genode Labs GmbH
 *
 * This file is part of the Genode OS framework, which is distributed
 * under the terms of the GNU Affero General Public License version 3.
 */

/* Genode includes */
#include <vfs/file_system_factory.h>
#include <vfs/file_system.h>
#include <vfs/vfs_handle.h>
#include <os/path.h>

/* Genode block backend */
#include <fatfs/block.h>

namespace Fatfs {

/* FatFS includes */
#include <fatfs/ff.h>

	using namespace Vfs;
	using namespace Fatfs;
	struct File_system;

};


class Fatfs::File_system : public Vfs::File_system
{
	private:

		typedef Genode::Path<FF_MAX_LFN> Path;

		struct Fatfs_file_handle;
		struct Fatfs_dir_handle;
		struct Fatfs_file_watch_handle;
		struct Fatfs_dir_watch_handle;
		typedef Genode::List<Fatfs_file_handle>       Fatfs_file_handles;
		typedef Genode::List<Fatfs_dir_watch_handle>  Fatfs_dir_watch_handles;
		typedef Genode::List<Fatfs_file_watch_handle> Fatfs_watch_handles;

		/**
		 * The FatFS library does not support opening a file
		 * for writing twice, so this plugin manages a tree of
		 * open files shared across open VFS handles.
		 */

		struct File : Genode::Avl_node<File>
		{
			Path                path;
			Fatfs::FIL          fil;
			Fatfs_file_handles  handles;
			Fatfs_watch_handles watchers;

			bool opened() const {
				return (handles.first() || watchers.first()); }

			/************************
			 ** Avl node interface **
			 ************************/

			bool higher(File *other) {
				return (Genode::strcmp(other->path.base(), path.base()) > 0); }

			File *lookup(char const *path_str)
			{
				int const cmp = Genode::strcmp(path_str, path.base());
				if (cmp == 0)
					return this;

				File *f = Genode::Avl_node<File>::child(cmp);
				return f ? f->lookup(path_str) : nullptr;
			}
		};

		struct Fatfs_handle : Vfs_handle
		{
			using Vfs_handle::Vfs_handle;

			virtual Read_result complete_read(char *buf,
			                                  file_size buf_size,
			                                  file_size &out_count) = 0;
		};

		struct Fatfs_file_watch_handle : Vfs_watch_handle,  Fatfs_watch_handles::Element
		{
			File *file;

			Fatfs_file_watch_handle(Vfs::File_system &fs,
			                        Allocator        &alloc,
			                        File             &file)
			: Vfs_watch_handle(fs, alloc), file(&file) { }
		};

		struct Fatfs_dir_watch_handle : Vfs_watch_handle, Fatfs_dir_watch_handles::Element
		{
			Path const path;

			Fatfs_dir_watch_handle(Vfs::File_system &fs,
			                       Allocator        &alloc,
			                       Path       const &path)
			: Vfs_watch_handle(fs, alloc), path(path) { }
		};

		struct Fatfs_file_handle : Fatfs_handle, Fatfs_file_handles::Element
		{
			File *file = nullptr;
			bool modifying = false;

			Fatfs_file_handle(File_system &fs, Allocator &alloc, int status_flags)
			: Fatfs_handle(fs, fs, alloc, status_flags) { }

			Read_result complete_read(char *buf,
			                          file_size buf_size,
			                          file_size &out_count) override
			{
				if (!file) {
					Genode::error("READ_ERR_INVALID");
					return READ_ERR_INVALID;
				}
				if ((status_flags()&OPEN_MODE_ACCMODE) == OPEN_MODE_WRONLY)
					return READ_ERR_INVALID;

				FRESULT fres;
				FIL *fil = &file->fil;

				fres = f_lseek(fil, seek());
				if (fres == FR_OK) {
					UINT bw = 0;
					fres = f_read(fil, buf, buf_size, &bw);
					out_count = bw;
				}

				switch (fres) {
				case FR_OK:             return READ_OK;
				case FR_INVALID_OBJECT: return READ_ERR_INVALID;
				case FR_TIMEOUT:        return READ_ERR_WOULD_BLOCK;
				case FR_DISK_ERR:       return READ_ERR_IO;
				case FR_INT_ERR:        return READ_ERR_IO;
				case FR_DENIED:         return READ_ERR_IO;
				default:                return READ_ERR_IO;
				}
			}
		};

		struct Fatfs_dir_handle : Fatfs_handle
		{
			file_size cur_index = 0;
			Path const path;
			DIR dir;

			Fatfs_dir_handle(File_system &fs, Allocator &alloc, char const *path)
			: Fatfs_handle(fs, fs, alloc, 0), path(path) { }

			Read_result complete_read(char *buf,
			                          file_size buf_size,
			                          file_size &out_count) override
			{
				/* not very efficient, just N calls to f_readdir */

				out_count = 0;

				if (buf_size < sizeof(Dirent))
					return READ_ERR_INVALID;

				file_size dir_index = seek() / sizeof(Dirent);
				if (dir_index < cur_index) {
					/* reset the seek position */
					f_readdir(&dir, nullptr);
					cur_index = 0;
				}

				Dirent &vfs_dirent = *(Dirent*)buf;

				FILINFO info;
				FRESULT res;

				unsigned const fileno = 1; /* inode 0 is a pending unlink */

				while (cur_index <= dir_index) {
					res = f_readdir (&dir, &info);
					if ((res != FR_OK) || (!info.fname[0])) {
						f_readdir(&dir, nullptr);
						cur_index = 0;

						vfs_dirent = {
							.fileno = fileno,
							.type   = Dirent_type::END,
							.rwx    = Node_rwx::rwx(),
							.name   = { }
						};
						out_count = sizeof(Dirent);
						return READ_OK;
					}
					cur_index++;
				}

				vfs_dirent = {
					.fileno = fileno,
					.type   = (info.fattrib & AM_DIR)
					        ? Dirent_type::DIRECTORY
					        : Dirent_type::CONTINUOUS_FILE,
					.rwx    = Node_rwx::rwx(),
					.name   = { (char const *)info.fname }
				};
				out_count = sizeof(Dirent);
				return READ_OK;
			}
		};

		Vfs::Env &_vfs_env;

		FATFS _fatfs;

		/* List of all open directory handles */
		Fatfs_dir_watch_handles _dir_watchers;

		/* Tree of open FatFS file objects */
		Genode::Avl_tree<File> _open_files;

		/* Pre-allocated FIL */
		File *_next_file = nullptr;

		/**
		 * Return an open FatFS file matching path or null.
		 */
		File *_opened_file(char const *path)
		{
			return _open_files.first() ?
				_open_files.first()->lookup(path) : nullptr;
		}

		/**
		 * Notify the application for each handle on a given file.
		 */
		void _notify(File &file)
		{
			for (Fatfs_file_watch_handle *h = file.watchers.first(); h; h = h->next())
				h->watch_response();
		}

		/**
		 * Notify the application for each handle on the parent
		 * directory of a given path.
		 */
		void _notify_parent_of(char const *path)
		{
			Path parent(path);
			parent.strip_last_element();

			for (Fatfs_dir_watch_handle *h = _dir_watchers.first(); h; h = h->next())
				if (h->path == parent)
					h->watch_response();
		}

		/**
		 * Close an open FatFS file
		 */
		void _close(File &file)
		{
			/* close file */
			_open_files.remove(&file);
			f_close(&file.fil);

			if (_next_file == nullptr) {
				/* reclaim heap space */
				file.path.import("");
				_next_file = &file;
			} else {
				destroy(_vfs_env.alloc(), &file);
			}
		}

		/**
		 * Invalidate all handles on a FatFS file
		 * and close the file
		 */
		void _close_all(File &file)
		{
			/* invalidate handles */
			for (auto *handle = file.handles.first();
			     handle; handle = file.handles.first())
			{
				handle->file = nullptr;
				file.handles.remove(handle);
			}

			for (auto *handle = file.watchers.first();
			     handle; handle = file.watchers.first())
			{
				handle->file = nullptr;
				file.watchers.remove(handle);
				handle->watch_response();
			}
			_close(file);
		}

	public:

		File_system(Vfs::Env &env, Genode::Xml_node config)
		: _vfs_env(env)
		{
			{
				static unsigned codepage = 0;
				unsigned const cp = config.attribute_value<unsigned>(
					"codepage", 0);

				if (codepage != 0 && codepage != cp) {
					Genode::error(
						"cannot reinitialize codepage for FAT library, please "
						"use additional VFS instances for additional codepages");
					throw ~0;
				}

				if (f_setcp(cp) != FR_OK) {
					Genode::error("invalid OEM code page '", cp, "'");
					throw FR_INVALID_PARAMETER;
				}
				codepage = cp;
			}

			auto const drive_num = config.attribute_value(
				"drive", Genode::String<4>("0"));

#if _USE_MKFS == 1
			if (config.attribute_value("format", false)) {
				Genode::log("formatting drive ", drive_num, "...");
				if (f_mkfs((const TCHAR*)drive_num.string(), 1, 0) != FR_OK) {
					Genode::error("format of drive ", drive_num, " failed");
					throw ~0;
				}
			}
#endif

			/* mount the file system */
			switch (f_mount(&_fatfs, (const TCHAR*)drive_num.string(), 1)) {
			case FR_OK: {
				TCHAR label[24] = { '\0' };
				f_getlabel((const TCHAR*)drive_num.string(), label, nullptr);
				Genode::log("FAT file system \"", (char const *)label, "\" mounted");
				return;
			}
			case FR_INVALID_DRIVE:
				Genode::error("invalid drive ", drive_num);           throw ~0;
			case FR_DISK_ERR:
				Genode::error("drive ", drive_num, " disk error");    throw ~0;
			case FR_NOT_READY:
				Genode::error("drive ", drive_num, " not ready");     throw ~0;
			case FR_NO_FILESYSTEM:
				Genode::error("no file system on drive ", drive_num); throw ~0;
			default:
				Genode::error("failed to mount drive ", drive_num);   throw ~0;
			}
		}

		/***************************
		 ** File_system interface **
		 ***************************/

		char const *type() override { return "fatfs"; }


		/*********************************
		 ** Directory service interface **
		 *********************************/

		Open_result open(char const *path, unsigned vfs_mode,
		                 Vfs_handle **vfs_handle,
		                 Allocator  &alloc) override
		{
			Fatfs_file_handle *handle;

			File *file = _opened_file(path);

			bool create = vfs_mode & OPEN_MODE_CREATE;

			if (file && create) {
				return OPEN_ERR_EXISTS;
			}

			if (file && f_error(&file->fil)) {
				Genode::error("FatFS: hard error on file '", path, "'");
				return OPEN_ERR_NO_PERM;
			};

			/* attempt allocation before modifying blocks */
			if (!_next_file)
				_next_file = new (_vfs_env.alloc()) File();
			handle = new (alloc) Fatfs_file_handle(*this, alloc, vfs_mode);

			if (!file) {
				file = _next_file;
				FRESULT fres = f_open(
					&_next_file->fil, (TCHAR const *)path,
					FA_READ | FA_WRITE | (create ? FA_CREATE_NEW : FA_OPEN_EXISTING));
				if (fres != FR_OK) {
					destroy(alloc, handle);
					switch(fres) {
					case FR_NO_FILE:
					case FR_NO_PATH:      return OPEN_ERR_UNACCESSIBLE;
					case FR_EXIST:        return OPEN_ERR_EXISTS;
					case FR_INVALID_NAME: return OPEN_ERR_NAME_TOO_LONG;
					default:              return OPEN_ERR_NO_PERM;
					}
				}

				file->path.import(path);
				_open_files.insert(file);
				_next_file = nullptr;
			}

			if (create)
				_notify_parent_of(path);

			file->handles.insert(handle);
			handle->file = file;
			*vfs_handle = handle;
			return OPEN_OK;
		}

		Opendir_result opendir(char const *path, bool create,
		                       Vfs_handle **vfs_handle,
		                       Allocator &alloc) override
		{
			Fatfs_dir_handle *handle;

			/* attempt allocation before modifying blocks */
			handle = new (alloc) Fatfs_dir_handle(*this, alloc, path);

			if (create) {
				FRESULT res = f_mkdir((const TCHAR*)path);
				if (res != FR_OK) {
					destroy(alloc, handle);
					switch (res) {
					case FR_EXIST:        return OPENDIR_ERR_NODE_ALREADY_EXISTS;
					case FR_NO_PATH:      return OPENDIR_ERR_LOOKUP_FAILED;
					case FR_INVALID_NAME: return OPENDIR_ERR_NAME_TOO_LONG;
					default:              return OPENDIR_ERR_PERMISSION_DENIED;
					}
				}
			}

			FRESULT res = f_opendir(&handle->dir, (const TCHAR*)path);
			if (res != FR_OK) {
				destroy(alloc, handle);
				switch (res) {
				case FR_NO_PATH: return OPENDIR_ERR_LOOKUP_FAILED;
				default:         return OPENDIR_ERR_PERMISSION_DENIED;
				}
			}

			*vfs_handle = handle;

			return OPENDIR_OK;
		}

		void close(Vfs_handle *vfs_handle) override
		{
			{
				auto *handle = dynamic_cast<Fatfs_file_handle *>(vfs_handle);
				bool notify = false;

				if (handle) {
					File *file = handle->file;
					if (file) {
						file->handles.remove(handle);
						if (file->opened()) {
							notify = handle->modifying;
						} else {
							_close(*file);
						}
					}
					destroy(handle->alloc(), handle);

					if (notify)
						_notify(*file);
					return;
				}
			}

			{
				auto *handle = dynamic_cast<Fatfs_dir_handle *>(vfs_handle);

				if (handle) {
					f_closedir(&handle->dir);
					destroy(handle->alloc(), handle);
				}
			}
		}

		Watch_result watch(char const      *path,
		                   Vfs_watch_handle **handle,
		                   Allocator        &alloc) override
		{
			/*
			 * checking for the presence of an open file is
			 * cheaper than calling dircetory and reading blocks
			 */
			File *file = _opened_file(path);

			if (!file && directory(path)) {
				auto *watch_handle = new (alloc)
					Fatfs_dir_watch_handle(*this, alloc, path);
				_dir_watchers.insert(watch_handle);
				*handle = watch_handle;
				return WATCH_OK;
			} else {
				if (!file) {
					if (!_next_file)
						_next_file = new (_vfs_env.alloc()) File();

					file = _next_file;
					FRESULT fres = f_open(
						&file->fil, (TCHAR const *)path,
						FA_READ | FA_WRITE | FA_OPEN_EXISTING);
					if (fres != FR_OK) {
						return WATCH_ERR_UNACCESSIBLE;
					}

					file->path.import(path);
					_open_files.insert(file);
					_next_file = nullptr;
				}

				auto *watch_handle = new (alloc)
					Fatfs_file_watch_handle(*this, alloc, *file);
				file->watchers.insert(watch_handle);
				*handle = watch_handle;
				return WATCH_OK;
			}
			return WATCH_ERR_UNACCESSIBLE;
		}

		void close(Vfs_watch_handle *vfs_handle) override
		{
			{
				auto *handle =
					dynamic_cast<Fatfs_file_watch_handle *>(vfs_handle);

				if (handle) {
					File *file = handle->file;
					if (file)
						file->watchers.remove(handle);
					destroy(handle->alloc(), handle);
					return;
				}
			}

			{
				auto *handle =
					dynamic_cast<Fatfs_dir_watch_handle *>(vfs_handle);

				if (handle) {
					_dir_watchers.remove(handle);
					destroy(handle->alloc(), handle);
				}
			}
		}


		Genode::Dataspace_capability dataspace(char const *path) override
		{
			Genode::warning(__func__, " not implemented in FAT plugin");
			return Genode::Dataspace_capability();
		}

		void release(char const *path,
		             Genode::Dataspace_capability ds_cap) override { }

		file_size num_dirent(char const *path) override
		{
			DIR       dir;
			FILINFO   fno;
			file_size count = 0;

			if (f_opendir(&dir, (const TCHAR*)path) != FR_OK) return 0;

			fno.fname[0] = 0xFF;
			while ((f_readdir (&dir, &fno) == FR_OK) && fno.fname[0])
				++count;
			f_closedir(&dir);
			return count;
		}

		bool directory(char const *path) override
		{
			if (path[0] == '/' && path[1] == '\0') return true;

			FILINFO fno;

			return f_stat((const TCHAR*)path, &fno) == FR_OK ?
				(fno.fattrib & AM_DIR) : false;
		}

		char const *leaf_path(char const *path) override
		{
			if (_opened_file(path)) {
				return path;
			} else {
				FILINFO fno;
				return (f_stat((const TCHAR*)path, &fno) == FR_OK) ?
					path : 0;
			}
		}

		Stat_result stat(char const *path, Stat &stat) override
		{
			stat = Stat { };

			FILINFO info;

			FRESULT const err = f_stat((const TCHAR*)path, &info);
			switch (err) {
			case FR_OK:
				stat.inode  = 1;
				stat.device = (Genode::addr_t)this;
				stat.type   = (info.fattrib & AM_DIR)
				            ? Vfs::Node_type::DIRECTORY
				            : Vfs::Node_type::CONTINUOUS_FILE;
				stat.rwx    = Vfs::Node_rwx::rwx();

				/* XXX: size in f_stat is always zero */
				if ((stat.type == Vfs::Node_type::CONTINUOUS_FILE) && (info.fsize == 0)) {
					File *file = _opened_file(path);
					if (file) {
						stat.size = f_size(&file->fil);
					} else {
						FIL fil;
						if (f_open(&fil, (TCHAR const *)path, FA_READ) == FR_OK) {
							stat.size = f_size(&fil);
							f_close(&fil);
						}
					}
				} else {
					stat.size = info.fsize;
				}
				return STAT_OK;

			case FR_NO_FILE:
			case FR_NO_PATH:
				return STAT_ERR_NO_ENTRY;

			default:
				Genode::error("unhandled FatFS::f_stat error ", (int)err);
				return STAT_ERR_NO_PERM;
			}
			return STAT_ERR_NO_PERM;
		}

		Unlink_result unlink(char const *path) override
		{
			/* close the file if it is open */
			if (File *file = _opened_file(path)) {
				_notify(*file);
				_close_all(*file);
			}

			switch (f_unlink((const TCHAR*)path)) {
			case FR_OK: break;
			case FR_NO_FILE:
			case FR_NO_PATH: return UNLINK_ERR_NO_ENTRY;
			default:         return UNLINK_ERR_NO_PERM;
			}

			_notify_parent_of(path);
			return UNLINK_OK;
		}

		Rename_result rename(char const *from, char const *to) override
		{
			if (File *to_file = _opened_file(to)) {
				_notify(*to_file);
				_close_all(*to_file);
				f_unlink((TCHAR const *)to);
			} else {
				FILINFO info;
				if (FR_OK == f_stat((TCHAR const *)to, &info)) {
					if (info.fattrib & AM_DIR) {
						return RENAME_ERR_NO_PERM;
					} else {
						f_unlink((TCHAR const *)to);
					}
				}
			}

			if (File *from_file = _opened_file(from)) {
				_notify(*from_file);
				_close_all(*from_file);
			}

			switch (f_rename((const TCHAR*)from, (const TCHAR*)to)) {
			case FR_OK: break;
			case FR_NO_FILE:
			case FR_NO_PATH: return RENAME_ERR_NO_ENTRY;
			default:         return RENAME_ERR_NO_PERM;
			}

			_notify_parent_of(from);
			if (Genode::strcmp(from, to) != 0)
				_notify_parent_of(to);
			return RENAME_OK;
		}


		/*******************************
		 ** File io service interface **
		 *******************************/

		Write_result write(Vfs_handle *vfs_handle,
		                   char const *buf, file_size buf_size,
		                   file_size &out_count) override
		{
			Fatfs_file_handle *handle = static_cast<Fatfs_file_handle *>(vfs_handle);
			if (!handle->file)
				return WRITE_ERR_INVALID;
			if ((handle->status_flags()&OPEN_MODE_ACCMODE) == OPEN_MODE_RDONLY)
				return WRITE_ERR_INVALID;

			FRESULT fres = FR_OK;
			FIL *fil = &handle->file->fil;
			FSIZE_t const wpos = handle->seek();

			/* seek file pointer */
			if (f_tell(fil) != wpos) {
				/*
				 * seeking beyond the EOF will expand the file size
				 * and is not the expected behavior
				 */
				if (f_size(fil) < wpos)
					return WRITE_ERR_INVALID;

				fres = f_lseek(fil, wpos);
				/* check the seek again */
				if (f_tell(fil) != handle->seek())
					return WRITE_ERR_IO;
			}

			if (fres == FR_OK) {
				UINT bw = 0;
				fres = f_write(fil, buf, buf_size, &bw);
				f_sync(fil);
				handle->modifying = true;
				out_count = bw;
			}

			switch (fres) {
			case FR_OK:
				return WRITE_OK;
			case FR_INVALID_OBJECT: return WRITE_ERR_INVALID;
			case FR_TIMEOUT:        return WRITE_ERR_WOULD_BLOCK;
			default:                return WRITE_ERR_IO;
			}
		}

		Read_result complete_read(Vfs_handle *vfs_handle, char *buf,
		                          file_size buf_size,
		                          file_size &out_count) override
		{
			Fatfs_file_handle *handle = static_cast<Fatfs_file_handle *>(vfs_handle);
			return handle->complete_read(buf, buf_size, out_count);
		}

		Ftruncate_result ftruncate(Vfs_handle *vfs_handle, file_size len) override
		{
			Fatfs_file_handle *handle = static_cast<Fatfs_file_handle *>(vfs_handle);

			if (!handle->file)
				return FTRUNCATE_ERR_NO_PERM;
			if ((handle->status_flags()&OPEN_MODE_ACCMODE) == OPEN_MODE_RDONLY)
				return FTRUNCATE_ERR_NO_PERM;

			FIL *fil = &handle->file->fil;
			FRESULT res = FR_OK;

			/* f_lseek will expand a file... */
			res = f_lseek(fil, len);
			if (f_tell(fil) != len)
				return f_size(fil) < len ?
					FTRUNCATE_ERR_NO_SPACE : FTRUNCATE_ERR_NO_PERM;

			/* ... otherwise truncate will shorten to the seek position */
			if ((res == FR_OK) && (len < f_size(fil))) {
				res = f_truncate(fil);
				if (res == FR_OK && len < handle->seek())
					handle->seek(len);
			}

			handle->modifying = true;

			return res == FR_OK ?
				FTRUNCATE_OK : FTRUNCATE_ERR_NO_PERM;
		}

		bool read_ready(Vfs_handle *) override { return true; }

		/**
		 * Notify other handles if this handle has modified its file.
		 *
		 * Files are flushed to blocks after every write.
		 */
		Sync_result complete_sync(Vfs_handle *vfs_handle) override
		{
			Fatfs_file_handle *handle =
				static_cast<Fatfs_file_handle*>(vfs_handle);
			if (handle && handle->file && handle->modifying) {
				File &file = *handle->file;
				handle->modifying = false;
				file.handles.remove(handle);
				_notify(file);
				file.handles.insert(handle);
			}
			return SYNC_OK;
		}
};


struct Fatfs_factory : Vfs::File_system_factory
{
	struct Inner : Vfs::File_system_factory
	{
		Inner(Genode::Env &env, Genode::Allocator &alloc) {
			Fatfs::block_init(env, alloc); }

		Vfs::File_system *create(Vfs::Env &env, Genode::Xml_node node) override
		{ 
			return new (env.alloc())
				Fatfs::File_system(env, node);
		}
	};

	Vfs::File_system *create(Vfs::Env &vfs_env, Genode::Xml_node node) override
	{
		static Inner factory(vfs_env.env(), vfs_env.alloc());
		return factory.create(vfs_env, node);
	}
};


extern "C" Vfs::File_system_factory *vfs_file_system_factory(void)
{
	static Fatfs_factory factory;
	return &factory;
}
