#include <sys/file.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <dprintf.h>
#include <syslinux/sysappend.h>
#include "core.h"
#include "dev.h"
#include "fs.h"
#include "cache.h"

/* The currently mounted filesystem */
__export struct fs_info *this_fs = NULL;		/* Root filesystem */

/* Actual file structures (we don't have malloc yet...) */
__export struct file files[MAX_OPEN];

/* Symlink hard limits */
#define MAX_SYMLINK_CNT	20
#define MAX_SYMLINK_BUF 4096

/*
 * Get a new inode structure
 */
struct inode *alloc_inode(struct fs_info *fs, uint32_t ino, size_t data)
{
    struct inode *inode = zalloc(sizeof(struct inode) + data);
    if (inode) {
	inode->fs = fs;
	inode->ino = ino;
	inode->refcnt = 1;
    }
    return inode;
}

/*
 * Free a refcounted inode
 */
void put_inode(struct inode *inode)
{
    while (inode) {
	struct inode *dead = inode;
	int refcnt = --(dead->refcnt);
	dprintf("put_inode %p name %s refcnt %u\n", dead, dead->name, refcnt);
	if (refcnt)
	    break;		/* We still have references */
	inode = dead->parent;
	if (dead->name)
	    free((char *)dead->name);
	free(dead);
    }
}

/*
 * Get an empty file structure
 */
static struct file *alloc_file(void)
{
    int i;
    struct file *file = files;

    for (i = 0; i < MAX_OPEN; i++) {
	if (!file->fs)
	    return file;
	file++;
    }

    return NULL;
}

/*
 * Close and free a file structure
 */
static inline void free_file(struct file *file)
{
    memset(file, 0, sizeof *file);
}

__export void _close_file(struct file *file)
{
    if (file->fs)
	file->fs->fs_ops->close_file(file);
    free_file(file);
}

/*
 * Find and open the configuration file
 */
__export int open_config(void)
{
    int fd, handle;
    struct file_info *fp;

    fd = opendev(&__file_dev, NULL, O_RDONLY);
    if (fd < 0)
	return -1;

    fp = &__file_info[fd];

    handle = this_fs->fs_ops->open_config(&fp->i.fd);
    if (handle < 0) {
	close(fd);
	errno = ENOENT;
	return -1;
    }

    fp->i.offset = 0;
    fp->i.nbytes = 0;

    return fd;
}

__export void mangle_name(char *dst, const char *src)
{
    this_fs->fs_ops->mangle_name(dst, src);
}

size_t pmapi_read_file(uint16_t *handle, void *buf, size_t sectors)
{
    bool have_more;
    size_t bytes_read;
    struct file *file;

    file = handle_to_file(*handle);
    bytes_read = file->fs->fs_ops->getfssec(file, buf, sectors, &have_more);

    /*
     * If we reach EOF, the filesystem driver will have already closed
     * the underlying file... this really should be cleaner.
     */
    if (!have_more) {
	_close_file(file);
	*handle = 0;
    }

    return bytes_read;
}

int searchdir(const char *name, int flags)
{
    static char root_name[] = "/";
    struct file *file;
    char *path, *inode_name, *next_inode_name;
    struct inode *tmp, *inode = NULL;
    int symlink_count = MAX_SYMLINK_CNT;

    dprintf("searchdir: %s  root: %p  cwd: %p\n",
	    name, this_fs->root, this_fs->cwd);

    if (!(file = alloc_file()))
	goto err_no_close;
    file->fs = this_fs;

    /* if we have ->searchdir method, call it */
    if (file->fs->fs_ops->searchdir) {
	file->fs->fs_ops->searchdir(name, flags, file);

	if (file->inode)
	    return file_to_handle(file);
	else
	    goto err;
    }

    /* else, try the generic-path-lookup method */

    /* Copy the path */
    path = strdup(name);
    if (!path) {
	dprintf("searchdir: Couldn't copy path\n");
	goto err_path;
    }

    /* Work with the current directory, by default */
    inode = get_inode(this_fs->cwd);
    if (!inode) {
	dprintf("searchdir: Couldn't use current directory\n");
	goto err_curdir;
    }

    for (inode_name = path; inode_name; inode_name = next_inode_name) {
	/* Root directory? */
	if (inode_name[0] == '/') {
	    next_inode_name = inode_name + 1;
	    inode_name = root_name;
	} else {
	    /* Find the next inode name */
	    next_inode_name = strchr(inode_name + 1, '/');
	    if (next_inode_name) {
		/* Terminate the current inode name and point to next */
		*next_inode_name++ = '\0';
	    }
	}
	if (next_inode_name) {
	    /* Advance beyond redundant slashes */
	    while (*next_inode_name == '/')
		next_inode_name++;

	    /* Check if we're at the end */
	    if (*next_inode_name == '\0')
		next_inode_name = NULL;
	}
	dprintf("searchdir: inode_name: %s\n", inode_name);
	if (next_inode_name)
	    dprintf("searchdir: Remaining: %s\n", next_inode_name);

	/* Root directory? */
	if (inode_name[0] == '/') {
	    /* Release any chain that's already been established */
	    put_inode(inode);
	    inode = get_inode(this_fs->root);
	    continue;
	}

	/* Current directory? */
	if (!strncmp(inode_name, ".", sizeof "."))
	    continue;

	/* Parent directory? */
	if (!strncmp(inode_name, "..", sizeof "..")) {
	    /* If there is no parent, just ignore it */
	    if (!inode->parent)
		continue;

	    /* Add a reference to the parent so we can release the child */
	    tmp = get_inode(inode->parent);

	    /* Releasing the child will drop the parent back down to 1 */
	    put_inode(inode);

	    inode = tmp;
	    continue;
	}

	/* Anything else */
	tmp = inode;
	inode = this_fs->fs_ops->iget(inode_name, inode);
	if (!inode) {
	    /* Failure.  Release the chain */
	    put_inode(tmp);
	    break;
	}

	/* Sanity-check */
	if (inode->parent && inode->parent != tmp) {
	    dprintf("searchdir: iget returned a different parent\n");
	    put_inode(inode);
	    inode = NULL;
	    put_inode(tmp);
	    break;
	}
	inode->parent = tmp;
	inode->name = strdup(inode_name);
	dprintf("searchdir: path component: %s\n", inode->name);

	/* Symlink handling */
	if (inode->mode == DT_LNK) {
	    char *new_path;
	    int new_len, copied;

	    /* target path + NUL */
	    new_len = inode->size + 1;

	    if (next_inode_name) {
		/* target path + slash + remaining + NUL */
		new_len += strlen(next_inode_name) + 1;
	    }

	    if (!this_fs->fs_ops->readlink ||
		/* limit checks */
		--symlink_count == 0 ||
		new_len > MAX_SYMLINK_BUF)
		goto err_new_len;

	    new_path = malloc(new_len);
	    if (!new_path)
		goto err_new_path;

	    copied = this_fs->fs_ops->readlink(inode, new_path);
	    if (copied <= 0)
		goto err_copied;
	    new_path[copied] = '\0';
	    dprintf("searchdir: Symlink: %s\n", new_path);

	    if (next_inode_name) {
		new_path[copied] = '/';
		strcpy(new_path + copied + 1, next_inode_name);
		dprintf("searchdir: New path: %s\n", new_path);
	    }

	    free(path);
	    path = next_inode_name = new_path;

            /* Add a reference to the parent so we can release the child */
            tmp = get_inode(inode->parent);

            /* Releasing the child will drop the parent back down to 1 */
            put_inode(inode);

            inode = tmp;
	    continue;
err_copied:
	    free(new_path);
err_new_path:
err_new_len:
	    put_inode(inode);
	    inode = NULL;
	    break;
	}

	/* If there's more to process, this should be a directory */
	if (next_inode_name && inode->mode != DT_DIR) {
	    dprintf("searchdir: Expected a directory\n");
	    put_inode(inode);
	    inode = NULL;
	    break;
	}
    }
err_curdir:
    free(path);
err_path:
    if (!inode) {
	dprintf("searchdir: Not found\n");
	goto err;
    }

    file->inode  = inode;
    file->offset = 0;

    return file_to_handle(file);

err:
    dprintf("searchdir: error searching file %s\n", name);
    _close_file(file);
err_no_close:
    return -1;
}

__export int open_file(const char *name, int flags, struct com32_filedata *filedata)
{
    int rv;
    struct file *file;
    char mangled_name[FILENAME_MAX];

    dprintf("open_file %s\n", name);

    mangle_name(mangled_name, name);
    rv = searchdir(mangled_name, flags);

    if (rv < 0)
	return rv;

    file = handle_to_file(rv);

    if (file->inode->mode != DT_REG) {
	_close_file(file);
	return -1;
    }

    filedata->size	= file->inode->size;
    filedata->blocklg2	= SECTOR_SHIFT(file->fs);
    filedata->handle	= rv;

    return rv;
}

__export void close_file(uint16_t handle)
{
    struct file *file;

    if (handle) {
	file = handle_to_file(handle);
	_close_file(file);
    }
}

__export char *fs_uuid(void)
{
    if (!this_fs || !this_fs->fs_ops || !this_fs->fs_ops->fs_uuid)
	return NULL;
    return this_fs->fs_ops->fs_uuid(this_fs);
}

/*
 * it will do:
 *    initialize the memory management function;
 *    set up the vfs fs structure;
 *    initialize the device structure;
 *    invoke the fs-specific init function;
 *    initialize the cache if we need one;
 *    finally, get the current inode for relative path looking.
 *
 * ops is a ptr list for several fs_ops
 */
__bss16 uint16_t SectorSize, SectorShift;

void fs_init(const struct fs_ops **ops, void *priv)
{
    static struct fs_info fs;	/* The actual filesystem buffer */
    int blk_shift = -1;
    struct device *dev = NULL;

    /* Default name for the root directory */
    fs.cwd_name[0] = '/';

    while ((blk_shift < 0) && *ops) {
	/* set up the fs stucture */
	fs.fs_ops = *ops;

	/*
	 * This boldly assumes that we don't mix FS_NODEV filesystems
	 * with FS_DEV filesystems...
	 */
	if (fs.fs_ops->fs_flags & FS_NODEV) {
	    fs.fs_dev = NULL;
	} else {
	    if (!dev)
		dev = device_init(priv);
	    fs.fs_dev = dev;
	}
	/* invoke the fs-specific init code */
	blk_shift = fs.fs_ops->fs_init(&fs);
	ops++;
    }
    if (blk_shift < 0) {
	printf("No valid file system found!\n");
	while (1)
		;
    }
    this_fs = &fs;

    /* initialize the cache only if it wasn't already initialized
     * by the fs driver */
    if (fs.fs_dev && fs.fs_dev->cache_data && !fs.fs_dev->cache_init)
        cache_init(fs.fs_dev, blk_shift);

    /* start out in the root directory */
    if (fs.fs_ops->iget_root) {
	fs.root = fs.fs_ops->iget_root(&fs);
	fs.cwd = get_inode(fs.root);
	dprintf("init: root inode %p, cwd inode %p\n", fs.root, fs.cwd);
    }

    if (fs.fs_ops->chdir_start) {
	    if (fs.fs_ops->chdir_start() < 0)
		    printf("Failed to chdir to start directory\n");
    }

    SectorShift = fs.sector_shift;
    SectorSize  = fs.sector_size;

    /* Add FSUUID=... string to cmdline */
    sysappend_set_fs_uuid();

}
