/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/*
** Copyright 2006, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
**     http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/

#include "BluetoothUtils.h"

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

#include "nsAutoPtr.h"
#include "nsThreadUtils.h"
#include "nsDebug.h"
#include "nsClassHashtable.h"
#include "mozilla/ipc/DBusUtils.h"
#include "mozilla/ipc/RawDBusConnection.h"


using namespace mozilla::ipc;

namespace mozilla {
namespace dom {
namespace bluetooth {

static nsAutoPtr<RawDBusConnection> sDBusConnection;

#undef LOG
#if defined(MOZ_WIDGET_GONK)
#include <android/log.h>
#define LOG(args...)  __android_log_print(ANDROID_LOG_INFO, "GonkDBus", args);
#else
#define BTDEBUG true
#define LOG(args...) if (BTDEBUG) printf(args);
#endif

#define DBUS_ADAPTER_IFACE BLUEZ_DBUS_BASE_IFC ".Adapter"
#define DBUS_DEVICE_IFACE BLUEZ_DBUS_BASE_IFC ".Device"
#define BLUEZ_DBUS_BASE_PATH      "/org/bluez"
#define BLUEZ_DBUS_BASE_IFC       "org.bluez"
#define BLUEZ_ERROR_IFC           "org.bluez.Error"

static const char* BLUETOOTH_DBUS_SIGNALS[] =
{
  "type='signal',interface='org.freedesktop.DBus'",
  "type='signal',interface='org.bluez.Adapter'",
  "type='signal',interface='org.bluez.Manager'",
  "type='signal',interface='org.bluez.Device'",
  "type='signal',interface='org.bluez.Input'",
  "type='signal',interface='org.bluez.Network'",
  "type='signal',interface='org.bluez.NetworkServer'",
  "type='signal',interface='org.bluez.HealthDevice'",
  "type='signal',interface='org.bluez.AudioSink'"
};

typedef nsClassHashtable<nsCStringHashKey, BluetoothEventObserverList >
        BluetoothEventObserverTable;
static nsAutoPtr<BluetoothEventObserverTable> sBluetoothEventObserverTable;

nsresult
RegisterBluetoothEventHandler(const nsCString& aNodeName,
                              BluetoothEventObserver* aHandler)
{
  MOZ_ASSERT(NS_IsMainThread());
  BluetoothEventObserverList *ol;

  NS_ENSURE_TRUE(sBluetoothEventObserverTable, NS_ERROR_FAILURE);
  if (!sBluetoothEventObserverTable->Get(aNodeName, &ol)) {
    sBluetoothEventObserverTable->Put(aNodeName,
                                      new BluetoothEventObserverList());
  }
  sBluetoothEventObserverTable->Get(aNodeName, &ol);
  ol->AddObserver(aHandler);
  return NS_OK;
}

nsresult
UnregisterBluetoothEventHandler(const nsCString& aNodeName,
                                BluetoothEventObserver* aHandler)
{
  MOZ_ASSERT(NS_IsMainThread());
  BluetoothEventObserverList *ol;

  NS_ENSURE_TRUE(sBluetoothEventObserverTable, NS_ERROR_FAILURE);
  if (!sBluetoothEventObserverTable->Get(aNodeName, &ol)) {
    NS_WARNING("Node does not exist to remove BluetoothEventListener from!");
    return NS_ERROR_FAILURE;
  }
  sBluetoothEventObserverTable->Get(aNodeName, &ol);  
  ol->RemoveObserver(aHandler);
  if (ol->Length() == 0) {
    sBluetoothEventObserverTable->Remove(aNodeName);
  }
  return NS_OK;
}

struct DistributeDBusMessageTask : public nsRunnable {

  DistributeDBusMessageTask(DBusMessage* aMsg) : mMsg(aMsg)
  {
  }
  
  NS_IMETHOD Run()
  {
    if (dbus_message_get_path(mMsg.get()) == NULL) {
      return NS_OK;
    }    
    MOZ_ASSERT(NS_IsMainThread());
    
    // Notify observers that a message has been sent
    nsDependentCString path(dbus_message_get_path(mMsg.get()));
    nsDependentCString member(dbus_message_get_member(mMsg.get()));
    BluetoothEventObserverList *ol;
    if (!sBluetoothEventObserverTable->Get(path, &ol)) {
      LOG("No objects registered for %s, returning\n",
          dbus_message_get_path(mMsg.get()));
      return NS_OK;
    }
    BluetoothEvent e;
    e.mEventName = member;
    ol->Broadcast(e);
    return NS_OK;
  }

  DBusMessageRefPtr mMsg;
};

// Called by dbus during WaitForAndDispatchEventNative()
// This function is called on the IOThread
static DBusHandlerResult
EventFilter(DBusConnection *aConn, DBusMessage *aMsg,
            void *aData)
{
  DBusError err;

  dbus_error_init(&err);

  if (dbus_message_get_type(aMsg) != DBUS_MESSAGE_TYPE_SIGNAL) {
    LOG("%s: not interested (not a signal).\n", __FUNCTION__);
    return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
  }

  LOG("%s: Received signal %s:%s from %s\n", __FUNCTION__,
      dbus_message_get_interface(aMsg), dbus_message_get_member(aMsg),
      dbus_message_get_path(aMsg));

  // TODO: Parse DBusMessage* on the IOThread and return as a BluetoothEvent so
  // we aren't passing the pointer at all, as well as offloading parsing (not
  // that it's that heavy.)
  nsCOMPtr<DistributeDBusMessageTask> t(new DistributeDBusMessageTask(aMsg));
  if (NS_FAILED(NS_DispatchToMainThread(t))) {
    NS_WARNING("Failed to dispatch to main thread!");
  }

  return DBUS_HANDLER_RESULT_HANDLED;
}

nsresult
StartBluetoothConnection()
{
  if (sDBusConnection) {
    NS_WARNING("DBusConnection already established, skipping");
    return NS_OK;    
  }
  sBluetoothEventObserverTable = new BluetoothEventObserverTable();
  sBluetoothEventObserverTable->Init(100);

  sDBusConnection = new RawDBusConnection();
  sDBusConnection->EstablishDBusConnection();
	
  // Add a filter for all incoming messages_base
  if (!dbus_connection_add_filter(sDBusConnection->mConnection, EventFilter,
                                  NULL, NULL)) {
    NS_WARNING("Cannot create DBus Event Filter for DBus Thread!");
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

nsresult
StopBluetoothConnection()
{
  if (!sDBusConnection) {
    NS_WARNING("DBusConnection does not exist, nothing to stop, skipping.");
    return NS_OK;
  }
  dbus_connection_remove_filter(sDBusConnection->mConnection, EventFilter, NULL);
  sDBusConnection = NULL;
  sBluetoothEventObserverTable->Clear();
  sBluetoothEventObserverTable = NULL;
  return NS_OK;
}

nsresult
GetDefaultAdapterPathInternal(nsCString& aAdapterPath)
{
  DBusMessage *msg = NULL, *reply = NULL;
  DBusError err;
  const char *device_path = NULL;
  int attempt = 0;

  for (attempt = 0; attempt < 1000 && reply == NULL; attempt ++) {
    msg = dbus_message_new_method_call("org.bluez", "/",
                                       "org.bluez.Manager", "DefaultAdapter");
    if (!msg) {
      LOG("%s: Can't allocate new method call for get_adapter_path!",
             __FUNCTION__);
      return NS_ERROR_FAILURE;
    }
    dbus_message_append_args(msg, DBUS_TYPE_INVALID);
    dbus_error_init(&err);
    reply = dbus_connection_send_with_reply_and_block(
      sDBusConnection->mConnection, msg, -1, &err);

    if (!reply) {
      if (dbus_error_is_set(&err)) {
        if (dbus_error_has_name(&err,
                                "org.freedesktop.DBus.Error.ServiceUnknown")) {
          // bluetoothd is still down, retry
          LOG("Service unknown\n");
          dbus_error_free(&err);
          //usleep(10000); // 10 ms
          continue;
        } else if (dbus_error_has_name(&err,
                                       "org.bluez.Error.NoSuchAdapter")) {
          LOG("No adapter found\n");
          dbus_error_free(&err);
          goto failed;
        } else {
          // Some other error we weren't expecting
          LOG("other error\n");
          dbus_error_free(&err);
        }
      }
    }
  }
  if (attempt == 1000) {
    LOG("timeout\n");
    //printfE("Time out while trying to get Adapter path, is bluetoothd up ?");
    goto failed;
  }

  if (!dbus_message_get_args(reply, &err, DBUS_TYPE_OBJECT_PATH,
                             &device_path, DBUS_TYPE_INVALID)
      || !device_path) {
    if (dbus_error_is_set(&err)) {
      dbus_error_free(&err);
    }
    goto failed;
  }
  dbus_message_unref(msg);
  aAdapterPath = nsDependentCString(device_path);
  return NS_OK;
failed:
  dbus_message_unref(msg);
  return NS_ERROR_FAILURE;
}

}
}
}
