#include "DirHandle.h"
#include "Workspace.h"

#include <algorithm>
#include <dirent.h>
#include <fcntl.h>
#include <fuse.h>
#include <iostream>
#include <string>
#include <sys/attr.h>
#include <sys/stat.h>
#include <sys/xattr.h>
#include <tclap/CmdLine.h>
#include <unistd.h>
#include <vector>

using std::cout;
using std::cerr;
using std::endl;
using std::string;
using std::transform;
using std::vector;

Workspace ws;

#define G_PREFIX                       "org"
#define G_KAUTH_FILESEC_XATTR G_PREFIX ".apple.system.Security"
#define A_PREFIX                       "com"
#define A_KAUTH_FILESEC_XATTR A_PREFIX ".apple.system.Security"
#define XATTR_APPLE_PREFIX             "com.apple."

//---------------------------------------------------------------------------
// Filesystem operations
//---------------------------------------------------------------------------

static int
fsclient_getattr(const char *path, struct stat *stbuf)
{
    if (strcmp(path, "/.status") == 0)
    {
        stbuf->st_mode = S_IFREG | 0444;
        stbuf->st_nlink = 1;
        stbuf->st_size = ws.status().size();
        return 0;
    }

    int status = lstat(ws.path(path).c_str(), stbuf);

    if (status == 0)
    {
        return 0;
    }
    else
    {
        return -errno;
    }
}


static int
fsclient_open(const char *path, struct fuse_file_info *fi)
{
    if (strcmp(path, "/.status") == 0)
    {
        return 0;
    }

    int fd = open(ws.path(path).c_str(), fi->flags);

    if (fd >= 0)
    {
        fi->fh = fd;
        return 0;
    }
    else
    {
        return -errno;
    }
}

static int
fsclient_flush(const char *path, struct fuse_file_info *fi)
{
    int res;
    (void)path;

    res = close(dup(fi->fh));
    if (res == -1) {
        return -errno;
    }

    return 0;
}

static int
fsclient_read(const char *path, char *buf, size_t size, off_t offset,
              struct fuse_file_info *fi)
{
    if (strcmp(path, "/.status") == 0)
    {
        string status = ws.status();
        if (offset > status.size())
            return 0;
        if (offset + size > status.size())
            size = status.size() - offset;
        memcpy(buf, status.c_str() + offset, size);
        return size;
    }

    ssize_t num_read = pread(fi->fh, buf, size, offset);

    if (num_read == -1)
    {
        return -errno;
    }

    return num_read;
}

static int
fsclient_release(const char *path, struct fuse_file_info *fi)
{
    if (strcmp(path, "/.status") == 0)
    {
        return 0;
    }

    int status = close(fi->fh);

    if (status == 0)
    {
        return 0;
    }
    else
    {
        return -errno;
    }
}

static int
fsclient_rename(const char *from, const char *to)
{
    int status = rename(ws.path(from).c_str(), ws.path(to).c_str());
    if (status == -1)
        return -errno;

    ws.enqueue(Notification(Notification::RENAME, from, to));

    return 0;
}

static int
fsclient_mknod(const char *path, mode_t mode, dev_t rdev)
{
    int res;

    if (S_ISFIFO(mode)) {
        res = mkfifo(ws.path(path).c_str(), mode);
    } else {
        res = mknod(ws.path(path).c_str(), mode, rdev);
    }

    if (res == -1) {
        return -errno;
    }

    ws.enqueue(Notification(Notification::MKNOD, path));

    return 0;
}

// Right now, we assign the DIR* to the fi->fh field.
static int
fsclient_opendir(const char *path, struct fuse_file_info *fi)
{
    DIR *dir = opendir(ws.path(path).c_str());

    if (dir == NULL)
    {
        return -errno;
    }
    else
    {
        // Create "DirHandle" instance.
        DirHandle *handle = new DirHandle(dir);
        fi->fh = reinterpret_cast<unsigned long>(handle);
        return 0;
    }
}

static int
fsclient_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
                 off_t offset, struct fuse_file_info *fi)
{
    (void) path; // Will not be used.

    // TODO this needs special handling to inject special files into '/'

    DirHandle *dirHandle = reinterpret_cast<DirHandle *>(fi->fh);

    if (offset != dirHandle->offset)
    {
        seekdir(dirHandle->dir, offset);
        dirHandle->entry = NULL;
        dirHandle->offset = offset;
    }

    while (1)
    {
        struct stat st;
        off_t nextoff;

        if (!dirHandle->entry)
        {
            // Note: I'm not sure how to detect if this failed or is just at
            // the end. It returns NULL in both cases.
            dirHandle->entry = readdir(dirHandle->dir);
            if (!dirHandle->entry)
                break;
        }

        memset(&st, 0, sizeof(st));

        st.st_ino = dirHandle->entry->d_ino;
        st.st_mode = DTTOIF(dirHandle->entry->d_type);

        nextoff = telldir(dirHandle->dir);
        if (filler(buf, dirHandle->entry->d_name, &st, nextoff))
            break;

        dirHandle->entry = NULL;
        dirHandle->offset = nextoff;
    }

    if (strcmp(path, "/") == 0)
    {
        filler(buf, ".status", NULL, 0);
    }

    return 0;
}

static int
fsclient_releasedir(const char *path, struct fuse_file_info *fi)
{
    (void) path; // Will not be used.

    DirHandle *dirHandle = reinterpret_cast<DirHandle *>(fi->fh);

    closedir(dirHandle->dir);
    delete dirHandle;

    return 0;
}

static int
fsclient_write(const char *path,
               const char *buf,
               size_t nbyte,
               off_t offset,
               struct fuse_file_info *fi)
{
    ssize_t num_written = pwrite(fi->fh, buf, nbyte, offset);

    if (num_written == -1)
    {
        return -errno;
    }

    ws.enqueue(Notification(Notification::WRITE, path));

    return num_written;
}

static int
fsclient_create(const char *path,
                mode_t mode,
                struct fuse_file_info *fi)
{
    int fd = open(ws.path(path).c_str(), fi->flags, mode);
    if (fd == -1)
    {
        return -errno;
    }

    ws.enqueue(Notification(Notification::CREATE, path));

    fi->fh = fd;
    return 0;
}

static int
fsclient_mkdir(const char *path, mode_t mode)
{
    int err = mkdir(ws.path(path).c_str(), mode);
    if (err == -1)
    {
        return -errno;
    }

    return 0;
}

static int
fsclient_unlink(const char *path)
{
    int status = unlink(ws.path(path).c_str());
    if (status == -1)
        return -errno;

    return 0;
}

// Note: This is pretty much the same as the fsclient example implementation.
// We need to do the same thing, and, there are a lot of macros in use that
// are not really documented anywhere.
static int
fsclient_setattr_x(const char *path, struct setattr_x *attr)
{
    int res;
    uid_t uid = -1;
    gid_t gid = -1;

    if (SETATTR_WANTS_MODE(attr))
    {
        res = lchmod(path, attr->mode);
        if (res == -1)
        {
            return -errno;
        }
    }

    if (SETATTR_WANTS_UID(attr))
    {
        uid = attr->uid;
    }

    if (SETATTR_WANTS_GID(attr))
    {
        gid = attr->gid;
    }

    if ((uid != -1) || (gid != -1))
    {
        res = lchown(path, uid, gid);
        if (res == -1)
        {
            return -errno;
        }
    }

    if (SETATTR_WANTS_SIZE(attr))
    {
        res = truncate(path, attr->size);
        if (res == -1)
        {
            return -errno;
        }
    }

    if (SETATTR_WANTS_MODTIME(attr))
    {
        struct timeval tv[2];
        if (!SETATTR_WANTS_ACCTIME(attr))
        {
            gettimeofday(&tv[0], NULL);
        }
        else
        {
            tv[0].tv_sec = attr->acctime.tv_sec;
            tv[0].tv_usec = attr->acctime.tv_nsec / 1000;
        }
        tv[1].tv_sec = attr->modtime.tv_sec;
        tv[1].tv_usec = attr->modtime.tv_nsec / 1000;
        res = utimes(path, tv);
        if (res == -1)
        {
            return -errno;
        }
    }

    if (SETATTR_WANTS_CRTIME(attr))
    {
        struct attrlist attributes;

        attributes.bitmapcount = ATTR_BIT_MAP_COUNT;
        attributes.reserved = 0;
        attributes.commonattr = ATTR_CMN_CRTIME;
        attributes.dirattr = 0;
        attributes.fileattr = 0;
        attributes.forkattr = 0;
        attributes.volattr = 0;

        res = setattrlist(path, &attributes, &attr->crtime,
                          sizeof(struct timespec), FSOPT_NOFOLLOW);

        if (res == -1)
        {
            return -errno;
        }
    }

    if (SETATTR_WANTS_CHGTIME(attr))
    {
        struct attrlist attributes;

        attributes.bitmapcount = ATTR_BIT_MAP_COUNT;
        attributes.reserved = 0;
        attributes.commonattr = ATTR_CMN_CHGTIME;
        attributes.dirattr = 0;
        attributes.fileattr = 0;
        attributes.forkattr = 0;
        attributes.volattr = 0;

        res = setattrlist(path, &attributes, &attr->chgtime,
                          sizeof(struct timespec), FSOPT_NOFOLLOW);

        if (res == -1)
        {
            return -errno;
        }
    }

    if (SETATTR_WANTS_BKUPTIME(attr))
    {
        struct attrlist attributes;

        attributes.bitmapcount = ATTR_BIT_MAP_COUNT;
        attributes.reserved = 0;
        attributes.commonattr = ATTR_CMN_BKUPTIME;
        attributes.dirattr = 0;
        attributes.fileattr = 0;
        attributes.forkattr = 0;
        attributes.volattr = 0;

        res = setattrlist(path, &attributes, &attr->bkuptime,
                          sizeof(struct timespec), FSOPT_NOFOLLOW);

        if (res == -1)
        {
            return -errno;
        }
    }

    if (SETATTR_WANTS_FLAGS(attr))
    {
        res = lchflags(path, attr->flags);
        if (res == -1)
        {
            return -errno;
        }
    }

    return 0;
}

// Note: This is pretty much the same as the fsclient example implementation.
// We need to do the same thing, and, there are a lot of macros in use that
// are not really documented anywhere.
static int
fsclient_fsetattr_x(const char *path, struct setattr_x *attr,
                    struct fuse_file_info *fi)
{
    int res;
    uid_t uid = -1;
    gid_t gid = -1;

    if (SETATTR_WANTS_MODE(attr))
    {
        res = fchmod(fi->fh, attr->mode);
        if (res == -1)
        {
            return -errno;
        }
    }

    if (SETATTR_WANTS_UID(attr))
    {
        uid = attr->uid;
    }

    if (SETATTR_WANTS_GID(attr))
    {
        gid = attr->gid;
    }

    if ((uid != -1) || (gid != -1))
    {
        res = fchown(fi->fh, uid, gid);
        if (res == -1)
        {
            return -errno;
        }
    }

    if (SETATTR_WANTS_SIZE(attr))
    {
        res = ftruncate(fi->fh, attr->size);
        if (res == -1)
        {
            return -errno;
        }
    }

    if (SETATTR_WANTS_MODTIME(attr))
    {
        struct timeval tv[2];
        if (!SETATTR_WANTS_ACCTIME(attr))
        {
            gettimeofday(&tv[0], NULL);
        }
        else
        {
            tv[0].tv_sec = attr->acctime.tv_sec;
            tv[0].tv_usec = attr->acctime.tv_nsec / 1000;
        }
        tv[1].tv_sec = attr->modtime.tv_sec;
        tv[1].tv_usec = attr->modtime.tv_nsec / 1000;
        res = futimes(fi->fh, tv);
        if (res == -1)
        {
            return -errno;
        }
    }

    if (SETATTR_WANTS_CRTIME(attr))
    {
        struct attrlist attributes;

        attributes.bitmapcount = ATTR_BIT_MAP_COUNT;
        attributes.reserved = 0;
        attributes.commonattr = ATTR_CMN_CRTIME;
        attributes.dirattr = 0;
        attributes.fileattr = 0;
        attributes.forkattr = 0;
        attributes.volattr = 0;

        res = fsetattrlist(fi->fh, &attributes, &attr->crtime,
                           sizeof(struct timespec), FSOPT_NOFOLLOW);

        if (res == -1)
        {
            return -errno;
        }
    }

    if (SETATTR_WANTS_CHGTIME(attr))
    {
        struct attrlist attributes;

        attributes.bitmapcount = ATTR_BIT_MAP_COUNT;
        attributes.reserved = 0;
        attributes.commonattr = ATTR_CMN_CHGTIME;
        attributes.dirattr = 0;
        attributes.fileattr = 0;
        attributes.forkattr = 0;
        attributes.volattr = 0;

        res = fsetattrlist(fi->fh, &attributes, &attr->chgtime,
                           sizeof(struct timespec), FSOPT_NOFOLLOW);

        if (res == -1)
        {
            return -errno;
        }
    }

    if (SETATTR_WANTS_BKUPTIME(attr))
    {
        struct attrlist attributes;

        attributes.bitmapcount = ATTR_BIT_MAP_COUNT;
        attributes.reserved = 0;
        attributes.commonattr = ATTR_CMN_BKUPTIME;
        attributes.dirattr = 0;
        attributes.fileattr = 0;
        attributes.forkattr = 0;
        attributes.volattr = 0;

        res = fsetattrlist(fi->fh, &attributes, &attr->bkuptime,
                           sizeof(struct timespec), FSOPT_NOFOLLOW);

        if (res == -1)
        {
            return -errno;
        }
    }

    if (SETATTR_WANTS_FLAGS(attr))
    {
        res = fchflags(fi->fh, attr->flags);
        if (res == -1)
        {
            return -errno;
        }
    }

    return 0;
}

static int
fsclient_statfs(const char *path, struct statvfs *stbuf)
{
    int res;

    res = statvfs(ws.path(path).c_str(), stbuf);
    if (res == -1)
    {
        return -errno;
    }

    return 0;
}


static int
fsclient_fsync(const char *path, int isdatasync, struct fuse_file_info *fi)
{
    int res;

    (void) path;

    (void) isdatasync;

    res = fsync(fi->fh);
    if (res == -1)
    {
        return -errno;
    }

    return 0;
}

static int
fsclient_setxattr(const char *path, const char *name, const char *value,
                  size_t size, int flags, uint32_t position)
{
    int res;

    if (!strncmp(name, XATTR_APPLE_PREFIX, sizeof(XATTR_APPLE_PREFIX) - 1))
    {
        flags &= ~(XATTR_NOSECURITY);
    }

    if (!strcmp(name, A_KAUTH_FILESEC_XATTR))
    {

        char new_name[MAXPATHLEN];

        memcpy(new_name, A_KAUTH_FILESEC_XATTR, sizeof(A_KAUTH_FILESEC_XATTR));
        memcpy(new_name, G_PREFIX, sizeof(G_PREFIX) - 1);

        res = setxattr(ws.path(path).c_str(),
                       new_name,
                       value,
                       size,
                       position,
                       XATTR_NOFOLLOW);

    }
    else
    {
        res = setxattr(ws.path(path).c_str(),
                       name,
                       value,
                       size,
                       position,
                       XATTR_NOFOLLOW);
    }

    if (res == -1)
    {
        return -errno;
    }

    return 0;
}

static int
fsclient_getxattr(const char *path, const char *name, char *value, size_t size,
                  uint32_t position)
{
    int res;

    if (strcmp(name, A_KAUTH_FILESEC_XATTR) == 0)
    {

        char new_name[MAXPATHLEN];

        memcpy(new_name, A_KAUTH_FILESEC_XATTR, sizeof(A_KAUTH_FILESEC_XATTR));
        memcpy(new_name, G_PREFIX, sizeof(G_PREFIX) - 1);

        res = getxattr(ws.path(path).c_str(),
                       new_name,
                       value,
                       size,
                       position,
                       XATTR_NOFOLLOW);

    }
    else
    {
        res = getxattr(ws.path(path).c_str(),
                       name,
                       value,
                       size,
                       position,
                       XATTR_NOFOLLOW);
    }

    if (res == -1)
    {
        return -errno;
    }

    return res;
}

static int
fsclient_listxattr(const char *path, char *list, size_t size)
{
    ssize_t res = listxattr(ws.path(path).c_str(), list, size, XATTR_NOFOLLOW);
    if (res > 0)
    {
        if (list)
        {
            size_t len = 0;
            char *curr = list;
            do
            {
                size_t thislen = strlen(curr) + 1;
                if (strcmp(curr, G_KAUTH_FILESEC_XATTR) == 0)
                {
                    memmove(curr, curr + thislen, res - len - thislen);
                    res -= thislen;
                    break;
                }
                curr += thislen;
                len += thislen;
            } while (len < res);
        }
        else
        {
            /*
            ssize_t res2 = getxattr(path, G_KAUTH_FILESEC_XATTR, NULL, 0, 0,
                                    XATTR_NOFOLLOW);
            if (res2 >= 0) {
                res -= sizeof(G_KAUTH_FILESEC_XATTR);
            }
            */
        }
    }

    if (res == -1)
    {
        return -errno;
    }

    return res;
}

static int
fsclient_removexattr(const char *path, const char *name)
{
    int res;

    if (strcmp(name, A_KAUTH_FILESEC_XATTR) == 0)
    {

        char new_name[MAXPATHLEN];

        memcpy(new_name, A_KAUTH_FILESEC_XATTR, sizeof(A_KAUTH_FILESEC_XATTR));
        memcpy(new_name, G_PREFIX, sizeof(G_PREFIX) - 1);

        res = removexattr(path, new_name, XATTR_NOFOLLOW);
    }
    else
    {
        res = removexattr(path, name, XATTR_NOFOLLOW);
    }

    if (res == -1)
    {
        return -errno;
    }

    return 0;
}

//---------------------------------------------------------------------------
// Entry point
//---------------------------------------------------------------------------

static struct fuse_operations fsclient_operations = {
        .create         = fsclient_create,
        .flush          = fsclient_flush,
        .fsetattr_x     = fsclient_fsetattr_x,
        .fsync          = fsclient_fsync,
        .getattr        = fsclient_getattr,
        .getxattr       = fsclient_getxattr,
        .listxattr      = fsclient_listxattr,
        .mkdir          = fsclient_mkdir,
        .mknod          = fsclient_mknod,
        .open           = fsclient_open,
        .opendir        = fsclient_opendir,
        .read           = fsclient_read,
        .readdir        = fsclient_readdir,
        .rename         = fsclient_rename,
        .release        = fsclient_release,
        .releasedir     = fsclient_releasedir,
        .removexattr    = fsclient_removexattr,
        .setattr_x      = fsclient_setattr_x,
        .setxattr       = fsclient_setxattr,
        .statfs         = fsclient_statfs,
        .write          = fsclient_write,
        .unlink         = fsclient_unlink,
};

int
main(int argc, char **argv)
{
    string workspace;
    string mount;
    bool debug = false;

    try
    {
        TCLAP::CmdLine cmd("./fsclient -w WS_DIR -m MOUNT_DIR",
                           ' ',
                           "0.1.0",
                           true);

        TCLAP::ValueArg<string> workspaceArg("w",
                                             "workspace",
                                             "Local p4 workspace directory",
                                             true,
                                             "",
                                             "string",
                                             cmd);

        TCLAP::ValueArg<string> mountArg("m",
                                         "mount",
                                         "Local directory to mount as filesystem",
                                         true,
                                         "",
                                         "string",
                                         cmd);

        TCLAP::MultiSwitchArg debugArg("d",
                                       "debug",
                                       "Run in foreground and print debug messages",
                                       cmd);

        cmd.parse(argc, const_cast<const char * const*>(argv));

        workspace = workspaceArg.getValue();
        mount = mountArg.getValue();
        debug = debugArg.getValue();
    }
    catch (TCLAP::ArgException & argException)
    {
        cerr << argException.error() << " for " << argException.argId() << endl;
    }


    cout << "Mirroring workspace: " << workspace << endl;
    cout << "Mounting at: " << mount << endl;

    ws.setWorkspace(workspace);

    int status = setenv("P4CONFIG", (workspace + "/.p4config").c_str(), 1);
    if (status == -1)
    {
        int err = errno;
        cerr << strerror(err) << endl;
        return err;
    }

    const char *debugArgv[] = {argv[0], "-d", mount.c_str()};
    const char *prodArgv[] = {argv[0], mount.c_str()};

    const char **fuseArgv;
    int fuseArgc;

    if (debug)
    {
        fuseArgc = 3;
        fuseArgv = debugArgv;
    }
    else
    {
        fuseArgc = 2;
        fuseArgv = prodArgv;
    }

    return fuse_main(fuseArgc,
                     const_cast<char **>(fuseArgv),
                     &fsclient_operations,
                     NULL);
}