/**
 * $Id$
 * Copyright (C) 2008 - 2014 Nils Asmussen
 *
 * 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include <fs/permissions.h>
#include <mem/cache.h>
#include <mem/dynarray.h>
#include <mem/pagedir.h>
#include <mem/physmem.h>
#include <task/groups.h>
#include <task/proc.h>
#include <vfs/channel.h>
#include <vfs/device.h>
#include <vfs/dir.h>
#include <vfs/file.h>
#include <vfs/info.h>
#include <vfs/node.h>
#include <vfs/openfile.h>
#include <vfs/vfs.h>
#include <assert.h>
#include <boot.h>
#include <common.h>
#include <cppsupport.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <string.h>
#include <util.h>
#include <video.h>

/* we have to use the max-size of all VFSNode sub-classes. that's unfortunate, but I don't see a
 * better way */
static size_t getMaxSize() {
	return esc::Util::max(sizeof(VFSChannel),
			esc::Util::max(sizeof(VFSDevice),
			esc::Util::max(sizeof(VFSDir),
			esc::Util::max(sizeof(VFSFile),sizeof(VFSMS)))));
}

/* all nodes (expand dynamically) */
DynArray VFSNode::nodeArray(getMaxSize(),VFSNODE_AREA,VFSNODE_AREA_SIZE);
/* a pointer to the first free node (which points to the next and so on) */
VFSNode *VFSNode::freeList = NULL;
uint VFSNode::nextUsageId = 0;
SpinLock VFSNode::nodesLock;
SpinLock VFSNode::treeLock;
size_t VFSNode::allocated;

/* we have 2 refs at the beginning because we expect the creator to release the node if he's done
 * working with it */
VFSNode::VFSNode(const fs::User &u,char *n,uint m,bool &success)
		: name(n), nameLen(), refCount(2), uid(u.uid), gid(u.gid), mode(m),
		  parent(), prev(), firstChild(), next() {
	if(this == nullptr || name == NULL || nameLen > NAME_MAX) {
		success = false;
		return;
	}

	nameLen = strlen(name);
	modtime = acctime = crttime = Timer::getTime();
}

bool VFSNode::isDeletable() const {
	return uid != fs::KERNEL_UID;
}

ssize_t VFSNode::open(const fs::User &u,const char *,ssize_t *,ino_t,uint flags,int,mode_t) {
	int err;
	if((err = VFS::hasAccess(u,this,flags)) < 0)
		return err;
	return 0;
}

const VFSNode *VFSNode::openDir(bool locked,bool *valid) const {
	if(locked)
		treeLock.down();
	*valid = name != NULL;
	return firstChild;
}

void VFSNode::getInfo(struct stat *info) {
	info->st_dev = VFS_DEV_NO;
	info->st_atime = acctime;
	info->st_mtime = modtime;
	info->st_ctime = crttime;
	info->st_ino = getNo();
	// TODO set that properly
	info->st_nlink = 1;
	// we don't want to expose the kernel uid to userspace
	info->st_uid = uid == fs::KERNEL_UID ? ROOT_UID : uid;
	info->st_gid = gid;
	info->st_mode = mode;
	info->st_size = getSize();
	info->st_blksize = 512;
	info->st_blocks = info->st_size / 512;
}

void VFSNode::getPathTo(char *dst,size_t size) const {
	size_t nlen,len = 0,total = 0;
	const VFSNode *n = this;
	if(!n->isAlive()) {
		strnzcpy(dst,"<destroyed>",size);
		return;
	}

	while(n->parent != NULL) {
		/* name + slash */
		total += n->nameLen + 1;
		n = n->parent;
	}

	/* not nice, but ensures that we don't overwrite something :) */
	if(total > size)
		total = size;

	n = this;
	len = total;
	while(n->parent != NULL) {
		nlen = n->nameLen + 1;
		/* insert the new element */
		*(dst + total - nlen) = '/';
		memcpy(dst + total + 1 - nlen,n->name,nlen - 1);
		total -= nlen;
		n = n->parent;
	}

	/* terminate */
	*(dst + len) = '\0';
}

int VFSNode::chmod(const fs::User &u,mode_t m) {
	int res;
	if(!fs::Permissions::canChmod(&u,uid))
		res = -EPERM;
	/* chmod is forbidden for symlinks */
	else if(S_ISLNK(mode))
		res = -ENOTSUP;
	else {
		mode = (mode & ~MODE_PERM) | (m & MODE_PERM);
		res = 0;
	}
	return res;
}

int VFSNode::chown(const fs::User &u,uid_t nuid,gid_t ngid) {
	int res;
	if(!fs::Permissions::canChown(&u,uid,gid,nuid,ngid))
		res = -EPERM;
	/* chown is forbidden for symlinks */
	else if(S_ISLNK(mode))
		res = -ENOTSUP;
	else {
		if(nuid != (uid_t)-1)
			uid = nuid;
		if(ngid != (gid_t)-1)
			gid = ngid;
		res = 0;
	}
	return res;
}

int VFSNode::utime(const fs::User &u,const struct utimbuf *utimes) {
	int res;
	if(!fs::Permissions::canUtime(&u,uid))
		res = -EPERM;
	else {
		modtime = utimes->modtime;
		acctime = utimes->actime;
		res = 0;
	}
	return res;
}

bool VFSNode::canRemove(const fs::User &u,const VFSNode *node) const {
	if(u.isKernel())
		return true;
	if(!node->isDeletable())
		return false;

	return fs::Permissions::canRemove(&u,mode,uid,node->uid) == 0;
}

int VFSNode::unlink(const fs::User &u,const char *name) {
	if(!S_ISDIR(mode))
		return -ENOTDIR;

	treeLock.down();
	VFSNode *n = const_cast<VFSNode*>(findInDir(name,strlen(name),false));
	if(!n || !canRemove(u,n)) {
		treeLock.up();
		return n ? -EPERM : -ENOENT;
	}
	n->ref();
	treeLock.up();

	n->destroy();
	release(n);
	return 0;
}

int VFSNode::rename(const fs::User &u,const char *oldName,VFSNode *newDir,const char *newName) {
	/* make copy of name */
	char *namecpy = strdup(newName);
	if(!namecpy)
		return -ENOMEM;

	/* get target */
	treeLock.down();
	VFSNode *target = const_cast<VFSNode*>(findInDir(oldName,strlen(oldName),false));
	if(!target || !canRemove(u,target)) {
		Cache::free(namecpy);
		treeLock.up();
		return target ? -EPERM : -ENOENT;
	}
	target->ref();

	/* remove from old directory */
	target->doRemove(true);
	doUnref(false);

	/* append to new directory */
	target->doAppend(newDir);

	/* set new name; the old one has already been free'd */
	target->name = namecpy;

	target->doUnref(false);
	treeLock.up();
	return 0;
}

int VFSNode::mkdir(const fs::User &u,const char *name,mode_t mode) {
	char *namecpy;
	VFSNode *child;
	int res;

	if(!S_ISDIR(this->mode))
		return -ENOTDIR;

	/* create a copy for the node */
	namecpy = strdup(name);
	if(namecpy == NULL)
		return -ENOMEM;

	/* does it exist? */
	if(findInDir(namecpy,strlen(namecpy)) != NULL) {
		res = -EEXIST;
		goto errorFree;
	}

	child = createObj<VFSDir>(u,this,namecpy,S_IFDIR | (mode & MODE_PERM));
	if(child == NULL) {
		res = -ENOMEM;
		goto errorFree;
	}
	release(child);
	return 0;

errorFree:
	Cache::free(namecpy);
	return res;
}

int VFSNode::rmdir(const fs::User &u,const char *name) {
	if(!S_ISDIR(this->mode))
		return -ENOTDIR;

	/* does it exist? */
	treeLock.down();
	VFSNode *dir = const_cast<VFSNode*>(findInDir(name,strlen(name),false));
	if(dir == NULL) {
		treeLock.up();
		return -ENOENT;
	}

	int res = 0;
	if(!S_ISDIR(dir->mode)) {
		res = -ENOTDIR;
		goto error;
	}

	/* check permissions */
	if(!canRemove(u,dir)) {
		res = -EPERM;
		goto error;
	}
	/* empty? */
	if(dir->firstChild) {
		res = -ENOTEMPTY;
		goto error;
	}

	dir->doUnref(true);
error:
	treeLock.up();
	return res;
}

int VFSNode::symlink(const fs::User &u,const char *name,const char *target) {
	/* ensure its a directory */
	if(!S_ISDIR(mode))
		return -ENOTDIR;

	/* check whether the device does already exist */
	if(findInDir(name,strlen(name)) != NULL)
		return -EEXIST;

	/* create link */
	VFSNode *link;
	int res = createFile(u,name,this,&link,VFS_SYMLINK,LNK_DEF_MODE);
	if(res < 0)
		return res;
	/* write target path */
	link->write(NULL,target,0,strlen(target));
	return res;
}

int VFSNode::createdev(const fs::User &u,const char *name,mode_t mode,uint type,uint ops,VFSNode **node) {
	/* ensure its a directory */
	if(!S_ISDIR(this->mode))
		return -ENOTDIR;

	/* check whether the device does already exist */
	if(findInDir(name,strlen(name)) != NULL)
		return -EEXIST;

	/* create a copy for the node */
	char *namecpy = strdup(name);
	if(namecpy == NULL)
		return -ENOMEM;

	/* create node */
	*node = createObj<VFSDevice>(u,this,namecpy,mode,type,ops);
	if(!*node) {
		Cache::free(namecpy);
		return -ENOMEM;
	}
	return 0;
}

int VFSNode::request(const fs::User &u,const char *path,VFSNode **node,uint flags,mode_t mode) {
	RequestResult res;
	int err = request(u,path,*node,&res,flags | VFS_NOFOLLOW,mode);
	if(err < 0)
		return err;
	*node = res.node;
	return err;
}

int VFSNode::request(const fs::User &u,const char *path,VFSNode *node,RequestResult *res,
		uint flags,mode_t mode) {
	const VFSNode *dir,*n = node;
	const char *opath = path,*lastpath = path;
	int pos = 0,err,depth,lastdepth;
	bool valid;
	if(n == NULL)
		n = get(0);

	res->sympos = -1;
	res->created = false;
	res->node = NULL;

	/* skip slashes */
	while(*path == '/')
		path++;

	/* root/current node requested? */
	if(!*path) {
		res->node = const_cast<VFSNode*>(n);
		res->node->ref();
		return 0;
	}

	bool link = false;
	depth = 0;
	lastdepth = -1;
	dir = n;
	n = dir->openDir(true,&valid);
	if(valid) {
		while(n != NULL) {
			/* go to next '/' and check for invalid chars */
			if(depth != lastdepth) {
				char c;
				/* check if we can access this directory */
				if((err = VFS::hasAccess(u,dir,VFS_EXEC)) < 0)
					goto done;

				pos = 0;
				while((c = path[pos]) && c != '/') {
					if((c != ' ' && isspace(c)) || !isprint(c)) {
						err = -EINVAL;
						goto done;
					}
					pos++;
				}
				lastdepth = depth;

				/* handle "." and ".." */
				link = false;
				if((pos == 1 && path[0] == '.') ||
				   (pos == 2 && path[0] == '.' && path[1] == '.')) {
					if(pos == 1)
						n = dir;
					else
						n = dir->parent == NULL ? dir : dir->parent;
					link = true;
				}
			}

			if(link || ((int)n->nameLen == pos && strncmp(n->name,path,pos) == 0)) {
				lastpath = path;
				path += pos;
				/* finished? */
				if(!*path)
					break;

				/* skip slashes */
				while(*path == '/')
					path++;
				/* "/" at the end is optional */
				if(!*path)
					break;

				if(IS_DEVICE(n->mode) || S_ISLNK(n->mode))
					break;

				/* move to childs of this node */
				dir = n;
				n = dir->openDir(false,&valid);
				if(!valid) {
					err = -EDESTROYED;
					goto done;
				}
				depth++;
				continue;
			}
			n = n->next;
		}
	}

	err = 0;
	if(n == NULL) {
		dir->closeDir(true);
		/* should we create a default-file? */
		if((flags & VFS_CREATE) && S_ISDIR(dir->mode)) {
			/* can we create files in this directory? */
			if((err = VFS::hasAccess(u,dir,VFS_WRITE)) < 0)
				return err;
			err = createFile(u,path,const_cast<VFSNode*>(dir),&res->node,flags,mode);
			if(err == 0)
				res->created = true;
		}
		else
			err = -ENOENT;
	}
	else {
		if(flags & VFS_EXCL) {
			err = -EEXIST;
			goto done;
		}

		/* virtual node */
		res->node = const_cast<VFSNode*>(n);
		res->node->ref();
		if(res->node == NULL)
			err = -ENOENT;
		else if(S_ISLNK(n->mode) && (!(flags & VFS_NOFOLLOW) || *path))
			res->sympos = lastpath - opath;
		dir->closeDir(true);
	}
	res->end = path;
	return err;

done:
	dir->closeDir(true);
	return err;
}

char *VFSNode::basename(char *path,size_t *len) {
	char *p = path + *len - 1;
	while(*p == '/') {
		p--;
		(*len)--;
	}
	*(p + 1) = '\0';
	if((p = strrchr(path,'/')) == NULL)
		return path;
	return p + 1;
}

void VFSNode::dirname(char *path,size_t len) {
	char *p = path + len - 1;
	/* remove last '/' */
	while(*p == '/') {
		p--;
		len--;
	}

	/* nothing to remove? */
	if(len == 0)
		return;

	/* remove last path component */
	while(len > 0 && *p != '/') {
		p--;
		len--;
	}

	/* set new end */
	*(p + 1) = '\0';
}

const VFSNode *VFSNode::findInDir(const char *ename,size_t enameLen,bool locked) const {
	bool valid = false;
	const VFSNode *res = NULL;
	const VFSNode *n = openDir(locked,&valid);
	if(valid) {
		while(n != NULL) {
			if(n->nameLen == enameLen && strncmp(n->name,ename,enameLen) == 0) {
				res = n;
				break;
			}
			n = n->next;
		}
	}
	closeDir(locked);
	return res;
}

void VFSNode::doAppend(VFSNode *p) {
	if(p != NULL) {
		prev = NULL;
		next = p->firstChild;
		if(next)
			next->prev = this;
		p->firstChild = this;

		p->ref();
	}
	parent = p;
}

void VFSNode::doRemove(bool force) {
	const char *nameptr = NULL;

	/* take care that we don't destroy the node twice */
	if(refCount == 0)
		invalidate();
	if((refCount == 0 || force) && name) {
		/* remove from parent and release (attention: maybe its not yet in the tree) */
		if(prev)
			prev->next = next;
		else if(parent)
			parent->firstChild = next;
		if(next)
			next->prev = prev;

		prev = NULL;
		next = NULL;

		/* free name (do that afterwards, unlocked) */
		if(Cache::contains(name))
			nameptr = name;
		name = NULL;
	}

	if(nameptr)
		Cache::free(const_cast<char*>(nameptr));
}

ushort VFSNode::doUnref(bool force) {
	/* first check whether we have the last ref */
	ushort remRefs = refCount;

	/* no remove the child-nodes, if necessary */
	if(remRefs == 1 || force) {
		VFSNode *child = firstChild;
		while(child != NULL) {
			VFSNode *tn = child->next;
			/* if the parent is destroyed, the childs have to be destroyed as well, regardless of
			 * whether they have still references or not (of course, the node isn't free'd if there
			 * are still references) */
			child->doUnref(true);
			child = tn;
		}
	}

	/* don't decrease the refs twice with remove */
	if(!force || name)
		remRefs = Atomic::fetch_and_add(&refCount,-1) - 1;

	doRemove(force);

	/* if there are no references anymore, we can put the node on the freelist */
	if(remRefs == 0) {
		parent->doUnref(false);
		delete this;
	}
	return remRefs;
}

char *VFSNode::generateId() {
	/* we want a id in the form <pid>.<x>, i.e. 2 ints, a '.' and '\0'. thus, allowing up to 31
	 * digits per int is enough, even for 64-bit ints */
	const size_t size = 64;
	char *name = (char*)Cache::alloc(size);
	if(name == NULL)
		return NULL;

	/* create usage-node */
	itoa(name,size,Proc::getRunning());
	size_t len = strlen(name);
	*(name + len) = '.';
	uint id;
	{
		LockGuard<SpinLock> g(&nodesLock);
		id = nextUsageId++;
	}
	itoa(name + len + 1,size - (len + 1),id);
	return name;
}

int VFSNode::createFile(const fs::User &u,const char *path,VFSNode *dir,VFSNode **child,
		uint flags,mode_t mode) {
	size_t nameLen;

	char *nextSlash = strchr(path,'/');
	if(nextSlash) {
		/* if there is still a slash in the path, we can't create the file */
		if(*(nextSlash + 1) != '\0')
			return -EINVAL;
		*nextSlash = '\0';
		nameLen = nextSlash - path;
	}
	else
		nameLen = strlen(path);
	/* copy the name because vfs_file_create() will store the pointer */
	char *nameCpy = (char*)Cache::alloc(nameLen + 1);
	if(nameCpy == NULL)
		return -ENOMEM;
	memcpy(nameCpy,path,nameLen + 1);

	/* now create the node and pass the node-number back */
	if(flags & VFS_SYMLINK)
		*child = createObj<VFSFile>(u,dir,nameCpy,S_IFLNK | LNK_DEF_MODE);
	else
		*child = createObj<VFSFile>(u,dir,nameCpy,S_IFREG | (mode & MODE_PERM));
	if(*child == NULL) {
		Cache::free(nameCpy);
		return -ENOMEM;
	}
	return 0;
}

void *VFSNode::operator new(size_t) throw() {
	LockGuard<SpinLock> g(&nodesLock);
	VFSNode *node = NULL;
	if(freeList == NULL) {
		size_t oldCount = nodeArray.getObjCount();
		if(!nodeArray.extend())
			return NULL;
		freeList = get(oldCount);
		for(size_t i = oldCount; i < nodeArray.getObjCount() - 1; i++) {
			node = get(i);
			node->next = get(i + 1);
		}
		node->next = NULL;
	}

	node = freeList;
	freeList = freeList->next;
	allocated++;
	return node;
}

void VFSNode::operator delete(void *ptr) throw() {
	VFSNode *node = static_cast<VFSNode*>(ptr);
	vassert(node != NULL,"node == NULL");
	/* mark unused */
	node->name = NULL;
	node->nameLen = 0;
	LockGuard<SpinLock> g(&nodesLock);
	node->next = freeList;
	freeList = node;
	allocated--;
}

void VFSNode::printTree(OStream &os) {
	os.writef("VFS:\n");
	os.writef("/\n");
	LockGuard<SpinLock> g(&treeLock);
	doPrintTree(os,1,get(0));
}

void VFSNode::print(OStream &os) const {
	os.writef("VFSNode @ %p:\n",this);
	if(this) {
		os.writef("\tname: %s\n",name ? name : "NULL");
		os.writef("\tfirstChild: %p\n",firstChild);
		os.writef("\tnext: %p\n",next);
		os.writef("\tprev: %p\n",prev);
	}
}

void VFSNode::doPrintTree(OStream &os,size_t level,const VFSNode *parent) {
	bool valid;
	const VFSNode *n = parent->openDir(false,&valid);
	if(valid) {
		while(n != NULL) {
			for(size_t i = 0;i < level;i++)
				os.writef(" |");
			os.writef("- %s\n",n->name);
			doPrintTree(os,level + 1,n);
			n = n->next;
		}
	}
	parent->closeDir(false);
}
