/*
 * Copyright (C) 2007-2024 Kim Woelders
 *
 * 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 shall be included in
 * all copies of the Software, its documentation and marketing & publicity
 * materials, and acknowledgment shall be given in the documentation, materials
 * and software packages that this Software was used.
 *
 * 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 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.
 */
#include "config.h"

#include <dbus/dbus.h>

#include "E.h"
#include "edbus.h"
#include "events.h"
#include "ipc.h"
#include "xwin.h"

#define ENABLE_INTROSPECTION 1

#define DEBUG_DBUS 1
#if DEBUG_DBUS
#define Dprintf(fmt...)  if(EDebug(EDBUG_TYPE_DBUS))Eprintf(fmt)
#define D2printf(fmt...) if(EDebug(EDBUG_TYPE_DBUS)>1)Eprintf(fmt)
#else
#define Dprintf(fmt...)
#define D2printf(fmt...)
#endif

typedef struct {
    char           *name;
    DBusConnection *conn;
    DBusWatch      *watch;
    int             fd;
} DbusData;

static DbusData dbus_data;

static int      db_efd = 0;

static          dbus_bool_t
DbusWatchAdd(DBusWatch *watch, void *data __UNUSED__)
{
    dbus_data.watch = watch;
    dbus_data.fd = dbus_watch_get_unix_fd(watch);

    D2printf("DbusWatchAdd fd=%d flags=%d\n", dbus_data.fd,
             dbus_watch_get_flags(dbus_data.watch));

    return TRUE;
}

static void
DbusWatchRemove(DBusWatch *watch __UNUSED__, void *data __UNUSED__)
{
    D2printf("DbusWatchRemove\n");
}

#if 0                           /* Don't need this */
static void
DbusWatchToggle(DBusWatch *watch __UNUSED__, void *data __UNUSED__)
{
    D2printf("DbusWatchToggle\n");
}
#else
#define DbusWatchToggle NULL
#endif

static void
DbusReplyString(DBusConnection *conn, DBusMessage *msg, const char *str)
{
    DBusMessage    *reply;
    DBusMessageIter args;

    reply = dbus_message_new_method_return(msg);

    dbus_message_iter_init_append(reply, &args);
    if (!dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &str))
        goto done;
    if (!dbus_connection_send(conn, reply, NULL))
        goto done;
    dbus_connection_flush(conn);

  done:
    dbus_message_unref(reply);
}

static void
DbusIpcReply(void *data, const char *str)
{
    DBusMessage    *msg = (DBusMessage *) data;

    if (!str)
        str = "ok";
    DbusReplyString(dbus_data.conn, msg, str);
}

static void
DbusMethodCommand(DBusConnection *conn, DBusMessage *msg)
{
    DBusMessageIter args;
    const char     *param = "";

    if (!dbus_message_iter_init(msg, &args) ||
        dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_STRING)
    {
        DbusReplyString(conn, msg, "String arg required\n");
        return;
    }
    else
        dbus_message_iter_get_basic(&args, &param);

    IpcExecReply(param, DbusIpcReply, msg);
}

#if ENABLE_INTROSPECTION
/* *INDENT-OFF* */
static const char   dbus_introspect_data[] =
   "<!DOCTYPE node PUBLIC '-//freedesktop//DTD D-BUS Object Introspection 1.0//EN'\n"
   "'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd'>\n"
   "<node>\n"
   " <interface name='org.freedesktop.DBus.Introspectable'>\n"
   "  <method name='Introspect'>\n"
   "   <arg name='data' direction='out' type='s'/>\n"
   "  </method>\n"
   " </interface>\n"
   " <interface name='%s'>\n"
   "  <method name='Command'>\n"
   "   <arg name='data' direction='out' type='s'/>\n"
   "  </method>\n"
   "  <signal name='Signal'>\n"
   "   <arg name='data' direction='in' type='s'/>\n"
   "  </signal>\n"
   " </interface>\n"
   "</node>";
/* *INDENT-ON* */

static void
DbusMsgIntrospect(DBusConnection *conn, DBusMessage *msg)
{
    char            buf[1024];

    D2printf("Introspect\n");

    Esnprintf(buf, sizeof(buf), dbus_introspect_data, dbus_data.name);
    if (!strcmp(dbus_message_get_path(msg), "/"))
        DbusReplyString(conn, msg, buf);
    else
        DbusReplyString(conn, msg, "");
}
#endif

static          DBusHandlerResult
DbusMsgHandler(DBusConnection *conn, DBusMessage *msg,
               void *user_data __UNUSED__)
{
    int             msg_type;
    const char     *msg_dest, *msg_ifc, *msg_memb;

    D2printf("DbusMsgHandler\n");

    msg_type = dbus_message_get_type(msg);
    msg_dest = dbus_message_get_destination(msg);
    msg_ifc = dbus_message_get_interface(msg);
    msg_memb = dbus_message_get_member(msg);

    switch (msg_type)
    {
    default:                   /* Should not be possible */
        Dprintf("MESSAGE type=%d\n", msg_type);
        break;

    case DBUS_MESSAGE_TYPE_METHOD_CALL:
        Dprintf("METHOD %s %s\n", msg_ifc, msg_memb);
        if (strcmp(msg_dest, dbus_data.name))
            break;
        if (!strcmp(msg_ifc, "org.e16"))
        {
            if (!strcmp(msg_memb, "Command"))
                DbusMethodCommand(conn, msg);
            else
                DbusReplyString(conn, msg, "Error");
        }
#if ENABLE_INTROSPECTION
        else if (!strcmp(msg_ifc, "org.freedesktop.DBus.Introspectable"))
        {
            if (!strcmp(msg_memb, "Introspect"))
                DbusMsgIntrospect(conn, msg);
        }
#endif
        break;

    case DBUS_MESSAGE_TYPE_SIGNAL:
        Dprintf("SIGNAL %s %s\n", msg_ifc, msg_memb);
        if (!strcmp(msg_ifc, "org.e16"))
        {
            Dprintf("... for me!\n");
        }
        break;
    }

    D2printf("sender    = %s\n", dbus_message_get_sender(msg));
    D2printf("dest      = %s\n", dbus_message_get_destination(msg));
    D2printf("path      = %s\n", dbus_message_get_path(msg));
    D2printf("interface = %s\n", dbus_message_get_interface(msg));
    D2printf("member    = %s\n", dbus_message_get_member(msg));

    return DBUS_HANDLER_RESULT_HANDLED;
}

static void
DbusHandleFd(void)
{
    int             rc;

    D2printf("DbusHandleFd flags=%d\n", dbus_watch_get_flags(dbus_data.watch));
    dbus_watch_handle(dbus_data.watch,
                      DBUS_WATCH_READABLE | DBUS_WATCH_WRITABLE);

    for (;;)
    {
        D2printf("DbusHandleFd: Dispatch flags=%d\n",
                 dbus_watch_get_flags(dbus_data.watch));
        rc = dbus_connection_dispatch(dbus_data.conn);
        if (rc == DBUS_DISPATCH_COMPLETE)
            break;
    }
}

void
DbusInit(void)
{
    DBusError       dberr;
    int             err;
    char            buf[128];

    if (Mode.wm.window)
    {
        sprintf(buf, "org.e16.wm.p%u", (unsigned int)Mode.wm.pid);
    }
    else
    {
        const char     *s;

        s = strchr(Dpy.name, ':');
        if (!s)
            return;
        sprintf(buf, "org.e16.wm.d%ds%d", atoi(s + 1), Dpy.screen);
    }
    dbus_data.name = Estrdup(buf);
    Esetenv("ENL_DBUS_NAME", dbus_data.name);

    dbus_error_init(&dberr);

    dbus_data.fd = -1;

    dbus_data.conn = dbus_bus_get(DBUS_BUS_SESSION, &dberr);
    if (dbus_error_is_set(&dberr))
        goto bail_out;
    dbus_connection_set_exit_on_disconnect(dbus_data.conn, FALSE);

    err = dbus_bus_request_name(dbus_data.conn, dbus_data.name,
                                DBUS_NAME_FLAG_DO_NOT_QUEUE, &dberr);
    if (dbus_error_is_set(&dberr))
        goto bail_out;
    if (err != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER)
    {
        Eprintf("*** DbusInit error: Not Primary Owner (%d)\n", err);
        return;
    }

    Esnprintf(buf, sizeof(buf), "type='signal',destination='%s'",
              dbus_data.name);
    if (EDebug(EDBUG_TYPE_DBUS) > 1)    /* Catch all signals if extra debug is enabled */
        dbus_bus_add_match(dbus_data.conn, "type='signal'", &dberr);
    else
        dbus_bus_add_match(dbus_data.conn, buf, &dberr);
    if (dbus_error_is_set(&dberr))
        goto bail_out;
    Esnprintf(buf, sizeof(buf), "type='method_call',destination='%s'",
              dbus_data.name);
    dbus_bus_add_match(dbus_data.conn, buf, &dberr);
    if (dbus_error_is_set(&dberr))
        goto bail_out;
#if 0                           /* Debug */
    dbus_bus_add_match(dbus_data.conn, "type='method_return'", &dberr);
    if (dbus_error_is_set(&dberr))
        goto bail_out;
    dbus_bus_add_match(dbus_data.conn, "type='error'", &dberr);
    if (dbus_error_is_set(&dberr))
        goto bail_out;
#endif

    if (!dbus_connection_add_filter(dbus_data.conn, DbusMsgHandler, NULL, NULL))
        return;

    dbus_connection_set_watch_functions(dbus_data.conn,
                                        DbusWatchAdd, DbusWatchRemove,
                                        DbusWatchToggle, NULL, NULL);

    /* Handle pending D-Bus stuff */
    DbusHandleFd();
    db_efd = EventFdRegister(dbus_data.fd, DbusHandleFd);
    return;

  bail_out:
    if (dbus_error_is_set(&dberr))
    {
        Eprintf("*** DbusInit error: %s\n", dberr.message);
        dbus_error_free(&dberr);
    }
    return;
}

#if 0                           /* No need? */
void
DbusExit(void)
{
}
#endif

static int
_DbusSendCommand(DBusBusType bus, const char *dest, const char *path,
                 const char *meth, const char *args)
{
    int             err;
    DBusError       dberr;
    DBusConnection *conn;
    DBusMessage    *msg, *rpl;
    DBusMessageIter iter;
    const char     *iface = dest;

    err = -1;
    msg = rpl = NULL;
    dbus_error_init(&dberr);

    conn = dbus_bus_get(bus, &dberr);
    if (dbus_error_is_set(&dberr))
        goto quit;

    msg = dbus_message_new_method_call(dest, path, iface, meth);

    if (args)
    {
        const char     *s = args;
        int             nr, type, value;
        dbus_message_iter_init_append(msg, &iter);
        for (;;)
        {
            type = *s++;
            if (type == '\0')
                break;
            if (type == ';')
                continue;
            if (*s++ != '=')
                break;
            nr = -1;
            if (type == 'u')
            {
                sscanf(s, "%u%n", &value, &nr);
                if (nr <= 0)
                    goto quit;
                s += nr;
                if (!dbus_message_iter_append_basic(&iter,
                                                    DBUS_TYPE_UINT32, &value))
                    goto quit;
            }
            else
            {
                goto quit;      /* Unknown type */
            }
        }
    }

    rpl = dbus_connection_send_with_reply_and_block(conn, msg, 100, &dberr);
    if (!rpl)
        goto quit;

    dbus_connection_flush(conn);

    err = 0;

  quit:
    if (dbus_error_is_set(&dberr))
    {
        Eprintf("*** Dbus error: %s\n", dberr.message);
        dbus_error_free(&dberr);
    }

    if (msg)
        dbus_message_unref(msg);
    if (rpl)
        dbus_message_unref(rpl);

    return err;
}

int
DbusRequestLogout(void)
{
    return _DbusSendCommand(DBUS_BUS_SESSION, "org.gnome.SessionManager",
                            "/org/gnome/SessionManager", "Logout", "u=0");
}

int
DbusRequestShutdown(void)
{
    return _DbusSendCommand(DBUS_BUS_SESSION, "org.gnome.SessionManager",
                            "/org/gnome/SessionManager", "Shutdown", NULL);
}
