/*
 * Copyright (C) 2018 Purism SPC
 *
 * 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 2 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/>.
 *
 */


#define G_LOG_DOMAIN "mm-sms"
#define _GNU_SOURCE 1

#include <glib.h>
#include <glib/gi18n.h>
#include <string.h>
#include <time.h>
#include <gio/gio.h>
#include <purple.h>
#include <debug.h>
#include <libmm-glib.h>
#include "mm-sms.h"

#define PUR_MM_SMS_PLUGIN_VERSION   "0.1.4"

#define PUR_MM_MODEM_TIMEOUT       20000
#define PUR_MM_MODEM_SMS_TIMEOUT   35000

// Relative Validity Period Values according to GSM spec
// 0–143    |   (TP-VP + 1) x 5 minutes
// 144–167  |   (12 + (TP-VP - 143) / 2 ) hours
// 168–196  |   (TP-VP - 166) days
// 197–255  |   (TP-VP - 192) weeks

#define SMS_VALIDITY_2_DAYS   168
#define SMS_VALIDITY_7_DAYS   173
#define SMS_VALIDITY_30_DAYS  196
#define SMS_VALIDITY_NOT_SET  0

static pur_mm_data_t pur_mm_data;

pur_mm_data_t *pur_mm_get_data (void)
{
  return &pur_mm_data;
}

typedef struct {
  MMObject *object;
  MMModem  *modem;
  MMSim    *sim;
} PurMmDevice;

typedef struct {
  MMSms        *sms;
  MMSmsState    state;
  const gchar  *number;
  const gchar  *message;
  guint         message_ref;
  time_t        t_stamp;
  const gchar  *path;
  gchar        *sms_id;
} PurSmsProps;

static void pur_mm_state (int state);
static void pur_mm_get_modems (void);
static void pur_mm_purple_connect (void);
static void pur_mm_signal_emit (gchar *sms_id, int state);
static void pur_mm_get_new_sms (char *path);
static void pur_mm_delete_sms (PurSmsProps *sms_props);
static void pur_mm_check_pdu_type (PurSmsProps *sms_props);
static void pur_mm_get_sms_properties (PurSmsProps *sms_props);
static void pur_mm_send_sms_to_purple_serv (PurSmsProps *sms_props);

PurplePlugin *mm_sms_plugin = NULL;

static PurpleCmdRet
cb_mm_sms_cmd (PurpleConversation  *conv,
               const gchar         *cmd,
               gchar              **args,
               gchar              **error,
               void                *data)
{
  char *msg = NULL;

  pur_mm_data_t *mm_sms = pur_mm_get_data();

  if (args[0] == NULL || !g_strcmp0 (args[0], "help")) {
    msg = g_strdup ("Available commands:\n\n"
                    " - '/mm-sms help': Displays this message\n"
                    " - '/mm-sms info': Show modem info\n"
                    " - '/mm-sms rm_s [on; off]': Remove SMS from modem when sent\n"
                    " - '/mm-sms rm_r [on; off]': Remove SMS from modem when received\n"
                    " - '/mm-sms deli [on; off]': Request delivery reports from SMC\n"
                    " - '/mm-sms vali [2; 7; 30]': Set SMS validity period in days\n");
  } else if (!g_strcmp0 (args[0], "info")) {
    msg = g_strdup_printf ("Manufacturer: %s\n"
                           "Model: %s\n"
                           "Revision: %s\n"
                           "Hardware Version: %s\n"
                           "Device Id: %s\n",
                           mm_modem_dup_manufacturer (mm_sms->modem),
                           mm_modem_dup_model (mm_sms->modem),
                           mm_modem_dup_revision (mm_sms->modem),
                           mm_modem_dup_hardware_revision (mm_sms->modem),
                           mm_modem_dup_device_identifier (mm_sms->modem));
  } else if (!g_strcmp0 (args[1], "on")) {
    if (!g_strcmp0 (args[0], "rm_s")) {
      purple_account_set_bool (mm_sms->account, "sms_delete_sent", TRUE);
      mm_sms->sms_delete_sent = TRUE;
      msg = g_strdup ("SMS will be removed from modem storage");
    } else if (!g_strcmp0 (args[0], "rm_r")) {
      purple_account_set_bool (mm_sms->account, "sms_delete_received", TRUE);
      mm_sms->sms_delete_received = TRUE;
      msg = g_strdup ("SMS will be removed from modem storage");
    } else if (!g_strcmp0 (args[0], "deli")) {
      purple_account_set_bool (mm_sms->account, "sms_delivery_report", TRUE);
      mm_sms->sms_delivery_report = TRUE;
      msg = g_strdup ("Delivery reports will be requested\n"
                      "(Your SMC may not support delivery reports)");
    }
  } else if (!g_strcmp0 (args[1], "off")) {
    if (!g_strcmp0 (args[0], "rm_s")) {
      purple_account_set_bool (mm_sms->account, "sms_delete_sent", FALSE);
      mm_sms->sms_delete_sent = FALSE;
      msg = g_strdup ("SMS remain in modem storage");
    } else if (!g_strcmp0 (args[0], "rm_r")) {
      purple_account_set_bool (mm_sms->account, "sms_delete_received", FALSE);
      mm_sms->sms_delete_received = FALSE;
      msg = g_strdup ("SMS remain in modem storage");
    } else if (!g_strcmp0 (args[0], "deli")) {
      purple_account_set_bool (mm_sms->account, "sms_delivery_report", FALSE);
      mm_sms->sms_delivery_report = FALSE;
      msg = g_strdup ("Delivery reports won't be requested");
    }
  } else if (!g_strcmp0 (args[1], "2")) {
    if (!g_strcmp0 (args[0], "vali")) {
      purple_account_set_int (mm_sms->account, "sms_validity", SMS_VALIDITY_2_DAYS);
      mm_sms->sms_validity = SMS_VALIDITY_2_DAYS;
      msg = g_strdup ("Set SMS validity period to 2 days\n"
                      "(Your SMC may not supports this setting");
    }
  } else if (!g_strcmp0 (args[1], "7")) {
    if (!g_strcmp0 (args[0], "vali")) {
      purple_account_set_int (mm_sms->account, "sms_validity", SMS_VALIDITY_7_DAYS);
      mm_sms->sms_validity = SMS_VALIDITY_7_DAYS;
      msg = g_strdup ("Set SMS validity period to 7 days\n"
                      "(Your SMC may not supports this setting");
    }
  } else if (!g_strcmp0 (args[1], "30")) {
    if (!g_strcmp0 (args[0], "vali")) {
      purple_account_set_int (mm_sms->account, "sms_validity", SMS_VALIDITY_30_DAYS);
      mm_sms->sms_validity = SMS_VALIDITY_30_DAYS;
      msg = g_strdup ("Set SMS validity period to 30 days\n"
                      "(Your SMC may not supports this setting");
    }
  }

  if (msg) {
    purple_conversation_write (conv,
                               "mm-sms",
                               msg,
                               PURPLE_MESSAGE_SYSTEM | PURPLE_MESSAGE_NO_LOG,
                               time(NULL));

    g_free (msg);
  }

  return PURPLE_CMD_RET_OK;
}


static gboolean
cb_dbus_signal_sms_added (MMModemMessaging  *device,
                          gchar             *path,
                          gpointer           user_data)
{
  pur_mm_get_new_sms (g_strdup (path));

  purple_debug_info ("mm-sms", "New SMS at '%s'", path);
  g_debug ("%s: New SMS at %s", __func__, path);

  return TRUE;
}


static void
cb_dbus_signal_sms_deleted (MMModemMessaging  *device,
                            gchar             *path,
                            gpointer           user_data)
{
  g_debug ("%s: Removed SMS at '%s'", __func__, path);
}


static void
cb_sms_delete_finish (MMModemMessaging *modem,
                      GAsyncResult     *result,
                      PurSmsProps      *sms_props)
{
  g_autoptr(GError)  error = NULL;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  if (mm_modem_messaging_delete_finish (modem, result, &error)) {
    g_ptr_array_remove (mm_sms->sms_arr, sms_props);
    g_debug ("Message delete finish");
  } else {
    g_debug ("Couldn't delete SMS - error: %s", error ? error->message : "unknown");
  }
}


static void
cb_sms_send_finish (MMSms        *sms,
                    GAsyncResult *result,
                    gpointer      user_data)
{
  g_autoptr(GError)  error = NULL;
  gboolean           fin;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  fin = mm_sms_send_finish (sms, result, &error);

  if (!fin) {
    pur_mm_signal_emit (mm_sms->sms_id, SMS_RECEIPT_NONE);
    g_debug ("Couldn't send SMS - error: %s", error ? error->message : "unknown");
  } else {
    pur_mm_signal_emit (mm_sms->sms_id, SMS_RECEIPT_MM_ACKN);
    g_debug ("Successfully sent SMS: %s", mm_sms_get_path (sms));
  }
}


static void
cb_sms_create_finish (MMModemMessaging *modem,
                      GAsyncResult     *result,
                      gpointer          user_data)
{
  MMSms             *sms;
  g_autoptr(GError)  error = NULL;

  sms = mm_modem_messaging_create_finish (modem, result, &error);

  if (!sms) {
    g_debug ("Couldn't create new SMS - error: %s", error ? error->message : "unknown");
  } else {
    g_debug ("Successfully created new SMS: %s", mm_sms_get_path (sms));

    mm_sms_send (sms,
                 NULL,
                 (GAsyncReadyCallback)cb_sms_send_finish,
                 NULL);

    g_object_unref (sms);
  }
}


static void
cb_sms_state_change (MMSms       *sms,
                     GParamSpec  *pspec,
                     PurSmsProps *sms_props)
{
  pur_mm_get_sms_properties (sms_props);

  if (sms_props->state == MM_SMS_STATE_RECEIVED) {
    // The message has been completely received,
    // hand the SMS message to the libpurple core
    pur_mm_get_sms_properties (sms_props);
    pur_mm_send_sms_to_purple_serv (sms_props);
  }

  g_debug ("%s:  state %s", __func__,
                            mm_sms_state_get_string (mm_sms_get_state (sms)));
}


static void
cb_sms_list_new_ready (MMModemMessaging *modem,
                       GAsyncResult     *result,
                       gchar            *path)
{
  GList             *l, *list;
  g_autoptr(GError)  error = NULL;
  PurSmsProps       *sms_props;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  list = mm_modem_messaging_list_finish (modem, result, &error);

  if (!list) {
    g_debug ("Couldn't get SMS list - error: %s", error ? error->message : "unknown");
  } else {
    for (l = list; l; l = g_list_next (l)) {
      if (!g_strcmp0 (mm_sms_get_path (MM_SMS (l->data)), path)) {
        sms_props = g_new0 (PurSmsProps, 1);
        sms_props->sms = g_object_ref (MM_SMS (l->data));
        g_ptr_array_add (mm_sms->sms_arr, sms_props);

        break;
      }
    }

    pur_mm_check_pdu_type (sms_props);

    g_list_free_full (list, g_object_unref);
    g_free (path);
  }

  g_debug ("%s", __func__);
}


static void
cb_sms_list_all_ready (MMModemMessaging *modem,
                       GAsyncResult     *result,
                       gpointer          user_data)
{
  GList             *l, *list;
  g_autoptr(GError)  error = NULL;
  PurSmsProps       *sms_props;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  list = mm_modem_messaging_list_finish (modem, result, &error);

  if (!list) {
    g_debug ("Couldn't get SMS list - error: %s", error ? error->message : "unknown");
  } else {
    for (l = list; l; l = g_list_next (l)) {
      sms_props = g_new0 (PurSmsProps, 1);
      sms_props->sms = g_object_ref (MM_SMS (l->data));
      g_ptr_array_add (mm_sms->sms_arr, sms_props);
    }

    g_ptr_array_foreach (mm_sms->sms_arr,
                         (GFunc) pur_mm_check_pdu_type,
                         NULL);

    g_list_free_full (list, g_object_unref);
  }

  g_debug ("%s", __func__);
}


static void
cb_get_sim_ready (MMModem      *modem,
                  GAsyncResult *res,
                  gpointer      user_data)
{
  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  mm_sms->sim = mm_modem_get_sim_finish (modem, res, NULL);

  g_debug ("Need to unlock sim %s (%s)",
           mm_sim_get_path (mm_sms->sim),
           mm_sim_get_identifier (mm_sms->sim));
}


static gboolean
pur_mm_get_modem_state (void)
{
  MMModemState state;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  if (!mm_sms->modem) {
    pur_mm_state (PUR_MM_STATE_NO_MODEM);
    return FALSE;
  }

  if (!mm_sms->modem_messaging) {
    pur_mm_state (PUR_MM_STATE_NO_MESSAGING_MODEM);
    return FALSE;
  }

  state = mm_modem_get_state (mm_sms->modem);

  if (state < MM_MODEM_STATE_ENABLED) {
    pur_mm_state (PUR_MM_STATE_MODEM_DISABLED);
    return FALSE;
  }

  return TRUE;
}


static void
pur_mm_get_sim_ready (MMModem *modem)
{
  if (!modem) {
    g_debug ("%s: No Modem", __func__);
    pur_mm_state (PUR_MM_STATE_NO_MODEM);
    return;
  }

  mm_modem_get_sim (modem,
                    NULL,
                    (GAsyncReadyCallback)cb_get_sim_ready,
                    NULL);
}


static void
cb_sim_send_pin_ready (MMSim        *sim,
                       GAsyncResult *res,
                       gpointer      user_data)
{
  g_autoptr(GError)  error = NULL;

  if (!mm_sim_send_pin_finish (sim, res, &error)) {
    g_debug ("Couldn't unlock SIM - error: %s", error ? error->message : "unknown");
    pur_mm_state (PUR_MM_STATE_MODEM_UNLOCK_ERROR);
  } else {
    pur_mm_purple_connect ();
    g_debug ("Succesfully unlocked %s", mm_sim_get_identifier (sim));
  }
}


static void
pur_mm_send_code_to_sim (const gchar *code)
{
  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  MMModemLock lock = mm_modem_get_unlock_required (mm_sms->modem);

  g_return_if_fail (code);

  if (lock == MM_MODEM_LOCK_SIM_PIN) {
    mm_sim_send_pin (mm_sms->sim,
                     code,
                     NULL,
                     (GAsyncReadyCallback)cb_sim_send_pin_ready,
                     NULL);
  } else {
    g_debug ("Unhandled lock type %u", lock);
  }
}


static void
pur_mm_send_sms_to_purple_serv (PurSmsProps *sms_props)
{
  PurpleConnection  *pc;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  if (pur_mm_get_modem_state ()) {
    pc = purple_account_get_connection (mm_sms->account);

    if (sms_props->message &&
        sms_props->number  &&
        (sms_props->message[0] != '\0') &&
        (sms_props->number [0] != '\0')) {

      serv_got_im (pc,
                   sms_props->number,
                   sms_props->message,
                   PURPLE_MESSAGE_RECV,
                   time (NULL));

      if (mm_sms->sms_delete_received) {
        pur_mm_delete_sms (sms_props);
      }
    }
  }
}


static void
pur_mm_get_sms_properties (PurSmsProps *sms_props)
{
  MMSms      *sms;
  const char *mm_time;
  struct tm   t_8601;

  sms = sms_props->sms;

  sms_props->state = mm_sms_get_state (sms);
  sms_props->path = mm_sms_get_path (sms);
  sms_props->number = mm_sms_get_number (sms);
  sms_props->message = mm_sms_get_text (sms);

  mm_time = mm_sms_get_timestamp (sms);
  strptime (mm_time, "%y%m%d%H%M%S%z", &t_8601);
  sms_props->t_stamp = mktime (&t_8601);
}


static gboolean
sms_props_match_by_msg_id (PurSmsProps  *sms_props,
                           gconstpointer message_ref)
{
  return GPOINTER_TO_INT(message_ref) == sms_props->message_ref;
}


static void
pur_mm_check_pdu_type (PurSmsProps *sms_props)
{
  MMSms        *sms;
  MMSmsState    state;
  MMSmsPduType  pdu_type;
  guint         message_ref;
  guint         delivery_state;
  guint         index;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  sms = sms_props->sms;
  state = mm_sms_get_state (sms);
  message_ref = mm_sms_get_message_reference (sms);
  pdu_type = mm_sms_get_pdu_type (sms);

  switch (pdu_type) {
    case MM_SMS_PDU_TYPE_SUBMIT:
      if (mm_sms_get_delivery_report_request (sms)) {
        sms_props->sms_id = mm_sms->sms_id;
      } else {
        if (mm_sms->sms_delete_sent) {
          pur_mm_delete_sms (sms_props);
        }
      }
      break;

    case MM_SMS_PDU_TYPE_DELIVER:
      if (state == MM_SMS_STATE_RECEIVED) {
        // The message has been completely received,
        // hand the SMS message to the libpurple core
        pur_mm_get_sms_properties (sms_props);
        pur_mm_send_sms_to_purple_serv (sms_props);
      }

      if (state == MM_SMS_STATE_RECEIVING) {
        // The first chunk of a multipart SMS has been
        // received -> wait for MM_SMS_STATE_RECEIVED
        g_signal_connect (sms_props->sms,
                          "notify::state",
                          G_CALLBACK (cb_sms_state_change),
                          sms_props);
      }
      break;

    case MM_SMS_PDU_TYPE_STATUS_REPORT:
      // with a state < 4 the SMS has been successfully delivered
      delivery_state = mm_sms_get_delivery_state (sms);

      g_return_if_fail (mm_sms->sms_arr);

      if (message_ref && delivery_state <= 3) {
        if (g_ptr_array_find_with_equal_func (mm_sms->sms_arr,
                                              GINT_TO_POINTER(message_ref),
                                              (GEqualFunc)sms_props_match_by_msg_id,
                                              &index)) {

          pur_mm_signal_emit (sms_props->sms_id, SMS_RECEIPT_SMC_ACKN);
          // Delete the acknowledged sms
          g_ptr_array_remove_index_fast (mm_sms->sms_arr, index);
        } else {
          pur_mm_signal_emit (sms_props->sms_id, SMS_RECEIPT_SMC_NACK);
        }
        // Delete the delivery-receipt sms
        g_ptr_array_remove (mm_sms->sms_arr, sms_props);
      }
      break;

    case MM_SMS_PDU_TYPE_UNKNOWN:
      g_debug ("Unknown PDU type");
      break;

    default:
      g_debug ("PDU type not handled");
  }

  g_debug ("%s: pdu type %s", __func__,
                              mm_sms_pdu_type_get_string (pdu_type));
}


static void
pur_mm_get_new_sms (char *path)
{
  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  mm_modem_messaging_list (mm_sms->modem_messaging,
                           NULL,
                           (GAsyncReadyCallback)cb_sms_list_new_ready,
                           path);

  g_debug ("%s path %s", __func__, (char*)path);
}


static void
pur_mm_get_all_sms (void)
{
  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  g_return_if_fail (MM_IS_MODEM_MESSAGING (mm_sms->modem_messaging));

  mm_modem_messaging_list (mm_sms->modem_messaging,
                           NULL,
                           (GAsyncReadyCallback)cb_sms_list_all_ready,
                           NULL);

  g_debug ("%s", __func__);
}


static void
pur_mm_delete_sms (PurSmsProps *sms_props)
{
  const char *path;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  path = mm_sms_get_path (sms_props->sms);

  if (path) {
    mm_modem_messaging_delete (mm_sms->modem_messaging,
                               path,
                               NULL,
                               (GAsyncReadyCallback)cb_sms_delete_finish,
                               sms_props);
  }
}


static gboolean
pur_mm_create_sms (const gchar *number,
                   const gchar *message,
                   guint        valadity,
                   gboolean     delivery_report)
{
  MMSmsProperties *properties;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  properties = mm_sms_properties_new ();

  if (!pur_mm_get_modem_state ()) {
    return FALSE;
  }

  if ((message[0] != '\0') && (number[0] != '\0')) {
    mm_sms_properties_set_text (properties, message);
    mm_sms_properties_set_number (properties, number);
    mm_sms_properties_set_delivery_report_request (properties, delivery_report);

    if (valadity) {
      mm_sms_properties_set_validity_relative (properties, valadity);
    }
  } else {
    purple_debug_info ("mm-sms", "No SMS text or number provided");
    g_debug ("No SMS text or number provided");
    g_object_unref (properties);
    return FALSE;
  }

  g_debug ("Creating new SMS");

  mm_modem_messaging_create (mm_sms->modem_messaging,
                             properties,
                             NULL,
                             (GAsyncReadyCallback)cb_sms_create_finish,
                             NULL);

  g_object_unref (properties);

  return TRUE;
}


static void
pur_mm_init_modem (MMObject *obj)
{
  MmGdbusModemMessaging *gdbus_sms;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  mm_sms->object = obj;
  mm_sms->modem = mm_object_get_modem (MM_OBJECT(obj));

  g_dbus_proxy_set_default_timeout (G_DBUS_PROXY(mm_sms->modem),
                                    PUR_MM_MODEM_TIMEOUT);

  mm_sms->modem_messaging = mm_object_get_modem_messaging (MM_OBJECT(obj));

  g_return_if_fail (MM_IS_MODEM_MESSAGING (mm_sms->modem_messaging));

  g_dbus_proxy_set_default_timeout (G_DBUS_PROXY(mm_sms->modem_messaging),
                                    PUR_MM_MODEM_SMS_TIMEOUT);

  gdbus_sms = MM_GDBUS_MODEM_MESSAGING(mm_sms->modem_messaging);

  g_signal_connect (gdbus_sms,
                    "added",
                    G_CALLBACK (cb_dbus_signal_sms_added),
                    NULL);

  g_signal_connect (gdbus_sms,
                    "deleted",
                    G_CALLBACK (cb_dbus_signal_sms_deleted),
                    NULL);

  g_debug ("%s", __func__);
}


static gboolean
device_match_by_object (PurMmDevice *device,
                        GDBusObject *object)

{
  g_return_val_if_fail (G_IS_DBUS_OBJECT(object), FALSE);
  g_return_val_if_fail (MM_OBJECT(device->object), FALSE);
  
  return object == G_DBUS_OBJECT (device->object);
}


static void
pur_mm_add_object (MMObject *obj)
{
  PurMmDevice *device;
  const gchar *object_path;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  object_path = g_dbus_object_get_object_path (G_DBUS_OBJECT (obj));

  g_return_if_fail (object_path);

  if (g_ptr_array_find_with_equal_func (mm_sms->device_arr,
                                        obj,
                                        (GEqualFunc)device_match_by_object,
                                        NULL)) {
    g_debug("Device %s already added", object_path);

    return;
  }

  g_debug ("Added device at: %s", object_path);

  // TODO choose default modem if devices->len > 1

  device = g_new0 (PurMmDevice, 1);
  device->object = MM_OBJECT (obj);
  device->modem = mm_object_get_modem (MM_OBJECT(obj));
  g_ptr_array_add (mm_sms->device_arr, device);

  pur_mm_init_modem (obj);
  pur_mm_get_all_sms ();

  pur_mm_state (PUR_MM_STATE_MODEM_FOUND);
}


static void
cb_object_added (GDBusObjectManager *manager,
                 GDBusObject        *object,
                 gpointer            user_data)
{
  MMModem *modem;

  modem = mm_object_get_modem (MM_OBJECT (object));

  if (mm_object_peek_modem_messaging (MM_OBJECT (object)))
    purple_signal_emit (purple_plugins_get_handle (),
                        "mm-sms-modem-added",
                        (int)mm_modem_get_state (modem));

  g_debug ("%s", __func__);
}


static void
cb_object_removed (GDBusObjectManager *manager,
                   GDBusObject        *object,
                   gpointer            user_data)
{
  guint index;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  g_return_if_fail (G_IS_DBUS_OBJECT(object));
  g_return_if_fail (G_IS_DBUS_OBJECT_MANAGER(manager));

  if (g_ptr_array_find_with_equal_func (mm_sms->device_arr,
                                        object,
                                        (GEqualFunc)device_match_by_object,
                                        &index)) {
    g_ptr_array_remove_index_fast (mm_sms->device_arr, index);
  }

  if (MM_OBJECT(object) == mm_sms->object) {
    pur_mm_state (PUR_MM_STATE_NO_MODEM);
  }

  g_debug ("Modem removed: %s", g_dbus_object_get_object_path (object));
}


static void
cb_name_owner_changed (GDBusObjectManager *manager,
                       GDBusObject        *object,
                       gpointer            user_data)
{
  gchar *name_owner;

  name_owner = g_dbus_object_manager_client_get_name_owner (G_DBUS_OBJECT_MANAGER_CLIENT (manager));

  if (!name_owner) {
    pur_mm_state (PUR_MM_STATE_NO_MANAGER);
  }

  g_debug ("Name owner changed");

  g_free (name_owner);
}


static void
pur_mm_get_modems (void)
{
  GList *list, *l;
  gboolean has_modem = FALSE;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  g_return_if_fail (MM_IS_MANAGER (mm_sms->mm));

  list = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (mm_sms->mm));

  for (l = list; l != NULL; l = l->next) {
    if (!mm_object_peek_modem_messaging (l->data))
      continue;

    has_modem = TRUE;
    pur_mm_add_object (MM_OBJECT(l->data));
  }

  if (!has_modem) {
    pur_mm_state (PUR_MM_STATE_NO_MODEM);
  } else if (list) {
    g_list_free_full (list, g_object_unref);
  }
}


static void
cb_mm_manager_new (GDBusConnection *connection,
                   GAsyncResult    *res,
                   gpointer         user_data)
{
  gchar             *name_owner;
  g_autoptr(GError)  error = NULL;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  mm_sms->mm = mm_manager_new_finish (res, &error);

  mm_sms->sms_arr = g_ptr_array_new ();
  mm_sms->device_arr = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);

  if (mm_sms->mm) {
    g_signal_connect (mm_sms->mm,
                      "interface-added",
                      G_CALLBACK (cb_object_added),
                      NULL);

    g_signal_connect (mm_sms->mm,
                      "object-added",
                      G_CALLBACK (cb_object_added),
                      NULL);

    g_signal_connect (mm_sms->mm,
                      "object-removed",
                      G_CALLBACK (cb_object_removed),
                      NULL);

    g_signal_connect (mm_sms->mm,
                      "notify::name-owner",
                      G_CALLBACK (cb_name_owner_changed),
                      NULL);

    name_owner = g_dbus_object_manager_client_get_name_owner (G_DBUS_OBJECT_MANAGER_CLIENT (mm_sms->mm));
    purple_debug_info ("mm-sms", "ModemManager found: %s\n", name_owner);
    g_free (name_owner);

    pur_mm_get_modems ();

    pur_mm_state (PUR_MM_STATE_MANAGER_FOUND);
  } else {
    purple_debug_info ("mm-sms", "Error connecting to ModemManager: %s\n", error->message);
    g_debug ("Error connecting to ModemManager: %s", error->message);

    pur_mm_state (PUR_MM_STATE_NO_MANAGER);
  }
}


static gboolean
pur_mm_get_modem_manager (void)
{
  GDBusConnection   *connection;
  g_autoptr(GError)  error = NULL;

  connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);

  if (connection) {
    mm_manager_new (connection,
                    G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE,
                    NULL,
                    (GAsyncReadyCallback) cb_mm_manager_new,
                    NULL);

    g_object_unref (connection);

    return TRUE;
  } else {
    purple_debug_info ("mm-sms", "Error connecting to ModemManager: %s\n", error->message);
    g_debug ("Error connecting to ModemManager: %s", error->message);

    return FALSE;
  }
}


static void
pur_mm_signal_emit (gchar *sms_id,
                    int    state)
{
  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  purple_signal_emit (purple_conversations_get_handle (),
                      "sms-sent",
                      sms_id,
                      state);

  if (mm_sms->sms_id != NULL) {
    g_free (mm_sms->sms_id);
  }
}


static const char *
pur_mm_list_icon (PurpleAccount *account, PurpleBuddy *buddy)
{
  return "mm-sms";
}


static const gchar *
pur_mm_list_emblem (PurpleBuddy *buddy)
{
  return "mobile";
}


GList *
pur_mm_status_types (PurpleAccount *account)
{
  GList            *types = NULL;
  PurpleStatusType *status;

  status = purple_status_type_new_full (PURPLE_STATUS_AVAILABLE,
                                        NULL,
                                        "Online",
                                        TRUE,
                                        TRUE,
                                        FALSE);

  types = g_list_append (types, status);

  status = purple_status_type_new_full (PURPLE_STATUS_MOBILE,
                                        "mobile",
                                        "Online",
                                        FALSE,
                                        FALSE,
                                        TRUE);

  types = g_list_append (types, status);

  status = purple_status_type_new_full (PURPLE_STATUS_OFFLINE,
                                        NULL,
                                        "Offline",
                                        FALSE,
                                        FALSE,
                                        FALSE);

  types = g_list_append (types, status);

  return types;
}


static void
pur_mm_add_buddy (PurpleConnection *pc,
                  PurpleBuddy      *buddy,
                  PurpleGroup      *group)
{
  const gchar *buddy_name;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  buddy_name = purple_buddy_get_name (buddy);

  purple_prpl_got_user_status (mm_sms->account,
                               buddy_name,
                               "available",
                               NULL);
}


static void
pur_mm_set_buddy_status (void)
{
  const gchar *buddy_name;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  PurpleBlistNode *node = purple_blist_get_root ();

  while (node) {
    if (PURPLE_BLIST_NODE_IS_BUDDY(node) &&
        purple_buddy_get_account ((PurpleBuddy *)node) == mm_sms->account) {

      buddy_name = purple_buddy_get_name ((PurpleBuddy *)node);

      purple_prpl_got_user_status (mm_sms->account,
                                   buddy_name,
                                   "available",
                                   NULL);

    }

    node = purple_blist_node_next (node, FALSE);
  }
}


static void
pur_mm_disconnect (void)
{
  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  if (mm_sms->device_arr->len) {
    g_ptr_array_set_size (mm_sms->device_arr, 0);
    g_ptr_array_unref(mm_sms->device_arr);
  }

  if (mm_sms->sms_arr->len) {
    g_ptr_array_unref(mm_sms->sms_arr);
  }

  mm_sms->modem_available = FALSE;
}


static void
pur_mm_purple_connect (void)
{
  PurpleConnection *pc;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  if (!pur_mm_get_modem_state ()) {
    return;
  }

  pc = purple_account_get_connection (mm_sms->account);

  purple_connection_update_progress (pc, "Connected", 1, 2);
  purple_blist_add_account (mm_sms->account);
  purple_connection_set_state (pc, PURPLE_CONNECTED);

  pur_mm_set_buddy_status ();

  pur_mm_state (PUR_MM_STATE_READY);
}


static void
pur_mm_state (int state)
{
  PurpleConnection *pc;
  MMModemState      modem_state;
  const char       *pin = NULL;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  pc = purple_account_get_connection (mm_sms->account);

  purple_signal_emit (purple_plugins_get_handle (),
                      "mm-sms-state",
                      state);

  switch (state) {
    case PUR_MM_STATE_MODEM_FOUND:
      if (!mm_sms->modem_available) {
        if (mm_sms->modem) {
          modem_state = mm_modem_get_state (mm_sms->modem);

          if (modem_state == MM_MODEM_STATE_LOCKED) {
            pin = purple_account_get_password (mm_sms->account);

            pur_mm_get_sim_ready (mm_sms->modem);
            pur_mm_send_code_to_sim (pin);
          } else {
            pur_mm_purple_connect ();
          }
        }

        mm_sms->modem_available = TRUE;
      }

      g_debug ("PUR_MM_STATE_MODEM_FOUND");
      break;

    case PUR_MM_STATE_NO_MODEM:
      if (mm_sms->modem_available) {
        pur_mm_disconnect ();

        purple_connection_error_reason  (pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
                                         "Modem vanished");
      } else {
        purple_connection_error_reason  (pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
                                         "Could not connect to modem");
      }

      mm_sms->modem_available = FALSE;
      g_debug ("PUR_MM_STATE_NO_MODEM");
      break;

    case PUR_MM_STATE_NO_MESSAGING_MODEM:
      if (mm_sms->modem_available) {
        pur_mm_disconnect ();

        purple_connection_error_reason  (pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
                                         "Modem has no messaging capabilities");
      }

      mm_sms->modem_available = FALSE;
      g_debug ("PUR_MM_STATE_NO_MESSAGING_MODEM");
      break;

    case PUR_MM_STATE_MODEM_DISABLED:

      purple_connection_error_reason  (pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
                                       "Modem disabled");
      mm_sms->modem_available = FALSE;
      g_debug ("PUR_MM_STATE_MODEM_DISABLED");
      break;

    case PUR_MM_STATE_MANAGER_FOUND:
      if (!mm_sms->manager_available) {
        mm_sms->manager_available = TRUE;
      }

      g_debug ("PUR_MM_STATE_MANAGER_FOUND");
      break;

    case PUR_MM_STATE_NO_MANAGER:
      if (mm_sms->manager_available) {
        pur_mm_disconnect ();

        if (mm_sms->mm) {
          g_object_unref (mm_sms->mm);
        }

        purple_connection_error_reason  (pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
                                         "ModemManager vanished");
      } else {
        purple_connection_error_reason  (pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
                                         "Could not connect to ModemManager");
      }

      mm_sms->manager_available = FALSE;
      g_debug ("PUR_MM_STATE_NO_MANAGER");
      break;

    case PUR_MM_STATE_MODEM_UNLOCK_ERROR:
      purple_connection_error_reason (pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED,
                                      "SIM card unlock failed");

      g_debug ("PUR_MM_STATE_MODEM_UNLOCK_ERROR");
      break;

    case PUR_MM_STATE_READY:
      g_debug ("PUR_MM_STATE_READY");
      break;

    default:
        ;
  }
}


static void
pur_mm_login (PurpleAccount *account)
{
  PurpleConnection *pc;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  pc = purple_account_get_connection (account);

  mm_sms->manager_available = FALSE;
  mm_sms->modem_available = FALSE;

  mm_sms->account = account;

  mm_sms->sms_delivery_report = purple_account_get_bool (account, "sms_delivery_report", TRUE);
  mm_sms->sms_delete_sent = purple_account_get_bool (account, "sms_delete_sent", TRUE);
  mm_sms->sms_delete_received = purple_account_get_bool (account, "sms_delete_received", TRUE);
  mm_sms->sms_validity = purple_account_get_int (account, "sms_validity", SMS_VALIDITY_NOT_SET);

  purple_connection_update_progress (pc, "Connecting to modem...", 0, 2);
  purple_connection_set_state (pc, PURPLE_CONNECTING);

  pur_mm_get_modem_manager ();
}


static void
pur_mm_close (PurpleConnection *pc)
{
  pur_mm_disconnect ();

  purple_connection_set_protocol_data (pc, NULL);
  purple_connection_set_state (pc, PURPLE_DISCONNECTED);
}


static int
pur_mm_send_im (PurpleConnection  *pc,
                const char        *who,
                const char        *message,
                PurpleMessageFlags flags)
{
  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  if (flags & PURPLE_MESSAGE_NOTIFY) {
    mm_sms->sms_id = g_strdup (message);

    return 1;
  }

  if (pur_mm_create_sms (who, message,
                         mm_sms->sms_validity,
                         mm_sms->sms_delivery_report)) {
    return 1;
  } else {
    return -1;
  }

  return 1;
}


static gboolean
plugin_load (PurplePlugin * plugin)
{
  return TRUE;
}


static gboolean
plugin_unload (PurplePlugin *plugin)
{
  return TRUE;
}

static void
plugin_destroy (PurplePlugin *plugin)
{
  purple_debug_info ("mm-sms", "shutting down\n");
  g_debug ("Shutting down");
}


static gboolean
plugin_can_receive_file (PurpleConnection *gc,
                         const char       *who)
{
  return FALSE;
}


static gboolean
plugin_offline_message (const PurpleBuddy *buddy)
{
  return TRUE;
}


static GHashTable *
pur_mm_account_text_table (PurpleAccount *account)
{
  GHashTable *table;

  table = g_hash_table_new (g_str_hash, g_str_equal);

  g_hash_table_insert (table,
                       (gpointer)"login_label",
                       (gpointer)_("Anything"));

  g_hash_table_insert (table,
                       (gpointer)"password_label",
                       (gpointer)_("PIN Number"));

  return table;
}


static void
init_plugin (PurplePlugin * plugin)
{
  PurpleAccountOption      *option;
  PurplePluginInfo         *info = plugin->info;
  PurplePluginProtocolInfo *prpl_info = info->extra_info;
  PurpleKeyValuePair       *kvp;
  GList                    *list = NULL;

  pur_mm_data_t *mm_sms = pur_mm_get_data ();

  memset (mm_sms, 0, sizeof (pur_mm_data_t));

  purple_debug_info ("mm-sms", "starting up\n");

  mm_sms->sms_validity         = 0;
  mm_sms->sms_delete_sent      = TRUE;
  mm_sms->sms_delete_received  = TRUE;
  mm_sms->sms_delivery_report  = TRUE;

  #define ADD_OPTION(list, desc, v) { \
    kvp = g_new0 (PurpleKeyValuePair, 1); \
    kvp->key = g_strdup ((desc)); \
    kvp->value = g_strdup_printf ("%d", v); \
    list = g_list_prepend (list, kvp); \
  }

  ADD_OPTION(list, _("30 days"), SMS_VALIDITY_30_DAYS);
  ADD_OPTION(list, _("7 days"), SMS_VALIDITY_7_DAYS);
  ADD_OPTION(list, _("2 days"), SMS_VALIDITY_2_DAYS);
  ADD_OPTION(list, _("SMC Default"), SMS_VALIDITY_NOT_SET);

  option = purple_account_option_list_new (_("SMS validity period"),
                                           "validity",
                                           list);

  prpl_info->protocol_options =
    g_list_append (prpl_info->protocol_options, option);

  option = purple_account_option_bool_new (_("Request delivery reports from SMC"),
                                           "sms_delivery_report",
                                           TRUE);

  prpl_info->protocol_options =
    g_list_append (prpl_info->protocol_options, option);

  option = purple_account_option_bool_new (_("Remove sent SMS from modem storage"),
                                           "sms_delete_sent",
                                           TRUE);

  prpl_info->protocol_options =
    g_list_append (prpl_info->protocol_options, option);

  option = purple_account_option_bool_new (_("Remove received SMS from modem storage"),
                                           "sms_delete_received",
                                           TRUE);

  prpl_info->protocol_options =
    g_list_append (prpl_info->protocol_options, option);

  purple_signal_register (purple_plugins_get_handle (),
                          "mm-sms-modem-added",
                          purple_marshal_VOID__INT, NULL, 1,
                          purple_value_new (PURPLE_TYPE_INT));

  purple_signal_register (purple_plugins_get_handle (),
                          "mm-sms-state",
                          purple_marshal_VOID__INT, NULL, 1,
                          purple_value_new (PURPLE_TYPE_INT));

  purple_signal_register (purple_conversations_get_handle (),
                          "sms-sent",
                          purple_marshal_VOID__POINTER_UINT, NULL, 2,
                          purple_value_new (PURPLE_TYPE_STRING),
                          purple_value_new (PURPLE_TYPE_INT));

  purple_cmd_register ("mm-sms",
                       "ww",
                       PURPLE_CMD_P_PLUGIN,
                       PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS,
                       NULL,
                       cb_mm_sms_cmd,
                       "mm-sms &lt;help&gt;:  "
                       "For a list of commands use the 'help' argument.",
                       NULL);
}


static PurplePluginProtocolInfo prpl_info = {
  /* options */
  OPT_PROTO_PASSWORD_OPTIONAL | OPT_PROTO_IM_IMAGE,
  NULL,                   /* user_splits */
  NULL,                   /* protocol_options */
  {   /* icon_spec, a PurpleBuddyIconSpec */
    "png,jpg,gif",                   /* format */
    0,                               /* min_width */
    0,                               /* min_height */
    128,                             /* max_width */
    128,                             /* max_height */
    10000,                           /* max_filesize */
    PURPLE_ICON_SCALE_DISPLAY,       /* scale_rules */
  },                      /* icon_spec */
  pur_mm_list_icon,       /* list_icon */
  pur_mm_list_emblem,     /* list_emblems */
  NULL,                   /* status_text */
  NULL,                   /* tooltip_text */
  pur_mm_status_types,    /* status_types */
  NULL,                   /* blist_node_menu */
  NULL,                   /* chat_info */
  NULL,                   /* chat_info_defaults */
  pur_mm_login,           /* login */
  pur_mm_close,           /* close */
  pur_mm_send_im,         /* send_im */
  NULL,                   /* set_info */
  NULL,                   /* send_typing */
  NULL,                   /* get_info */
  NULL,                   /* set_status */
  NULL,                   /* set_idle */
  NULL,                   /* change_passwd */
  pur_mm_add_buddy,       /* add_buddy */
  NULL,                   /* add_buddies */
  NULL,                   /* remove_buddy */
  NULL,                   /* remove_buddies */
  NULL,                   /* add_permit */
  NULL,                   /* add_deny */
  NULL,                   /* rem_permit */
  NULL,                   /* rem_deny */
  NULL,                   /* set_permit_deny */
  NULL,                   /* join_chat */
  NULL,                   /* reject chat invite */
  NULL,                   /* get_chat_name */
  NULL,                   /* chat_invite */
  NULL,                   /* chat_leave */
  NULL,                   /* chat_whisper */
  NULL,                   /* chat_send */
  NULL,                   /* keepalive */
  NULL,                   /* register_user */
  NULL,                   /* get_cb_info */
  NULL,                   /* get_cb_away */
  NULL,                   /* alias_buddy */
  NULL,                   /* group_buddy */
  NULL,                   /* rename_group */
  NULL,                   /* buddy_free */
  NULL,                   /* convo_closed */
  purple_normalize_nocase,/* normalize */
  NULL,                   /* set_buddy_icon */
  NULL,                   /* remove_group */
  NULL,                   /* get_cb_real_name */
  NULL,                   /* set_chat_topic */
  NULL,                   /* find_blist_chat */
  NULL,                   /* roomlist_get_list */
  NULL,                   /* roomlist_cancel */
  NULL,                   /* roomlist_expand_category */
  plugin_can_receive_file,    /* can_receive_file */
  NULL,                   /* send_file */
  NULL,                   /* new_xfer */
  plugin_offline_message,     /* offline_message */
  NULL,                   /* whiteboard_prpl_ops */
  NULL,                   /* send_raw */
  NULL,                   /* roomlist_room_serialize */
  NULL,                   /* unregister_user */
  NULL,                   /* send_attention */
  NULL,                   /* attention_types */
  sizeof(PurplePluginProtocolInfo), /* struct_size */
  pur_mm_account_text_table,        /* get_account_text_table */
  NULL,                    // initiate_media
  NULL,                    // get_media_caps
  NULL,                    // get_moods
  NULL,                    // set_public_alias
  NULL,                    // get_public_alias
  NULL,                    // add_buddy_with_invite
  NULL                     // add_buddies_with_invite
};


static PurplePluginInfo info = {
  PURPLE_PLUGIN_MAGIC,
  PURPLE_MAJOR_VERSION,
  PURPLE_MINOR_VERSION,
  PURPLE_PLUGIN_PROTOCOL,
  NULL,
  0,
  NULL,
  PURPLE_PRIORITY_DEFAULT,
  "prpl-mm-sms",
  "ModemManager SMS",
  PUR_MM_SMS_PLUGIN_VERSION,
  "Modemmanager Protocol Plugin",
  "Modemmanager SMS Plugin",
  "Andrea Schäfer <andrea.schaefer@puri.sm>",
  "https://source.puri.sm/Librem5/purple-mm-sms",
  plugin_load,
  plugin_unload,
  plugin_destroy,
  NULL,
  &prpl_info,
  NULL,
  NULL,
  NULL,
  NULL,
  NULL,
  NULL
};

PURPLE_INIT_PLUGIN (mm_sms, init_plugin, info)
