/*
 * Copyright © 2006-2007 Daniel Stone
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 * Author: Daniel Stone <daniel@fooishbar.org>
 */

#ifdef HAVE_DIX_CONFIG_H
#include <dix-config.h>
#endif

#include <dbus/dbus.h>
#include <string.h>

#include <X11/X.h>

#include "config-backends.h"
#include "opaque.h" /* for 'display': there should be a better way. */
#include "input.h"
#include "inputstr.h"

#define API_VERSION 2

#define MATCH_RULE "type='method_call',interface='org.x.config.input'"

#define MALFORMED_MSG "[config/dbus] malformed message, dropping"
#define MALFORMED_MESSAGE() { DebugF(MALFORMED_MSG "\n"); \
                            ret = BadValue; \
                            goto unwind; }
#define MALFORMED_MESSAGE_ERROR() { DebugF(MALFORMED_MSG ": %s, %s", \
                                       error->name, error->message); \
                                  ret = BadValue; \
                                  goto unwind; }

struct connection_info {
    char busobject[32];
    char busname[64];
    DBusConnection *connection;
};

static void
reset_info(struct connection_info *info)
{
    info->connection = NULL;
    info->busname[0] = '\0';
    info->busobject[0] = '\0';
}

static int
add_device(DBusMessage *message, DBusMessage *reply, DBusError *error)
{
    DBusMessageIter iter, reply_iter, subiter;
    InputOption *tmpo = NULL, *options = NULL;
    char *tmp = NULL;
    int ret, err;
    DeviceIntPtr dev = NULL;

    dbus_message_iter_init_append(reply, &reply_iter);

    if (!dbus_message_iter_init(message, &iter)) {
        ErrorF("[config/dbus] couldn't initialise iterator\n");
        MALFORMED_MESSAGE();
    }

    options = calloc(sizeof(*options), 1);
    if (!options) {
        ErrorF("[config/dbus] couldn't allocate option\n");
        return BadAlloc;
    }

    options->key = strdup("_source");
    options->value = strdup("client/dbus");
    if (!options->key || !options->value) {
        ErrorF("[config/dbus] couldn't allocate first key/value pair\n");
        ret = BadAlloc;
        goto unwind;
    }

    /* signature should be [ss][ss]... */
    while (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_ARRAY) {
        tmpo = calloc(sizeof(*tmpo), 1);
        if (!tmpo) {
            ErrorF("[config/dbus] couldn't allocate option\n");
            ret = BadAlloc;
            goto unwind;
        }
        tmpo->next = options;
        options = tmpo;

        dbus_message_iter_recurse(&iter, &subiter);

        if (dbus_message_iter_get_arg_type(&subiter) != DBUS_TYPE_STRING)
            MALFORMED_MESSAGE();

        dbus_message_iter_get_basic(&subiter, &tmp);
        if (!tmp)
            MALFORMED_MESSAGE();
        /* The _ prefix refers to internal settings, and may not be given by
         * the client. */
        if (tmp[0] == '_') {
            ErrorF("[config/dbus] attempted subterfuge: option name %s given\n",
                   tmp);
            MALFORMED_MESSAGE();
        }
        options->key = strdup(tmp);
        if (!options->key) {
            ErrorF("[config/dbus] couldn't duplicate key!\n");
            ret = BadAlloc;
            goto unwind;
        }

        if (!dbus_message_iter_has_next(&subiter))
            MALFORMED_MESSAGE();
        dbus_message_iter_next(&subiter);
        if (dbus_message_iter_get_arg_type(&subiter) != DBUS_TYPE_STRING)
            MALFORMED_MESSAGE();

        dbus_message_iter_get_basic(&subiter, &tmp);
        if (!tmp)
            MALFORMED_MESSAGE();
        options->value = strdup(tmp);
        if (!options->value) {
            ErrorF("[config/dbus] couldn't duplicate option!\n");
            ret = BadAlloc;
            goto unwind;
        }

        dbus_message_iter_next(&iter);
    }

    ret = NewInputDeviceRequest(options, NULL, &dev);
    if (ret != Success) {
        DebugF("[config/dbus] NewInputDeviceRequest failed\n");
        goto unwind;
    }

    if (!dev) {
        DebugF("[config/dbus] NewInputDeviceRequest provided no device\n");
        ret = BadImplementation;
        goto unwind;
    }

    /* XXX: If we fail halfway through, we don't seem to have any way to
     *      empty the iterator, so you'll end up with some device IDs,
     *      plus an error.  This seems to be a shortcoming in the D-Bus
     *      API. */
    for (; dev; dev = dev->next) {
        if (!dbus_message_iter_append_basic(&reply_iter, DBUS_TYPE_INT32,
                                            &dev->id)) {
            ErrorF("[config/dbus] couldn't append to iterator\n");
            ret = BadAlloc;
            goto unwind;
        }
    }

unwind:
    if (ret != Success) {
        if (dev)
            RemoveDevice(dev, TRUE);

        err = -ret;
        dbus_message_iter_append_basic(&reply_iter, DBUS_TYPE_INT32, &err);
    }

    while (options) {
        tmpo = options;
        options = options->next;
        free(tmpo->key);
        free(tmpo->value);
        free(tmpo);
    }

    return ret;
}

static int
remove_device(DBusMessage *message, DBusMessage *reply, DBusError *error)
{
    int deviceid, ret, err;
    DeviceIntPtr dev;
    DBusMessageIter iter, reply_iter;

    dbus_message_iter_init_append(reply, &reply_iter);

    if (!dbus_message_iter_init(message, &iter)) {
        ErrorF("[config/dbus] failed to init iterator\n");
        MALFORMED_MESSAGE();
    }

    if (!dbus_message_get_args(message, error, DBUS_TYPE_UINT32,
                               &deviceid, DBUS_TYPE_INVALID)) {
        MALFORMED_MESSAGE_ERROR();
    }

    dixLookupDevice(&dev, deviceid, serverClient, DixDestroyAccess);
    if (!dev) {
        DebugF("[config/dbus] bogus device id %d given\n", deviceid);
        ret = BadMatch;
        goto unwind;
    }

    DebugF("[config/dbus] removing device %s (id %d)\n", dev->name, deviceid);

    /* Call PIE here so we don't try to dereference a device that's
     * already been removed. */
    OsBlockSignals();
    ProcessInputEvents();
    DeleteInputDeviceRequest(dev);
    OsReleaseSignals();

    ret = Success;

unwind:
    err = (ret == Success) ? ret : -ret;
    dbus_message_iter_append_basic(&reply_iter, DBUS_TYPE_INT32, &err);

    return ret;
}

static int
list_devices(DBusMessage *message, DBusMessage *reply, DBusError *error)
{
    DeviceIntPtr dev;
    DBusMessageIter iter, subiter;

    dbus_message_iter_init_append(reply, &iter);

    for (dev = inputInfo.devices; dev; dev = dev->next) {
        if (!dbus_message_iter_open_container(&iter, DBUS_TYPE_STRUCT, NULL,
                                              &subiter)) {
            ErrorF("[config/dbus] couldn't init container\n");
            return BadAlloc;
        }
        if (!dbus_message_iter_append_basic(&subiter, DBUS_TYPE_UINT32,
                                            &dev->id)) {
            ErrorF("[config/dbus] couldn't append to iterator\n");
            return BadAlloc;
        }
        if (!dbus_message_iter_append_basic(&subiter, DBUS_TYPE_STRING,
                                            &dev->name)) {
            ErrorF("[config/dbus] couldn't append to iterator\n");
            return BadAlloc;
        }
        if (!dbus_message_iter_close_container(&iter, &subiter)) {
            ErrorF("[config/dbus] couldn't close container\n");
            return BadAlloc;
        }
    }

    return Success;
}

static int
get_version(DBusMessage *message, DBusMessage *reply, DBusError *error)
{
    DBusMessageIter iter;
    unsigned int version = API_VERSION;

    dbus_message_iter_init_append(reply, &iter);
    if (!dbus_message_iter_append_basic(&iter, DBUS_TYPE_UINT32, &version)) {
        ErrorF("[config/dbus] couldn't append version\n");
        return BadAlloc;
    }

    return Success;
}

static DBusHandlerResult
message_handler(DBusConnection *connection, DBusMessage *message, void *data)
{
    DBusError error;
    DBusMessage *reply;
    struct connection_info *info = data;

    /* ret is the overall D-Bus handler result, whereas err is the internal
     * X error from our individual functions. */
    int ret = DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
    int err;

    DebugF("[config/dbus] received a message for %s\n",
           dbus_message_get_interface(message));

    dbus_error_init(&error);

    reply = dbus_message_new_method_return(message);
    if (!reply) {
        ErrorF("[config/dbus] failed to create reply\n");
        ret = DBUS_HANDLER_RESULT_NEED_MEMORY;
        goto err_start;
    }

    if (strcmp(dbus_message_get_member(message), "add") == 0)
        err = add_device(message, reply, &error);
    else if (strcmp(dbus_message_get_member(message), "remove") == 0)
        err = remove_device(message, reply, &error);
    else if (strcmp(dbus_message_get_member(message), "listDevices") == 0)
        err = list_devices(message, reply, &error);
    else if (strcmp(dbus_message_get_member(message), "version") == 0)
        err = get_version(message, reply, &error);
    else
        goto err_reply;

    /* Failure to allocate is a special case. */
    if (err == BadAlloc) {
        ret = DBUS_HANDLER_RESULT_NEED_MEMORY;
        goto err_reply;
    }

    /* While failure here is always an OOM, we don't return that,
     * since that would result in devices being double-added/removed. */
    if (dbus_connection_send(info->connection, reply, NULL))
        dbus_connection_flush(info->connection);
    else
        ErrorF("[config/dbus] failed to send reply\n");

    ret = DBUS_HANDLER_RESULT_HANDLED;

err_reply:
    dbus_message_unref(reply);
err_start:
    dbus_error_free(&error);

    return ret;
}

static void
connect_hook(DBusConnection *connection, void *data)
{
    DBusError error;
    DBusObjectPathVTable vtable = { .message_function = message_handler, };
    struct connection_info *info = data;

    info->connection = connection;

    dbus_error_init(&error);

    dbus_bus_request_name(info->connection, info->busname, 0, &error);
    if (dbus_error_is_set(&error)) {
        ErrorF("[config/dbus] couldn't take over org.x.config: %s (%s)\n",
               error.name, error.message);
        goto err_start;
    }

    /* blocks until we get a reply. */
    dbus_bus_add_match(info->connection, MATCH_RULE, &error);
    if (dbus_error_is_set(&error)) {
        ErrorF("[config/dbus] couldn't add match: %s (%s)\n", error.name,
               error.message);
        goto err_name;
    }

    if (!dbus_connection_register_object_path(info->connection,
                                              info->busobject, &vtable,
                                              info)) {
        ErrorF("[config/dbus] couldn't register object path\n");
        goto err_match;
    }

    DebugF("[dbus] registered %s, %s\n", info->busname, info->busobject);

    dbus_error_free(&error);

    return;

err_match:
    dbus_bus_remove_match(info->connection, MATCH_RULE, &error);
err_name:
    dbus_bus_release_name(info->connection, info->busname, &error);
err_start:
    dbus_error_free(&error);

    reset_info(info);
}

static void
disconnect_hook(void *data)
{
}

#if 0
void
pre_disconnect_hook(void)
{
    DBusError error;

    dbus_error_init(&error);
    dbus_connection_unregister_object_path(connection_data->connection,
                                           connection_data->busobject);
    dbus_bus_remove_match(connection_data->connection, MATCH_RULE,
                          &error);
    dbus_bus_release_name(connection_data->connection,
                          connection_data->busname, &error);
    dbus_error_free(&error);
}
#endif

static struct connection_info connection_data;
static struct config_dbus_core_hook core_hook = {
    .connect = connect_hook,
    .disconnect = disconnect_hook,
    .data = &connection_data,
};

int
config_dbus_init(void)
{
    snprintf(connection_data.busname, sizeof(connection_data.busname),
             "org.x.config.display%d", atoi(display));
    snprintf(connection_data.busobject, sizeof(connection_data.busobject),
             "/org/x/config/%d", atoi(display));

    return config_dbus_core_add_hook(&core_hook);
}

void
config_dbus_fini(void)
{
    config_dbus_core_remove_hook(&core_hook);
    connection_data.busname[0] = '\0';
    connection_data.busobject[0] = '\0';
}