/* handler.c
** strophe XMPP client library -- event handler management
**
** Copyright (C) 2005-2009 Collecta, Inc.
**
**  This software is provided AS-IS with no warranty, either express
**  or implied.
**
**  This program is dual licensed under the MIT and GPLv3 licenses.
*/

/** @file
 *  Event handler management.
 */

/** @defgroup Handlers Stanza and timed event handlers
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "strophe.h"
#include "common.h"
#include "ostypes.h"

typedef int (*xmpp_void_handler)();

/* Remove item from the list pointed by head, but don't free it.
 * There can be a situation when user's handler deletes another handler which
 * is the previous in the list. handler_fire_stanza() and handler_fire_timed()
 * must handle this situation correctly. Current function helps to avoid
 * list corruption in described scenario.
 *
 * TODO Convert handler lists to double-linked lists. Current implementation
 * works for O(n).
 */
static void _handler_item_remove(xmpp_handlist_t **head, xmpp_handlist_t *item)
{
    while (*head) {
        if (*head == item) {
            *head = item->next;
            break;
        }
        head = &(*head)->next;
    }
}

/** Fire off all stanza handlers that match.
 *  This function is called internally by the event loop whenever stanzas
 *  are received from the XMPP server.
 *
 *  @param conn a Strophe connection object
 *  @param stanza a Strophe stanza object
 */
void handler_fire_stanza(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza)
{
    xmpp_handlist_t *item, *next, *head, *head_old;
    const char *id, *ns, *name, *type;
    int ret;

    /* call id handlers */
    id = xmpp_stanza_get_id(stanza);
    if (id) {
        head = (xmpp_handlist_t *)hash_get(conn->id_handlers, id);
        /* enable all added handlers */
        for (item = head; item; item = item->next)
            item->enabled = 1;

        item = head;
        while (item) {
            /* don't fire user handlers until authentication succeeds and
               and skip newly added handlers */
            if ((item->user_handler && !conn->authenticated) ||
                !item->enabled) {
                item = item->next;
                continue;
            }

            ret = ((xmpp_handler)(item->handler))(conn, stanza, item->userdata);
            next = item->next;
            if (!ret) {
                /* handler is one-shot, so delete it */
                head_old = head;
                _handler_item_remove(&head, item);
                if (head != head_old) {
                    /* replace old value */
                    hash_add(conn->id_handlers, id, head);
                }
                xmpp_free(conn->ctx, item->u.id);
                xmpp_free(conn->ctx, item);
            }
            item = next;
        }
    }

    /* call handlers */
    ns = xmpp_stanza_get_ns(stanza);
    name = xmpp_stanza_get_name(stanza);
    type = xmpp_stanza_get_type(stanza);

    /* enable all added handlers */
    for (item = conn->handlers; item; item = item->next)
        item->enabled = 1;

    item = conn->handlers;
    while (item) {
        /* don't fire user handlers until authentication succeeds and
           skip newly added handlers */
        if ((item->user_handler && !conn->authenticated) || !item->enabled) {
            item = item->next;
            continue;
        }

        next = item->next;
        if ((!item->u.ns || (ns && strcmp(ns, item->u.ns) == 0) ||
             xmpp_stanza_get_child_by_ns(stanza, item->u.ns)) &&
            (!item->u.name || (name && strcmp(name, item->u.name) == 0)) &&
            (!item->u.type || (type && strcmp(type, item->u.type) == 0))) {

            ret = ((xmpp_handler)(item->handler))(conn, stanza, item->userdata);
            /* list may be changed during execution of a handler */
            next = item->next;
            if (!ret) {
                /* handler is one-shot, so delete it */
                _handler_item_remove(&conn->handlers, item);
                if (item->u.ns)
                    xmpp_free(conn->ctx, item->u.ns);
                if (item->u.name)
                    xmpp_free(conn->ctx, item->u.name);
                if (item->u.type)
                    xmpp_free(conn->ctx, item->u.type);
                xmpp_free(conn->ctx, item);
            }
        }
        item = next;
    }
}

/** Fire off all timed handlers that are ready.
 *  This function is called internally by the event loop.
 *
 *  @param ctx a Strophe context object
 *
 *  @return the time in milliseconds until the next handler will be ready
 */
uint64_t handler_fire_timed(xmpp_ctx_t *const ctx)
{
    xmpp_connlist_t *connitem;
    xmpp_handlist_t *item, *next;
    xmpp_conn_t *conn;
    uint64_t elapsed, min;
    uint64_t timestamp;
    int ret;

    min = (uint64_t)(-1);

    connitem = ctx->connlist;
    while (connitem) {
        conn = connitem->conn;
        if (conn->state != XMPP_STATE_CONNECTED) {
            connitem = connitem->next;
            continue;
        }

        /* enable all handlers that were added */
        for (item = conn->timed_handlers; item; item = item->next)
            item->enabled = 1;

        item = conn->timed_handlers;
        while (item) {
            /* don't fire user handlers until authentication succeeds and
               skip newly added handlers */
            if ((item->user_handler && !conn->authenticated) ||
                !item->enabled) {
                item = item->next;
                continue;
            }

            next = item->next;
            timestamp = time_stamp();
            elapsed = time_elapsed(item->u.last_stamp, timestamp);
            if (elapsed >= item->u.period) {
                /* fire! */
                item->u.last_stamp = timestamp;
                ret = ((xmpp_timed_handler)item->handler)(conn, item->userdata);
                /* list may be changed during execution of a handler */
                next = item->next;
                if (!ret) {
                    /* delete handler if it returned false */
                    _handler_item_remove(&conn->timed_handlers, item);
                    xmpp_free(ctx, item);
                }
            } else if (min > (item->u.period - elapsed))
                min = item->u.period - elapsed;

            item = next;
        }

        connitem = connitem->next;
    }

    /*
     * Check timed handlers in context. These handlers fire periodically
     * regardless of connections state.
     * TODO Reduce copy-paste.
     */
    item = ctx->timed_handlers;
    while (item) {
        next = item->next;
        timestamp = time_stamp();
        elapsed = time_elapsed(item->u.last_stamp, timestamp);
        if (elapsed >= item->u.period) {
            /* fire! */
            item->u.last_stamp = timestamp;
            ret =
                ((xmpp_global_timed_handler)item->handler)(ctx, item->userdata);
            /* list may be changed during execution of a handler */
            next = item->next;
            if (!ret) {
                /* delete handler if it returned false */
                _handler_item_remove(&ctx->timed_handlers, item);
                xmpp_free(ctx, item);
            }
        } else if (min > (item->u.period - elapsed))
            min = item->u.period - elapsed;

        item = next;
    }

    return min;
}

/** Reset all timed handlers.
 *  This function is called internally when a connection is successful.
 *
 *  @param conn a Strophe connection object
 *  @param user_only whether to reset all handlers or only user ones
 */
void handler_reset_timed(xmpp_conn_t *conn, int user_only)
{
    xmpp_handlist_t *handitem;

    handitem = conn->timed_handlers;
    while (handitem) {
        if ((user_only && handitem->user_handler) || !user_only)
            handitem->u.last_stamp = time_stamp();

        handitem = handitem->next;
    }
}

static void _timed_handler_add(xmpp_ctx_t *ctx,
                               xmpp_handlist_t **handlers_list,
                               xmpp_void_handler handler,
                               const unsigned long period,
                               void *const userdata,
                               const int user_handler)
{
    xmpp_handlist_t *item;

    /* check if handler is already in the list */
    for (item = *handlers_list; item; item = item->next) {
        if (item->handler == handler && item->userdata == userdata) {
            xmpp_warn(ctx, "xmpp", "Timed handler already exists.");
            break;
        }
    }
    if (item)
        return;

    /* build new item */
    item = xmpp_alloc(ctx, sizeof(xmpp_handlist_t));
    if (!item)
        return;

    item->user_handler = user_handler;
    item->handler = handler;
    item->userdata = userdata;
    item->enabled = 0;

    item->u.period = period;
    item->u.last_stamp = time_stamp();

    /* append item to list */
    item->next = *handlers_list;
    *handlers_list = item;
}

static void _timed_handler_delete(xmpp_ctx_t *ctx,
                                  xmpp_handlist_t **handlers_list,
                                  xmpp_void_handler handler)
{
    xmpp_handlist_t *item;

    while (*handlers_list) {
        item = *handlers_list;
        if (item->handler == handler) {
            *handlers_list = item->next;
            xmpp_free(ctx, item);
        } else {
            handlers_list = &item->next;
        }
    }
}

/** Delete a timed handler.
 *
 *  @param conn a Strophe connection object
 *  @param handler function pointer to the handler
 *
 *  @ingroup Handlers
 */
void xmpp_timed_handler_delete(xmpp_conn_t *const conn,
                               xmpp_timed_handler handler)
{
    _timed_handler_delete(conn->ctx, &conn->timed_handlers, handler);
}

static void _id_handler_add(xmpp_conn_t *const conn,
                            xmpp_handler handler,
                            const char *const id,
                            void *const userdata,
                            int user_handler)
{
    xmpp_handlist_t *item, *tail;

    /* check if handler is already in the list */
    item = (xmpp_handlist_t *)hash_get(conn->id_handlers, id);
    while (item) {
        if (item->handler == handler && item->userdata == userdata) {
            xmpp_warn(conn->ctx, "xmpp", "Id handler already exists.");
            break;
        }
        item = item->next;
    }
    if (item)
        return;

    /* build new item */
    item = xmpp_alloc(conn->ctx, sizeof(xmpp_handlist_t));
    if (!item)
        return;

    item->user_handler = user_handler;
    item->handler = handler;
    item->userdata = userdata;
    item->enabled = 0;
    item->next = NULL;

    item->u.id = xmpp_strdup(conn->ctx, id);
    if (!item->u.id) {
        xmpp_free(conn->ctx, item);
        return;
    }

    /* put on list in hash table */
    tail = (xmpp_handlist_t *)hash_get(conn->id_handlers, id);
    if (!tail)
        hash_add(conn->id_handlers, id, item);
    else {
        while (tail->next)
            tail = tail->next;
        tail->next = item;
    }
}

/** Delete an id based stanza handler.
 *
 *  @param conn a Strophe connection object
 *  @param handler a function pointer to a stanza handler
 *  @param id a string containing the id the handler is for
 *
 *  @ingroup Handlers
 */
void xmpp_id_handler_delete(xmpp_conn_t *const conn,
                            xmpp_handler handler,
                            const char *const id)
{
    xmpp_handlist_t *item, *prev, *next;

    prev = NULL;
    item = (xmpp_handlist_t *)hash_get(conn->id_handlers, id);
    if (!item)
        return;

    while (item) {
        next = item->next;

        if (item->handler == handler) {
            if (prev)
                prev->next = next;
            else {
                hash_drop(conn->id_handlers, id);
                hash_add(conn->id_handlers, id, next);
            }

            xmpp_free(conn->ctx, item->u.id);
            xmpp_free(conn->ctx, item);
            item = next;
        } else {
            prev = item;
            item = next;
        }
    }
}

/* add a stanza handler */
static void _handler_add(xmpp_conn_t *const conn,
                         xmpp_handler handler,
                         const char *const ns,
                         const char *const name,
                         const char *const type,
                         void *const userdata,
                         int user_handler)
{
    xmpp_handlist_t *item, *tail;

    /* check if handler already in list */
    for (item = conn->handlers; item; item = item->next) {
        /* same handler function can process different stanzas and
           distinguish them according to userdata. */
        if (item->handler == handler && item->userdata == userdata) {
            xmpp_warn(conn->ctx, "xmpp", "Stanza handler already exists.");
            break;
        }
    }
    if (item)
        return;

    /* build new item */
    item = (xmpp_handlist_t *)xmpp_alloc(conn->ctx, sizeof(xmpp_handlist_t));
    if (!item)
        return;

    item->user_handler = user_handler;
    item->handler = handler;
    item->userdata = userdata;
    item->enabled = 0;
    item->next = NULL;

    if (ns) {
        item->u.ns = xmpp_strdup(conn->ctx, ns);
        if (!item->u.ns) {
            xmpp_free(conn->ctx, item);
            return;
        }
    } else
        item->u.ns = NULL;

    if (name) {
        item->u.name = xmpp_strdup(conn->ctx, name);
        if (!item->u.name) {
            if (item->u.ns)
                xmpp_free(conn->ctx, item->u.ns);
            xmpp_free(conn->ctx, item);
            return;
        }
    } else
        item->u.name = NULL;

    if (type) {
        item->u.type = xmpp_strdup(conn->ctx, type);
        if (!item->u.type) {
            if (item->u.ns)
                xmpp_free(conn->ctx, item->u.ns);
            if (item->u.name)
                xmpp_free(conn->ctx, item->u.name);
            xmpp_free(conn->ctx, item);
        }
    } else
        item->u.type = NULL;

    /* append to list */
    if (!conn->handlers)
        conn->handlers = item;
    else {
        tail = conn->handlers;
        while (tail->next)
            tail = tail->next;
        tail->next = item;
    }
}

/** Delete a stanza handler.
 *
 *  @param conn a Strophe connection object
 *  @param handler a function pointer to a stanza handler
 *
 *  @ingroup Handlers
 */
void xmpp_handler_delete(xmpp_conn_t *const conn, xmpp_handler handler)
{
    xmpp_handlist_t *prev, *item;

    if (!conn->handlers)
        return;

    prev = NULL;
    item = conn->handlers;
    while (item) {
        if (item->handler == handler) {
            if (prev)
                prev->next = item->next;
            else
                conn->handlers = item->next;

            if (item->u.ns)
                xmpp_free(conn->ctx, item->u.ns);
            if (item->u.name)
                xmpp_free(conn->ctx, item->u.name);
            if (item->u.type)
                xmpp_free(conn->ctx, item->u.type);
            xmpp_free(conn->ctx, item);
            item = prev ? prev->next : conn->handlers;
        } else {
            prev = item;
            item = item->next;
        }
    }
}

/** Add a timed handler.
 *  The handler will fire for the first time once the period has elapsed,
 *  and continue firing regularly after that.  Strophe will try its best
 *  to fire handlers as close to the period times as it can, but accuracy
 *  will vary depending on the resolution of the event loop.
 *
 *  If the handler function returns true, it will be kept, and if it
 *  returns false, it will be deleted from the list of handlers.
 *
 *  @param conn a Strophe connection object
 *  @param handler a function pointer to a timed handler
 *  @param period the time in milliseconds between firings
 *  @param userdata an opaque data pointer that will be passed to the handler
 *
 *  @ingroup Handlers
 */
void xmpp_timed_handler_add(xmpp_conn_t *const conn,
                            xmpp_timed_handler handler,
                            const unsigned long period,
                            void *const userdata)
{
    _timed_handler_add(conn->ctx, &conn->timed_handlers, handler, period,
                       userdata, 1);
}

/** Add a timed system handler.
 *  This function is used to add internal timed handlers and should not be
 *  used outside of the library.
 *
 *  @param conn a Strophe connection object
 *  @param handler a function pointer to a timed handler
 *  @param period the time in milliseconds between firings
 *  @param userdata an opaque data pointer that will be passed to the handler
 */
void handler_add_timed(xmpp_conn_t *const conn,
                       xmpp_timed_handler handler,
                       const unsigned long period,
                       void *const userdata)
{
    _timed_handler_add(conn->ctx, &conn->timed_handlers, handler, period,
                       userdata, 0);
}

/** Add an id based stanza handler.

 *  This function adds a stanza handler for an &lt;iq/&gt; stanza of
 *  type 'result' or 'error' with a specific id attribute.  This can
 *  be used to handle responses to specific &lt;iq/&gt;s.
 *
 *  If the handler function returns true, it will be kept, and if it
 *  returns false, it will be deleted from the list of handlers.
 *
 *  @param conn a Strophe connection object
 *  @param handler a function pointer to a stanza handler
 *  @param id a string with the id
 *  @param userdata an opaque data pointer that will be passed to the handler
 *
 *  @ingroup Handlers
 */
void xmpp_id_handler_add(xmpp_conn_t *const conn,
                         xmpp_handler handler,
                         const char *const id,
                         void *const userdata)
{
    _id_handler_add(conn, handler, id, userdata, 1);
}

/** Add an id based system stanza handler.
 *  This function is used to add internal id based stanza handlers and should
 *  not be used outside of the library.
 *
 *  @param conn a Strophe connection object
 *  @param handler a function pointer to a stanza handler
 *  @param id a string with the id
 *  @param userdata an opaque data pointer that will be passed to the handler
 */
void handler_add_id(xmpp_conn_t *const conn,
                    xmpp_handler handler,
                    const char *const id,
                    void *const userdata)
{
    _id_handler_add(conn, handler, id, userdata, 0);
}

/** Add a stanza handler.
 *  This function is used to add a stanza handler to a connection.
 *  The handler will be called when the any of the filters match.  The
 *  name filter matches to the top level stanza name.  The type filter
 *  matches the 'type' attribute of the top level stanza.  The ns
 *  filter matches the namespace ('xmlns' attribute) of either the top
 *  level stanza or any of it's immediate children (this allows you do
 *  handle specific &lt;iq/&gt; stanzas based on the &lt;query/&gt;
 *  child namespace.
 *
 *  If the handler function returns true, it will be kept, and if it
 *  returns false, it will be deleted from the list of handlers.
 *
 *  @param conn a Strophe connection object
 *  @param handler a function pointer to a stanza handler
 *  @param ns a string with the namespace to match
 *  @param name a string with the stanza name to match
 *  @param type a string with the 'type' attribute to match
 *  @param userdata an opaque data pointer that will be passed to the handler
 *
 *  @ingroup Handlers
 */
void xmpp_handler_add(xmpp_conn_t *const conn,
                      xmpp_handler handler,
                      const char *const ns,
                      const char *const name,
                      const char *const type,
                      void *const userdata)
{
    _handler_add(conn, handler, ns, name, type, userdata, 1);
}

/** Add a system stanza handler.
 *  This function is used to add internal stanza handlers and should
 *  not be used outside of the library.
 *
 *  @param conn a Strophe connection object
 *  @param handler a function pointer to a stanza handler
 *  @param ns a string with the namespace to match
 *  @param name a string with the stanza name to match
 *  @param type a string with the 'type' attribute value to match
 *  @param userdata an opaque data pointer that will be passed to the handler
 */
void handler_add(xmpp_conn_t *const conn,
                 xmpp_handler handler,
                 const char *const ns,
                 const char *const name,
                 const char *const type,
                 void *const userdata)
{
    _handler_add(conn, handler, ns, name, type, userdata, 0);
}

/** Delete all system handlers.
 *  This function is used to reset conn object before re-connecting.
 *
 *  @param conn a Strophe connection object
 */
void handler_system_delete_all(xmpp_conn_t *conn)
{
    xmpp_handlist_t *item, *next, *head, *head_old;
    hash_iterator_t *iter;
    const char *key;
    char *key2 = NULL;

    /* TODO unify all kinds of handlers and avoid copy-paste below */

    item = conn->handlers;
    while (item) {
        if (!item->user_handler) {
            next = item->next;
            _handler_item_remove(&conn->handlers, item);
            if (item->u.ns)
                xmpp_free(conn->ctx, item->u.ns);
            if (item->u.name)
                xmpp_free(conn->ctx, item->u.name);
            if (item->u.type)
                xmpp_free(conn->ctx, item->u.type);
            xmpp_free(conn->ctx, item);
            item = next;
        } else
            item = item->next;
    }

    item = conn->timed_handlers;
    while (item) {
        if (!item->user_handler) {
            next = item->next;
            _handler_item_remove(&conn->timed_handlers, item);
            xmpp_free(conn->ctx, item);
            item = next;
        } else
            item = item->next;
    }

    iter = hash_iter_new(conn->id_handlers);
    key = iter == NULL ? NULL : hash_iter_next(iter);
    while (key != NULL) {
        head = head_old = (xmpp_handlist_t *)hash_get(conn->id_handlers, key);
        item = head;
        while (item) {
            if (!item->user_handler) {
                next = item->next;
                _handler_item_remove(&head, item);
                xmpp_free(conn->ctx, item->u.id);
                xmpp_free(conn->ctx, item);
                item = next;
            } else
                item = item->next;
        }
        if (head != head_old)
            key2 = xmpp_strdup(conn->ctx, key);
        /* Hash table implementation is not perfect, so we need to find next
           key before dropping current one. Otherwise, we will get access to
           freed memory. */
        key = hash_iter_next(iter);
        if (head != head_old) {
            /* hash_add() replaces value if the key exists */
            if (head != NULL)
                hash_add(conn->id_handlers, key2, head);
            else
                hash_drop(conn->id_handlers, key2);
            xmpp_free(conn->ctx, key2);
        }
    }
    if (iter)
        hash_iter_release(iter);
}

/** Add a global timed handler.
 *  The handler will fire for the first time once the period has elapsed,
 *  and continue firing regularly after that.  Strophe will try its best
 *  to fire handlers as close to the period times as it can, but accuracy
 *  will vary depending on the resolution of the event loop.
 *
 *  The main difference between global and ordinary handlers:
 *  - Ordinary handler is related to a connection, fires only when the
 *    connection is in connected state and is removed once the connection is
 *    destroyed.
 *  - Global handler fires regardless of connections state and is related to
 *    a Strophe context.
 *
 *  The handler is executed in context of the respective event loop.
 *
 *  If the handler function returns true, it will be kept, and if it
 *  returns false, it will be deleted from the list of handlers.
 *
 *  Notice, the same handler pointer may be added multiple times with different
 *  userdata pointers. However, xmpp_global_timed_handler_delete() deletes
 *  all occurrences.
 *
 *  @param ctx a Strophe context object
 *  @param handler a function pointer to a timed handler
 *  @param period the time in milliseconds between firings
 *  @param userdata an opaque data pointer that will be passed to the handler
 *
 *  @ingroup Handlers
 */
void xmpp_global_timed_handler_add(xmpp_ctx_t *const ctx,
                                   xmpp_global_timed_handler handler,
                                   const unsigned long period,
                                   void *const userdata)
{
    _timed_handler_add(ctx, &ctx->timed_handlers, handler, period, userdata, 1);
}

/** Delete a global timed handler.
 *
 *  @param ctx a Strophe context object
 *  @param handler function pointer to the handler
 *
 *  @ingroup Handlers
 */
void xmpp_global_timed_handler_delete(xmpp_ctx_t *const ctx,
                                      xmpp_global_timed_handler handler)
{
    _timed_handler_delete(ctx, &ctx->timed_handlers, handler);
}
