/* Copyright (C) 2005-2010 by Howard Chu.
 * http://www.highlandsun.com/hyc/
 *
 * You may distribute this program under the terms of the
 * GNU General Public License version 2.
 *
 * This program receives caller ID info that was broadcast
 * over UDP and displays it to stdout. It decodes both
 * SDMF (used for caller ID number-only service) as well as
 * MDMF (used for name-and-number service). The checksum
 * at the end is always ignored; presumably the data arrived
 * intact at the sender and it didn't get garbled by the IP
 * network.
 *
 * This program has been compiled and run under Linux and
 * Windows (using MSVC as well as MSYS/GCC). It's fairly
 * generic code. You could easily extend it to feed events
 * to some other system, to initiate complex reactions to
 * incoming call data, but all I needed was a text display.
 *
 * 2010-04-18
 * Added support for DBUS Notification service on Linux.
 * compile with -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include
 * link with -ldbus-1
 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#ifdef _WIN32
#include <winsock2.h>
#define USE_WINSOCK
#define	sock_err(msg)	fprintf(stderr,"%s error %d\n",msg,WSAGetLastError())
#else
#include <sys/socket.h>
#include <netinet/in.h>
#define	sock_err(msg)	perror(msg)
#endif
#include <string.h>
#ifdef linux
#include <dbus/dbus.h>
#endif

#define	DEFPORT	10288

int udp;
unsigned short port = DEFPORT;
struct sockaddr_in sa;
unsigned char buf[BUFSIZ];
char outbuf[64];

#ifdef linux
void dbus_notify(char *msg);
DBusConnection *dbc;
#endif

main( int argc, char *argv[] )
{
	int i, j;


	if (argc > 2 && !strcmp(argv[1], "-p")) {
		port = atoi(argv[2]);
		argv += 2;
		argc -= 2;
	}

	if (argc != 1) {
		fprintf(stderr, "usage: %s [-p port]\n", argv[0]);
		exit(1);
	}

#ifdef USE_WINSOCK
	{
		WORD wVersionRequested;
		WSADATA wsaData;

		wVersionRequested = MAKEWORD(2, 0);
		i = WSAStartup(wVersionRequested, &wsaData);
		if (i != 0) {
			fprintf(stderr,"WSAStartup failed %d\n", i);
			exit(1);
		}
	}
#endif
	if ((udp = socket(PF_INET, SOCK_DGRAM, 0)) < 0) {
		sock_err("udp socket");
		exit(1);
	}

	sa.sin_family = AF_INET;
	sa.sin_port = htons(port);
	sa.sin_addr.s_addr = INADDR_ANY;

	if (bind(udp, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
		sock_err("udp bind");
		exit(1);
	}


	for(;;) {
		unsigned char *ptr, *ptr2, *end;
		char *optr = outbuf;
		int len = sizeof(buf);

		i = recv(udp, buf, len, 0);
		if (i<0) {
			sock_err("recv data");
			exit(1);
		}
		buf[i] = '\0';
		end = buf+i;

		switch(buf[0]) {
		case 0x04:	/* SDMF */
			buf[buf[1]+2] = '\0';	/* wipe out checksum byte */
			ptr = buf;
			optr += sprintf(optr, "%.2s/%.2s %.2s:%.2s %s",
				ptr+2, ptr+4, ptr+6, ptr+8, ptr+10);
			break;
		case 0x80:	/* MDMF */
			buf[buf[1]+2] = '\0';	/* wipe out checksum byte */
			ptr = buf+2;
			while(*ptr) {
			len = ptr[1];
			if (ptr+len+1 > end)	/* sanity check */
				len = end - ptr - 1;
			switch(*ptr) {
			case 0x01:	/* Date & Time */
				optr += sprintf(optr, "%.2s/%.2s %.2s:%.2s",
				ptr+2, ptr+4, ptr+6, ptr+8);
				break;
			case 0x02:	/* Number */
				if (ptr[2] == 'O')
					optr += sprintf(optr, " UNAVAIL");
				else if (ptr[2] == 'P')
					optr += sprintf(optr, " PRIVATE");
				else
					optr += sprintf(optr, " %.*s", len, ptr+2);
				break;
			case 0x07:	/* Name */
				optr += sprintf(optr, " (%.*s)", len, ptr+2);
				break;
			case 0x04:	/* Number not present */
				if (ptr[2] == 'O')
					optr += sprintf(optr, "  UNAVAIL# ");
				else if (ptr[2] == 'P')
					optr += sprintf(optr, "  PRIVATE# ");
				break;
			case 0x08:	/* Name not present */
				if (ptr[2] == 'O')
					optr += sprintf(optr, " UNAVAIL@");
				else if (ptr[2] == 'P')
					optr += sprintf(optr, " PRIVATE@");
				break;
			default:
				sprintf(optr, " Unknown data type %2x, %.*s", *ptr, len, ptr+2);
				break;
			}
			ptr += len + 2;
			}
			break;
		}
		puts(outbuf);
		fflush(stdout);
#ifdef linux
		if (!dbc) {
			DBusError dberr = {0};
			dbc = dbus_bus_get(DBUS_BUS_SESSION, &dberr);
		}
		if (dbc) 
			dbus_notify(outbuf);
#endif
	}
}

#ifdef linux

#define NOTIFY_NAME	"org.freedesktop.Notifications"
#define NOTIFY_PATH	"/org/freedesktop/Notifications"

enum mtype { BYTE=0, STRING };

const char *stype[] = { DBUS_TYPE_BYTE_AS_STRING, DBUS_TYPE_STRING_AS_STRING };

typedef struct map {
	char *key;
	enum mtype vtype;
	union {
		char byte;
		char *str;
	} value;
} Map;

#define APPENDB(i,t,v)	dbus_message_iter_append_basic(i, t, v)

append_strv(DBusMessageIter *i0, char **strv, int nvals)
{
	int i;
	DBusMessageIter i1;

	dbus_message_iter_open_container(i0, DBUS_TYPE_ARRAY,
		DBUS_TYPE_STRING_AS_STRING, &i1);
	for (i=0; i<nvals; i++) {
		APPENDB(&i1, DBUS_TYPE_STRING, &strv[i]);
	}
	dbus_message_iter_close_container(i0, &i1);
}

append_map(DBusMessageIter *i0, Map *map)
{
	DBusMessageIter i1, i2, i3;
	int i;

	dbus_message_iter_open_container(i0, DBUS_TYPE_ARRAY, "{sv}", &i1);
	for (i=0; map[i].key; i++) {
		dbus_message_iter_open_container(&i1, DBUS_TYPE_DICT_ENTRY, NULL, &i2);
		dbus_message_iter_append_basic(&i2, DBUS_TYPE_STRING, &map[i].key);
		dbus_message_iter_open_container(&i2, DBUS_TYPE_VARIANT,
			stype[map[i].vtype], &i3);
		dbus_message_iter_append_basic(&i3, stype[map[i].vtype][0],
			&map[i].value);
		dbus_message_iter_close_container(&i2, &i3);
		dbus_message_iter_close_container(&i1, &i2);
	}
	dbus_message_iter_close_container(i0, &i1);
}

void dbus_notify(char *body) {
	DBusMessage *dbm;
	DBusMessageIter i0;
	char *app = "callerID";
	dbus_uint32_t id = 0;
	char *icon = "";
	char *summary = "Caller ID";
	char *actions[2] = {NULL, NULL}, **actp = actions;
	Map hints[3], *hintsp = hints;

	dbus_int32_t exp = 10000;	/* display for 10 seconds */
	dbus_bool_t rw;

	dbm = dbus_message_new_method_call(NOTIFY_NAME, NOTIFY_PATH,
		NOTIFY_NAME, "Notify");

	dbus_message_set_no_reply(dbm, TRUE);

	dbus_message_iter_init_append(dbm, &i0);
	APPENDB(&i0, DBUS_TYPE_STRING, &app);
	APPENDB(&i0, DBUS_TYPE_UINT32, &id);
	APPENDB(&i0, DBUS_TYPE_STRING, &icon);
	APPENDB(&i0, DBUS_TYPE_STRING, &summary);
	APPENDB(&i0, DBUS_TYPE_STRING, &body);
	append_strv(&i0, actp, 0);

	hints[0].key = "urgency";
	hints[0].vtype = BYTE;
	hints[0].value.byte = 1;
	hints[1].key = "category";
	hints[1].vtype = STRING;
	hints[1].value.str = "";
	hints[2].key = NULL;

	append_map(&i0, hints);

	APPENDB(&i0, DBUS_TYPE_INT32, &exp);

	dbus_connection_send(dbc, dbm, &id);
	dbus_connection_flush(dbc);
	rw = dbus_connection_read_write_dispatch(dbc, 10);
}
#endif

