#include <getopt.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

#include <linux/inotify.h>

struct op_table {
        char            *str;
        int             event;
};

struct ievent_table {
        __u32           event;
        char            *str;
};

struct counters {
        long long       s;      /* success */
        long long       f;      /* failure */
};

#define TMP_TEMPLATE "/tmp/inotify.XXXXXXXX"
#define SUB_TEMPLATE "/foo"

#define EVENTS_COMMON   IN_OPEN|IN_MODIFY|IN_ATTRIB|IN_DELETE_SELF|IN_MOVE_SELF
#define EVENTS_FILES    EVENTS_COMMON
#define EVENTS_DIRS     EVENTS_COMMON|IN_CREATE|IN_DELETE|IN_MOVE

#define OPS_FILES       8
#define OPS_DIRS        11

#define OPS_START       2
#define OPS_END         OPS_DIRS

#define WATCH_ADD       0
#define WATCH_REMOVE    1
#define OP_OPEN         2
#define OP_MODIFY       3
#define OP_ATTRIB       4
#define OP_CREATE_SELF  5
#define OP_DELETE_SELF  6
#define OP_MOVE_SELF    7
#define OP_CREATE       8
#define OP_DELETE       9
#define OP_MOVE         10

/* global lookup tables */
static struct op_table optab[] = {
        { "watch_add",      -1             },
        { "watch_remove",   -1             },
        { "op_open",        IN_OPEN        },
        { "op_modify",      IN_MODIFY      },
        { "op_attrib",      IN_ATTRIB      },
        { "op_create_self", -1             },
        { "op_delete_self", IN_DELETE_SELF },
        { "op_move_self",   IN_MOVE_SELF   },
        { "op_create",      IN_CREATE      },
        { "op_delete",      IN_DELETE      },
        { "op_move",        IN_MOVED_FROM  },
#if OPS_END != 11
#error fix optab initialization
#endif
};

static struct ievent_table itab[] = {
        { IN_OPEN,        "IN_OPEN"        },
        { IN_MODIFY,      "IN_MODIFY"      },
        { IN_ATTRIB,      "IN_ATTRIB"      },
        { IN_DELETE_SELF, "IN_DELETE_SELF" },
        { IN_MOVE_SELF,   "IN_MOVE_SELF"   },
        { IN_CREATE,      "IN_CREATE"      },
        { IN_DELETE,      "IN_DELETE"      },
        { IN_MOVED_FROM,  "IN_MOVED_FROM"  },
        { IN_IGNORED,     "IN_IGNORED"     },
};

char **fsobjs;
int should_quit = 0;

/* defaults */
int fsn = 8;    /* number of filesystem objects */
int mode = 0;   /* 0 = files, 1 = dirs */
int verbose = 0;

static char *ievent_to_string(__u32 mask)
{
        int i;

        for (i = 0; i < sizeof(itab)/sizeof(itab[0]); i++) {
                if (itab[i].event == mask)
                        return itab[i].str;
        }
        return NULL;
}

static char *init_subname(char *dirname)
{
        char *tmpname;

        tmpname = malloc(strlen(dirname) + strlen(SUB_TEMPLATE) +1);
        if (!tmpname) {
                perror("malloc");
                return NULL;
        }
        if (!strcpy(tmpname, dirname)) {
                perror("strcpy");
                free(tmpname);
                return NULL;
        }

        return strcat(tmpname, SUB_TEMPLATE);
}

static int init_fsobjs()
{
        int i;

        fsobjs = malloc(sizeof(char *) * fsn);
        if (!fsobjs) {
                perror("malloc");
                return -1;
        }

        for (i = 0; i < fsn; i++) {
                char *fname, *f;
                int fd;

                f = fname = strdup(TMP_TEMPLATE);
                if (!fname) {
                        perror("strdup");
                        return -1;
                }

                switch(mode) {
                case 0:
                        fd = mkstemp(fname);
                        if (fd < 0) {
                                perror("mkstemp");
                                free(f);
                                return -1;
                        }
                        close(fd);
                        break;
                case 1:
                        fname = mkdtemp(fname);
                        if (fname == NULL) {
                                perror("mkdtemp");
                                free(f);
                                return -1;
                        }
                        break;
                }

                fsobjs[i] = fname;
        }

        return 0;
}

static void cleanup_fsobjs()
{
        int i;

        for (i = 0; i < fsn; i++) {
                char *tmp;

                switch(mode) {
                case 0:
                        unlink(fsobjs[i]);
                        break;
                case 1:
                        tmp = init_subname(fsobjs[i]);
                        if (tmp) {
                                unlink(tmp);
                                free(tmp);
                        }
                        rmdir(fsobjs[i]);
                        break;
                }
                free(fsobjs[i]);
        }
        free(fsobjs);
}

static void print_result(int i, char *name, int wd, int op, int res)
{
        printf("[%4d] %4d %s: %s: %s\n", i, wd, name, optab[op].str,
               res == 0 ? "success" : strerror(-res));
}

static void print_fsobjs(int *wds)
{
        int i;
        char buf[(fsn * 40) + 7];
        int count = 0;

        count = snprintf(buf, sizeof(buf), "\nObjs:\n");
        for (i = 0; i < fsn; i++) {

                if (count == sizeof(buf))
                        break;

                count += snprintf(&buf[count], sizeof(buf),
                                  "[%4d] %4d %s\n", i, wds[i], fsobjs[i]);
        }
        printf("%s\n", buf);
}

static void print_totals(struct counters *ctrs)
{
        int i;
        char buf[1500];
        int count = 0;

        count = snprintf(buf, sizeof(buf), "\nVerified:\n");
        for (i = 0; i < OPS_END; i++) {

                if (i == OPS_FILES && mode == 0)
                        break;

                if (count == sizeof(buf))
                        break;

                count += snprintf(&buf[count], sizeof(buf),
                                  "%15s %5lli (%5lli success %5lli failure)\n",
                                  ievent_to_string(optab[i].event) ?: optab[i].str,
                                  ctrs[i].s + ctrs[i].f, ctrs[i].s, ctrs[i].f);
        }
        printf("%s\n", buf);
}

static int watch_add(int idev, char *name, int *wd)
{
        int res;

        switch(mode) {
        case 0:
                res = syscall(__NR_inotify_add_watch, idev, name, EVENTS_FILES);
                break;
        case 1:
                res = syscall(__NR_inotify_add_watch, idev, name, EVENTS_DIRS);
                break;
        }
        if (res < 0) {
                *wd = -1;
                return -errno;
        }

        *wd = res;
        return 0;
}

static int watch_remove(int idev, char *name, int *wd)
{
        if (syscall(__NR_inotify_rm_watch, idev, *wd) < 0)
                return -errno;

        *wd = -1;
        return 0;
}

static void check_event_queue(int idev, int *wds)
{
        char *buf;
        int queue_len, read_len, b;
        fd_set rset;
        struct timeval tv;

        FD_ZERO(&rset);
        FD_SET(idev, &rset);

        tv.tv_sec = 0;
        tv.tv_usec = 0;

        if (select(idev+1, &rset, NULL, NULL, &tv) > 0) {

                if (ioctl(idev, FIONREAD, &queue_len) < 0) {
                        perror("ioctl");
                        return;
                }

                buf = malloc(queue_len);
                if (!buf) {
                        perror("malloc");
                        return;
                }

                read_len = read(idev, buf, queue_len);
                for (b = 0; b < read_len;) {
                        struct inotify_event *event =
                                (struct inotify_event *)&buf[b];

                        if (event->mask & IN_IGNORED) {
                                int i;

                                for (i = 0; i < fsn; i++)
                                        if (wds[i] == event->wd) {
                                                wds[i] = -1;
                                                break;
                                        }
                        }

                        /* advance to next event */
                        b += sizeof(struct inotify_event) + event->len;
                } 
                free(buf);
        }
}

static int do_op_open(char *name)
{
        int fd = open(name, 0);

        if (fd == -1)
                return -errno;

        close(fd);
        return 0;
}

static int do_op_modify(char *name)
{
        int fd = open(name, O_WRONLY|O_APPEND);

        if (fd == -1)
                return -errno;

        if (write(fd, "x", 1) < 0) {
                close(fd);
                return -errno;
        }

        close(fd);
        return 0;
}

static int do_op_attrib(char *name)
{
        int ret = chmod(name, S_IRUSR|S_IWUSR|S_IXUSR);

        return (ret == -1 ? -errno : 0);
}

static int do_op_create(char *name, int create_self)
{
        int fd, ret;

        if (mode == 1 && create_self) {
                ret = mkdir(name, 0);
                return (ret == -1 ? -errno : 0);
        }

        fd = open(name, O_CREAT|O_EXCL);
        if (fd == -1)
                return -errno;

        close(fd);
        return 0;
}

static int do_op_delete(char *name, int delete_self)
{
        int ret;

        if (mode == 1 && delete_self)
                ret = rmdir(name);
        else
                ret = unlink(name);

        return (ret == -1 ? -errno : 0);
}

static int do_op_move(char *name, char *newname)
{
        int ret = rename(name, newname);

        return (ret == -1 ? -errno : 0);
}

static int do_op(int idev, int op, int i, int *wds)
{
        int res, n;
        char *tmp, *tmp2;

        switch(op) {
        case WATCH_ADD:
                res = watch_add(idev, fsobjs[i], &wds[i]);
                break;
        case WATCH_REMOVE:
                res = watch_remove(idev, fsobjs[i], &wds[i]);
                break;
        case OP_OPEN:
                if (mode == 0) {
                        res = do_op_open(fsobjs[i]);
                        break;
                }
                tmp = init_subname(fsobjs[i]);
                if (!tmp)
                        return -errno;
                res = do_op_open(tmp);
                free(tmp);
                break;
        case OP_MODIFY:
                if (mode == 0) {
                        res = do_op_modify(fsobjs[i]);
                        break;
                }
                tmp = init_subname(fsobjs[i]);
                if (!tmp)
                        return -errno;
                res = do_op_modify(tmp);
                free(tmp);
                break;
        case OP_ATTRIB:
                res = do_op_attrib(fsobjs[i]);
                break;
        case OP_CREATE_SELF:
                res = do_op_create(fsobjs[i], 1);
                break;
        case OP_DELETE_SELF:
                res = do_op_delete(fsobjs[i], 1);
                if (res == 0)
                        wds[i] = -1;
                break;
        case OP_MOVE_SELF:
                if (fsn < 2)
                        return -EOPNOTSUPP;

                n = (i == (fsn - 1)) ? 0 : i + 1;
                res = do_op_move(fsobjs[i], fsobjs[n]);
                if (res == 0) {
                        wds[n] = wds[i];
                        wds[i] = -1;
                }
                break;
        case OP_CREATE:
                tmp = init_subname(fsobjs[i]);
                if (!tmp)
                        return -errno;
                res = do_op_create(tmp, 0);
                free(tmp);
                break;
        case OP_DELETE:
                tmp = init_subname(fsobjs[i]);
                if (!tmp)
                        return -errno;
                res = do_op_delete(tmp, 0);
                free(tmp);
                break;
        case OP_MOVE:
                if (fsn < 2)
                        return -EOPNOTSUPP;

                n = (i == (fsn - 1)) ? 0 : i + 1;
                tmp = init_subname(fsobjs[i]);
                if (!tmp)
                        return -errno;
                tmp2 = init_subname(fsobjs[n]);
                if (!tmp2) {
                        free(tmp);
                        return -errno;
                }
                res = do_op_move(tmp, tmp2);
                break;
        }

        return res;
}

static void *do_test(void *arg)
{
        int i, idev, res, ret = 0;
        int ops_max;
        int wds[fsn];
        struct counters ctrs[OPS_END] = { { 0, 0 } };

        if ((idev = syscall(__NR_inotify_init)) < 0) {
                perror("inotify_init");
                ret = -1;
                goto out;
        }

        for (i = 0; i < fsn; i++) {
                res = watch_add(idev, fsobjs[i], &wds[i]);
                if (verbose)
                        print_result(i, fsobjs[i], wds[i], WATCH_ADD, res);
        }
        if (verbose)
                printf("\n");

        ops_max = (mode ? OPS_DIRS : OPS_FILES);
        while (1) {
                int op = random() % ops_max;
                int f = random() % fsn;
                __s32 o_wd = wds[f];

                res = do_op(idev, op, f, wds);

                if (res)
                        ctrs[op].f++;
                else
                        ctrs[op].s++;

                if (verbose) {
                        if (op == WATCH_ADD)
                                o_wd = wds[f];
                        print_result(f, fsobjs[f], o_wd, op, res);
                }

                check_event_queue(idev, wds);

                if (should_quit)
                        break;
        }

        if (verbose)
                print_fsobjs(wds);
        print_totals(ctrs);

        close(idev);
out:
        pthread_exit(&ret);
}

static void usage(void)
{
        printf(
        "usage: stress_inotify [options]\n"
        "       -f <objects>    Number of filesystem objects (default 8).\n"
        "       -r <seed>       Random number seed.\n"
        "       -s <seconds>    Number of seconds to run (default 60).\n"
        "       -t <threads>    Number of threads (default 4).\n"
        "       -d              Test directory watches (default file watches).\n"
        "       -v              Verbose mode.\n"
        "       -h              This help.\n"
        );
}

int main(int argc, char **argv)
{
        int t;
        pthread_t *tids;

        /* defaults */
        int seed = 77;
        int t_max = 4;
        int secs = 60;

        for (;;) {
                int c = getopt(argc, argv, "f:r:s:t:dvh");
                if (c == -1)
                        break;
                switch(c) {
                        case 'f':
                                fsn = atoi(optarg);
                                break;
                        case 'r':
                                seed = atoi(optarg);
                                break;
                        case 's':
                                secs = atoi(optarg);
                                break;
                        case 't':
                                t_max = atoi(optarg);
                                break;
                        case 'd':
                                mode = 1;
                                break;
                        case 'v':
                                verbose = 1;
                                break;
                        case 'h':
                                usage();
                                exit(1);
                        default:
                                usage();
                                exit(1);
                }
        }
        if (optind < argc) {
                fprintf(stderr, "%s: unknown arg: %s\n", argv[0], argv[optind]);
                usage();
                exit(1);
        }

        srandom(seed);

        tids = malloc(t_max * sizeof(*tids));
        if (!tids) {
                perror("malloc");
                return -1;
        }

        if (init_fsobjs()) {
                cleanup_fsobjs();
                return -1;
        }

        for (t = 0; t < t_max; t++)
                pthread_create(&tids[t], NULL, do_test, NULL);

        sleep(secs);
        should_quit = 1;

        for (t = 0; t < t_max; t++)
                pthread_join(tids[t], NULL);

        cleanup_fsobjs();
        return 0;
}
