#include "dbustcl.h"
#include <ctype.h>

TCL_DECLARE_MUTEX(dbusMutex);

/* Slot number for attaching Tcl related data to a dbus connection */
dbus_int32_t dataSlot = -1;

const char *libname = "DBUS";

/* Names of the standard dbusses */
const char *busnames[] = {
    "session", "system", "starter", NULL
};

int Tcl_CheckHashEmpty(Tcl_HashTable *hash)
{
   Tcl_HashSearch search;

   return (Tcl_FirstHashEntry(hash, &search) == NULL);
}

void Tcl_DBusErrorCode(Tcl_Interp *interp, const char *op, DBusError err)
{
   char *s1, *s2, buf[32];

   s1 = strrchr(err.name, '.');
   s2 = buf;
   while ((*s2++ = toupper(*++s1)) != '\0');
   Tcl_SetErrorCode(interp, libname, op, buf, err.message, NULL);
}

int DBus_SignalCleanup(Tcl_Interp *interp, Tcl_HashTable *members)
{
   Tcl_HashEntry *memberPtr;
   Tcl_HashSearch search;
   Tcl_DBusSignalData *signal;

   for (memberPtr = Tcl_FirstHashEntry(members, &search);
	memberPtr != NULL; memberPtr = Tcl_NextHashEntry(&search)) {
      signal = Tcl_GetHashValue(memberPtr);
      Tcl_DecrRefCount(signal->script);
      ckfree((char *) signal);
      Tcl_DeleteHashEntry(memberPtr);
   }
   return Tcl_CheckHashEmpty(members);
}

int DBus_MethodCleanup(Tcl_Interp *interp, Tcl_HashTable *members)
{
   Tcl_HashEntry *memberPtr;
   Tcl_HashSearch search;
   Tcl_DBusMethodData *method;

   for (memberPtr = Tcl_FirstHashEntry(members, &search);
	memberPtr != NULL; memberPtr = Tcl_NextHashEntry(&search)) {
      method = Tcl_GetHashValue(memberPtr);
      Tcl_DecrRefCount(method->script);
      ckfree((char *) method);
      Tcl_DeleteHashEntry(memberPtr);
   }
   return Tcl_CheckHashEmpty(members);
}

int DBus_HandlerCleanup(Tcl_Interp *interp, Tcl_DBusHandlerData *data)
{
   if (data->signal != NULL) {
      if (DBus_SignalCleanup(interp, data->signal)) {
	 Tcl_DeleteHashTable(data->signal);
	 ckfree((char *) data->signal);
	 data->signal = NULL;
      }
   }
   if (data->method != NULL) {
      if (DBus_MethodCleanup(interp, data->method)) {
	 Tcl_DeleteHashTable(data->method);
	 ckfree((char *) data->method);
	 data->method = NULL;
      }
   }
   return (data->signal == NULL && data->method == NULL);
}

void DBus_SnoopCleanup(DBusConnection *conn)
{
   Tcl_DBusMonitorData *snoop;
   Tcl_DBusBus *dbus;

   dbus = dbus_connection_get_data(conn, dataSlot);
   snoop = dbus->snoop;
   if (snoop != NULL) {
      /* Uninstall the monitor script */
      dbus_connection_remove_filter(conn, DBus_Monitor, snoop);
      Tcl_DecrRefCount(snoop->script);
      ckfree((char *) snoop);
   }
   dbus->snoop = NULL;
}

int Dbus_Init(Tcl_Interp *interp)
{
   if (Tcl_InitStubs(interp, TCL_VERSION, 0) == NULL) {
      return TCL_ERROR;
   }
#if TCL_MAJOR_VERSION < 9
   if (Tcl_PkgRequire(interp, "Tcl", "8.5", 0) == NULL) return TCL_ERROR;
#else
   if (Tcl_PkgRequire(interp, "Tcl", "9.0", 0) == NULL) return TCL_ERROR;
#endif

   Tcl_MutexLock(&dbusMutex);
   if (dataSlot == -1) {
      /* Application-wide initialization */
#ifdef TCL_THREADS
      dbus_threads_init_default();
#endif
      /* Allocate a data slot to use for tracking new connections */
      dbus_connection_allocate_data_slot(&dataSlot);
   }
   Tcl_MutexUnlock(&dbusMutex);

#ifdef _WIN32
   if (0) {
      /* Per thread initialization */
      Tcl_CreateEventSource(DBus_SetupProc, DBus_CheckProc, dataPtr);
   }
#endif

   TclInitDBusCmd(interp);
   /* Provide the historical name for compatibility */
   Tcl_PkgProvide(interp, "dbus-tcl", PACKAGE_VERSION);
   return Tcl_PkgProvide(interp, PACKAGE_NAME, PACKAGE_VERSION);
}

DBusConnection *DBus_GetConnection(Tcl_Interp *interp,
				   const char *op, Tcl_Obj *const name)
{
   Tcl_DBusInterpData *dataPtr;
   Tcl_HashEntry *entry = NULL;
   int type = DBUS_BUS_SESSION;

   if ((dataPtr = Tcl_GetAssocData(interp, DBUS_ASSOC_KEY, NULL)) != NULL) {
      if (name) {
         entry = Tcl_FindHashEntry(&dataPtr->bus, (char *)name);
         if (entry != NULL) {
            return (DBusConnection *)Tcl_GetHashValue(entry);
         }
      }
   }
   type = DBus_BusType(interp, name);
   if (dataPtr != NULL && type >= 0 && type < TCL_DBUS_BUS_TYPES) {
      if (dataPtr->alias[type] != NULL) {
         return dataPtr->alias[type];
      }
   }

   if (type < 0) {
      Tcl_SetObjResult(interp,
        Tcl_ObjPrintf("bad busid \"%s\"", Tcl_GetString(name)));
      Tcl_SetErrorCode(interp, libname, op, "BUSNAME", NULL);
   } else {
      Tcl_SetObjResult(interp, Tcl_NewStringObj("not connected", -1));
      Tcl_SetErrorCode(interp, libname, op, "CONNECTION", NULL);
   }

   return NULL;
}

void DBus_PathCleanup(Tcl_Interp *interp, DBusConnection *conn, char *path)
{
   char **entries, **entry, *newpath, *pathentry;
   Tcl_DBusHandlerData *data;

   dbus_connection_get_object_path_data(conn, path, (void **)&data);
   if (data != NULL) {
      if (DBus_HandlerCleanup(interp, data)) {
	 dbus_connection_unregister_object_path(conn, path);
	 ckfree((char *)data);
      }
   }
   dbus_connection_list_registered(conn, path, &entries);
   if (*entries != NULL) {
      newpath = ckalloc(strlen(path) + 256);
      strcpy(newpath, path);
      pathentry = newpath + strlen(path) - 1;
      if (*pathentry++ != '/') *pathentry++ = '/';
      for (entry = entries; *entry != NULL; entry++) {
	 strncpy(pathentry, *entry, 255);
	 /* Get a list of descendents from the child */
	 DBus_PathCleanup(interp, conn, newpath);
      }
      ckfree(newpath);
   }
   /* Release the entries array */
   dbus_free_string_array(entries);
}

const char *DBus_InterpPath(Tcl_Interp *interp)
{
   Tcl_Interp *master;
   master = Tcl_GetMaster(interp);
   if (master == NULL) return "";
   Tcl_GetInterpPath(master, interp);
   return (Tcl_GetStringResult(master));
}

void DBus_Close(Tcl_Interp *interp, DBusConnection *conn)
{
   Tcl_DBusBus *dbus;
   Tcl_DBusInterpData *dataPtr;
   Tcl_HashEntry *busPtr;

   dbus = dbus_connection_get_data(conn, dataSlot);
   /* Find all paths with handlers registered by the interp */
   DBus_PathCleanup(interp, conn, "/");
   /* Find all handlers of the interp without a path */
   if (dbus->fallback != NULL) {
      if (DBus_HandlerCleanup(interp, dbus->fallback)) {
	 ckfree((char *)dbus->fallback);
	 dbus->fallback = NULL;
      }
   }
   /* Remove snoop handler */
   DBus_SnoopCleanup(conn);

   /* Remove the hash table entry */
   if ((dataPtr = Tcl_GetAssocData(interp, DBUS_ASSOC_KEY, NULL)) != NULL) {
      busPtr = Tcl_FindHashEntry(&dataPtr->bus, (char *)dbus->name);
      if (busPtr) {
         Tcl_DeleteHashEntry(busPtr);
         if (dbus->type < TCL_DBUS_BUS_TYPES) {
            if (dataPtr->alias[dbus->type] == conn) {
               dataPtr->alias[dbus->type] = NULL;
            }
         }
      }
   }

   /* Release the dbus connection */
   dbus_connection_close(conn);
   dbus_connection_unref(conn);

   /* Cancel any pending dispatch actions */
   DBusDispatchCancel(conn);
}

void DBus_InterpDelete(ClientData data, Tcl_Interp *interp)
{
   Tcl_DBusInterpData *dataPtr = (Tcl_DBusInterpData *)data;
   Tcl_HashEntry *hPtr;
   Tcl_HashSearch search;
   DBusConnection *conn;
   
#ifdef _WIN32
   Tcl_DeleteEventSource(DBus_SetupProc, DBus_CheckProc, dataPtr);
#endif
   
   hPtr = Tcl_FirstHashEntry(&dataPtr->bus, &search);
   while (hPtr != NULL) {
      conn = (DBusConnection *)Tcl_GetHashValue(hPtr);
      DBus_Close(interp, conn);
      hPtr = Tcl_NextHashEntry(&search);
   }
   Tcl_DeleteHashTable(&dataPtr->bus);
   ckfree(data);
}

#ifdef DBUS_MEM_DEBUG
char *DBus_Alloc(int size, char *file, int line)
{
   char *rc;

   rc = Tcl_Alloc(size);
   printf("Alloc %d bytes -> %p (%s:%d)\n", size, rc, file, line);
   return rc;
}

char *DBus_AttemptAlloc(int size, char *file, int line)
{
   char *rc;

   rc = Tcl_AttemptAlloc(size);
   printf("Alloc %d bytes -> %p (%s:%d)\n", size, rc, file, line);
   return rc;
}

void DBus_Free(char *ptr, char *file, int line)
{
   printf("Free %p (%s:%d)\n", ptr, file, line);
   Tcl_Free(ptr);
}

void DBus_RefCount(Tcl_Obj *objPtr, int delta, char *file, int line)
{
   static const char *dir[] = {"Increment", "Decrement"};

   printf("%s %p = %d (%s:%d)",
	  dir[delta < 0], objPtr, objPtr->refCount + delta, file, line);

   if (delta > 0)
     ++(objPtr)->refCount;

   /* Don't use Tcl_GetString() as that may invalidate the intrep */
   if (objPtr->bytes != NULL) {
      if (objPtr->length > 40)
	printf(": %-38s...", objPtr->bytes);
      else
	printf(": %s", objPtr->bytes);
   }
   if (objPtr->typePtr != NULL) {
      printf(", type = %s", objPtr->typePtr->name);
   }
   printf("\n");

   if (delta < 0)
     if (--(objPtr)->refCount <= 0) {
	printf("Freeing %p\n", objPtr);
	TclFreeObj(objPtr);
     }
}
#endif
