/*
 * Copyright (c) 2013-2017 Isode Limited.
 * All rights reserved.
 * See the COPYING file for more information.
 */

#include <boost/assign/list_of.hpp>
#include <boost/lambda/bind.hpp>
#include <boost/lambda/lambda.hpp>

#include <Swiften/Base/IDGenerator.h>
#include <Swiften/Disco/ClientDiscoManager.h>
#include <Swiften/Elements/DiscoInfo.h>
#include <Swiften/Elements/Message.h>
#include <Swiften/Elements/Presence.h>
#include <Swiften/Elements/RawXMLPayload.h>
#include <Swiften/Elements/RosterItemPayload.h>
#include <Swiften/Elements/RosterPayload.h>
#include <Swiften/Elements/SoftwareVersion.h>
#include <Swiften/JID/JID.h>
#include <Swiften/Presence/PresenceSender.h>
#include <Swiften/Presence/SubscriptionManager.h>
#include <Swiften/Queries/GenericRequest.h>
#include <Swiften/Queries/IQRouter.h>
#include <Swiften/Queries/Requests/GetSoftwareVersionRequest.h>
#include <Swiften/Roster/SetRosterRequest.h>
#include <Swiften/Roster/XMPPRoster.h>
#include <Swiften/Roster/XMPPRosterItem.h>
#include <Swiften/TLS/PKCS12Certificate.h>

#include <Sluift/ElementConvertors/IQConvertor.h>
#include <Sluift/ElementConvertors/MessageConvertor.h>
#include <Sluift/ElementConvertors/PresenceConvertor.h>
#include <Sluift/ElementConvertors/StanzaConvertor.h>
#include <Sluift/ElementConvertors/StatusShowConvertor.h>
#include <Sluift/Lua/Check.h>
#include <Sluift/Lua/Exception.h>
#include <Sluift/Lua/FunctionRegistration.h>
#include <Sluift/Lua/LuaUtils.h>
#include <Sluift/Lua/Value.h>
#include <Sluift/SluiftClient.h>
#include <Sluift/globals.h>

using namespace Swift;
namespace lambda = boost::lambda;

static inline SluiftClient* getClient(lua_State* L) {
    return *Lua::checkUserData<SluiftClient>(L, 1);
}

static inline int getGlobalTimeout(lua_State* L) {
    lua_rawgeti(L, LUA_REGISTRYINDEX, Sluift::globals.moduleLibIndex);
    lua_getfield(L, -1, "timeout");
    int result = boost::numeric_cast<int>(lua_tointeger(L, -1));
    lua_pop(L, 2);
    return result;
}

static void addPayloadsToTable(lua_State* L, const std::vector<std::shared_ptr<Payload> >& payloads) {
    if (!payloads.empty()) {
        lua_createtable(L, boost::numeric_cast<int>(payloads.size()), 0);
        for (size_t i = 0; i < payloads.size(); ++i) {
            Sluift::globals.elementConvertor.convertToLua(L, payloads[i]);
            lua_rawseti(L, -2, boost::numeric_cast<int>(i+1));
        }
        Lua::registerGetByTypeIndex(L, -1);
        lua_setfield(L, -2, "payloads");
    }
}

static std::shared_ptr<Payload> getPayload(lua_State* L, int index) {
    if (lua_type(L, index) == LUA_TTABLE) {
        return std::dynamic_pointer_cast<Payload>(Sluift::globals.elementConvertor.convertFromLua(L, index));
    }
    else if (lua_type(L, index) == LUA_TSTRING) {
        return std::make_shared<RawXMLPayload>(Lua::checkString(L, index));
    }
    else {
        return std::shared_ptr<Payload>();
    }
}

static std::vector< std::shared_ptr<Payload> > getPayloadsFromTable(lua_State* L, int index) {
    index = Lua::absoluteOffset(L, index);
    std::vector< std::shared_ptr<Payload> > result;
    lua_getfield(L, index, "payloads");
    if (lua_istable(L, -1)) {
        for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1)) {
            std::shared_ptr<Payload> payload = getPayload(L, -1);
            if (payload) {
                result.push_back(payload);
            }
        }
    }
    lua_pop(L, 1);
    return result;
}

static void setOptions(lua_State* L, SluiftClient* client) {
    Lua::checkType(L, 2, LUA_TTABLE);
    lua_getfield(L, 2, "host");
    if (!lua_isnil(L, -1)) {
        client->getOptions().manualHostname = lua_tostring(L, -1);
    }
    lua_getfield(L, 2, "port");
    if (!lua_isnil(L, -1)) {
        client->getOptions().manualPort = boost::numeric_cast<int>(lua_tointeger(L, -1));
    }
    lua_getfield(L, 2, "ack");
    if (!lua_isnil(L, -1)) {
        client->getOptions().useAcks = lua_toboolean(L, -1);
    }
    lua_getfield(L, 2, "compress");
    if (!lua_isnil(L, -1)) {
        client->getOptions().useStreamCompression = lua_toboolean(L, -1);
    }
    lua_getfield(L, 2, "tls");
    if (!lua_isnil(L, -1)) {
        bool useTLS = lua_toboolean(L, -1);
        client->getOptions().useTLS = (useTLS ? ClientOptions::UseTLSWhenAvailable : ClientOptions::NeverUseTLS);
    }
    lua_getfield(L, 2, "bosh_url");
    if (!lua_isnil(L, -1)) {
        client->getOptions().boshURL = URL::fromString(lua_tostring(L, -1));
    }
    lua_getfield(L, 2, "allow_plain_without_tls");
    if (!lua_isnil(L, -1)) {
        client->getOptions().allowPLAINWithoutTLS = lua_toboolean(L, -1);
    }
    lua_pushvalue(L, 1);
}


SLUIFT_LUA_FUNCTION(Client, async_connect) {
    SluiftClient* client = getClient(L);
    setOptions(L, client);
    client->connect();
    return 0;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, set_trace_enabled,
        "Enable/disable tracing of the data sent/received.\n\n.",
        "self\n"
        "enable  a boolean specifying whether to enable/disable tracing",
        ""
) {
    getClient(L)->setTraceEnabled(lua_toboolean(L, 1));
    return 0;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, wait_connected,
        "Block until the client is connected.\n\nThis is useful after an `async_connect`.",
        "self",
        ""
) {
    getClient(L)->waitConnected(getGlobalTimeout(L));
    return 0;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, is_connected,
        "Checks whether this client is still connected.\n\nReturns a boolean.",
        "self\n",
        ""
) {
    lua_pushboolean(L, getClient(L)->isConnected());
    return 1;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, disconnect,
        "Disconnect from the server",
        "self\n",
        ""
) {
    Sluift::globals.eventLoop.runOnce();
    getClient(L)->disconnect();
    return 0;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, set_version,

        "Sets the published version of this client.",

        "self",

        "name  the name of the client software\n"
        "version  the version identifier of this client\n"
        "os  the OS this client is running on\n"
) {
    Sluift::globals.eventLoop.runOnce();
    SluiftClient* client = getClient(L);
    if (std::shared_ptr<SoftwareVersion> version = std::dynamic_pointer_cast<SoftwareVersion>(Sluift::globals.elementConvertor.convertFromLuaUntyped(L, 2, "software_version"))) {
        client->setSoftwareVersion(version->getName(), version->getVersion(), version->getOS());
    }
    return 0;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, get_contacts,
        "Returns a table of all the contacts in the contact list.",
        "self\n",
        ""
) {
    Sluift::globals.eventLoop.runOnce();

    SluiftClient* client = getClient(L);
    Lua::Table contactsTable;
    for (const auto& item : client->getRoster(getGlobalTimeout(L))) {
        std::string subscription;
        switch(item.getSubscription()) {
            case RosterItemPayload::None: subscription = "none"; break;
            case RosterItemPayload::To: subscription = "to"; break;
            case RosterItemPayload::From: subscription = "from"; break;
            case RosterItemPayload::Both: subscription = "both"; break;
            case RosterItemPayload::Remove: subscription = "remove"; break;
        }
        Lua::Table itemTable = boost::assign::map_list_of
            ("jid", std::make_shared<Lua::Value>(item.getJID().toString()))
            ("name", std::make_shared<Lua::Value>(item.getName()))
            ("subscription", std::make_shared<Lua::Value>(subscription))
            ("groups", std::make_shared<Lua::Value>(std::vector<Lua::Value>(item.getGroups().begin(), item.getGroups().end())));
        contactsTable[item.getJID().toString()] = std::make_shared<Lua::Value>(itemTable);
    }
    pushValue(L, contactsTable);
    Lua::registerTableToString(L, -1);
    return 1;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, get_block_list,
        "Returns a table of all the items in the blocking list.",
        "self\n",
        ""
) {
    Sluift::globals.eventLoop.runOnce();
    SluiftClient* client = getClient(L);
    lua_newtable(L);
    int i = 0;
    for (const auto& item : client->getBlockList(getGlobalTimeout(L))) {
        lua_pushstring(L, item.toString().c_str());
        lua_rawseti(L, -2, boost::numeric_cast<int>(++i));
    }
    Lua::registerTableToString(L, -1);
    return 1;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, add_block,
        "Adds a user or domain to the blocking list.",
        "self\n",
        "jid  the JID of the user or domain to add to the blocking list\n"

) {
    SluiftClient* client = getClient(L);
    JID jid(Lua::checkString(L, 2));
    int timeout = getGlobalTimeout(L);

    std::shared_ptr<BlockPayload> payload = std::make_shared<BlockPayload>(std::vector<JID>(1, jid));
    return client->sendRequest(
        std::make_shared< GenericRequest<BlockPayload> >(IQ::Set, JID(), payload, client->getClient()->getIQRouter()), timeout).convertToLuaResult(L);
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, remove_block,
        "Removes a user or domain from the blocking list.",
        "self\n",
        "jid  the JID of the user or domain to remove from the blocking list\n"

) {
    SluiftClient* client = getClient(L);
    JID jid(Lua::checkString(L, 2));
    int timeout = getGlobalTimeout(L);

    std::shared_ptr<UnblockPayload> payload = std::make_shared<UnblockPayload>(std::vector<JID>(1, jid));
    return client->sendRequest(
        std::make_shared< GenericRequest<UnblockPayload> >(IQ::Set, JID(), payload, client->getClient()->getIQRouter()), timeout).convertToLuaResult(L);
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, remove_all_block,
        "Removes all users and domains from the blocking list.",
        "self\n",
        ""
) {
    SluiftClient* client = getClient(L);
    int timeout = getGlobalTimeout(L);

    std::shared_ptr<UnblockPayload> payload = std::make_shared<UnblockPayload>(std::vector<JID>());
    return client->sendRequest(
        std::make_shared< GenericRequest<UnblockPayload> >(IQ::Set, JID(), payload, client->getClient()->getIQRouter()), timeout).convertToLuaResult(L);
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, send_message,
        "Send a message.",
        "self\n"
        "to  the JID to send the message to\n"
        "body  the body of the message. Can alternatively be specified using the `body` option\n",

        "to  the JID to send the message to\n"
        "body  the body of the message\n"
        "subject  the subject of the MUC room to set\n"
        "type  the type of message to send (`normal`, `chat`, `error`, `groupchat`, `headline`)\n"
        "payloads  payloads to add to the message\n"
) {
    Sluift::globals.eventLoop.runOnce();
    JID to;
    boost::optional<std::string> body;
    boost::optional<std::string> subject;
    std::vector<std::shared_ptr<Payload> > payloads;
    int index = 2;
    Message::Type type = Message::Chat;
    if (lua_isstring(L, index)) {
        to = std::string(lua_tostring(L, index));
        ++index;
        if (lua_isstring(L, index)) {
            body = lua_tostring(L, index);
            ++index;
        }
    }
    if (lua_istable(L, index)) {
        if (boost::optional<std::string> value = Lua::getStringField(L, index, "to")) {
            to = *value;
        }

        if (boost::optional<std::string> value = Lua::getStringField(L, index, "body")) {
            body = value;
        }

        if (boost::optional<std::string> value = Lua::getStringField(L, index, "type")) {
            type = MessageConvertor::convertMessageTypeFromString(*value);
        }

        if (boost::optional<std::string> value = Lua::getStringField(L, index, "subject")) {
            subject = value;
        }

        payloads = getPayloadsFromTable(L, index);
    }

    if (!to.isValid()) {
        throw Lua::Exception("Missing 'to'");
    }
    if ((!body || body->empty()) && !subject && payloads.empty()) {
        throw Lua::Exception("Missing any of 'body', 'subject' or 'payloads'");
    }
    Message::ref message = std::make_shared<Message>();
    message->setTo(to);
    if (body && !body->empty()) {
        message->setBody(*body);
    }
    if (subject) {
        message->setSubject(*subject);
    }
    message->addPayloads(payloads.begin(), payloads.end());
    message->setType(type);
    if (!getClient(L)->getClient()->isAvailable()) {
        throw Lua::Exception("Trying to send message while client is offline.");
    }
    getClient(L)->getClient()->sendMessage(message);
    return 0;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, send_presence,
        "Send presence.",

        "self\n"
        "body  the text of the presence. Can alternatively be specified using the `status` option\n",

        "to  the JID to send the message to\n"
        "status  the text of the presence\n"
        "show  the availability of the presence (`online`, `ffc`, `away`, `xa`, `dnd`)\n"
        "priority  the priority of the presence\n"
        "type  the type of message to send (`available`, `error`, `probe`, `subscribe`, `subscribed`, `unavailable`, `unsubscribe`, `unsubscribed`)\n"
        "payloads  payloads to add to the presence\n"
) {
    Sluift::globals.eventLoop.runOnce();
    std::shared_ptr<Presence> presence = std::make_shared<Presence>();

    int index = 2;
    if (lua_isstring(L, index)) {
        presence->setStatus(lua_tostring(L, index));
        ++index;
    }
    if (lua_istable(L, index)) {
        if (boost::optional<std::string> value = Lua::getStringField(L, index, "to")) {
            presence->setTo(*value);
        }
        if (boost::optional<std::string> value = Lua::getStringField(L, index, "status")) {
            presence->setStatus(*value);
        }
        if (boost::optional<int> value = Lua::getIntField(L, index, "priority")) {
            presence->setPriority(*value);
        }
        if (boost::optional<std::string> value = Lua::getStringField(L, index, "type")) {
            presence->setType(PresenceConvertor::convertPresenceTypeFromString(*value));
        }
        if (boost::optional<std::string> value = Lua::getStringField(L, index, "show")) {
            presence->setShow(StatusShowConvertor::convertStatusShowTypeFromString(*value));
        }
        std::vector< std::shared_ptr<Payload> > payloads = getPayloadsFromTable(L, index);
        presence->addPayloads(payloads.begin(), payloads.end());
    }
    if (!getClient(L)->getClient()->getPresenceSender()->isAvailable()) {
        throw Lua::Exception("Trying to send presence while client is offline.");
    }
    getClient(L)->getClient()->getPresenceSender()->sendPresence(presence);
    lua_pushvalue(L, 1);
    return 0;
}

static int sendQuery(lua_State* L, IQ::Type type) {
    SluiftClient* client = getClient(L);

    JID to;
    if (boost::optional<std::string> toString = Lua::getStringField(L, 2, "to")) {
        to = JID(*toString);
    }

    int timeout = getGlobalTimeout(L);
    if (boost::optional<int> timeoutInt = Lua::getIntField(L, 2, "timeout")) {
        timeout = *timeoutInt;
    }

    std::shared_ptr<Payload> payload;
    lua_getfield(L, 2, "query");
    payload = getPayload(L, -1);
    lua_pop(L, 1);

    return client->sendRequest(
        std::make_shared< GenericRequest<Payload> >(type, to, payload, client->getClient()->getIQRouter()), timeout).convertToLuaResult(L);
}

#define DISPATCH_PUBSUB_PAYLOAD(payloadType, container, response) \
    else if (std::shared_ptr<payloadType> p = std::dynamic_pointer_cast<payloadType>(payload)) { \
        return client->sendPubSubRequest(type, to, p, timeout).convertToLuaResult(L); \
    }

SLUIFT_LUA_FUNCTION(Client, query_pubsub) {
    SluiftClient* client = getClient(L);

    JID to;
    if (boost::optional<std::string> toString = Lua::getStringField(L, 2, "to")) {
        to = JID(*toString);
    }

    int timeout = getGlobalTimeout(L);
    if (boost::optional<int> timeoutInt = Lua::getIntField(L, 2, "timeout")) {
        timeout = *timeoutInt;
    }

    IQ::Type type;
    if (boost::optional<std::string> queryType = Lua::getStringField(L, 2, "type")) {
        type = IQConvertor::convertIQTypeFromString(*queryType);
    }
    else {
        throw Lua::Exception("Missing query type");
    }

    lua_getfield(L, 2, "query");
    if (!lua_istable(L, -1)) {
        throw Lua::Exception("Missing/incorrect query");
    }
    std::shared_ptr<Payload> payload = getPayload(L, -1);

    if (false) { }
    SWIFTEN_PUBSUB_FOREACH_PUBSUB_PAYLOAD_TYPE(DISPATCH_PUBSUB_PAYLOAD)
    else {
        throw Lua::Exception("Incorrect PubSub payload");
    }
}


SLUIFT_LUA_FUNCTION(Client, get) {
    return sendQuery(L, IQ::Get);
}

SLUIFT_LUA_FUNCTION(Client, set) {
    return sendQuery(L, IQ::Set);
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, send,
        "Sends a raw string",

        "self\n"
        "data  the string to send\n",

        ""
) {
    Sluift::globals.eventLoop.runOnce();
    if (!getClient(L)->getClient()->isAvailable()) {
        throw Lua::Exception("Trying to send data while client is offline.");
    }
    getClient(L)->getClient()->sendData(std::string(Lua::checkString(L, 2)));
    lua_pushvalue(L, 1);
    return 0;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, set_options,

        "Sets the connection options of this client.",

        "self",

        "host  The host to connect to. When omitted, is determined from resolving the JID domain.\n"
        "port  The port to connect to. When omitted, is determined from resolving the JID domain.\n"
        "ack  Request acknowledgements\n"
        "compress  Use stream compression when available\n"
        "tls  Use TLS when available\n"
        "bosh_url  Connect using the specified BOSH URL\n"
        "allow_plain_without_tls  Allow PLAIN authentication without a TLS encrypted connection\n"
) {
    SluiftClient* client = getClient(L);
    setOptions(L, client);
    return 0;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, get_options,
        "Returns a table with all the connection options of this client.",
        "self\n",
        ""
) {
    Sluift::globals.eventLoop.runOnce();

    SluiftClient* client = getClient(L);
    Lua::Table optionsTable = boost::assign::map_list_of
        ("host", std::make_shared<Lua::Value>(client->getOptions().manualHostname))
        ("port", std::make_shared<Lua::Value>(client->getOptions().manualPort))
        ("ack", std::make_shared<Lua::Value>(client->getOptions().useAcks))
        ("compress", std::make_shared<Lua::Value>(client->getOptions().useStreamCompression))
        ("tls", std::make_shared<Lua::Value>(client->getOptions().useTLS == ClientOptions::NeverUseTLS ? false : true))
        ("bosh_url", std::make_shared<Lua::Value>(client->getOptions().boshURL.toString()))
        ("allow_plain_without_tls", std::make_shared<Lua::Value>(client->getOptions().allowPLAINWithoutTLS));
    pushValue(L, optionsTable);
    Lua::registerTableToString(L, -1);
    return 1;
}

static void pushEvent(lua_State* L, const SluiftClient::Event& event) {
    switch (event.type) {
        case SluiftClient::Event::MessageType: {
            Message::ref message = std::dynamic_pointer_cast<Message>(event.stanza);
            Lua::Table result = boost::assign::map_list_of
                ("type", std::make_shared<Lua::Value>(std::string("message")))
                ("from", std::make_shared<Lua::Value>(message->getFrom().toString()))
                ("to", std::make_shared<Lua::Value>(message->getTo().toString()))
                ("body", std::make_shared<Lua::Value>(message->getBody().get_value_or("")))
                ("message_type", std::make_shared<Lua::Value>(MessageConvertor::convertMessageTypeToString(message->getType())));
            Lua::pushValue(L, result);
            addPayloadsToTable(L, message->getPayloads());
            Lua::registerTableToString(L, -1);
            break;
        }
        case SluiftClient::Event::PresenceType: {
            Presence::ref presence = std::dynamic_pointer_cast<Presence>(event.stanza);
            Lua::Table result = boost::assign::map_list_of
                ("type", std::make_shared<Lua::Value>(std::string("presence")))
                ("from", std::make_shared<Lua::Value>(presence->getFrom().toString()))
                ("to", std::make_shared<Lua::Value>(presence->getTo().toString()))
                ("status", std::make_shared<Lua::Value>(presence->getStatus()))
                ("presence_type", std::make_shared<Lua::Value>(PresenceConvertor::convertPresenceTypeToString(presence->getType())));
            Lua::pushValue(L, result);
            addPayloadsToTable(L, presence->getPayloads());
            Lua::registerTableToString(L, -1);
            break;
        }
        case SluiftClient::Event::PubSubEventType: {
            Sluift::globals.elementConvertor.convertToLua(L, event.pubsubEvent);
            lua_pushstring(L, "pubsub");
            lua_setfield(L, -2, "type");
            lua_pushstring(L, event.from.toString().c_str());
            lua_setfield(L, -2, "from");

            lua_rawgeti(L, LUA_REGISTRYINDEX, Sluift::globals.coreLibIndex);
            lua_getfield(L, -1, "process_pubsub_event");
            lua_pushvalue(L, -3);
            lua_call(L, 1, 0);
            lua_pop(L, 1);
            break;
        }
        case SluiftClient::Event::BlockEventType: {
            Lua::Table result = boost::assign::map_list_of
                ("type", std::make_shared<Lua::Value>(std::string("block")))
                ("jid",  std::make_shared<Lua::Value>(event.item.toString()));
            Lua::pushValue(L, result);
            Lua::registerTableToString(L, -1);
            break;
        }
        case SluiftClient::Event::UnblockEventType: {
            Lua::Table result = boost::assign::map_list_of
                ("type", std::make_shared<Lua::Value>(std::string("unblock")))
                ("jid",  std::make_shared<Lua::Value>(event.item.toString()));
            Lua::pushValue(L, result);
            Lua::registerTableToString(L, -1);
            break;
        }
    }
}

struct CallUnaryLuaPredicateOnEvent {
    CallUnaryLuaPredicateOnEvent(lua_State* L, int index) : L(L), index(index) {
    }

    bool operator()(const SluiftClient::Event& event) {
        lua_pushvalue(L, index);
        pushEvent(L, event);
        if (lua_pcall(L, 1, 1, 0) != 0) {
            throw Lua::Exception(lua_tostring(L, -1));
        }
        bool result = lua_toboolean(L, -1);
        lua_pop(L, 1);
        return result;
    }

    lua_State* L;
    int index;
};


SLUIFT_LUA_FUNCTION(Client, get_next_event) {
    Sluift::globals.eventLoop.runOnce();
    SluiftClient* client = getClient(L);

    int timeout = getGlobalTimeout(L);
    boost::optional<SluiftClient::Event::Type> type;
    int condition = 0;
    if (lua_istable(L, 2)) {
        if (boost::optional<std::string> typeString = Lua::getStringField(L, 2, "type")) {
            if (*typeString == "message") {
                type = SluiftClient::Event::MessageType;
            }
            else if (*typeString == "presence") {
                type = SluiftClient::Event::PresenceType;
            }
            else if (*typeString == "pubsub") {
                type = SluiftClient::Event::PubSubEventType;
            }
        }
        if (boost::optional<int> timeoutInt = Lua::getIntField(L, 2, "timeout")) {
            timeout = *timeoutInt;
        }
        lua_getfield(L, 2, "if");
        if (lua_isfunction(L, -1)) {
            condition = Lua::absoluteOffset(L, -1);
        }
    }

    boost::optional<SluiftClient::Event> event;
    if (condition) {
        event = client->getNextEvent(timeout, CallUnaryLuaPredicateOnEvent(L, condition));
    }
    else if (type) {
        event = client->getNextEvent(
                timeout, lambda::bind(&SluiftClient::Event::type, lambda::_1) == *type);
    }
    else {
        event = client->getNextEvent(timeout);
    }

    if (event) {
        pushEvent(L, *event);
    }
    else {
        lua_pushnil(L);
    }
    return 1;
}


SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, add_contact,
        "Add a contact to the contact list.",
        "self\n",
        "jid  The JID of the contact to add\n"
        "name  The name to use in the contact list\n"
        "groups  An array of group names to add the contact to\n") {
    Sluift::globals.eventLoop.runOnce();
    SluiftClient* client = getClient(L);
    RosterItemPayload item;
    int timeout = getGlobalTimeout(L);

    if (lua_type(L, 2) == LUA_TTABLE) {
        lua_getfield(L, 2, "jid");
        const char* rawJID = lua_tostring(L, -1);
        if (rawJID) {
            item.setJID(std::string(rawJID));
        }
        lua_getfield(L, 2, "name");
        const char* rawName = lua_tostring(L, -1);
        if (rawName) {
            item.setName(rawName);
        }
        lua_getfield(L, 2, "groups");
        if (!lua_isnil(L, -1)) {
            if (lua_type(L, -1) == LUA_TTABLE) {
                for (size_t i = 1; i <= lua_objlen(L, -1); ++i) {
                    lua_rawgeti(L, -1, boost::numeric_cast<int>(i));
                    const char* rawGroup = lua_tostring(L, -1);
                    if (rawGroup) {
                        item.addGroup(rawGroup);
                    }
                    lua_pop(L, 1);
                }
            }
            else {
                throw Lua::Exception("Groups should be a table");
            }
        }
    }
    else {
        item.setJID(Lua::checkString(L, 2));
    }

    client->getRoster(timeout);
    if (!client->getClient()->getRoster()->containsJID(item.getJID())) {
        RosterPayload::ref roster = std::make_shared<RosterPayload>();
        roster->addItem(item);

        Sluift::Response response = client->sendVoidRequest(
            SetRosterRequest::create(roster, client->getClient()->getIQRouter()), timeout);
        if (response.error) {
            return response.convertToLuaResult(L);
        }
    }
    client->getClient()->getSubscriptionManager()->requestSubscription(item.getJID());
    lua_pushboolean(L, true);
    return 1;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, remove_contact,
        "Remove a contact from the contact list.",
        "self\n"
        "jid  the JID of the contact to remove\n",
        ""
) {
    Sluift::globals.eventLoop.runOnce();
    SluiftClient* client = getClient(L);
    JID jid(Lua::checkString(L, 2));
    int timeout = getGlobalTimeout(L);

    RosterPayload::ref roster = std::make_shared<RosterPayload>();
    roster->addItem(RosterItemPayload(JID(Lua::checkString(L, 2)), "", RosterItemPayload::Remove));

    return client->sendVoidRequest(
        SetRosterRequest::create(roster, client->getClient()->getIQRouter()), timeout).convertToLuaResult(L);
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, confirm_subscription,
        "Confirm subscription of a contact.",
        "self\n"
        "jid  the JID of the contact to confirm the subscription of\n",
        ""
) {
    Sluift::globals.eventLoop.runOnce();
    SluiftClient* client = getClient(L);
    JID jid(Lua::checkString(L, 2));
    client->getClient()->getSubscriptionManager()->confirmSubscription(jid);
    return 0;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, cancel_subscription,
        "Cancel the subscription of a contact.",
        "self\n"
        "jid  the JID of the contact to cancel the subscription of\n",
        ""
) {
    Sluift::globals.eventLoop.runOnce();
    SluiftClient* client = getClient(L);
    JID jid(Lua::checkString(L, 2));
    client->getClient()->getSubscriptionManager()->cancelSubscription(jid);
    return 0;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, set_disco_info,
        "Sets the service discovery information for this client",
        "self\n"
        "disco_info  A structured representation of the service discovery information\n",
        ""
) {
    SluiftClient* client = getClient(L);
    if (!lua_istable(L, 2)) {
        throw Lua::Exception("Missing disco info");
    }
    if (std::shared_ptr<DiscoInfo> discoInfo = std::dynamic_pointer_cast<DiscoInfo>(Sluift::globals.elementConvertor.convertFromLuaUntyped(L, 2, "disco_info"))) {
        client->getClient()->getDiscoManager()->setDiscoInfo(*discoInfo);
    }
    else {
        throw Lua::Exception("Illegal disco info");
    }
    return 0;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, set_caps_node,
        "Sets the caps node of this client",
        "self\n"
        "node  The caps node (e.g. 'http://swift.im/sluift')\n",
        ""
) {
    SluiftClient* client = getClient(L);
    std::string node(Lua::checkString(L, 2));
    client->getClient()->getDiscoManager()->setCapsNode(Lua::checkString(L, 2));
    return 0;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, set_certificate,
        "Sets a client certificate to use for strong authentication with the server.",
        "self\n"
        "file  PKCS #12 file\n"
        "pwd  passphrase for the certificate private key\n",
        ""
) {
    std::string file;
    std::string pwd;
    int index = 2;
    if (!lua_isnoneornil(L, index)) {
        file = Lua::checkString(L, index);
        ++index;
        if (!lua_isnoneornil(L, index)) {
            pwd = Lua::checkString(L, index);
            ++index;
        }
    }
    if (file.empty()) {
        getClient(L)->getClient()->setCertificate(CertificateWithKey::ref());
    } else {
        getClient(L)->getClient()->setCertificate(std::make_shared<PKCS12Certificate>(file, createSafeByteArray(pwd)));
    }
    return 0;
}

SLUIFT_LUA_FUNCTION_WITH_HELP(
        Client, jid,
        "Returns the JID of this client",
        "self\n",
        ""
) {
    SluiftClient* client = getClient(L);
    lua_pushstring(L, client->getClient()->getJID().toString().c_str());
    return 1;
}

SLUIFT_LUA_FUNCTION(Client, __gc) {
    SluiftClient* client = getClient(L);
    delete client;
    return 0;
}
