/*
 * This file Copyright (C) 2009-2014 Mnemosyne LLC
 *
 * It may be used under the GNU GPL versions 2 or 3
 * or any future license endorsed by Mnemosyne LLC.
 *
 * $Id: watch.c 14241 2014-01-21 03:10:30Z jordan $
 */

#ifdef WITH_INOTIFY
  #include <sys/inotify.h>
  #include <sys/select.h>
  #include <unistd.h> /* close */
#else
  #include <sys/types.h> /* stat */
  #include <sys/stat.h> /* stat */
  #include <event2/buffer.h> /* evbuffer */
#endif

#include <errno.h>
#include <string.h> /* strlen () */
#include <stdio.h> /* perror () */

#include <dirent.h> /* readdir */

#include <libtransmission/transmission.h>
#include <libtransmission/log.h>
#include <libtransmission/utils.h> /* tr_buildPath (), tr_logAddInfo () */
#include "watch.h"

struct dtr_watchdir
{
    tr_session * session;
    char * dir;
    dtr_watchdir_callback * callback;
#ifdef WITH_INOTIFY
    int inotify_fd;
#else /* readdir implementation */
    time_t lastTimeChecked;
    struct evbuffer * lastFiles;
#endif
};

/***
****  INOTIFY IMPLEMENTATION
***/

#if defined (WITH_INOTIFY)

/* how many inotify events to try to batch into a single read */
#define EVENT_BATCH_COUNT 50
/* size of the event structure, not counting name */
#define EVENT_SIZE (sizeof (struct inotify_event))
/* reasonable guess as to size of 50 events */
#define BUF_LEN (EVENT_BATCH_COUNT * (EVENT_SIZE + 16) + 2048)

#define DTR_INOTIFY_MASK (IN_CLOSE_WRITE|IN_MOVED_TO|IN_CREATE|IN_ONLYDIR)

static void
watchdir_new_impl (dtr_watchdir * w)
{
    int i;
    DIR * odir;
    w->inotify_fd = inotify_init ();

    if (w->inotify_fd < 0)
    {
        i = -1;
    }
    else
    {
        tr_logAddInfo ("Using inotify to watch directory \"%s\"", w->dir);
        i = inotify_add_watch (w->inotify_fd, w->dir, DTR_INOTIFY_MASK);
    }

    if (i < 0)
    {
        tr_logAddError ("Unable to watch \"%s\": %s", w->dir, tr_strerror (errno));
    }
    else if ((odir = opendir (w->dir)))
    {
        struct dirent * d;

        while ((d = readdir (odir)))
        {
            const char * name = d->d_name;

            if (!tr_str_has_suffix (name, ".torrent")) /* skip non-torrents */
                continue;

            tr_logAddInfo ("Found new .torrent file \"%s\" in watchdir \"%s\"", name, w->dir);
            w->callback (w->session, w->dir, name);
        }

        closedir (odir);
    }

}
static void
watchdir_free_impl (dtr_watchdir * w)
{
    if (w->inotify_fd >= 0)
    {
        inotify_rm_watch (w->inotify_fd, DTR_INOTIFY_MASK);

        close (w->inotify_fd);
    }
}
static void
watchdir_update_impl (dtr_watchdir * w)
{
    int ret;
    fd_set rfds;
    struct timeval time;
    const int fd = w->inotify_fd;

    /* timeout after one second */
    time.tv_sec = 1;
    time.tv_usec = 0;

    /* make the fd_set hold the inotify fd */
    FD_ZERO (&rfds);
    FD_SET (fd, &rfds);

    /* check for added files */
    ret = select (fd+1, &rfds, NULL, NULL, &time);
    if (ret < 0) {
        perror ("select");
    } else if (!ret) {
        /* timed out! */
    } else if (FD_ISSET (fd, &rfds)) {
        int i = 0;
        char buf[BUF_LEN];
        int len = read (fd, buf, sizeof (buf));
        while (i < len) {
            struct inotify_event * event = (struct inotify_event *) &buf[i];
            const char * name = event->name;
            if (tr_str_has_suffix (name, ".torrent"))
            {
                tr_logAddInfo ("Found new .torrent file \"%s\" in watchdir \"%s\"", name, w->dir);
                w->callback (w->session, w->dir, name);
            }
            i += EVENT_SIZE +  event->len;
        }
    }
}

#else /* WITH_INOTIFY */

/***
****  READDIR IMPLEMENTATION
***/

#define WATCHDIR_POLL_INTERVAL_SECS 10

#define FILE_DELIMITER '\t'

static void
watchdir_new_impl (dtr_watchdir * w UNUSED)
{
    tr_logAddInfo ("Using readdir to watch directory \"%s\"", w->dir);
    w->lastFiles = evbuffer_new ();
}
static void
watchdir_free_impl (dtr_watchdir * w)
{
    evbuffer_free (w->lastFiles);
}

static char*
get_key_from_file (const char * filename, const size_t len)
{
    return tr_strdup_printf ("%c%*.*s%d", FILE_DELIMITER, (int)len, (int)len, filename, FILE_DELIMITER);
}

static void
add_file_to_list (struct evbuffer * buf, const char * filename, size_t len)
{
    char * key = get_key_from_file (filename, len);
    evbuffer_add (buf, key, strlen (key));
    tr_free (key);
}
static bool
is_file_in_list (struct evbuffer * buf, const char * filename, size_t len)
{
    bool in_list;
    struct evbuffer_ptr ptr;
    char * key = get_key_from_file (filename, len);

    ptr = evbuffer_search (buf, key, strlen (key), NULL);
    in_list = ptr.pos != -1;

    tr_free (key);
    return in_list;
}
static void
watchdir_update_impl (dtr_watchdir * w)
{
    struct stat sb;
    DIR * odir;
    const time_t oldTime = w->lastTimeChecked;
    const char * dirname = w->dir;
    struct evbuffer * curFiles = evbuffer_new ();

    if ((oldTime + WATCHDIR_POLL_INTERVAL_SECS < time (NULL))
         && !stat (dirname, &sb)
         && S_ISDIR (sb.st_mode)
         && ((odir = opendir (dirname))))
    {
        struct dirent * d;

        for (d = readdir (odir); d != NULL; d = readdir (odir))
        {
            size_t len;
            const char * name = d->d_name;

            if (!name || *name=='.') /* skip dotfiles */
                continue;
            if (!tr_str_has_suffix (name, ".torrent")) /* skip non-torrents */
                continue;

            len = strlen (name);
            add_file_to_list (curFiles, name, len);

            /* if this file wasn't here last time, try adding it */
            if (!is_file_in_list (w->lastFiles, name, len)) {
                tr_logAddInfo ("Found new .torrent file \"%s\" in watchdir \"%s\"", name, w->dir);
                w->callback (w->session, w->dir, name);
            }
        }

        closedir (odir);
        w->lastTimeChecked = time (NULL);
        evbuffer_free (w->lastFiles);
        w->lastFiles = curFiles;
    }
}

#endif

/***
****
***/

dtr_watchdir*
dtr_watchdir_new (tr_session * session, const char * dir, dtr_watchdir_callback * callback)
{
    dtr_watchdir * w = tr_new0 (dtr_watchdir, 1);

    w->session = session;
    w->dir = tr_strdup (dir);
    w->callback = callback;

    watchdir_new_impl (w);

    return w;
}

void
dtr_watchdir_update (dtr_watchdir * w)
{
    if (w != NULL)
        watchdir_update_impl (w);
}

void
dtr_watchdir_free (dtr_watchdir * w)
{
    if (w != NULL)
    {
        watchdir_free_impl (w);
        tr_free (w->dir);
        tr_free (w);
    }
}
