/*
 *
 *  Multimedia Messaging Service Daemon - The Next Generation
 *
 *  Copyright (C) 2021, Julian P Samaroo
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 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, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <dbus/dbus.h>
#include <json-c/json.h>
#include <getopt.h>

char *prog;
static DBusConnection *conn;
static DBusError err;
char *services_path;
void *messages;

// set by command line flags
char *target_message_path = NULL;
char *target_number = NULL;
char **recipients = NULL;
int recipients_len = 0;
int attachments_len = 0;
char **attachments = NULL;
char **ctypes = NULL;

typedef void (CALLBACK)(DBusMessageIter *);

void
send_recv (CALLBACK    arg_cb,
           CALLBACK    reply_cb,
           const char *target,
           const char *object,
           const char *interface,
           const char *method)
{
  DBusMessage *msg;
  DBusMessageIter args;
  DBusPendingCall *pending;

  // create a new method call and check for errors
  msg = dbus_message_new_method_call (target, object, interface, method);
  assert (msg && "Failed to allocate new message");

  // append arguments
  dbus_message_iter_init_append (msg, &args);
  arg_cb (&args);

  // send message and get a handle for a reply
  if (!dbus_connection_send_with_reply (conn, msg, &pending, -1))         // -1 is default timeout
    {
      fprintf (stderr, "Out Of Memory!\n");
      exit (EXIT_FAILURE);
    }
  if (pending == NULL)
    {
      fprintf (stderr, "Pending Call was NULL\n");
      exit (EXIT_FAILURE);
    }
  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 (msg == NULL)
    {
      fprintf (stderr, "Reply was NULL\n");
      exit (EXIT_FAILURE);
    }
  // free the pending message handle
  dbus_pending_call_unref (pending);

  // read the parameters
  if (dbus_message_iter_init (msg, &args))
    reply_cb (&args);

  // free reply and close connection
  dbus_message_unref (msg);
}

void
get_services_arg_cb (DBusMessageIter *args)
{
}

void
get_services_reply_cb (DBusMessageIter *args)
{
  char *error;
  int arg_type = dbus_message_iter_get_arg_type (args);
  if (arg_type == DBUS_TYPE_STRING)
    {
      dbus_message_iter_get_basic (args, &error);
      fprintf (stderr, "DBus error: %s\n", error);
      exit (EXIT_FAILURE);
    }
  assert (arg_type == DBUS_TYPE_ARRAY);

  DBusMessageIter subargs;
  dbus_message_iter_recurse (args, &subargs);
  assert (dbus_message_iter_get_arg_type (&subargs) == DBUS_TYPE_STRUCT);

  DBusMessageIter subsubargs;
  dbus_message_iter_recurse (&subargs, &subsubargs);
  assert (dbus_message_iter_get_arg_type (&subsubargs) == DBUS_TYPE_OBJECT_PATH);
  dbus_message_iter_get_basic (&subsubargs, &services_path);
}

json_object *
parse_message_dict_variant_inner (DBusMessageIter *args)
{
  int vartype = dbus_message_iter_get_arg_type (args);
  if (vartype == DBUS_TYPE_STRING)
    {
      char *var_str;
      dbus_message_iter_get_basic (args, &var_str);
      return json_object_new_string (var_str);
    }
  else if (vartype == DBUS_TYPE_BOOLEAN)
    {
      char var;
      dbus_message_iter_get_basic (args, &var);
      return json_object_new_boolean (var);
    }
  else if (vartype == DBUS_TYPE_ARRAY || vartype == DBUS_TYPE_STRUCT)
    {
      DBusMessageIter varelemargs;
      dbus_message_iter_recurse (args, &varelemargs);
      json_object *subattrs = json_object_new_array ();
      while (1 == 1)
        {
          int elem_type = dbus_message_iter_get_arg_type (&varelemargs);
          if (elem_type == DBUS_TYPE_STRING)
            {
              char *elem_str;
              dbus_message_iter_get_basic (&varelemargs, &elem_str);
              json_object_array_add (subattrs, json_object_new_string (elem_str));
            }
          else if (elem_type == DBUS_TYPE_UINT64)
            {
              uint64_t elem;
              dbus_message_iter_get_basic (&varelemargs, &elem);
              json_object_array_add (subattrs, json_object_new_int (elem));
            }
          else
            fprintf (stderr, "Unhandled DBus type: %d\n", elem_type);
          if (!dbus_message_iter_has_next (&varelemargs))
            break;
          dbus_message_iter_next (&varelemargs);
        }
      return subattrs;
    }
  else {
      fprintf (stderr, "Unhandled DBus type: %d\n", vartype);
      exit (EXIT_FAILURE);
    }
}

json_object *
parse_message_dict_variant (DBusMessageIter *args)
{
  DBusMessageIter varargs;
  dbus_message_iter_recurse (args, &varargs);

  int vartype = dbus_message_iter_get_arg_type (&varargs);
  if (vartype == DBUS_TYPE_ARRAY)
    {
      DBusMessageIter varargsarray;
      dbus_message_iter_recurse (&varargs, &varargsarray);
      json_object *subattrs = json_object_new_array ();
      while (1 == 1)
        {
          json_object_array_add (subattrs, parse_message_dict_variant_inner (&varargsarray));
          if (!dbus_message_iter_has_next (&varargsarray))
            break;
          dbus_message_iter_next (&varargsarray);
        }
      return subattrs;
    }
  else
    return parse_message_dict_variant_inner (&varargs);
}

void
parse_message (DBusMessageIter *args)
{
  json_object *root = json_object_new_object ();

  char *message_path;
  assert (dbus_message_iter_get_arg_type (args) == DBUS_TYPE_OBJECT_PATH);
  dbus_message_iter_get_basic (args, &message_path);
  if (target_message_path && (strcmp (message_path, target_message_path) != 0))
    return;
  json_object_object_add (root, "message_path", json_object_new_string (message_path));
  dbus_message_iter_next (args);

  json_object *attrs = json_object_new_object ();
  while (1 == 1)       // TODO: Unnecessary loop?
    {
      assert (dbus_message_iter_get_arg_type (args) == DBUS_TYPE_ARRAY);
      DBusMessageIter msgargs;
      dbus_message_iter_recurse (args, &msgargs);
      while (1 == 1)
        {
          char *arg_name;
          DBusMessageIter msgdictargs;
          assert (dbus_message_iter_get_arg_type (&msgargs) == DBUS_TYPE_DICT_ENTRY);
          dbus_message_iter_recurse (&msgargs, &msgdictargs);

          assert (dbus_message_iter_get_arg_type (&msgdictargs) == DBUS_TYPE_STRING);
          dbus_message_iter_get_basic (&msgdictargs, &arg_name);
          dbus_message_iter_next (&msgdictargs);

          assert (dbus_message_iter_get_arg_type (&msgdictargs) == DBUS_TYPE_VARIANT);
          json_object *value = parse_message_dict_variant (&msgdictargs);
          json_object_object_add (attrs, arg_name, value);

          if (!dbus_message_iter_has_next (&msgargs))
            break;
          dbus_message_iter_next (&msgargs);
        }
      if (!dbus_message_iter_has_next (args))
        break;
      dbus_message_iter_next (args);
    }
  json_object_object_add (root, "attrs", attrs);
  printf ("%s\n", json_object_to_json_string_ext (root, JSON_C_TO_STRING_PRETTY | JSON_C_TO_STRING_NOSLASHESCAPE));
}

void
listen ()
{
  DBusMessage *msg;
  DBusMessageIter args;

  dbus_bus_add_match (conn, "type='signal',interface='org.ofono.mms.Service'", &err);
  dbus_connection_flush (conn);
  if (dbus_error_is_set (&err))
    {
      fprintf (stderr, "Match Error (%s)\n", err.message);
      exit (EXIT_FAILURE);
    }

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

      // loop again if we haven't read a message
      if (!msg)
        {
          usleep (100000);
          continue;
        }

      // check if the message is a signal from the correct interface and with the correct name
      if (dbus_message_is_signal (msg, "org.ofono.mms.Service", "MessageAdded"))
        {
          // read the parameters
          if (!dbus_message_iter_init (msg, &args))
            {
              fprintf (stderr, "Message has no arguments!\n");
              goto free;
            }
          else if (DBUS_TYPE_OBJECT_PATH != dbus_message_iter_get_arg_type (&args))
            {
              fprintf (stderr, "Argument is not string!\n");
              goto free;
            }
          else
            parse_message (&args);
        }

 free:
      dbus_message_unref (msg);
    }
}

void
get_messages_arg_cb (DBusMessageIter *args)
{
}
void
get_messages_reply_cb (DBusMessageIter *args)
{
  char *error;
  int arg_type = dbus_message_iter_get_arg_type (args);
  if (arg_type == DBUS_TYPE_STRING)
    {
      dbus_message_iter_get_basic (args, &error);
      fprintf (stderr, "DBus error: %s\n", error);
      exit (EXIT_FAILURE);
    }
  assert (arg_type == DBUS_TYPE_ARRAY);

  DBusMessageIter msg_list;
  dbus_message_iter_recurse (args, &msg_list);

  while (dbus_message_iter_get_arg_type (&msg_list) != DBUS_TYPE_INVALID)
    {
      DBusMessageIter msg_content;
      assert (dbus_message_iter_get_arg_type (&msg_list) == DBUS_TYPE_STRUCT);

      dbus_message_iter_recurse (&msg_list, &msg_content);
      parse_message (&msg_content);

      dbus_message_iter_next (&msg_list);
    }
}

void
get_messages (void)
{
  send_recv (get_messages_arg_cb, get_messages_reply_cb, "org.ofono.mms", services_path, "org.ofono.mms.Service", "GetMessages");
}

void
send_message_arg_cb (DBusMessageIter *args)
{
  DBusMessageIter recipients_args;
  dbus_message_iter_open_container (args, DBUS_TYPE_ARRAY, "s", &recipients_args);
  for (int i = 0; i < recipients_len; i++)
    dbus_message_iter_append_basic (&recipients_args, DBUS_TYPE_STRING, &recipients[i]);
  dbus_message_iter_close_container (args, &recipients_args);

  char *smil = "";
  DBusMessageIter variant_args;
  dbus_message_iter_open_container (args, DBUS_TYPE_VARIANT, "s", &variant_args);
  dbus_message_iter_append_basic (&variant_args, DBUS_TYPE_STRING, &smil);
  dbus_message_iter_close_container (args, &variant_args);

  DBusMessageIter attachments_struct_args;
  dbus_message_iter_open_container (args, DBUS_TYPE_ARRAY, "(sss)", &attachments_struct_args);
  for (int i = 0; i < attachments_len; i++)
    {
      DBusMessageIter attachments_args;
      dbus_message_iter_open_container (&attachments_struct_args, DBUS_TYPE_STRUCT, NULL, &attachments_args);

      char *cid = malloc (8);          // TODO: This segfaults: char cid[8] = {0};
      assert (snprintf (cid, 8, "cid-%d", i));
      char *ctype = ctypes[i];
      char *att = attachments[i];
      if (ctype == NULL)
        {
          ctype = "text/plain";
          /* FIXME: Content Type detection
           * char file_cmd[255];
           * ctype = malloc(255);
           * freopen("/dev/null", "a", stdout);
           * setbuf(stdout, ctype);
           * assert(snprintf(file_cmd, 255, "file -b --mime-type %s", att));
           * assert(system(file_cmd) == 0);
           * freopen("/dev/tty", "a", stdout);
           */
        }
      dbus_message_iter_append_basic (&attachments_args, DBUS_TYPE_STRING, &cid);
      dbus_message_iter_append_basic (&attachments_args, DBUS_TYPE_STRING, &ctype);
      dbus_message_iter_append_basic (&attachments_args, DBUS_TYPE_STRING, &att);
      dbus_message_iter_close_container (&attachments_struct_args, &attachments_args);
    }
  dbus_message_iter_close_container (args, &attachments_struct_args);
}

void
send_message_reply_cb (DBusMessageIter *args)
{
}

void
send_message (void)
{
  send_recv (send_message_arg_cb, send_message_reply_cb, "org.ofono.mms", services_path, "org.ofono.mms.Service", "SendMessage");
}

void
delete_message_arg_cb (DBusMessageIter *args)
{
}
void
delete_message_reply_cb (DBusMessageIter *args)
{
}
void
delete_message (void)
{
  send_recv (delete_message_arg_cb, delete_message_reply_cb, "org.ofono.mms", target_message_path, "org.ofono.mms.Message", "Delete");
}

enum mms_command_t {
  MMS_COMMAND_NONE,
  MMS_COMMAND_LISTEN,
  MMS_COMMAND_LIST_MESSAGES,
  MMS_COMMAND_SEND_MESSAGE,
  MMS_COMMAND_DELETE_MESSAGE,
};

void
usage (void)
{
  fprintf (stderr, "Usage: %s [-L] [-M [-o message_path]] [-S [-r recipient ...] [-a attachment [-c ctype] ...]] [-D -o message_path [-o message_path ...]]\n", prog);
  exit (EXIT_FAILURE);
}

void
set_cmd (enum mms_command_t *cmd,
         enum mms_command_t  new_cmd)
{
  if (*cmd != MMS_COMMAND_NONE)
    {
      fprintf (stderr, "Conflicting commands!\n");
      usage ();
    }
  *cmd = new_cmd;
}

int
main (int   argc,
      char *argv[])
{
  int opt;
  enum mms_command_t cmd = MMS_COMMAND_NONE;

  prog = argv[0];
  while ((opt = getopt (argc, argv, "LMSDo:r:a:c:")) != -1)
    switch (opt)
      {
      case 'L':
        set_cmd (&cmd, MMS_COMMAND_LISTEN);
        break;
      case 'M':
        set_cmd (&cmd, MMS_COMMAND_LIST_MESSAGES);
        break;
      case 'S':
        set_cmd (&cmd, MMS_COMMAND_SEND_MESSAGE);
        break;
      case 'D':
        set_cmd (&cmd, MMS_COMMAND_DELETE_MESSAGE);
        break;
      case 'o':
        target_message_path = optarg;
        break;
      case 'r':
        if (cmd != MMS_COMMAND_SEND_MESSAGE)
          usage ();
        recipients_len++;
        if (recipients_len)
          recipients = realloc (recipients, recipients_len * sizeof(char *));
        else
          recipients = malloc (sizeof(char *));
        recipients[recipients_len - 1] = optarg;
        break;
      case 'a':
        if (cmd != MMS_COMMAND_SEND_MESSAGE)
          usage ();
        attachments_len++;
        if (attachments_len == 1)
          {
            attachments = malloc (sizeof(char *));
            ctypes = malloc (sizeof(char *));
          }
        else {
            attachments = realloc (attachments, attachments_len * sizeof(char *));
            ctypes = realloc (ctypes, attachments_len * sizeof(char *));
          }
        attachments[attachments_len - 1] = optarg;
        ctypes[attachments_len - 1] = NULL;               // Default to auto-detect ctype
        break;
      case 'c':
        if (cmd != MMS_COMMAND_SEND_MESSAGE)
          usage ();
        if (ctypes[attachments_len - 1])
          {
            fprintf (stderr, "Only one content type may be specified per attachment!\n");
            usage ();
          }
        ctypes[attachments_len - 1] = optarg;
        break;
      default:
        usage ();
      }

  dbus_error_init (&err);

  // connect to the session bus
  conn = dbus_bus_get (DBUS_BUS_SESSION, &err);
  if (dbus_error_is_set (&err))
    {
      fprintf (stderr, "Connection Error (%s)\n", err.message);
      dbus_error_free (&err);
    }
  if (!conn)
    {
      fprintf (stderr, "Connection is NULL\n");
      exit (EXIT_FAILURE);
    }

  send_recv (get_services_arg_cb, get_services_reply_cb, "org.ofono.mms", "/org/ofono/mms", "org.ofono.mms.Manager", "GetServices");

  if (cmd == MMS_COMMAND_LISTEN)
    listen ();
  else if (cmd == MMS_COMMAND_LIST_MESSAGES)
    get_messages ();
  else if (cmd == MMS_COMMAND_SEND_MESSAGE)
    {
      if ((recipients_len == 0) || (attachments_len == 0))
        {
          fprintf (stderr, "-S needs recipients and attachments!\n");
          usage ();
        }
      send_message ();
    }
  else if (cmd == MMS_COMMAND_DELETE_MESSAGE)
    {
      if (!target_message_path)
        {
          fprintf (stderr, "-D needs message path!\n");
          usage ();
        }
      delete_message ();
    }
  else if (cmd == MMS_COMMAND_NONE)
    {
      fprintf (stderr, "No command specified\n");
      exit (EXIT_SUCCESS);
    }
  else {
      fprintf (stderr, "Command not implemented!\n");
      exit (EXIT_FAILURE);
    }

  return 0;
}
