/*
 * modem/call-service.c - Interface towards oFono VoiceCallManager
 *
 * Copyright (C) 2008,2010 Nokia Corporation
 *   @author Pekka Pessi <first.surname@nokia.com>
 *   @author Lassi Syrjala <first.surname@nokia.com>
 *   @author Kai Vehmanen <first.surname@nokia.com>
 * Copyright (C) 2014 Jolla Ltd
 *
 * This work is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This work 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#define MODEM_DEBUG_FLAG MODEM_LOG_CALL

#include "modem/debug.h"

#include "modem/call.h"
#include "modem/ofono.h"
#include "modem/errors.h"

#include "modem/tones.h"

#include "modem/request-private.h"

#include <dbus/dbus-glib-lowlevel.h>

#include "signals-marshal.h"

#include <string.h>
#include <errno.h>

/* ---------------------------------------------------------------------- */

G_DEFINE_TYPE (ModemCallService, modem_call_service, MODEM_TYPE_OFACE);

#define VOICECALL_AGENT_PATH  "/voicecallagent"
#define VOICECALL_AGENT_IFACE "org.ofono.VoiceCallAgent"

/* Properties */
enum
{
  PROP_NONE,
  PROP_EMERGENCY_NUMBERS,
  PROP_ALERT_TONE_NEEDED,
  LAST_PROPERTY
};

/* Signals */
enum
{
  SIGNAL_INCOMING,
  SIGNAL_CREATED,
  SIGNAL_REMOVED,
  N_SIGNALS
};

static guint signals[N_SIGNALS];

struct _ModemCallServicePrivate
{
  /* < object_path, call instance > */
  GHashTable *instances;

  struct {
    GQueue queue[1];
    GQueue created[1];
  } dialing;

  struct {
    ModemCall *instance;
  } conference;

  char **emergency_numbers;

  gchar *forwarded;

  ModemCall *active, *hold;

  /* Do we have in-band connection? */
  DBusGProxy *call_agent_proxy;
  gboolean call_agent_registered;
  gboolean alert_tone_needed;

  unsigned signals :1;
  unsigned :0;
};

/* ---------------------------------------------------------------------- */

static void modem_call_service_connect_to_instance (ModemCallService *self,
    ModemCall *ci);

static void modem_call_service_disconnect_instance (ModemCallService *self,
    ModemCall *ci);

static ModemCall *modem_call_service_ensure_instance (ModemCallService *self,
    char const *object_path,
    GHashTable *properties);

static ModemRequestCallNotify modem_call_request_dial_reply;

static ModemRequestCallNotify modem_call_conference_request_reply;

static void on_modem_call_state (ModemCall *, ModemCallState,
    ModemCallService *);

static DBusHandlerResult modem_call_agent_dbus_message_handler(
    DBusConnection *, DBusMessage *, void *);

static void modem_call_agent_register(ModemCallService *);

static void modem_call_agent_register_reply (DBusGProxy *,
        DBusGProxyCall *, void *);

static void modem_call_agent_unregister(DBusConnection *, void *);

static DBusObjectPathVTable modem_call_agent_table = {
  .unregister_function = modem_call_agent_unregister,
  .message_function    = modem_call_agent_dbus_message_handler,
};

/* ---------------------------------------------------------------------- */

#define RETURN_IF_NOT_VALID(self)                       \
  g_return_if_fail (self != NULL &&                     \
      modem_oface_is_connected (MODEM_OFACE (self)))

#define RETURN_VAL_IF_NOT_VALID(self, val)                      \
  g_return_val_if_fail (self != NULL &&                         \
      modem_oface_is_connected (MODEM_OFACE (self)), (val))

#define RETURN_NULL_IF_NOT_VALID(self) RETURN_VAL_IF_NOT_VALID (self, NULL)

#define DBUS_PROXY(x) modem_oface_dbus_proxy (MODEM_OFACE (x))

/* ---------------------------------------------------------------------- */

static void
modem_call_service_constructed (GObject *object)
{
  if (G_OBJECT_CLASS (modem_call_service_parent_class)->constructed)
    G_OBJECT_CLASS (modem_call_service_parent_class)->constructed (object);
}

static void
modem_call_service_init (ModemCallService *self)
{
  DEBUG ("enter");

  self->priv = G_TYPE_INSTANCE_GET_PRIVATE(self,
      MODEM_TYPE_CALL_SERVICE, ModemCallServicePrivate);

  g_queue_init (self->priv->dialing.queue);
  g_queue_init (self->priv->dialing.created);

  self->priv->instances = g_hash_table_new_full (
      g_str_hash, g_str_equal, NULL, g_object_unref);

  self->priv->forwarded = NULL;
  self->priv->call_agent_proxy = NULL;
  self->priv->call_agent_registered = FALSE;
  self->priv->alert_tone_needed = FALSE;
}

static void
modem_call_service_get_property (GObject *object,
                                 guint property_id,
                                 GValue *value,
                                 GParamSpec *pspec)
{
  ModemCallService *self = MODEM_CALL_SERVICE (object);

  switch (property_id)
    {
    case PROP_EMERGENCY_NUMBERS:
      g_value_set_boxed (value, modem_call_get_emergency_numbers (self));
      break;

    case PROP_ALERT_TONE_NEEDED:
      g_value_set_boolean(value, self->priv->alert_tone_needed);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
modem_call_service_set_property (GObject *obj,
                                 guint property_id,
                                 const GValue *value,
                                 GParamSpec *pspec)
{
  ModemCallService *self = MODEM_CALL_SERVICE (obj);
  ModemCallServicePrivate *priv = self->priv;
  gpointer old;

  switch (property_id)
    {

    case PROP_EMERGENCY_NUMBERS:
      old = priv->emergency_numbers;
      priv->emergency_numbers = g_value_dup_boxed (value);
      g_strfreev (old);
      break;

    case PROP_ALERT_TONE_NEEDED:
      priv->alert_tone_needed = g_value_get_boolean (value);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec);
      break;
    }
}

static void
modem_call_service_dispose (GObject *object)
{
  DEBUG ("enter");

  if (G_OBJECT_CLASS (modem_call_service_parent_class)->dispose)
    G_OBJECT_CLASS (modem_call_service_parent_class)->dispose (object);
}


static void
modem_call_service_finalize (GObject *object)
{
  DEBUG ("enter");

  ModemCallService *self = MODEM_CALL_SERVICE (object);
  ModemCallServicePrivate *priv = self->priv;

  if (priv->forwarded)
    {
      g_free(priv->forwarded);
      priv->forwarded = NULL;
    }

  g_strfreev (priv->emergency_numbers), priv->emergency_numbers = NULL;

  g_hash_table_destroy (priv->instances);

  G_OBJECT_CLASS (modem_call_service_parent_class)->finalize (object);

  DEBUG ("leave");
}

/* ---------------------------------------------------------------------- */

static ModemOfaceManagedReply reply_to_call_manager_get_calls;

static void on_manager_call_added (DBusGProxy *proxy,
    char const *path, GHashTable *properties,
    gpointer user_data);

static void on_manager_call_removed (DBusGProxy *proxy,
    char const *path,
    gpointer user_data);

static void on_manager_call_forwarded (DBusGProxy *proxy,
    char const *type,
    gpointer user_data);

static char const *
modem_call_service_property_mapper (char const *name)
{
  if (!strcmp (name, "EmergencyNumbers"))
      return "emergency-numbers";
  return NULL;
}

static void
reply_to_call_manager_get_calls (ModemOface *_self,
                                 ModemRequest *request,
                                 GPtrArray *array,
                                 GError const *error,
                                 gpointer user_data)
{
  ModemCallService *self = MODEM_CALL_SERVICE (_self);

  DEBUG ("enter");

  if (!error)
    {
      guint i;

      for (i = 0; i < array->len; i++)
        {
          GValueArray *va = g_ptr_array_index (array, i);
          char const *path = g_value_get_boxed (va->values + 0);
          GHashTable *properties = g_value_get_boxed (va->values + 1);

          modem_call_service_ensure_instance (self, path, properties);
        }
    }

  modem_oface_check_connected (_self, request, error);
}

/** Connect to call service */
static void
modem_call_service_connect (ModemOface *_self)
{
  DEBUG ("(%p): enter", _self);

  ModemCallService *self = MODEM_CALL_SERVICE (_self);
  ModemCallServicePrivate *priv = self->priv;
  DBusGProxy *proxy = DBUS_PROXY (_self);

  if (!priv->signals)
    {
      priv->signals = TRUE;

#define CONNECT(p, handler, name, signature...) \
      dbus_g_proxy_add_signal (p, (name), ##signature);                 \
      dbus_g_proxy_connect_signal (p, (name), G_CALLBACK (handler), self, NULL)

      CONNECT (proxy, on_manager_call_added, "CallAdded",
          DBUS_TYPE_G_OBJECT_PATH, MODEM_TYPE_DBUS_DICT, G_TYPE_INVALID);

      CONNECT (proxy, on_manager_call_removed, "CallRemoved",
          DBUS_TYPE_G_OBJECT_PATH, G_TYPE_INVALID);

      CONNECT (proxy, on_manager_call_forwarded, "Forwarded",
          G_TYPE_STRING, G_TYPE_INVALID);

#undef CONNECT
    }

  modem_oface_connect_properties (_self, TRUE);

  modem_oface_add_connect_request (_self,
      modem_oface_request_managed (_self, "GetCalls",
          reply_to_call_manager_get_calls, NULL));

  modem_call_agent_register(self);
}


/** Disconnect from call service */
static void
modem_call_service_disconnect (ModemOface *_self)
{
  DEBUG ("(%p): enter", _self);

  ModemCallService *self = MODEM_CALL_SERVICE (_self);
  ModemCallServicePrivate *priv = self->priv;
  GHashTableIter iter[1];
  ModemCall *ci;

  while (!g_queue_is_empty (priv->dialing.queue))
    {
      ModemRequest *request = g_queue_pop_head (priv->dialing.queue);
      modem_request_cancel (request);
    }

  if (priv->signals)
    {
      priv->signals = FALSE;

      dbus_g_proxy_disconnect_signal (DBUS_PROXY (self), "CallAdded",
          G_CALLBACK (on_manager_call_added), self);
      dbus_g_proxy_disconnect_signal (DBUS_PROXY (self), "CallRemoved",
          G_CALLBACK (on_manager_call_removed), self);
      dbus_g_proxy_disconnect_signal (DBUS_PROXY (self), "Forwarded",
          G_CALLBACK (on_manager_call_forwarded), self);
    }

  for (g_hash_table_iter_init (iter, priv->instances);
       g_hash_table_iter_next (iter, NULL, (gpointer)&ci);
       g_hash_table_iter_init (iter, priv->instances))
    {
      modem_call_service_disconnect_instance (self, ci);
    }

  ci = priv->conference.instance;
  modem_call_service_disconnect_instance (self, ci);

  modem_oface_disconnect_properties (_self);
}

/* ---------------------------------------------------------------------- */

static void
modem_call_service_class_init (ModemCallServiceClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  ModemOfaceClass *oface_class = MODEM_OFACE_CLASS (klass);

  DEBUG ("enter");

  g_type_class_add_private (klass, sizeof (ModemCallServicePrivate));

  object_class->constructed = modem_call_service_constructed;
  object_class->get_property = modem_call_service_get_property;
  object_class->set_property = modem_call_service_set_property;
  object_class->dispose = modem_call_service_dispose;
  object_class->finalize = modem_call_service_finalize;

  oface_class->ofono_interface = MODEM_OFACE_CALL_MANAGER;
  oface_class->property_mapper = modem_call_service_property_mapper;
  oface_class->connect = modem_call_service_connect;
  oface_class->disconnect = modem_call_service_disconnect;

  /* Properties */
  g_object_class_install_property (object_class, PROP_EMERGENCY_NUMBERS,
      g_param_spec_boxed ("emergency-numbers",
          "Emergency Numbers",
          "List of emergency numbers obtained from modem",
          G_TYPE_STRV,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT |
          G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_ALERT_TONE_NEEDED,
      g_param_spec_boolean ("alert-tone-needed",
          "Alert Tone needed",
          "True if the network cannot send in-band alerting tone",
          FALSE, /* default assumption: user connection is OK */
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT |
          G_PARAM_STATIC_STRINGS));

  /* Signals to emit */
  signals[SIGNAL_INCOMING] =
    g_signal_new ("incoming", G_OBJECT_CLASS_TYPE (klass),
        G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
        0,
        NULL, NULL,
        _modem__marshal_VOID__OBJECT_STRING,
        G_TYPE_NONE, 2,
        MODEM_TYPE_CALL, G_TYPE_STRING);

  signals[SIGNAL_CREATED] =
    g_signal_new ("created", G_OBJECT_CLASS_TYPE (klass),
        G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
        0,
        NULL, NULL,
        _modem__marshal_VOID__OBJECT_STRING,
        G_TYPE_NONE, 2,
        MODEM_TYPE_CALL, G_TYPE_STRING);

  signals[SIGNAL_REMOVED] =
    g_signal_new ("removed", G_OBJECT_CLASS_TYPE (klass),
        G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
        0,
        NULL, NULL,
        _modem__marshal_VOID__OBJECT,
        G_TYPE_NONE, 1,
        MODEM_TYPE_CALL);

  DEBUG ("leave");
}

/* ---------------------------------------------------------------------- */

static void
modem_call_service_connect_to_instance (ModemCallService *self,
                                        ModemCall *instance)
{
  ModemCallServicePrivate *priv = self->priv;
  gchar const *object_path;

  if (!instance)
    return;

  g_signal_connect (instance, "state", G_CALLBACK (on_modem_call_state), self);

  modem_oface_connect (MODEM_OFACE (instance));

  object_path = modem_call_get_path (instance);

  g_hash_table_insert (priv->instances, (gpointer) object_path, instance);
}

static void
modem_call_service_disconnect_instance (ModemCallService *self,
                                        ModemCall *instance)
{
  ModemCallServicePrivate *priv = self->priv;

  if (!instance)
    return;

  g_hash_table_steal (priv->instances, modem_call_get_path (instance));

  g_signal_handlers_disconnect_by_func (instance, on_modem_call_state, self);

  g_signal_emit (self, signals[SIGNAL_REMOVED], 0, instance);

  modem_oface_disconnect (MODEM_OFACE (instance));

  g_object_unref (instance);
}

static ModemCall *
modem_call_service_ensure_instance (ModemCallService *self,
                                    char const *object_path,
                                    GHashTable *properties)
{
  ModemCallServicePrivate *priv = self->priv;
  char *key;
  GValue *value;
  GHashTableIter iter[1];
  gchar const *remote;
  ModemCallState state;
  gboolean incoming = FALSE, originating = FALSE;
  ModemCall *ci;

  DEBUG ("path %s", object_path);

  if (DEBUGGING)
    {
      for (g_hash_table_iter_init (iter, properties);
           g_hash_table_iter_next (iter, (gpointer)&key, (gpointer)&value);)
        {
          char *s = g_strdup_value_contents (value);
          DEBUG ("%s = %s", key, s);
          g_free (s);
        }
    }

  ci = g_hash_table_lookup (priv->instances, object_path);
  if (ci)
    {
      DEBUG ("call already exists %p", (void *)ci);
      return ci;
    }

  value = g_hash_table_lookup (properties, "LineIdentification");
  remote = g_value_get_string (value);

  value = g_hash_table_lookup (properties, "State");
  state = modem_call_state_from_ofono_state (g_value_get_string (value));

  switch (state)
    {
    case MODEM_CALL_STATE_INCOMING:
    case MODEM_CALL_STATE_WAITING:
      incoming = TRUE;
      originating = FALSE;
      break;

    case MODEM_CALL_STATE_DIALING:
    case MODEM_CALL_STATE_ALERTING:
    case MODEM_CALL_STATE_ACTIVE:
    case MODEM_CALL_STATE_HELD:
      incoming = FALSE;
      originating = TRUE;
      break;

    case MODEM_CALL_STATE_INVALID:
    case MODEM_CALL_STATE_DISCONNECTED:
      DEBUG ("call already in invalid state");
      return NULL;
    }

  ci = g_object_new (MODEM_TYPE_CALL,
      "object-path", object_path,
      "call-service", self,
      "state", state,
      "terminating", !originating,
      "originating",  originating,
      NULL);

  modem_oface_update_properties (MODEM_OFACE (ci), properties);
  modem_call_service_connect_to_instance (self, ci);

  if (incoming)
    {
      DEBUG ("emit \"incoming\" (\"%s\" (%p), \"%s\")",
          modem_call_get_name (ci), ci, remote);
      g_signal_emit (self, signals[SIGNAL_INCOMING], 0, ci, remote);
      if (priv->forwarded && !strcmp(priv->forwarded, "incoming"))
        {
          g_signal_emit_by_name (ci, "forwarded");
        }
    }
  else if (g_queue_is_empty (priv->dialing.queue))
    {
      DEBUG ("emit \"created\" (\"%s\" (%p), \"%s\")",
          modem_call_get_name (ci), ci, remote);
      g_signal_emit (self, signals[SIGNAL_CREATED], 0, ci, remote);
      if (priv->forwarded && !strcmp(priv->forwarded, "outgoing"))
        {
          g_signal_emit_by_name (ci, "forwarded");
        }
    }
  else {
    g_queue_push_tail (priv->dialing.created, ci);
  }

  if (priv->forwarded)
    {
      g_free(priv->forwarded);
      priv->forwarded = NULL;
    }

  return ci;
}

static ModemCall *
modem_call_service_get_dialed (ModemCallService *self,
                               char const *object_path,
                               char const *remote)
{
  ModemCallServicePrivate *priv = self->priv;
  ModemCall *ci;

  ci = g_hash_table_lookup (priv->instances, object_path);
  if (ci)
    {
      DEBUG ("call already exists %p", (void *)ci);

      if (g_queue_find (priv->dialing.created, ci))
        g_queue_remove (priv->dialing.created, ci);

      return ci;
    }

  ci = g_object_new (MODEM_TYPE_CALL,
      "object-path", object_path,
      "call-service", self,
      "remote", remote,
      "state", MODEM_CALL_STATE_DIALING,
      "ofono-state", "dialing",
      "terminating", FALSE,
      "originating", TRUE,
      NULL);

  modem_call_service_connect_to_instance (self, ci);

  return ci;
}

/* ---------------------------------------------------------------------- */
/* ModemCallService interface */

static void
on_manager_call_added (DBusGProxy *proxy,
                       char const *path,
                       GHashTable *properties,
                       gpointer user_data)
{
  ModemCallService *self = MODEM_CALL_SERVICE (user_data);

  DEBUG ("%s", path);

  modem_call_service_ensure_instance (self, path, properties);
}

static void
on_manager_call_removed (DBusGProxy *proxy,
                         char const *path,
                         gpointer user_data)
{
  DEBUG ("%s", path);

  ModemCallService *self = MODEM_CALL_SERVICE (user_data);
  ModemCallServicePrivate *priv = self->priv;
  ModemCall *ci = g_hash_table_lookup (priv->instances, path);

  if (ci)
    {
      modem_call_service_disconnect_instance (self, ci);
    }
}

static void
on_manager_call_forwarded (DBusGProxy *proxy,
                         char const *type,
                         gpointer user_data)
{
  DEBUG ("%s", type);

  ModemCallService *self = MODEM_CALL_SERVICE (user_data);
  ModemCall *ci = NULL;
  GHashTableIter iter[1];

  g_hash_table_iter_init (iter, self->priv->instances);
  while (g_hash_table_iter_next (iter, NULL, (gpointer)&ci))
    {
      ModemCallState state;
      g_object_get (ci, "state", &state, NULL);

      if (state == MODEM_CALL_STATE_INCOMING && strcmp(type, "incoming") == 0) 
        {
          break;
        }
      else if (state == MODEM_CALL_STATE_DIALING && strcmp(type, "outgoing") == 0) 
        {
          break;
        }
    }

  if (ci)
    {
      g_signal_emit_by_name (ci, "forwarded");
    }
  else
    {
      self->priv->forwarded = g_strdup(type);
    }
}

void
modem_call_service_resume (ModemCallService *self)
{
  GHashTableIter iter[1];
  ModemCall *ci;

  DEBUG ("enter");
  RETURN_IF_NOT_VALID (self);

  /* XXX/KV: no such signal */
#if 0
  g_signal_emit (self, signals[SIGNAL_EMERGENCY_NUMBERS_CHANGED], 0,
      modem_call_get_emergency_numbers (self));
#endif

  g_hash_table_iter_init (iter, self->priv->instances);
  while (g_hash_table_iter_next (iter, NULL, (gpointer)&ci))
    {
      char *remote;
      gboolean terminating = FALSE;
      ModemCallState state;

      g_object_get (ci,
          "state", &state,
          "remote", &remote,
          "terminating", &terminating,
          NULL);

      if (state != MODEM_CALL_STATE_DISCONNECTED &&
          state != MODEM_CALL_STATE_INVALID)
        {

          /* XXX - atm the value of 'terminating' cannot be trusted.
           * oFono should probably provide the direction as a property
           * since we cannot rely on the call state here. */
          if (terminating)
            {
              modem_message (MODEM_LOG_CALL,
                  "incoming [with state %s] call from \"%s\"",
                  modem_call_get_state_name (state), remote);
              DEBUG ("emit \"incoming\"(%s (%p), %s)",
                  modem_call_get_name (ci), ci, remote);
              g_signal_emit (self, signals[SIGNAL_INCOMING], 0, ci, remote);
            }
          else {
            modem_message (MODEM_LOG_CALL,
                "created [with state %s] call to \"%s\"",
                modem_call_get_state_name (state), remote);
            DEBUG ("emit \"created\"(%s (%p), %s)",
                modem_call_get_name (ci), ci, remote);
            g_signal_emit (self, signals[SIGNAL_CREATED], 0, ci, remote);
          }

          g_signal_emit_by_name (ci, "state", state, 0, 0);
        }

      g_free (remote);
    }
}

/* ---------------------------------------------------------------------- */
/* Obtain the list of emergency numbers (usually from SIM) */

static char const modem_call_sos[] = "urn:service:sos";

/** Get currently cached list of emergency numbers. */
char const * const *
modem_call_get_emergency_numbers (ModemCallService *self)
{
  static char const * const default_numbers[] = {
    "112", "911", "118", "119", "000", "110", "08", "999", NULL
  };

  if (MODEM_IS_CALL_SERVICE (self) && self->priv->emergency_numbers)
    {
      return (char const * const *)self->priv->emergency_numbers;
    }

  return default_numbers;
}

/** Get emergency service corresponding to number. */
char const *
modem_call_get_emergency_service (ModemCallService *self,
                                  char const *destination)
{
  char const * const *numbers;

  if (destination == NULL)
    return NULL;

  if (modem_call_get_valid_emergency_urn (destination))
    return modem_call_get_valid_emergency_urn (destination);

  numbers = modem_call_get_emergency_numbers (self);

  for (; *numbers; numbers++)
    {
      size_t n = strlen (*numbers);

      if (!g_str_has_prefix (destination, *numbers))
        continue;

      if (destination[n] && destination[n] != 'p' && destination[n] != 'w')
        continue;

      return modem_call_sos;
    }

  return NULL;
}

/** Check if @urn is an emergency service. */
char const *
modem_call_get_valid_emergency_urn (char const *urn)
{
  /* urn:service:sos see RFC 5031
   *
   * service-URN  = "URN:service:" service
   * service      = top-level *("." sub-service)
   * top-level    = let-dig [ *25let-dig-hyp let-dig ]
   * sub-service  = let-dig [ *let-dig-hyp let-dig ]
   * let-dig-hyp  = let-dig / "-"
   * let-dig      = ALPHA / DIGIT
   * ALPHA        = %x41-5A / %x61-7A   ; A-Z / a-z
   * DIGIT        = %x30-39 ; 0-9
   */
  char const *sos;
  int i, dot, hyp;
  int n = (sizeof modem_call_sos) - 1;

  if (urn == NULL)
    return NULL;

  if (g_ascii_strncasecmp (urn, modem_call_sos, n))
    return NULL;

  sos = urn + n;

  if (sos[0] == '\0')
    return urn;                 /* sos service */

  if (sos[0] != '.')
    return NULL;                /* Not a sos. subservice */

  for (i = 1, dot = 0, hyp = -2; ; i++)
    {
      /* A-Z a-z 0-9 - . */
      if (sos[i] == '\0')
        {
          if (i > hyp + 1 && i > dot + 1)
            return urn;
        }
      if (('a' <= sos[i] && sos[i] <= 'z') ||
          ('0' <= sos[i] && sos[i] <= '9') ||
          ('A' <= sos[i] && sos[i] <= 'Z'))
        continue;
      else if (sos[i] == '-')
        {
          if (i > dot + 1)
            {
              hyp = i;
              continue;
            }
        }
      else if (sos[i] == '.')
        {
          if (i > dot + 1 && i > hyp + 1)
            {
              dot = i;
              continue;
            }
        }

      return modem_call_sos;      /* Invalid syntax */
    }
}

/* ---------------------------------------------------------------------- */

static void request_notify_cancel (gpointer data);

ModemRequest *
modem_call_request_dial (ModemCallService *self,
                         char const *destination,
                         ModemClirOverride clir,
                         ModemCallRequestDialReply callback,
                         gpointer user_data)
{
  char const *clir_str;
  ModemRequest *request;
  ModemCallServicePrivate *priv = self->priv;

  DEBUG ("called");

  RETURN_NULL_IF_NOT_VALID (self);
  g_return_val_if_fail (destination != NULL, NULL);
  g_return_val_if_fail (callback != NULL, NULL);

  modem_message (MODEM_LOG_CALL,
      "trying to create call to \"%s\"",
      destination);

  if (clir == MODEM_CLIR_OVERRIDE_DISABLED)
    clir_str = "disabled";
  else if (clir == MODEM_CLIR_OVERRIDE_ENABLED)
    clir_str = "enabled";
  else
    clir_str = "";

  request = modem_request_begin (self, DBUS_PROXY (self),
      "Dial", modem_call_request_dial_reply,
      G_CALLBACK (callback), user_data,
      G_TYPE_STRING, destination,
      G_TYPE_STRING, clir_str,
      G_TYPE_INVALID);


  modem_request_add_cancel_notify (request, request_notify_cancel);

  modem_request_add_data_full (request,
      "call-destination",
      g_strdup (destination),
      g_free);

  g_queue_push_tail (priv->dialing.queue, request);

  return request;
}

static void
request_notify_cancel (gpointer _request)
{
  modem_request_add_qdata (_request,
      g_quark_from_static_string ("call-canceled"),
      GUINT_TO_POINTER (1));
}

static void
modem_call_request_dial_reply (DBusGProxy *proxy,
                               DBusGProxyCall *call,
                               void *_request)
{
  DEBUG ("enter");

  ModemRequest *request = _request;
  ModemCallService *self = MODEM_CALL_SERVICE (modem_request_object (request));
  ModemCallServicePrivate *priv = self->priv;
  ModemCallRequestDialReply *callback = modem_request_callback (request);
  gpointer user_data = modem_request_user_data (request);
  char *destination = modem_request_get_data (request, "call-destination");
  GError *error = NULL;
  ModemCall *ci = NULL;
  char *object_path = NULL;

  if (dbus_g_proxy_end_call (proxy, call, &error,
          DBUS_TYPE_G_OBJECT_PATH, &object_path,
          G_TYPE_INVALID))
    {
      ci = modem_call_service_get_dialed (self, object_path, destination);
    }
  else
    {
      object_path = NULL;
      modem_error_fix (&error);
    }

  if (ci)
    {
      DEBUG ("%s: instance %s (%p)", MODEM_OFACE_CALL_MANAGER ".Dial",
          object_path, (void *)ci);

      modem_message (MODEM_LOG_CALL,
          "call create request to \"%s\" successful",
          destination);
    }
  else
    {
      char ebuffer[32];

      modem_message (MODEM_LOG_CALL,
          "call create request to \"%s\" failed: %s.%s: %s",
          destination,
          modem_error_domain_prefix (error->domain),
          modem_error_name (error, ebuffer, sizeof ebuffer),
          error->message);

      DEBUG ("%s: " GERROR_MSG_FMT, MODEM_OFACE_CALL_MANAGER ".Dial",
          GERROR_MSG_CODE (error));
    }

  if (modem_request_get_data (request, "call-canceled"))
    {
      if (ci)
        modem_call_request_release (ci, NULL, NULL);
    }
  else
    {
      g_assert (ci || error);
      callback (self, request, ci, error, user_data);
    }

  if (g_queue_find (priv->dialing.queue, request))
    g_queue_remove (priv->dialing.queue, request);

  while (g_queue_is_empty (priv->dialing.queue) &&
      !g_queue_is_empty (priv->dialing.created))
    {
      char *remote;

      ci = g_queue_pop_head (priv->dialing.created);

      g_object_get (ci, "remote", &remote, NULL);

      g_signal_emit (self, signals[SIGNAL_CREATED], 0, ci, remote);

      g_free (remote);
    }

  g_free (object_path);
  g_clear_error (&error);
}

static void
on_modem_call_state (ModemCall *ci,
                     ModemCallState state,
                     ModemCallService *self)
{
  ModemCallServicePrivate *priv;
#if nomore
  gboolean releasing = FALSE;
#endif

  RETURN_IF_NOT_VALID (self);

  priv = self->priv;

  switch (state)
    {
    case MODEM_CALL_STATE_ACTIVE:
      if (priv->hold == ci)
        priv->hold = NULL;
      if (!modem_call_is_member (ci))
        priv->active = ci;
      break;

    case MODEM_CALL_STATE_HELD:
      if (priv->active == ci)
        priv->active = NULL;
      if (!modem_call_is_member (ci))
        priv->hold = ci;
      break;

    case MODEM_CALL_STATE_DISCONNECTED:
#if nomore
      releasing = TRUE;
#endif
      /* FALLTHROUGH */
    case MODEM_CALL_STATE_INVALID:
      if (priv->active == ci)
        priv->active = NULL;
      if (priv->hold == ci)
        priv->hold = NULL;
      break;

    default:
      break;
    }

#if nomore
  if (releasing)
    {
      GError *error = modem_call_new_error (causetype, cause, NULL);
      gboolean originating;
      char *remote;
      char const *what;

      g_object_get (ci,
          "originating", &originating,
          "remote", &remote,
          NULL);

      switch (state)
        {
        case MODEM_CALL_STATE_MO_RELEASE:
          what = "mo-released";
          break;
        case MODEM_CALL_STATE_MT_RELEASE:
          what = "mt-released";
          break;
        default:
          what = "terminated";
        }

      gboolean mpty = MODEM_IS_CALL_CONFERENCE (ci);

      modem_message (MODEM_LOG_CALL,
          "%s %s %s%s%s %s.%s: %s",
          what,
          mpty ? "conference"
          : originating ? "outgoing call to"
          : "incoming call from",
          mpty ? "" : "'",
          mpty ? "call" : remote,
          mpty ? "" : "'",
          modem_error_domain_prefix (error->domain),
          modem_error_name (error, NULL, 0),
          error->message);

      g_free (remote);
      g_error_free (error);
    }
#endif
}

ModemRequest *
modem_call_request_conference (ModemCallService *self,
                               ModemCallServiceReply *callback,
                               gpointer user_data)
{
  RETURN_NULL_IF_NOT_VALID (self);

  return modem_request (MODEM_CALL_SERVICE (self), DBUS_PROXY (self),
      "CreateMultiparty",
      modem_call_conference_request_reply,
      G_CALLBACK (callback), user_data,
      G_TYPE_INVALID);
}

static void
modem_call_conference_request_reply (DBusGProxy *proxy,
                                     DBusGProxyCall *call,
                                     void *_request)
{
  DEBUG ("enter");

  GPtrArray *paths;
  ModemRequest *request = _request;
  ModemCallService *self = modem_request_object (request);
  ModemCallServiceReply *callback = modem_request_callback (request);
  gpointer user_data = modem_request_user_data (request);
  GError *error = NULL;

  if (dbus_g_proxy_end_call (proxy, call, &error,
          MODEM_TYPE_ARRAY_OF_PATHS, &paths,
          G_TYPE_INVALID))
    {

      guint i;
      char const *path;
      ModemCall *ci;

      for (i = 0; i < paths->len; i++)
        {
          path = g_ptr_array_index (paths, i);
          ci = g_hash_table_lookup (self->priv->instances, path);
          if (ci != NULL)
            {
              g_object_set (ci, "multiparty", TRUE, NULL);
            }
        }

      g_boxed_free (MODEM_TYPE_ARRAY_OF_PATHS, paths);
    }
  else {
    modem_error_fix (&error);
  }

  callback (self, request, error, user_data);
  g_clear_error (&error);
}

static void
modem_call_service_noparams_request_reply (DBusGProxy *proxy,
            DBusGProxyCall *call,
            void *_request)
{
  ModemRequest *request = _request;
  ModemCallService *self = modem_request_object (request);
  ModemCallServiceReply *callback = modem_request_callback (request);
  gpointer user_data = modem_request_user_data (request);
  GError *error = NULL;

  DEBUG ("enter");

  if (dbus_g_proxy_end_call (proxy, call, &error, G_TYPE_INVALID))
    ;
  else
    modem_error_fix (&error);

  if (callback)
    callback (self, request, error, user_data);

  g_clear_error (&error);
}

ModemRequest *
modem_call_request_hangup_conference (ModemCallService *self,
            ModemCallServiceReply *callback,
            gpointer user_data)
{
  RETURN_NULL_IF_NOT_VALID (self);

  return modem_request (MODEM_CALL_SERVICE (self), DBUS_PROXY (self),
      "HangupMultiparty",
      modem_call_service_noparams_request_reply,
      G_CALLBACK (callback), user_data,
      G_TYPE_INVALID);
}

ModemCall *
modem_call_service_get_call (ModemCallService *self, char const *object_path)
{
  ModemCallServicePrivate *priv = self->priv;

  return g_hash_table_lookup (priv->instances, object_path);
}

/**
 * modem_call_service_swap_calls
 * @self ModemCallService object
 *
 * Swaps active and held calls. 0 or more active calls become
 * held, and 0 or more held calls become active.
 */
ModemRequest *
modem_call_service_swap_calls (ModemCallService *self,
            ModemCallServiceReply callback,
            gpointer user_data)
{
  RETURN_NULL_IF_NOT_VALID (self);

  DEBUG ("%s.%s", MODEM_OFACE_CALL_MANAGER, "SwapCalls");

  return modem_request (MODEM_CALL_SERVICE (self), DBUS_PROXY (self),
      "SwapCalls", modem_call_service_noparams_request_reply,
      G_CALLBACK (callback), user_data,
      G_TYPE_INVALID);
}

/**
 * modem_call_service_get_calls:
 * @self: ModemCallService object
 *
 * Obtains NULL-terminated list of ordinary ModemCall objects. Note that the
 * reference count of ModemCall objects is not incremented and object
 * pointers are valid only during the lifetime of the ModemCallService object.
 * The returned list can be reclaimed with g_free ().
 *
 * Returns: NULL-terminated list of pointers to ModemCall objects.
 */
ModemCall **
modem_call_service_get_calls (ModemCallService *self)
{
  ModemCall *ci;
  GPtrArray *calls;
  GHashTableIter iter[1];

  calls = g_ptr_array_sized_new (MODEM_MAX_CALLS + 1);

  g_hash_table_iter_init (iter, self->priv->instances);
  while (g_hash_table_iter_next (iter, NULL, (gpointer)&ci))
    {
      g_ptr_array_add (calls, ci);
    }

  g_ptr_array_add (calls, NULL);

  return (ModemCall **)g_ptr_array_free (calls, FALSE);
}

/* ------------------------------------------------------------------------- */

static char const *
_modem_call_validate_address (char const *address)
{
  size_t n, m;

  if (address == NULL)
    return "no destination";

  n = (sizeof modem_call_sos) - 1;

  if (g_ascii_strncasecmp (address, modem_call_sos, n) == 0)
    {
      if (address[n] != '\0' && address[n] != '.')
        return "invalid service urn";
      return NULL;
    }

  /* Remove regocnized service prefixes */
  if (g_str_has_prefix (address, "*31#"))
    address += 4;
  else if (g_str_has_prefix (address, "#31#"))
    address += 4;

  if (*address == '+')
    address++;

  n = strspn (address, "0123456789abc*#");
  if (n == 0)
    {
      if (address[n])
        return "not a phone number";
      else
        return "too short";
    }
  if (n > 20)
    return "too long";

  if (address[n - 1] == '#')
    return "invalid service code";

  /* Possible dialstring */
  m = strspn (address + n, "0123456789abc*#pwPW");
  if (address[n + m] != '\0')
    {
      if (m == 0)
        return "invalid address";
      else
        return "invalid dial string";
    }

  if (m == 1)
    return "invalid dial string";

  /* OK */
  return NULL;
}

gboolean
modem_call_is_valid_address (char const *address)
{
  return _modem_call_validate_address (address) == NULL;
}

gboolean
modem_call_validate_address (char const *address, GError **error)
{
  char const *message = _modem_call_validate_address (address);

  if (message)
    {
      g_set_error_literal (error,
          MODEM_CALL_ERRORS, MODEM_CALL_ERROR_INVALID_ADDRESS, message);
      return FALSE;
    }

  return TRUE;
}

void
modem_call_split_address (char const *address,
                          char **return_address,
                          char **return_dialstring,
                          ModemClirOverride *return_clir)
{
  char const *emergency;
  size_t nan;

  g_return_if_fail (address != NULL);
  g_return_if_fail (return_address != NULL);
  g_return_if_fail (return_dialstring != NULL);
  g_return_if_fail (return_clir);

  emergency = modem_call_get_valid_emergency_urn (address);
  if (emergency)
    {
      *return_address = g_strdup (emergency);
      *return_dialstring = NULL;
      *return_clir = MODEM_CLIR_OVERRIDE_DEFAULT;
      return;
    }

  if (g_str_has_prefix (address, "*31#"))
    {
      address += strlen ("*31#");
      /* According to 3GPP 22.030, *31# suppresses CLIR */
      *return_clir = MODEM_CLIR_OVERRIDE_DISABLED;
    }

  if (g_str_has_prefix (address, "#31#"))
    {
      /* According to 3GPP 22.030, #31# invokes CLIR */
      address += strlen ("#31#");
      *return_clir = MODEM_CLIR_OVERRIDE_ENABLED;
    }

  nan = strspn (address, "+0123456789*#ABCabc");

  *return_address = g_strndup (address, nan);
  if (address[nan])
    *return_dialstring = g_strdup (address + nan);
}
/* ---------------------------------------------------------------------- */
/* Voice Call Agent interface */
static DBusHandlerResult modem_call_agent_generic_dbus_message(
        DBusConnection *conn, DBusMessage *msg)
{
  DBusMessage *reply;

  reply = dbus_message_new_method_return(msg);
  if (!reply)
    return DBUS_HANDLER_RESULT_NEED_MEMORY;

  dbus_connection_send(conn, reply, NULL);
  dbus_message_unref(reply);

  return DBUS_HANDLER_RESULT_HANDLED;
}

static DBusHandlerResult modem_call_agent_dbus_message_handler(
        DBusConnection *conn,
        DBusMessage *msg, void *user_data)
{
  const char *method = dbus_message_get_member(msg);
  const char *iface = dbus_message_get_interface(msg);

  if (strcmp(VOICECALL_AGENT_IFACE, iface) != 0)
    return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

  if (strcmp("Release", method) == 0)
  {
    /* Nothing to do */
    return modem_call_agent_generic_dbus_message(conn, msg);
  }
  else if (strcmp("RingbackTone", method) == 0)
  {
    ModemCallService *self = user_data;
    dbus_bool_t playTone = 0;
    DBusMessageIter iter;
    dbus_message_iter_init(msg, &iter);
    if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_BOOLEAN)
    {
      dbus_message_iter_get_basic(&iter, &playTone);
    }
    else
    {
      DEBUG ("Invalid arguments received, ignore");
    }

    if (self->priv->alert_tone_needed != playTone)
    {
      DEBUG("'alert needed' changed from %d to %d",
          self->priv->alert_tone_needed, playTone);
    }

    self->priv->alert_tone_needed = playTone;

    return modem_call_agent_generic_dbus_message(conn, msg);
  }

  return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

/* Register as oFono voicecall agent, in order to receive
 * requests for playing local tones */
static void
modem_call_agent_register(ModemCallService *self)
{

  self->priv->call_agent_proxy =
     dbus_g_proxy_new_for_name(dbus_g_bus_get (DBUS_BUS_SYSTEM, NULL),
       VOICECALL_AGENT_IFACE,
       VOICECALL_AGENT_PATH,
       VOICECALL_AGENT_IFACE);

  if (!self->priv->call_agent_proxy)
  {
    DEBUG("Error in creating call agent proxy");
    return;
  }

  ModemRequest *agent_request = modem_request_begin (self, DBUS_PROXY (self),
      "RegisterVoicecallAgent", modem_call_agent_register_reply,
      NULL, self,
      DBUS_TYPE_G_OBJECT_PATH, dbus_g_proxy_get_path (
          self->priv->call_agent_proxy),
      G_TYPE_INVALID);

  if (!agent_request)
  {
    DEBUG("Error in registering call agent");
    return;
  }
}

static void
modem_call_agent_register_reply (DBusGProxy *proxy,
                               DBusGProxyCall *call,
                               void *_request)
{
  ModemRequest *request = _request;
  gpointer user_data = modem_request_user_data (request);
  ModemCallService *self = user_data;
  GError *error = NULL;

  if (!dbus_g_proxy_end_call (proxy, call, &error,
          G_TYPE_INVALID))
  {
    modem_error_fix (&error);
    DEBUG("Error while registering agent: %s",error->message);
    return;
  }

  if (!dbus_connection_register_object_path(
        dbus_bus_get (DBUS_BUS_SYSTEM, NULL),
        VOICECALL_AGENT_PATH,
        &modem_call_agent_table,
        self))
  {
    DEBUG("Error while registering object");
    return;
  }

  self->priv->call_agent_registered = TRUE;
  DEBUG("Call agent registered.");
}

static void modem_call_agent_unregister(DBusConnection *connection, void *user_data)
  {
  ModemCallService *self = user_data;
  self->priv->alert_tone_needed = FALSE;

  if (!self->priv->call_agent_proxy)
  {
    return;
  }

  if (self->priv->call_agent_registered)
  {
    dbus_connection_unregister_object_path(
          dbus_bus_get (DBUS_BUS_SYSTEM, NULL),
          VOICECALL_AGENT_PATH);
    self->priv->call_agent_registered = FALSE;
  }

  dbus_g_proxy_call_no_reply(DBUS_PROXY (self), "UnregisterVoicecallAgent",
        DBUS_TYPE_G_OBJECT_PATH, dbus_g_proxy_get_path (
        self->priv->call_agent_proxy),
        G_TYPE_INVALID);

  g_object_unref(self->priv->call_agent_proxy);
  self->priv->call_agent_proxy = NULL;
  DEBUG("Call agent unregistered.");
}
