/*
 * Copyright (C) 2011 Intel Corporation
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) version 3.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

#include "gdbus-cxx-bridge.h"
#include <gio/gunixinputstream.h>
#include <gio/gunixoutputstream.h>
#include <stdio.h>
#include <syncevo/GuardFD.h>
#include <syncevo/GLibSupport.h>
#include <syncevo/util.h>

#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>

SE_GOBJECT_TYPE(GIOStream)
SE_GOBJECT_TYPE(GInputStream)
SE_GOBJECT_TYPE(GOutputStream)

void intrusive_ptr_add_ref(GDBusConnection *con)  { g_object_ref(con); }
void intrusive_ptr_release(GDBusConnection *con)  { g_object_unref(con); }
void intrusive_ptr_add_ref(GDBusMessage    *msg)  { g_object_ref(msg); }
void intrusive_ptr_release(GDBusMessage    *msg)  { g_object_unref(msg); }

namespace GDBusCXX {

using namespace SyncEvo;

MethodHandler::MethodMap MethodHandler::m_methodMap;
std::function<void (void)> MethodHandler::m_callback;

void appendArgInfo(GPtrArray *pa, const std::string &type)
{
    // Empty if not used in the current direction (in or out),
    // ignore then.
    if (type.empty()) {
        // TODO: replace runtime check with compile-time check
        // via type specialization... not terribly important, though.
        return;
    }

    GDBusArgInfo *argInfo = g_new0(GDBusArgInfo, 1);
    argInfo->signature = g_strdup(type.c_str());
    argInfo->ref_count = 1;
    g_ptr_array_add(pa, argInfo);
}

struct OwnNameAsyncData
{
    enum State {
        OWN_NAME_WAITING,
        OWN_NAME_OBTAINED,
        OWN_NAME_LOST
    };

    OwnNameAsyncData(const std::string &name,
                     const std::function<void (bool)> &obtainedCB) :
        m_name(name),
        m_obtainedCB(obtainedCB),
        m_state(OWN_NAME_WAITING)
    {}

    static void busNameAcquired(GDBusConnection *connection,
                                const gchar *name,
                                gpointer userData) throw ()
    {
        std::shared_ptr<OwnNameAsyncData> *data = static_cast< std::shared_ptr<OwnNameAsyncData> *>(userData);
        (*data)->m_state = OWN_NAME_OBTAINED;
        try {
            g_debug("got D-Bus name %s", name);
            if ((*data)->m_obtainedCB) {
                (*data)->m_obtainedCB(true);
            }
        } catch (...) {
            (*data)->m_state = OWN_NAME_LOST;
        }
    }

    static void busNameLost(GDBusConnection *connection,
                            const gchar *name,
                            gpointer userData) throw ()
    {
        std::shared_ptr<OwnNameAsyncData> *data = static_cast< std::shared_ptr<OwnNameAsyncData> *>(userData);
        (*data)->m_state = OWN_NAME_LOST;
        try {
            g_debug("lost %s %s",
                    connection ? "D-Bus connection for name" :
                    "D-Bus name",
                    name);
            if ((*data)->m_obtainedCB) {
                (*data)->m_obtainedCB(false);
            }
        } catch (...) {
        }
    }

    static std::shared_ptr<OwnNameAsyncData> ownName(GDBusConnection *conn,
                                                       const std::string &name,
                                                       std::function<void (bool)> obtainedCB =
                                                       std::function<void (bool)>()) {
        auto data = std::make_shared<OwnNameAsyncData>(name, obtainedCB);
        auto free_data = [] (gpointer userData) noexcept {
            delete static_cast< std::shared_ptr<OwnNameAsyncData> *>(userData);
        };
        g_bus_own_name_on_connection(conn,
                                     data->m_name.c_str(),
                                     G_BUS_NAME_OWNER_FLAGS_NONE,
                                     OwnNameAsyncData::busNameAcquired,
                                     OwnNameAsyncData::busNameLost,
                                     new std::shared_ptr<OwnNameAsyncData>(data),
                                     free_data);
        return data;
    }

    const std::string m_name;
    const std::function<void (bool)> m_obtainedCB;
    State m_state;
};

void DBusConnectionPtr::undelay() const
{
    if (!m_name.empty()) {
        g_debug("starting to acquire D-Bus name %s", m_name.c_str());
        std::shared_ptr<OwnNameAsyncData> data = OwnNameAsyncData::ownName(get(),
                                                                             m_name);
        while (data->m_state == OwnNameAsyncData::OWN_NAME_WAITING) {
            g_main_context_iteration(nullptr, true);
        }
        g_debug("done with acquisition of %s", m_name.c_str());
        if (data->m_state == OwnNameAsyncData::OWN_NAME_LOST) {
            throw std::runtime_error("could not obtain D-Bus name - already running?");
        }
    }
    g_dbus_connection_start_message_processing(get());
}

void DBusConnectionPtr::ownNameAsync(const std::string &name,
                                     const std::function<void (bool)> &obtainedCB) const
{
    OwnNameAsyncData::ownName(get(), name, obtainedCB);
}

DBusConnectionPtr dbus_get_bus_connection(const char *busType,
                                          const char *name,
                                          bool unshared,
                                          DBusErrorCXX *err)
{
    DBusConnectionPtr conn;
    GError* error = nullptr;
    GBusType type =
        boost::iequals(busType, "SESSION") ?
        G_BUS_TYPE_SESSION :
        G_BUS_TYPE_SYSTEM;

    if (unshared) {
        char *address = g_dbus_address_get_for_bus_sync(type,
                                                        nullptr, &error);
        if(address == nullptr) {
            if (err) {
                err->set(error);
            }
            return nullptr;
        }
        // Here we set up a private client connection using the chosen bus' address.
        conn = DBusConnectionPtr(g_dbus_connection_new_for_address_sync(address,
                                                                        (GDBusConnectionFlags)
                                                                        (G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
                                                                         G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION),
                                                                        nullptr, nullptr, &error),
                                 false);
        g_free(address);

        if(conn == nullptr) {
            if (err) {
                err->set(error);
            } else {
                g_clear_error(&error);
            }
            return nullptr;
        }
    } else {
        // This returns a singleton, shared connection object.
        conn = DBusConnectionPtr(g_bus_get_sync(type,
                                                nullptr, &error),
                                 false);
        if(conn == nullptr) {
            if (err) {
                err->set(error);
            } else {
                g_clear_error(&error);
            }
            return nullptr;
        }
    }

    if (name) {
        // Request name later in undelay(), after the caller
        // had a chance to add objects.
        conn.addName(name);
        // Acting as client, need to stop when D-Bus daemon dies.
        g_dbus_connection_set_exit_on_close(conn.get(), TRUE);
    }

    return conn;
}

static GIOStreamCXX new_stream(int fd)
{
    GOutputStreamCXX out(g_unix_output_stream_new(fd, true), TRANSFER_REF);
    GInputStreamCXX in(g_unix_input_stream_new(fd, false), TRANSFER_REF);
    GIOStreamCXX stream(g_simple_io_stream_new(in.get(), out.get()), TRANSFER_REF);
    return stream;
}

DBusConnectionPtr dbus_get_bus_connection(const std::string &address,
                                          DBusErrorCXX *err)
{
    // "address" needs to be the file descriptor number set up
    // by DBusServerCXX::listen().
    GuardFD fd(atoi(address.c_str()));
    GIOStreamCXX stream(new_stream(fd));
    fd.release();

    GErrorCXX gerror;
    GDBusCXX::DBusConnectionPtr
        conn(g_dbus_connection_new_sync(stream.get(),
                                        nullptr,
                                        G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING,
                                        nullptr,
                                        nullptr,
                                        gerror));
    if (!conn && err) {
        err->set(gerror.release());
    }
    return conn;
}

static void ConnectionLost(GDBusConnection *connection,
                           gboolean remotePeerVanished,
                           GError *error,
                           gpointer data)
{
    DBusConnectionPtr::Disconnect_t *cb = static_cast<DBusConnectionPtr::Disconnect_t *>(data);
    (*cb)();
}

static void DestroyDisconnect(gpointer data,
                              GClosure *closure)
                           {
    DBusConnectionPtr::Disconnect_t *cb = static_cast<DBusConnectionPtr::Disconnect_t *>(data);
    delete cb;
}

void DBusConnectionPtr::flush()
{
    // ignore errors
    g_dbus_connection_flush_sync(get(), nullptr, nullptr);
}

void DBusConnectionPtr::setDisconnect(const Disconnect_t &func)
{
    g_signal_connect_closure(get(),
                             "closed",
                             g_cclosure_new(G_CALLBACK(ConnectionLost),
                                            new Disconnect_t(func),
                                            DestroyDisconnect),
                             true);
}

std::shared_ptr<DBusServerCXX> DBusServerCXX::listen(const NewConnection_t &newConnection, DBusErrorCXX *)
{
    // Create two fds connected via a two-way stream. The parent
    // keeps fd[0] which gets closed automatically when the child
    // execs. The parent closes the child's fd[1] after forking.
    int fds[2];
    int retval = socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, fds);
    if (retval) {
        SE_THROW(StringPrintf("socketpair: %s", strerror(errno)));
    }
    GuardFD parentfd(fds[0]);
    GuardFD childfd(fds[1]);

    // Child must inherit its fd.
    int fdflags;
    if ((fdflags = fcntl(childfd, F_GETFD)) == -1 ||
        fcntl(childfd, F_SETFD, fdflags & ~FD_CLOEXEC)) {
        SE_THROW(StringPrintf("fcntl: %s", strerror(errno)));
    }

    // Out listen "address" is the FD number.
    std::string address = StringPrintf("%d", childfd.get());

    // Transfer ownership of parent fd.
    GIOStreamCXX stream(new_stream(parentfd));
    parentfd.release();

    GErrorCXX gerror;
    GDBusCXX::DBusConnectionPtr
        connection(g_dbus_connection_new_sync(stream.get(),
                                              nullptr,
                                              G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING,
                                              nullptr,
                                              nullptr,
                                              gerror),
                   false);
    if (!connection) {
        gerror.throwError(SE_HERE, "creating GIO D-Bus connection");
    }

    // A fake DBusServerCXX which does nothing more than return the address, aka
    // our FD number, and store data for the idle callback.
    // Private constructor, can't use make_shared() here.
    std::shared_ptr<DBusServerCXX> res(new DBusServerCXX(address));
    res->m_newConnection = newConnection;
    res->m_connection = connection;
    // Will be freed in the idle callback. Caller must have forked by then.
    res->m_childfd = childfd.release();

    // The caller must have some time to set up connection handling and fork.
    // Delay the newConnection() callback until we enter the main event loop
    // again. Callback must be removed when destructing prematurely because it
    // has a plain "this" pointer.
    res->m_connectionIdle = g_idle_add(onIdleOnce, &*res);

    return res;
}

DBusServerCXX::DBusServerCXX(const std::string &address) :
    m_connectionIdle(0),
    m_childfd(-1),
    m_address(address)
{
}

DBusServerCXX::~DBusServerCXX()
{
    if (m_connectionIdle) {
        g_source_remove(m_connectionIdle);
    }
    if (m_childfd >= 0) {
        close(m_childfd);
    }
}

gboolean DBusServerCXX::onIdleOnce(gpointer custom)
{
    DBusServerCXX *me = static_cast<DBusServerCXX *>(custom);
    me->m_connectionIdle = 0;
    me->m_newConnection(*me, me->m_connection);
    me->m_connection.reset();
    close(me->m_childfd);
    me->m_childfd = -1;

    // not again
    return false;
}




void Watch::nameOwnerChanged(GDBusConnection *connection,
                             const gchar *sender_name,
                             const gchar *object_path,
                             const gchar *interface_name,
                             const gchar *signal_name,
                             GVariant *parameters,
                             gpointer user_data)
{
    Watch *watch = static_cast<Watch *>(user_data);
    if (!watch->m_called) {
        gchar *name = nullptr, *oldOwner = nullptr, *newOwner = nullptr;
        g_variant_get(parameters, "(sss)", &name, &oldOwner, &newOwner);
        bool matches = name && watch->m_peer == name &&
            newOwner && !*newOwner;
        g_free(name);
        g_free(oldOwner);
        g_free(newOwner);
        if (matches) {
            watch->disconnected();
        }
    }
}

void Watch::disconnected()
{
    if (!m_called) {
        m_called = true;
        if (m_callback) {
            m_callback();
        }
    }
}

Watch::Watch(const DBusConnectionPtr &conn,
                     const std::function<void (void)> &callback) :
    m_conn(conn),
    m_callback(callback),
    m_called(false),
    m_watchID(0)
{
}

void Watch::setCallback(const std::function<void (void)> &callback)
{
    m_callback = callback;
    if (m_called && m_callback) {
        m_callback();
    }
}

void Watch::activate(const char *peer)
{
    if (!peer) {
        throw std::runtime_error("Watch::activate(): no peer");
    }
    m_peer = peer;

    // Install watch first ...
    m_watchID = g_dbus_connection_signal_subscribe(m_conn.get(),
                                                   nullptr, // TODO org.freedesktop.DBus?
                                                   "org.freedesktop.DBus",
                                                   "NameOwnerChanged",
                                                   "/org/freedesktop/DBus",
                                                   nullptr,
                                                   G_DBUS_SIGNAL_FLAGS_NONE,
                                                   nameOwnerChanged,
                                                   this,
                                                   nullptr);
    if (!m_watchID) {
        throw std::runtime_error("g_dbus_connection_signal_subscribe(): NameLost failed");
    }

    // ... then check that the peer really exists,
    // otherwise we'll never notice the disconnect.
    // If it disconnects while we are doing this,
    // then disconnect() will be called twice,
    // but it handles that.
    GError *error = nullptr;

    GVariant *result = g_dbus_connection_call_sync(m_conn.get(),
                                                   "org.freedesktop.DBus",
                                                   "/org/freedesktop/DBus",
                                                   "org.freedesktop.DBus",
                                                   "NameHasOwner",
                                                   g_variant_new("(s)", peer),
                                                   G_VARIANT_TYPE("(b)"),
                                                   G_DBUS_CALL_FLAGS_NONE,
                                                   -1, // default timeout
                                                   nullptr,
                                                   &error);

    if (result != nullptr) {
        bool actual_result = false;

        g_variant_get(result, "(b)", &actual_result);
        if (!actual_result) {
            disconnected();
        }
    } else {
        std::string error_message(error->message);
        g_error_free(error);
        std::string err_msg("g_dbus_connection_call_sync(): NameHasOwner - ");
        throw std::runtime_error(err_msg + error_message);
    }
}

Watch::~Watch()
{
    if (m_watchID) {
        g_dbus_connection_signal_unsubscribe(m_conn.get(), m_watchID);
        m_watchID = 0;
    }
}

void getWatch(ExtractArgs &context,
              std::shared_ptr<Watch> &value)
{
    std::unique_ptr<Watch> watch(new Watch(context.m_conn));
    watch->activate((context.m_msg && *context.m_msg) ?
                    g_dbus_message_get_sender(*context.m_msg) :
                    context.m_sender);
    value.reset(watch.release());
}

void ExtractArgs::init(GDBusConnection *conn,
                       GDBusMessage **msg,
                       GVariant *msgBody,
                       const char *sender,
                       const char *path,
                       const char *interface,
                       const char *signal)
{
    m_conn = conn;
    m_msg = msg;
    m_sender = sender;
    m_path = path;
    m_interface = interface;
    m_signal = signal;
    if (msgBody != nullptr) {
        g_variant_iter_init(&m_iter, msgBody);
    }
}


ExtractArgs::ExtractArgs(GDBusConnection *conn, GDBusMessage *&msg)
{
    init(conn, &msg, g_dbus_message_get_body(msg), nullptr, nullptr, nullptr, nullptr);
}

ExtractArgs::ExtractArgs(GDBusConnection *conn,
                         const char *sender,
                         const char *path,
                         const char *interface,
                         const char *signal,
                         GVariant *params)
{
    init(conn, nullptr, params, sender, path, interface, signal);
}

ExtractResponse::ExtractResponse(GDBusConnection *conn, GDBusMessage *msg)
{
    init(conn, nullptr, g_dbus_message_get_body(msg),
         g_dbus_message_get_sender(msg), nullptr, nullptr, nullptr);
}

} // namespace GDBusCXX
