/*
    Authors:
        Simo Sorce <ssorce@redhat.com>
        Stephen Gallagher <sgallagh@redhat.com>

    Copyright (C) 2009 Red Hat

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <sys/time.h>
#include <tevent.h>
#include <dbus/dbus.h>

#include "util/util.h"
#include "sbus/sssd_dbus.h"
#include "sbus/sssd_dbus_private.h"

/* =Watches=============================================================== */

/* DBUS may ask us to add a watch to a file descriptor that already had a watch
 * associated. Need to check if that's the case */
static struct sbus_watch_ctx *fd_to_watch(struct sbus_watch_ctx *list, int fd)
{
    struct sbus_watch_ctx *watch_iter;

    watch_iter = list;
    while (watch_iter != NULL) {
        if (watch_iter->fd == fd) {
            return watch_iter;
        }

        watch_iter = watch_iter->next;
    }

    return NULL;
}

static int watch_destructor(void *mem)
{
    struct sbus_watch_ctx *watch;

    watch = talloc_get_type(mem, struct sbus_watch_ctx);
    DLIST_REMOVE(watch->conn->watch_list, watch);

    return 0;
}

/*
 * watch_handler
 * Callback for D-BUS to handle messages on a file-descriptor
 */
static void sbus_watch_handler(struct tevent_context *ev,
                               struct tevent_fd *fde,
                               uint16_t flags, void *data)
{
    struct sbus_watch_ctx *watch = talloc_get_type(data,
                                                   struct sbus_watch_ctx);
    enum dbus_conn_type type;
    union dbus_conn_pointer dbus_p;

    /* conn may get freed inside a handle, save the data we need for later */
    type = watch->conn->type;
    dbus_p = watch->conn->dbus;

    /* Take a reference while handling watch */
    if (type == SBUS_SERVER) {
        dbus_server_ref(dbus_p.server);
    } else {
        dbus_connection_ref(dbus_p.conn);
    }

    /* Fire if readable */
    if (flags & TEVENT_FD_READ) {
        if (watch->dbus_read_watch) {
            dbus_watch_handle(watch->dbus_read_watch, DBUS_WATCH_READABLE);
        }
    }

    /* Fire if writeable */
    if (flags & TEVENT_FD_WRITE) {
        if (watch->dbus_write_watch) {
            dbus_watch_handle(watch->dbus_write_watch, DBUS_WATCH_WRITABLE);
        }
    }

    /* Release reference once done */
    if (type == SBUS_SERVER) {
        dbus_server_unref(dbus_p.server);
    } else {
        dbus_connection_unref(dbus_p.conn);
    }
}

/*
 * add_watch
 * Set up hooks into the libevents mainloop for
 * D-BUS to add file descriptor-based events
 */
dbus_bool_t sbus_add_watch(DBusWatch *dbus_watch, void *data)
{
    unsigned int flags;
    uint16_t event_flags;
    struct sbus_connection *conn;
    struct sbus_watch_ctx *watch;
    dbus_bool_t enabled;
    int fd;

    conn = talloc_get_type(data, struct sbus_connection);

#ifdef HAVE_DBUS_WATCH_GET_UNIX_FD
    fd = dbus_watch_get_unix_fd(dbus_watch);
#else
    fd = dbus_watch_get_fd(dbus_watch);
#endif

    watch = fd_to_watch(conn->watch_list, fd);
    if (!watch) {
        /* does not exist, allocate new one */
        watch = talloc_zero(conn, struct sbus_watch_ctx);
        if (!watch) {
            DEBUG(SSSDBG_FATAL_FAILURE, "Out of Memory!\n");
            return FALSE;
        }
        watch->im_event = tevent_create_immediate(watch);
        if (watch->im_event == NULL) {
            DEBUG(SSSDBG_CRIT_FAILURE, "Out of Memory!\n");
            talloc_free(watch);
            return FALSE;
        }
        watch->conn = conn;
        watch->fd = fd;
    }

    enabled = dbus_watch_get_enabled(dbus_watch);
    flags = dbus_watch_get_flags(dbus_watch);

    /* Save the event to the watch object so it can be found later */
    if (flags & DBUS_WATCH_READABLE) {
        watch->dbus_read_watch = dbus_watch;
    }
    if (flags & DBUS_WATCH_WRITABLE) {
        watch->dbus_write_watch = dbus_watch;
    }
    dbus_watch_set_data(dbus_watch, watch, NULL);

    if (watch->fde) {
        /* pre-existing event, just toggle flags */
        sbus_toggle_watch(dbus_watch, data);
        return TRUE;
    }

    event_flags = 0;
    if (enabled) {
        if (flags & DBUS_WATCH_READABLE) {
            event_flags |= TEVENT_FD_READ;
        }
        if (flags & DBUS_WATCH_WRITABLE) {
            event_flags |= TEVENT_FD_WRITE;
        }
    }

    /* Add the file descriptor to the event loop */
    watch->fde = tevent_add_fd(conn->ev,
                               watch, fd, event_flags,
                               sbus_watch_handler, watch);
    if (!watch->fde) {
        DEBUG(SSSDBG_FATAL_FAILURE, "Failed to set up fd event!\n");
        talloc_zfree(watch);
        return FALSE;
    }

    DLIST_ADD(conn->watch_list, watch);
    talloc_set_destructor((TALLOC_CTX *)watch, watch_destructor);

    DEBUG(SSSDBG_TRACE_INTERNAL, "%p/%p (%d), %s/%s (%s)\n",
              watch, dbus_watch, fd,
              ((flags & DBUS_WATCH_READABLE)?"R":"-"),
              ((flags & DBUS_WATCH_WRITABLE)?"W":"-"),
              enabled?"enabled":"disabled");

    return TRUE;
}

/*
 * toggle_watch
 * Hook for D-BUS to toggle the enabled/disabled state of
 * an event in the mainloop
 */
void sbus_toggle_watch(DBusWatch *dbus_watch, void *data)
{
    struct sbus_watch_ctx *watch;
    unsigned int flags;
    dbus_bool_t enabled;
    void *watch_data;
    int fd = -1;

    enabled = dbus_watch_get_enabled(dbus_watch);
    flags = dbus_watch_get_flags(dbus_watch);

    watch_data = dbus_watch_get_data(dbus_watch);
    watch = talloc_get_type(watch_data, struct sbus_watch_ctx);
    if (!watch) {
        DEBUG(SSSDBG_OP_FAILURE,
              "[%p] does not carry watch context?!\n", dbus_watch);
        /* abort? */
        return;
    }

    if (enabled) {
        if (flags & DBUS_WATCH_READABLE) {
            TEVENT_FD_READABLE(watch->fde);
        }
        if (flags & DBUS_WATCH_WRITABLE) {
            TEVENT_FD_WRITEABLE(watch->fde);
        }
    } else {
        if (flags & DBUS_WATCH_READABLE) {
            TEVENT_FD_NOT_READABLE(watch->fde);
        }
        if (flags & DBUS_WATCH_WRITABLE) {
            TEVENT_FD_NOT_WRITEABLE(watch->fde);
        }
    }

    if (DEBUG_IS_SET(SSSDBG_TRACE_ALL)) {
#ifdef HAVE_DBUS_WATCH_GET_UNIX_FD
        fd = dbus_watch_get_unix_fd(dbus_watch);
#else
        fd = dbus_watch_get_fd(dbus_watch);
#endif
    }
    DEBUG(SSSDBG_TRACE_ALL,
          "%p/%p (%d), %s/%s (%s)\n",
           watch, dbus_watch, fd,
           ((flags & DBUS_WATCH_READABLE)?"R":"-"),
           ((flags & DBUS_WATCH_WRITABLE)?"W":"-"),
           enabled?"enabled":"disabled");
}

static void free_sbus_watch(struct tevent_context *ev,
                            struct tevent_immediate *im,
                            void *data)
{
    struct sbus_watch_ctx *w = talloc_get_type(data, struct sbus_watch_ctx);
    talloc_free(w); /* this will free attached 'im' as well */
}
/*
 * sbus_remove_watch
 * Hook for D-BUS to remove file descriptor-based events
 * from the libevents mainloop
 */
void sbus_remove_watch(DBusWatch *dbus_watch, void *data)
{
    struct sbus_watch_ctx *watch;
    void *watch_data;

    watch_data = dbus_watch_get_data(dbus_watch);
    watch = talloc_get_type(watch_data, struct sbus_watch_ctx);

    DEBUG(SSSDBG_TRACE_INTERNAL, "%p/%p\n", watch, dbus_watch);

    if (!watch) {
        DEBUG(SSSDBG_OP_FAILURE, "DBUS trying to remove unknown watch!\n");
        return;
    }

    /* remove dbus watch data */
    dbus_watch_set_data(dbus_watch, NULL, NULL);

    /* check which watch to remove, or free if none left */
    if (watch->dbus_read_watch == dbus_watch) {
        watch->dbus_read_watch = NULL;
    }
    if (watch->dbus_write_watch == dbus_watch) {
        watch->dbus_write_watch = NULL;
    }
    if (!watch->dbus_read_watch && !watch->dbus_write_watch) {
        /* libdus doesn't need this watch{fd} anymore, so associated
         * tevent_fd should be removed from monitoring at the spot.
         */
        talloc_zfree(watch->fde);
        /* watch itself can't be freed yet as it still may be referenced
         * in the current context (for example in sbus_watch_handler())
         * so instead schedule immediate event to delete it.
         */
        tevent_schedule_immediate(watch->im_event, watch->conn->ev,
                                  free_sbus_watch, watch);
    }
}

/* =Timeouts============================================================== */

static struct timeval _get_interval_tv(int interval) {
    struct timeval tv;
    struct timeval rightnow;

    gettimeofday(&rightnow,NULL);

    tv.tv_sec = interval / 1000 + rightnow.tv_sec;
    tv.tv_usec = (interval % 1000) * 1000 + rightnow.tv_usec;
    return tv;
}

/*
 * timeout_handler
 * Callback for D-BUS to handle timed events
 */
static void sbus_timeout_handler(struct tevent_context *ev,
                                 struct tevent_timer *te,
                                 struct timeval t, void *data)
{
    struct sbus_timeout_ctx *timeout;
    timeout = talloc_get_type(data, struct sbus_timeout_ctx);

    dbus_timeout_handle(timeout->dbus_timeout);
}

/*
 * add_timeout
 * Hook for D-BUS to add time-based events to the mainloop
 */
dbus_bool_t sbus_add_timeout(DBusTimeout *dbus_timeout, void *data)
{
    struct sbus_connection *conn;
    struct sbus_timeout_ctx *timeout;
    struct timeval tv;

    DEBUG(SSSDBG_TRACE_INTERNAL, "%p\n", dbus_timeout);

    if (!dbus_timeout_get_enabled(dbus_timeout)) {
        return TRUE;
    }

    conn = talloc_get_type(data, struct sbus_connection);

    timeout = talloc_zero(conn, struct sbus_timeout_ctx);
    if (!timeout) {
        DEBUG(SSSDBG_FATAL_FAILURE, "Out of Memory!\n");
        return FALSE;
    }
    timeout->dbus_timeout = dbus_timeout;

    tv = _get_interval_tv(dbus_timeout_get_interval(dbus_timeout));
    timeout->te = tevent_add_timer(conn->ev, timeout, tv,
                                   sbus_timeout_handler, timeout);
    if (!timeout->te) {
        DEBUG(SSSDBG_FATAL_FAILURE, "Failed to set up timeout event!\n");
        return FALSE;
    }

    /* Save the event to the watch object so it can be removed later */
    dbus_timeout_set_data(timeout->dbus_timeout, timeout, NULL);

    return TRUE;
}

/*
 * sbus_toggle_timeout
 * Hook for D-BUS to toggle the enabled/disabled state of a mainloop
 * event
 */
void sbus_toggle_timeout(DBusTimeout *dbus_timeout, void *data)
{
    DEBUG(SSSDBG_TRACE_INTERNAL, "%p\n", dbus_timeout);

    if (dbus_timeout_get_enabled(dbus_timeout)) {
        sbus_add_timeout(dbus_timeout, data);
    } else {
        sbus_remove_timeout(dbus_timeout, data);
    }
}

/*
 * sbus_remove_timeout
 * Hook for D-BUS to remove time-based events from the mainloop
 */
void sbus_remove_timeout(DBusTimeout *dbus_timeout, void *data)
{
    void *timeout;

    DEBUG(SSSDBG_TRACE_INTERNAL, "%p\n", dbus_timeout);

    timeout = dbus_timeout_get_data(dbus_timeout);

    /* remove dbus timeout data */
    dbus_timeout_set_data(dbus_timeout, NULL, NULL);

    /* Freeing the event object will remove it from the event loop */
    talloc_free(timeout);

}
