/* GPL */

#define FUSE_USE_VERSION 26
#define _GNU_SOURCE

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <utime.h>
#include <dirent.h>
#include <signal.h>

#include <fuse.h>
#include <fuse/fuse_lowlevel.h>


#include "tfs.h"

char *prefix = "/tmp/tfs";


#define REAL_PATH(real_path, prefix, path) \
    char real_path[1 + strlen(prefix) + strlen(path)]; \
    sprintf(real_path, "%s%s", prefix, path);

#define PATH(real_path, path) REAL_PATH(real_path, prefix, path)

int write_data = 1;


// pass errno upstream
#define TRY(rv, fn,...) if (-1 == (rv = fn(__VA_ARGS__))) { return -errno; }

struct original_file {
    int fd;
    int clobber;
};

inline struct original_file **fuse_original_file(struct fuse_file_info *fi) {
    return (struct original_file **)(&fi->fh);
}

void safe_write(int fd, void *buf, int bytes) {
    int sent;
  again:
    sent = write(fd, buf, bytes);
    if (sent < 0) {
        fprintf(stderr, "can't write %d bytes: %s\n", bytes, strerror(errno));
        exit(1);
    }
    if (sent < bytes) {
        bytes -= sent;
        buf += sent;
        goto again;
    }
}



void send_cmd_fds(struct tfs_cmd *cmd, void *payload, int payload_size, fd_set *set, int fd_endx) {

    // Round to command to 16 byte boundary.
    int bufsize = sizeof(*cmd) + (16 + ((payload_size - 1) & (-16)));
    cmd->size = buffer_to_size(bufsize);

    // concatenate command + payload
    char buf[bufsize];
    memset(buf, 0, bufsize);
    memcpy(buf, cmd, sizeof(*cmd));
    memcpy(buf + sizeof(*cmd), payload, payload_size);
    
    // send it to clients
    int fd;
    for (fd = 0; fd < fd_endx; fd++) {
        if (FD_ISSET(fd, set)) {
            safe_write(fd, buf, bufsize);
        }
    }
}

void send_cmd(struct tfs_cmd *cmd, void *payload, int payload_size) {
    fd_set set;
    FD_ZERO(&set);
    FD_SET(1, &set);  // currently only stdout
    send_cmd_fds(cmd, payload, payload_size, &set, 2);
}

#define DELEGATE(fn, path, ...) { \
        int rv; \
        PATH(real_path, path); \
        TRY(rv, fn, real_path, __VA_ARGS__); \
        return 0; }
#define DELEGATE0(fn, path) { \
        int rv; \
        PATH(real_path, path); \
        TRY(rv, fn, real_path); \
        return 0; }



int tfs_getattr(const char *path, struct stat *stbuf)  DELEGATE(stat, path, stbuf);
int tfs_chmod (const char *path, mode_t mode)          DELEGATE(chmod, path, mode);
int tfs_chown (const char *path, uid_t uid, gid_t gid) DELEGATE(chown, path, uid, gid);
int tfs_utime(const char *path, struct utimbuf *times) DELEGATE(utime, path, times);
int tfs_truncate(const char *path, off_t length)       DELEGATE(truncate, path, length);
int tfs_unlink(const char *path)                       DELEGATE0(unlink, path);
int tfs_rmdir(const char *path)                        DELEGATE0(rmdir, path);


int tfs_mkdir(const char *path, mode_t mode) {
    int rv;
    PATH(real_path, path);
    TRY(rv, mkdir, real_path, mode);

    struct tfs_cmd cmd;
    INIT_TFS_CMD(&cmd);
    cmd.cmd  = TFS_MKDIR;
    send_cmd(&cmd, (void *)path, 1 + strlen(path));
    
    return 0;
}


int tfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
                         off_t offset, struct fuse_file_info *fi)
{
    struct dirent *entry;
    PATH(real_path, path);
    DIR *dir = opendir(real_path);
    if (!dir) return -errno;
    while ((entry = readdir(dir))) {
        filler(buf, entry->d_name, NULL, 0);
    }
    return 0;
}


int tfs_release(const char *path, struct fuse_file_info *fi)
{
    struct original_file *orig = *fuse_original_file(fi);
    
    struct tfs_cmd cmd;
    INIT_TFS_CMD(&cmd);
    cmd.cmd  = TFS_CLOSE;
    cmd.fd   = orig->fd;
    send_cmd(&cmd, NULL, 0);

    return close(orig->fd);
}


int make_original(struct fuse_file_info *fi, int fd) {
    struct original_file *orig;
    *fuse_original_file(fi) = orig = malloc(sizeof(struct original_file));
    orig->fd = fd;
    orig->clobber = 0;
    return 0;
}


/* send out an 'open' message when file is freshly clobbered. */
void check_clobber(const char *path, struct fuse_file_info *fi) {
    struct original_file *orig = *fuse_original_file(fi);
    if (!orig->clobber) {
        struct tfs_cmd cmd;
        INIT_TFS_CMD(&cmd);
        cmd.cmd   = TFS_OPEN;
        cmd.fd    = orig->fd;
        send_cmd(&cmd, (void *)path, 1+strlen(path));
    }
    orig->clobber = 1;
}

int tfs_open(const char *path, struct fuse_file_info *fi)
{
    int fd;
    PATH(real_path, path);
    TRY(fd, open, real_path, fi->flags);
    return make_original(fi, fd);
}

int tfs_create(const char *path, mode_t mode, struct fuse_file_info *fi)
{
    int fd;
    PATH(real_path, path);
    TRY(fd, creat, real_path, mode);
    return make_original(fi, fd);
}  

int tfs_read(const char *path, char *buf, size_t size, off_t offset,
                      struct fuse_file_info *fi)
{
    int rv;
    struct original_file *orig = *fuse_original_file(fi);
    lseek(orig->fd, offset, SEEK_SET); // FIXME: check return value    
    TRY(rv, read, orig->fd, buf, size);
    return rv;
}

int tfs_write(const char *path, const char *buf, size_t size, off_t offset,
                       struct fuse_file_info *fi)
{
    int rv;
    struct original_file *orig = *fuse_original_file(fi);
    lseek(orig->fd, offset, SEEK_SET); // FIXME: check return value    
    TRY(rv, write, orig->fd, buf, size);

    check_clobber(path, fi); // lazy open

    struct tfs_cmd cmd;
    INIT_TFS_CMD(&cmd);
    cmd.cmd          = TFS_WRITE;
    cmd.fd           = orig->fd;

    cmd.write_offset = offset;
    cmd.write_size   = size;

    send_cmd(&cmd, (void *)buf, write_data ? size : 0);

    return rv;
}
  
struct fuse_operations tfs_oper = {
    .getattr   = tfs_getattr,
    .readdir   = tfs_readdir,
    .open      = tfs_open,
    .create    = tfs_create,
    .read      = tfs_read,
    .write     = tfs_write,
    .release   = tfs_release,
    .mkdir     = tfs_mkdir,
    .unlink    = tfs_unlink,
    .rmdir     = tfs_rmdir,


    /* SETATTR is actually 4 different methods rolled into one: chmod,
       chown, truncate and utime. You need to implement at least
       truncate() for open(... O_TRUNC) to work. */
    .utime     = tfs_utime,
    .chmod     = tfs_chmod,
    .chown     = tfs_chown,
    .truncate  = tfs_truncate,
};


struct fuse_task {
    struct fuse *f;
    struct fuse_session *se;
    struct fuse_chan *ch;
    size_t bufsize;
    char *buf;
    int fd;
};

struct fuse_task *fuse_task_create(struct fuse *f) {
    struct fuse_task *task = malloc(sizeof(*task));
    task->se = fuse_get_session(f);
    task->ch = fuse_session_next_chan(task->se, NULL);
    task->bufsize = fuse_chan_bufsize(task->ch);
    if (!(task->buf = malloc(task->bufsize))) {
        fprintf(stderr, "fuse: failed to allocate read buffer\n");
        return NULL;
    }
    task->fd = fuse_chan_fd(task->ch);
    return task;
}
void fuse_task_free(struct fuse_task *task) {
    free(task->buf);
    fuse_session_reset(task->se);
}

int fuse_task_handle(struct fuse_task *task) {
    int res = -1;
  again:
    if (!fuse_session_exited(task->se)) {
        struct fuse_chan *tmpch = task->ch;
        res = fuse_chan_recv(&tmpch, task->buf, task->bufsize);
        if (res == -EINTR) goto again;
        if (res > 0) {
            fuse_session_process(task->se, task->buf, res, tmpch);
        }
    }
    return res <= 0 ? -1 : 0;
}



int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "usage: %s <store-dir> [<fuse-option> ...] <mountpoint>\n",
                argv[0]);
        exit(1);
    }
    prefix = argv[1];
    DIR *dir = opendir(prefix);
    if (!dir) {
        fprintf(stderr, "Can't open store dir %s: %s\n",
                prefix, strerror(errno));
        exit(1);
    }
    closedir(dir);

    argv[1] = argv[0];
    argv++;
    argc--;

    int mt = 0;
    char *mountpoint = NULL;
    struct fuse* fuse = fuse_setup(argc, argv, &tfs_oper, sizeof(tfs_oper), &mountpoint, &mt, NULL);

    // request handler state machine
    struct fuse_task *task = fuse_task_create(fuse);
    for (;;) {
        fd_set set;
        FD_ZERO(&set);
        FD_SET(task->fd, &set);
        struct timeval tv = {1,0};
        if (-1 == select(1 + task->fd, &set, NULL, NULL, &tv)) { perror("select"); exit(1); }
        if (FD_ISSET(task->fd, &set)) {
            // fprintf(stderr, "x");
            if (fuse_task_handle(task)) {
                fuse_task_free(task);
                exit(0);
            }
        }
        else {
            // fprintf(stderr, ".");
        }
    }
}

