/*******************************************************************************
*                         Goggles Music Manager                                *
********************************************************************************
*           Copyright (C) 2007-2010 by Sander Jansen. All Rights Reserved      *
*                               ---                                            *
* This program is free software: you can redistribute it and/or modify         *
* it under the terms of the GNU General Public License as published by         *
* the Free Software Foundation, either version 3 of the License, or            *
* (at your option) any later version.                                          *
*                                                                              *
* This program 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 General Public License for more details.                                 *
*                                                                              *
* You should have received a copy of the GNU General Public License            *
* along with this program.  If not, see http://www.gnu.org/licenses.           *
********************************************************************************/

#ifdef HAVE_DBUS
#include "common.h"
#include "fxdbus.h"
#include "FXThread.h"

#ifndef DBUS_MAJOR_VERSION
#define DBUS_MAJOR_VERSION 1
#endif

#ifndef DBUS_MINOR_VERSION
#define DBUS_MINOR_VERSION 0
#endif

#ifndef DBUS_MICRO_VERSION
#define DBUS_MICRO_VERSION 0
#endif

#define DBUSVERSION ((DBUS_MICRO_VERSION) + (DBUS_MINOR_VERSION*1000) + (DBUS_MAJOR_VERSION*100000))
#define MKDBUSVERSION(major,minor,release) ((release)+(minor*1000)+(major*100000))


/*******************************************************************************************************/
/* Thread Support Wrappers                                                                             */
/*******************************************************************************************************/
/*
static DBusMutex * fxdbus_mutex_new() {
    return reinterpret_cast<DBusMutex*>(new FXMutex());
    }

static void fxdbus_mutex_free(DBusMutex* m) {
    FXMutex * mutex = reinterpret_cast<FXMutex*>(m);
    delete mutex;
    }

static dbus_bool_t fxdbus_mutex_lock(DBusMutex* m) {
    FXMutex * mutex = reinterpret_cast<FXMutex*>(m);
    return mutex->trylock();
    }

static dbus_bool_t fxdbus_mutex_unlock(DBusMutex* m) {
    FXMutex * mutex = reinterpret_cast<FXMutex*>(m);
    mutex->unlock();
    return TRUE;
    }

static DBusCondVar * fxdbus_condition_new() {
    return reinterpret_cast<DBusCondVar*>(new FXCondition());
    }

static void fxdbus_condition_free(DBusCondVar* c) {
    FXCondition * condition = reinterpret_cast<FXCondition*>(c);
    delete condition;
    }

static void fxdbus_condition_wait(DBusCondVar* c,DBusMutex * m) {
    FXCondition * condition = reinterpret_cast<FXCondition*>(c);
    FXMutex     * mutex     = reinterpret_cast<FXMutex*>(m);
    condition->wait(*mutex);
    }

static dbus_bool_t fxdbus_condition_wait_timeout(DBusCondVar* c,DBusMutex * m,int timeout_msec) {
    FXCondition * condition = reinterpret_cast<FXCondition*>(c);
    FXMutex     * mutex     = reinterpret_cast<FXMutex*>(m);
    return condition->wait(*mutex,TIME_MSEC(timeout_msec));
    }

static void fxdbus_condition_wake_one(DBusCondVar* c) {
    FXCondition * condition = reinterpret_cast<FXCondition*>(c);
    condition->signal();
    }

static void fxdbus_condition_wake_all(DBusCondVar* c) {
    FXCondition * condition = (FXCondition*)c;
    condition->broadcast();
    }
*/
/*******************************************************************************************************/
/* GLOBAL INIT                                                                                         */
/*******************************************************************************************************/

class FXDBusTimeout;

class FXDBusGlobal {
public:
  FXHash  tm;
private:
  FXMutex mutex;
  FXHash  connections;
#if FOXVERSION < FXVERSION(1,7,0)
  FXHash  watches;
#endif
public:
  FXDBusGlobal() {
/*
    DBusThreadFunctions functions={
      DBUS_THREAD_FUNCTIONS_ALL_MASK,
      fxdbus_mutex_new,
      fxdbus_mutex_free,
      fxdbus_mutex_lock,
      fxdbus_mutex_unlock,
      fxdbus_condition_new,
      fxdbus_condition_free,
      fxdbus_condition_wait,
      fxdbus_condition_wait_timeout,
      fxdbus_condition_wake_one,
      fxdbus_condition_wake_all,
      fxdbus_recursive_mutex_new,
      fxdbus_recursive_mutex_free,
      fxdbus_recursive_mutex_lock,
      fxdbus_recursive_mutex_unlock,
      0,0,0,0
      };
*/
		dbus_threads_init_default();
    //dbus_threads_init(&functions);
    }

  FXDBusConnection * find(DBusConnection* dc) {
    FXMutexLock lock(mutex);
    return (FXDBusConnection*) connections.find(dc);
    }

  void setuphooks() {
    //FXMutexLock lock(mutex); // setup_event_loop calls into FXDbBusGlobal....
#if FOXVERSION < FXVERSION(1,7,0)
    for (FXint i=0;i<connections.size();i++) {
#else
    for (FXuint i=0;i<connections.size();i++) {
#endif
      if (!connections.empty(i)) {
        ((FXDBusConnection*)connections.value(i))->setup_event_loop();
        }
      }
    }

  void insert(DBusConnection * dc,FXDBusConnection * fxdc) {
    FXMutexLock lock(mutex);
    connections.insert(dc,fxdc);
    }

	void remove(DBusConnection * dc) {
    FXMutexLock lock(mutex);
    connections.remove(dc);
		}
#if FOXVERSION < FXVERSION(1,7,0)

  DBusWatch * find(FXint fd) {
    FXMutexLock lock(mutex);
    return (DBusWatch*)watches.find((void*)(FXival)fd);
    }

  void insert(FXint fd,DBusWatch * watch){
    FXMutexLock lock(mutex);
    watches.insert((void*)(FXival)fd,watch);
    }

  void remove(FXint fd){
    FXMutexLock lock(mutex);
    watches.remove((void*)(FXival)fd);
    }
#endif

  FXDBusTimeout * find(DBusTimeout*t) {
    return reinterpret_cast<FXDBusTimeout*>(tm.find(t));
    }

  void insert(DBusTimeout*t,FXDBusTimeout*f) {
    tm.insert(t,f);
    }

  void remove(DBusTimeout*t) {
    tm.remove(t);
    }
  };

static FXDBusGlobal fxdbus;

class FXDBusTimeout : public FXObject {
FXDECLARE(FXDBusTimeout);
protected:
  DBusTimeout* timeout;
  FXuchar      flags;
protected:
  FXDBusTimeout();
private:
  FXDBusTimeout(const FXDBusTimeout*);
  FXDBusTimeout& operator=(const FXDBusTimeout&);
protected:
  enum {
    FLAG_CALLBACK  = 0x1,
    FLAG_REMOVED   = 0x2
    };
public:
  enum {
    ID_TIMEOUT = 1
    };
public:
  long onTimeout(FXObject*,FXSelector,void*);
public:
  FXDBusTimeout(DBusTimeout *t);
  void add();
  void remove();
  ~FXDBusTimeout();
  };

FXDEFMAP(FXDBusTimeout) FXDBusTimeoutMap[]={
  FXMAPFUNC(SEL_TIMEOUT,FXDBusTimeout::ID_TIMEOUT,FXDBusTimeout::onTimeout),
  };
FXIMPLEMENT(FXDBusTimeout,FXObject,FXDBusTimeoutMap,ARRAYNUMBER(FXDBusTimeoutMap));



FXDBusTimeout::FXDBusTimeout() {
  }

FXDBusTimeout::FXDBusTimeout(DBusTimeout *t) : timeout(t),flags(0) {
  fxdbus.insert(timeout,this);
  }

FXDBusTimeout::~FXDBusTimeout() {
  fxdbus.remove(timeout);
  }

void FXDBusTimeout::add() {
  flags&=~FLAG_REMOVED;
  FXApp::instance()->addTimeout(this,FXDBusTimeout::ID_TIMEOUT,TIME_MSEC(dbus_timeout_get_interval(timeout)));
  }

void FXDBusTimeout::remove() {
  if (flags&FLAG_CALLBACK) {
    flags|=FLAG_REMOVED;
    return;
    }
  FXApp::instance()->removeTimeout(this,FXDBusTimeout::ID_TIMEOUT);
  delete this;
  }


static dbus_bool_t fxdbus_addtimeout(DBusTimeout *timeout,void*) {
  //fxmessage("fxdbus_addtimeout %p\n",timeout);
  FXDBusTimeout * tm = fxdbus.find(timeout);
  if (tm==NULL)
    tm = new FXDBusTimeout(timeout);

  tm->add();
  return true;
	}


static void fxdbus_removetimeout(DBusTimeout *timeout,void*) {
  //fxmessage("fxdbus_removetimeout %p\n",timeout);
  FXDBusTimeout * tm = fxdbus.find(timeout);
  if (tm) tm->remove();
	}

static void fxdbus_toggletimeout(DBusTimeout *timeout,void*ptr) {
  //fxmessage("fxdbus_toggletimeout %p\n",timeout);
	if (dbus_timeout_get_enabled(timeout) && dbus_timeout_get_interval(timeout)>0)
		fxdbus_addtimeout(timeout,ptr);
	else
		fxdbus_removetimeout(timeout,ptr);
  }

long FXDBusTimeout::onTimeout(FXObject*,FXSelector,void*){
  //fxmessage("onTimeout() %p {\n",timeout);
  flags|=FLAG_CALLBACK;
  dbus_timeout_handle(timeout);
  if (flags&FLAG_REMOVED) {
    delete this;
    //fxmessage("}\n");
    return 1;
    }
  else {
    if (dbus_timeout_get_enabled(timeout) && dbus_timeout_get_interval(timeout)>0 && !FXApp::instance()->hasTimeout(this,ID_TIMEOUT)){
      FXApp::instance()->addTimeout(this,FXDBusTimeout::ID_TIMEOUT,TIME_MSEC(dbus_timeout_get_interval(timeout)));
      }
    }
  flags&=~FLAG_CALLBACK;
  //fxmessage("}\n");
  return 1;
  }



/*******************************************************************************************************/
/* Call Backs                                                                                          */
/*******************************************************************************************************/

static dbus_bool_t fxdbus_addwatch(DBusWatch *watch,void * data) {
  FXTRACE((35,"fxdbus_addwatch()\n"));
	FXDBusConnection * dc = reinterpret_cast<FXDBusConnection*>(data);
	FXuint mode  = INPUT_EXCEPT;
	FXuint flags = dbus_watch_get_flags(watch);

	/// If it's not enabled, we're not going to add it
	if (!dbus_watch_get_enabled(watch)) return true;

	if (flags&DBUS_WATCH_READABLE)
			mode|=INPUT_READ;
	if (flags&DBUS_WATCH_WRITABLE)
			mode|=INPUT_WRITE;

#if DBUSVERSION == MKDBUSVERSION(1,1,20) || DBUSVERSION >= MKDBUSVERSION(1,2,0)
#if FOXVERSION < FXVERSION(1,7,0)
  fxdbus.insert(dbus_watch_get_unix_fd(watch),watch);
	return FXApp::instance()->addInput((FXInputHandle)dbus_watch_get_unix_fd(watch),mode,dc,FXDBusConnection::ID_HANDLE);
#else
	return FXApp::instance()->addInput(dc,FXDBusConnection::ID_HANDLE,(FXInputHandle)dbus_watch_get_unix_fd(watch),mode,watch);
#endif
#else
#if FOXVERSION < FXVERSION(1,7,0)
  fxdbus.insert(dbus_watch_get_fd(watch),watch);
	return FXApp::instance()->addInput((FXInputHandle)dbus_watch_get_fd(watch),mode,dc,FXDBusConnection::ID_HANDLE);
#else
	return FXApp::instance()->addInput(dc,FXDBusConnection::ID_HANDLE,(FXInputHandle)dbus_watch_get_fd(watch),mode,watch);
#endif
#endif

	}

static void fxdbus_removewatch(DBusWatch *watch,void *) {
  FXTRACE((35,"fxdbus_removewatch()\n"));
/*
	FXuint mode=INPUT_EXCEPT;
	unsigned int flags = dbus_watch_get_flags(watch);
	if (flags&DBUS_WATCH_READABLE)
			mode|=INPUT_READ;
	if (flags&DBUS_WATCH_WRITABLE) {
			mode|=INPUT_WRITE;
			return;
			}
*/
  FXuint mode=INPUT_READ|INPUT_WRITE|INPUT_EXCEPT;
#if DBUSVERSION == MKDBUSVERSION(1,1,20) || DBUSVERSION >= MKDBUSVERSION(1,2,0)
#if FOXVERSION < FXVERSION(1,7,0)
  fxdbus.remove(dbus_watch_get_unix_fd(watch));
#endif
	FXApp::instance()->removeInput(dbus_watch_get_unix_fd(watch),mode);
#else
#if FOXVERSION < FXVERSION(1,7,0)
  fxdbus.remove(dbus_watch_get_fd(watch));
#endif
	FXApp::instance()->removeInput(dbus_watch_get_fd(watch),mode);
#endif

  }

static void fxdbus_togglewatch(DBusWatch *watch,void* data) {
  FXTRACE((35,"fxdbus_togglewatch()\n"));
	if (dbus_watch_get_enabled(watch))
		fxdbus_addwatch(watch,data);
	else
		fxdbus_removewatch(watch,data);
	}



static void fxdbus_wakeup(void *){
  /// To Do
  }



/*******************************************************************************************************/
/* PUBLIC API                                                                                          */
/*******************************************************************************************************/

FXDEFMAP(FXDBusConnection) FXDBusConnectionMap[]={
	FXMAPFUNC(SEL_IO_READ,FXDBusConnection::ID_HANDLE,FXDBusConnection::onHandleRead),
	FXMAPFUNC(SEL_IO_WRITE,FXDBusConnection::ID_HANDLE,FXDBusConnection::onHandleWrite),
	FXMAPFUNC(SEL_IO_EXCEPT,FXDBusConnection::ID_HANDLE,FXDBusConnection::onHandleExcept),
	FXMAPFUNC(SEL_CHORE,FXDBusConnection::ID_DISPATCH,FXDBusConnection::onDispatch)
  };

FXIMPLEMENT(FXDBusConnection,FXObject,FXDBusConnectionMap,ARRAYNUMBER(FXDBusConnectionMap));

FXDBusConnection::FXDBusConnection() : dc(NULL) {
  }

FXDBusConnection::~FXDBusConnection(){
  if (dc) {
    fxdbus.remove(dc);
    dbus_connection_unref(dc);
    dc=NULL;
    FXApp::instance()->removeChore(this,ID_DISPATCH);
    }
  }


FXDBusConnection * FXDBusConnection::find(DBusConnection * dc) {
  return fxdbus.find(dc);
  }

void FXDBusConnection::initEventLoop() {
  fxdbus.setuphooks();
  }


FXbool FXDBusConnection::open(DBusConnection * connection,FXbool owned/*=true*/) {

  if (dc)
    return false;

  if (connection==NULL)
    return false;

  if (fxdbus.find(dc))
    return false;

  dc = connection;

  if (!owned)
    dbus_connection_ref(dc);

  fxdbus.insert(dc,this);

  //setup_hooks();
  return true;
  }


FXbool FXDBusConnection::open(DBusBusType bustype/*=DBUS_BUS_SESSION*/,FXbool dedicated/*=false*/){
  if (dc)
    return false;

  if (dedicated)
    dc = dbus_bus_get_private(bustype,NULL);
  else
    dc = dbus_bus_get(bustype,NULL);

  if (dc==NULL)
    return false;

  if (fxdbus.find(dc)) {
    dbus_connection_unref(dc);
    dc=NULL;
    return false;
    }

  fxdbus.insert(dc,this);

  dbus_connection_set_exit_on_disconnect(dc,false);
  //setup_hooks();
  return true;
  }

#if 0
FXbool FXDBusConnection::open(const FXString & name,FXbool dedicated/*=false*/) {

  if (dedicated)
    dc = dbus_connection_open_private(name.text(),NULL);
  else
    dc = dbus_connection_open(name.text(),NULL);

  if (dc==NULL)
    return false;

  if (fxdbus.find(dc)) {
    dbus_connection_unref(dc);
    dc=NULL;
    return false;
    }

  fxdbus.insert(dc,this);

  //setup_hooks();
  return true;
  }
#endif

FXbool FXDBusConnection::connected() const {
  if (dc)
    return dbus_connection_get_is_connected(dc);
  else
    return false;
  }

FXbool FXDBusConnection::authenticated() const {
  if (dc)
    return dbus_connection_get_is_authenticated(dc);
  else
    return false;
  }

void FXDBusConnection::flush() {
  if (dc)
    dbus_connection_flush(dc);
  }


FXString FXDBusConnection::dbusversion() {
#if (DBUSVERSION == MKDBUSVERSION(1,1,20)) || DBUSVERSION >= MKDBUSVERSION(1,2,0)
	int major,minor,micro;
	dbus_get_version(&major,&minor,&micro);
	return GMStringFormat("%d.%d.%d",major,minor,micro);
#else
	return FXString("1.0.x");
#endif
	}

struct CallTarget{
  FXObject * target;
  FXSelector message;
  };

static void fxdbus_pendingcallfree(void *memory){
  //fxmessage("fxdbuspendingcallfree\n");
  if (memory){
    CallTarget * call = (CallTarget*)memory;
    delete call;
    }
  }

static void fxdbus_pendingcallnotify(DBusPendingCall*pending,void*data){
  //fxmessage("fxdbuspendingcallnotify\n");
  DBusMessage * msg = dbus_pending_call_steal_reply(pending);
  //fxmessage("received reply %p\n",msg);
  if (msg) {
    CallTarget * call = (CallTarget*)data;
    if (call && call->target && call->message) {
      call->target->handle(NULL,FXSEL(SEL_COMMAND,call->message),msg);
      }
    dbus_message_unref(msg);
    }
  }


FXbool FXDBusConnection::sendWithReply(DBusMessage * msg,FXint timeout,FXObject*tgt,FXSelector sel){
  FXASSERT(msg);
  DBusPendingCall * pending = NULL;
  if (dbus_connection_send_with_reply(dc,msg,&pending,timeout)) {
    if (pending) {
      CallTarget * call = new CallTarget;
      call->target=tgt;
      call->message=sel;
      dbus_pending_call_set_notify(pending,fxdbus_pendingcallnotify,(void*)call,fxdbus_pendingcallfree);
      dbus_pending_call_unref(pending);
      }
    dbus_message_unref(msg);
    return true;
    }
  return false;
  }


/*******************************************************************************************************/
/* MESSAGE HANDLERS                                                                                    */
/*******************************************************************************************************/

long FXDBusConnection::onHandleRead(FXObject*,FXSelector,void*ptr){
#if FOXVERSION < FXVERSION(1,7,0)
	DBusWatch * watch = fxdbus.find((FXint)(FXival)ptr);
#else
	DBusWatch * watch = reinterpret_cast<DBusWatch*>(ptr);
#endif
  dbus_watch_handle(watch,DBUS_WATCH_READABLE);
  FXApp::instance()->addChore(this,ID_DISPATCH);
  return 0;
  }

long FXDBusConnection::onHandleWrite(FXObject*,FXSelector,void*ptr){
#if FOXVERSION < FXVERSION(1,7,0)
	DBusWatch * watch = fxdbus.find((FXint)(FXival)ptr);
#else
	DBusWatch * watch = reinterpret_cast<DBusWatch*>(ptr);
#endif
  dbus_watch_handle(watch,DBUS_WATCH_WRITABLE);
  return 0;
  }

long FXDBusConnection::onHandleExcept(FXObject*,FXSelector,void*ptr){
#if FOXVERSION < FXVERSION(1,7,0)
	DBusWatch * watch = fxdbus.find((FXint)(FXival)ptr);
#else
	DBusWatch * watch = reinterpret_cast<DBusWatch*>(ptr);
#endif
  dbus_watch_handle(watch,DBUS_WATCH_ERROR|DBUS_WATCH_HANGUP);
  return 0;
  }

long FXDBusConnection::onDispatch(FXObject*,FXSelector,void*){
  if (dbus_connection_dispatch((DBusConnection*)dc)==DBUS_DISPATCH_DATA_REMAINS) {
    FXApp::instance()->addChore(this,ID_DISPATCH);
    }
  return 0;
  }

/*******************************************************************************************************/
/* PROTECTED API                                                                                       */
/*******************************************************************************************************/

void FXDBusConnection::setup_event_loop() {
  FXASSERT(dc);

  dbus_connection_set_watch_functions(dc,
                                      fxdbus_addwatch,
                                      fxdbus_removewatch,
                                      fxdbus_togglewatch,
                                      this,NULL);

  dbus_connection_set_timeout_functions(dc,
                                        fxdbus_addtimeout,
                                        fxdbus_removetimeout,
                                        fxdbus_toggletimeout,
                                        this,NULL);

  dbus_connection_set_wakeup_main_function(dc,fxdbus_wakeup,NULL,NULL);
  }
#endif
