// Copyright (c) 2009 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "brillo/glib/dbus.h"

#include <dbus/dbus.h>
#include <dbus/dbus-glib-bindings.h>
#include <dbus/dbus-glib-lowlevel.h>

#include <base/logging.h>
#include <base/strings/stringprintf.h>

namespace brillo {
namespace dbus {

bool CallPtrArray(const Proxy& proxy,
                  const char* method,
                  glib::ScopedPtrArray<const char*>* result) {
  glib::ScopedError error;

  ::GType g_type_array = ::dbus_g_type_get_collection("GPtrArray",
                                                       DBUS_TYPE_G_OBJECT_PATH);


  if (!::dbus_g_proxy_call(proxy.gproxy(), method, &Resetter(&error).lvalue(),
                           G_TYPE_INVALID, g_type_array,
                           &Resetter(result).lvalue(), G_TYPE_INVALID)) {
    LOG(WARNING) << "CallPtrArray failed: "
        << (error->message ? error->message : "Unknown Error.");
    return false;
  }

  return true;
}

BusConnection GetSystemBusConnection() {
  glib::ScopedError error;
  ::DBusGConnection* result = ::dbus_g_bus_get(DBUS_BUS_SYSTEM,
                                               &Resetter(&error).lvalue());
  if (!result) {
    LOG(ERROR) << "dbus_g_bus_get(DBUS_BUS_SYSTEM) failed: "
               << ((error.get() && error->message) ?
                   error->message : "Unknown Error");
    return BusConnection(nullptr);
  }
  // Set to not exit when system bus is disconnected.
  // This fixes the problem where when the dbus daemon is stopped, exit is
  // called which kills Chrome.
  ::dbus_connection_set_exit_on_disconnect(
      ::dbus_g_connection_get_connection(result), FALSE);
  return BusConnection(result);
}

BusConnection GetPrivateBusConnection(const char* address) {
  // Since dbus-glib does not have an API like dbus_g_connection_open_private(),
  // we have to implement our own.

  // We have to call _dbus_g_value_types_init() to register standard marshalers
  // just like as dbus_g_bus_get() and dbus_g_connection_open() do, but the
  // function is not exported. So we call GetPrivateBusConnection() which calls
  // dbus_g_bus_get() here instead. Note that if we don't call
  // _dbus_g_value_types_init(), we might get "WARNING **: No demarshaller
  // registered for type xxxxx" error and might not be able to handle incoming
  // signals nor method calls.
  {
    BusConnection system_bus_connection = GetSystemBusConnection();
    if (!system_bus_connection.HasConnection()) {
      return system_bus_connection;  // returns NULL connection.
    }
  }

  ::DBusError error;
  ::dbus_error_init(&error);

  ::DBusGConnection* result = nullptr;
  ::DBusConnection* raw_connection
        = ::dbus_connection_open_private(address, &error);
  if (!raw_connection) {
    LOG(WARNING) << "dbus_connection_open_private failed: " << address;
    return BusConnection(nullptr);
  }

  if (!::dbus_bus_register(raw_connection, &error)) {
    LOG(ERROR) << "dbus_bus_register failed: "
               << (error.message ? error.message : "Unknown Error.");
    ::dbus_error_free(&error);
    // TODO(yusukes): We don't call dbus_connection_close() nor g_object_unref()
    // here for now since these calls might interfere with IBusBus connections
    // in libcros and Chrome. See the comment in ~InputMethodStatusConnection()
    // function in platform/cros/chromeos_input_method.cc for details.
    return BusConnection(nullptr);
  }

  ::dbus_connection_setup_with_g_main(
      raw_connection, nullptr /* default context */);

  // A reference count of |raw_connection| is transferred to |result|. You don't
  // have to (and should not) unref the |raw_connection|.
  result = ::dbus_connection_get_g_connection(raw_connection);
  CHECK(result);

  ::dbus_connection_set_exit_on_disconnect(
      ::dbus_g_connection_get_connection(result), FALSE);

  return BusConnection(result);
}

bool RetrieveProperties(const Proxy& proxy,
                        const char* interface,
                        glib::ScopedHashTable* result) {
  glib::ScopedError error;

  if (!::dbus_g_proxy_call(proxy.gproxy(), "GetAll", &Resetter(&error).lvalue(),
                           G_TYPE_STRING, interface, G_TYPE_INVALID,
                           ::dbus_g_type_get_map("GHashTable", G_TYPE_STRING,
                                                 G_TYPE_VALUE),
                           &Resetter(result).lvalue(), G_TYPE_INVALID)) {
    LOG(WARNING) << "RetrieveProperties failed: "
        << (error->message ? error->message : "Unknown Error.");
    return false;
  }
  return true;
}

Proxy::Proxy()
    : object_(nullptr) {
}

// Set |connect_to_name_owner| true if you'd like to use
// dbus_g_proxy_new_for_name_owner() rather than dbus_g_proxy_new_for_name().
Proxy::Proxy(const BusConnection& connection,
             const char* name,
             const char* path,
             const char* interface,
             bool connect_to_name_owner)
    : object_(GetGProxy(
        connection, name, path, interface, connect_to_name_owner)) {
}

// Equivalent to Proxy(connection, name, path, interface, false).
Proxy::Proxy(const BusConnection& connection,
             const char* name,
             const char* path,
             const char* interface)
    : object_(GetGProxy(connection, name, path, interface, false)) {
}

// Creates a peer proxy using dbus_g_proxy_new_for_peer.
Proxy::Proxy(const BusConnection& connection,
             const char* path,
             const char* interface)
    : object_(GetGPeerProxy(connection, path, interface)) {
}

Proxy::Proxy(const Proxy& x)
    : object_(x.object_) {
  if (object_)
    ::g_object_ref(object_);
}

Proxy::~Proxy() {
  if (object_)
    ::g_object_unref(object_);
}

/* static */
Proxy::value_type Proxy::GetGProxy(const BusConnection& connection,
                                   const char* name,
                                   const char* path,
                                   const char* interface,
                                   bool connect_to_name_owner) {
  value_type result = nullptr;
  if (connect_to_name_owner) {
    glib::ScopedError error;
    result = ::dbus_g_proxy_new_for_name_owner(connection.object_,
                                               name,
                                               path,
                                               interface,
                                               &Resetter(&error).lvalue());
    if (!result) {
      DLOG(ERROR) << "Failed to construct proxy: "
                  << (error->message ? error->message : "Unknown Error")
                  << ": " << path;
    }
  } else {
    result = ::dbus_g_proxy_new_for_name(connection.object_,
                                         name,
                                         path,
                                         interface);
    if (!result) {
      LOG(ERROR) << "Failed to construct proxy: " << path;
    }
  }
  return result;
}

/* static */
Proxy::value_type Proxy::GetGPeerProxy(const BusConnection& connection,
                                       const char* path,
                                       const char* interface) {
  value_type result = ::dbus_g_proxy_new_for_peer(connection.object_,
                                                  path,
                                                  interface);
  if (!result)
    LOG(ERROR) << "Failed to construct peer proxy: " << path;

  return result;
}

bool RegisterExclusiveService(const BusConnection& connection,
                              const char* interface_name,
                              const char* service_name,
                              const char* service_path,
                              GObject* object) {
  CHECK(object);
  CHECK(interface_name);
  CHECK(service_name);
  // Create a proxy to DBus itself so that we can request to become a
  // service name owner and then register an object at the related service path.
  Proxy proxy = brillo::dbus::Proxy(connection,
                                      DBUS_SERVICE_DBUS,
                                      DBUS_PATH_DBUS,
                                      DBUS_INTERFACE_DBUS);
  // Exclusivity is determined by replacing any existing
  // service, not queuing, and ensuring we are the primary
  // owner after the name is ours.
  glib::ScopedError err;
  guint result = 0;
  // TODO(wad) determine if we are moving away from using generated functions
  if (!org_freedesktop_DBus_request_name(proxy.gproxy(),
                                         service_name,
                                         0,
                                         &result,
                                         &Resetter(&err).lvalue())) {
    LOG(ERROR) << "Unable to request service name: "
               << (err->message ? err->message : "Unknown Error.");
    return false;
  }

  // Handle the error codes, releasing the name if exclusivity conditions
  // are not met.
  bool needs_release = false;
  if (result != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
    LOG(ERROR) << "Failed to become the primary owner. Releasing . . .";
    needs_release = true;
  }
  if (result == DBUS_REQUEST_NAME_REPLY_EXISTS) {
    LOG(ERROR) << "Service name exists: " << service_name;
    return false;
  } else if (result == DBUS_REQUEST_NAME_REPLY_IN_QUEUE) {
    LOG(ERROR) << "Service name request enqueued despite our flags. Releasing";
    needs_release = true;
  }
  LOG_IF(WARNING, result == DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER)
    << "Service name already owned by this process";
  if (needs_release) {
    if (!org_freedesktop_DBus_release_name(
           proxy.gproxy(),
           service_name,
           &result,
           &Resetter(&err).lvalue())) {
      LOG(ERROR) << "Unabled to release service name: "
                 << (err->message ? err->message : "Unknown Error.");
    }
    DLOG(INFO) << "ReleaseName returned code " << result;
    return false;
  }

  // Determine a path from the service name and register the object.
  dbus_g_connection_register_g_object(connection.g_connection(),
                                      service_path,
                                      object);
  return true;
}

void CallMethodWithNoArguments(const char* service_name,
                               const char* path,
                               const char* interface_name,
                               const char* method_name) {
  Proxy proxy(dbus::GetSystemBusConnection(),
              service_name,
              path,
              interface_name);
  ::dbus_g_proxy_call_no_reply(proxy.gproxy(), method_name, G_TYPE_INVALID);
}

void SignalWatcher::StartMonitoring(const std::string& interface,
                                    const std::string& signal) {
  DCHECK(interface_.empty()) << "StartMonitoring() must be called only once";
  interface_ = interface;
  signal_ = signal;

  // Snoop on D-Bus messages so we can get notified about signals.
  DBusConnection* dbus_conn = dbus_g_connection_get_connection(
      GetSystemBusConnection().g_connection());
  DCHECK(dbus_conn);

  DBusError error;
  dbus_error_init(&error);
  dbus_bus_add_match(dbus_conn, GetDBusMatchString().c_str(), &error);
  if (dbus_error_is_set(&error)) {
    LOG(DFATAL) << "Got error while adding D-Bus match rule: " << error.name
                << " (" << error.message << ")";
  }

  if (!dbus_connection_add_filter(dbus_conn,
                                  &SignalWatcher::FilterDBusMessage,
                                  this,        // user_data
                                  nullptr)) {  // free_data_function
    LOG(DFATAL) << "Unable to add D-Bus filter";
  }
}

SignalWatcher::~SignalWatcher() {
  if (interface_.empty())
    return;

  DBusConnection* dbus_conn = dbus_g_connection_get_connection(
      dbus::GetSystemBusConnection().g_connection());
  DCHECK(dbus_conn);

  dbus_connection_remove_filter(dbus_conn,
                                &SignalWatcher::FilterDBusMessage,
                                this);

  DBusError error;
  dbus_error_init(&error);
  dbus_bus_remove_match(dbus_conn, GetDBusMatchString().c_str(), &error);
  if (dbus_error_is_set(&error)) {
    LOG(DFATAL) << "Got error while removing D-Bus match rule: " << error.name
                << " (" << error.message << ")";
  }
}

std::string SignalWatcher::GetDBusMatchString() const {
  return base::StringPrintf("type='signal', interface='%s', member='%s'",
                            interface_.c_str(), signal_.c_str());
}

/* static */
DBusHandlerResult SignalWatcher::FilterDBusMessage(DBusConnection* dbus_conn,
                                                   DBusMessage* message,
                                                   void* data) {
  SignalWatcher* self = static_cast<SignalWatcher*>(data);
  if (dbus_message_is_signal(
          message, self->interface_.c_str(), self->signal_.c_str())) {
    self->OnSignal(message);
    return DBUS_HANDLER_RESULT_HANDLED;
  } else {
    return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
  }
}

}  // namespace dbus
}  // namespace brillo