/*
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation;
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 *  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS.
 *  IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) AND AUTHOR(S) BE LIABLE FOR ANY
 *  CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES 
 *  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 
 *  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 
 *  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 *  ALL LIABILITY, INCLUDING LIABILITY FOR INFRINGEMENT OF ANY PATENTS, 
 *  COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS, RELATING TO USE OF THIS 
 *  SOFTWARE IS DISCLAIMED.
 *
 */

#define DBUS_API_SUBJECT_TO_CHANGE
#include <dbus/dbus.h>
#include <stdbool.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include <getopt.h>
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <time.h>

char* progname = NULL;
bool verbose = false;

void syntax()
{
   printf("Syntax: %s <options>\n", progname);
   printf("   -u upcmd --upcmd=upcmd\n");
   printf("   -d downcmd --downcmd=downcmd\n");
   printf("   -b aa:bb:cc:dd:ee:ff --btid=aa:bb:cc:dd:ee:ff\n");
   printf("   -v --verbose\n");
   printf("   -V --version\n");
   printf("   -p --protect\n");
   printf("   -h --help\n");
   exit(1);
}

void message(bool verb, char* msg, ...)
{
   if (!verbose && verb) return;
   va_list vargs;
   va_start(vargs, msg);
   vprintf(msg, vargs);
   va_end(vargs);
}

#define assert(a, b) if(!(a)) real_assert(b)
void real_assert (char* msg)
{
   if (0 != errno) {
      char* errstr = strerror(errno);
      message(false, "[Assert] failed with errno %d/%s: %s\n", errno, errstr, msg);
   } else
      message(false, "[Assert] failed: %s\n", msg);
#ifdef DEBUG
   exit(1);
#endif
}

void fork_and_run(char* cmd)
{
   pid_t pid;
   pid = fork();
   assert(0 <= pid, "Could not fork");
   if (0 == pid) {
      system(cmd);
      exit(0);
   }
}

uint32_t getConnectionUnixUser(const char* service, DBusConnection* conn)
{
   DBusMessage* msg;
   DBusMessageIter args;
   DBusPendingCall* pending;
   uint32_t uid;
   char* errmsg;

   if (NULL == service) return -1;
   msg = dbus_message_new_method_call("org.freedesktop.DBus", // target for the method call
         "/org/freedesktop/DBus", // object to call on
         "org.freedesktop.DBus", // interface to call on
         "GetConnectionUnixUser"); // method name
   if (NULL == msg) { 
      message(false, "Message Null\n");
      exit(1);
   }

   // append arguments
   dbus_message_iter_init_append(msg, &args);
   if (!dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &service)) { 
      message(false, "Out Of Memory!\n"); 
      exit(1);
   }

   // send message and get a handle for a reply
   if (!dbus_connection_send_with_reply (conn, msg, &pending, -1)) { // -1 is default timeout
      message(false, "Out Of Memory!\n"); 
      exit(1);
   }
   if (NULL == pending) { 
      message(false, "Pending Call Null\n"); 
      exit(1); 
   }
   dbus_connection_flush(conn);

   // free message
   dbus_message_unref(msg);

   // block until we recieve a reply
   dbus_pending_call_block(pending);

   // get the reply message
   msg = dbus_pending_call_steal_reply(pending);
   if (NULL == msg) {
      message(false, "Reply Null\n"); 
      exit(1); 
   }
   // free the pending message handle
   dbus_pending_call_unref(pending);

   if (DBUS_MESSAGE_TYPE_ERROR == dbus_message_get_type(msg)) {
      message(false, "Failed to check owner\n");
   }
   // read the parameters
   else if (!dbus_message_iter_init(msg, &args))
      message(false, "Message has no arguments!\n"); 
   else if (DBUS_TYPE_UINT32 != dbus_message_iter_get_arg_type(&args)) 
      message(false, "Argument is not uint32 (is %c)!\n",  dbus_message_iter_get_arg_type(&args)); 
    else {
      dbus_message_iter_get_basic(&args, &uid);
      dbus_message_unref(msg);   
      return uid;
   }

   dbus_message_unref(msg);   
   return -1;
}

DBusConnection* dbusConnect()
{
   DBusConnection* conn;
   DBusError err;
   int ret;

   // initialise the errors
   dbus_error_init(&err);
   
   // connect to the bus and check for errors
   conn = dbus_bus_get(DBUS_BUS_SYSTEM, &err);
   if (dbus_error_is_set(&err)) { 
      message(false, "Connection Error (%s)\n", err.message);
      dbus_error_free(&err); 
   }
   if (NULL == conn) { 
      exit(1);
   }
   
   // add a rule for which messages we want to see
   dbus_bus_add_match(conn, "type='signal',interface='cx.ath.matthew.bluemon.ProximitySignal'", &err); // see signals from the given interface
   dbus_connection_flush(conn);
   if (dbus_error_is_set(&err)) { 
      message(false, "Match Error (%s)\n", err.message);
      exit(1); 
   }
   return conn; 
}

void spurn_children()
{
   struct sigaction act;
   act.sa_flags = SA_NOCLDWAIT;
   act.sa_handler = SIG_IGN;
   sigaction(SIGCHLD, &act, NULL);
}

int main(int argc, char ** argv)
{
   DBusMessage* msg;
   DBusMessageIter args;
   DBusConnection* conn;
   char* sigvalue;
   char* upcmd = NULL;
   char* downcmd = NULL;
   char* btid = NULL;
   char* servicename = NULL;
   char s[30];
   time_t t;
   bool protect = false;
   uint32_t uid;

   spurn_children();

   progname = argv[0];
   static struct option long_opts[] = {
      {"help", 0, 0, 'h'},
      {"verbose", 0, 0, 'v'},
      {"version", 0, 0, 'V'},
      {"upcmd", 0, 0, 'u'},
      {"downcmd", 0, 0, 'd'},
      {"btid", 0, 0, 'b'},
      {"protect", 0, 0, 'p'},
   };
   int c;
   int idx = 0;
   while (-1 != (c = getopt_long(argc, argv, "-u:d:b:hVvp",long_opts, &idx))) {
      switch (c) {
          case 'u':
            upcmd = optarg;
            break;
          case 'd':
            downcmd = optarg;
            break;
          case 'b':
            btid = optarg;
            break;
          case 'p':
            protect = true;
            break;
          case 'v':
            verbose = true;
            break;
          case 'V':
            printf("Bluemon client version %s.\n", VERSION);
            break;
          case 'h':
          case ':':
          case '?':
          case 1:
            syntax();
            break;
      }
   }
   if (NULL == upcmd || NULL == downcmd || btid == NULL) syntax();

   conn = dbusConnect();

   // loop listening for signals being emmitted
   while (true) {

      // non blocking read of the next available message
      dbus_connection_read_write(conn, -1);
      while (NULL != (msg = dbus_connection_pop_message(conn))) {

         message(true, "Got Message: %s.%s\n", dbus_message_get_interface(msg),
               dbus_message_get_member(msg));

         // check if the message is a signal from the correct interface and with the correct name
         // and from the correct source on the bus
         if (dbus_message_is_signal(msg, "cx.ath.matthew.bluemon.ProximitySignal", "Connect")) {
            if (!protect || 0 == (uid = getConnectionUnixUser(dbus_message_get_sender(msg), conn))) {

               // read the parameters
               if (!dbus_message_iter_init(msg, &args))
                  message(true, "Message Has No Parameters\n");
               else if (DBUS_TYPE_STRING != dbus_message_iter_get_arg_type(&args)) 
                  message(true, "Argument is not string!\n"); 
               else {
                  dbus_message_iter_get_basic(&args, &sigvalue);
                  time(&t);
                  strftime(s, 30, "%H:%M", localtime(&t));
                  message(true, "[%s] Got Connect Signal for %s\n", s, sigvalue);
                  if (0 == strncasecmp(btid, sigvalue, strlen(btid)))
                     fork_and_run(upcmd);
               }
            }
            else
               message(true, "Got signal from %d, not root\n", uid);
         }
         else if (dbus_message_is_signal(msg, "cx.ath.matthew.bluemon.ProximitySignal", "Disconnect")) {
            if (!protect || 0 == (uid = getConnectionUnixUser(dbus_message_get_sender(msg), conn))) {

               // read the parameters
               if (!dbus_message_iter_init(msg, &args))
                  message(true, "Message Has No Parameters\n");
               else if (DBUS_TYPE_STRING != dbus_message_iter_get_arg_type(&args)) 
                  message(true, "Argument is not string!\n"); 
               else {
                  dbus_message_iter_get_basic(&args, &sigvalue);
                  time(&t);
                  strftime(s, 30, "%H:%M", localtime(&t));
                  message(true, "[%s] Got Disconnect Signal for %s\n", s, sigvalue);
                  if (0 == strncasecmp(btid, sigvalue, strlen(btid)))
                     fork_and_run(downcmd);
               }
            }
            else
               message(true, "Got signal from %d, not root\n", uid);
         }
         else
            message(true, "Spurious message from %s\n",  dbus_message_get_sender(msg));

         // free the message
         dbus_message_unref(msg);
      }
   }
   // close the connection
   dbus_connection_close(conn);

}

