/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * 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:
 *
 * Copyright (C) 2016 Aleksander Morgado <aleksander@aleksander.es>
 */

#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>

#include <ModemManager.h>
#define _LIBMM_INSIDE_MM
#include <libmm-glib.h>

#include "mm-broadband-bearer-ublox.h"
#include "mm-base-modem-at.h"
#include "mm-log.h"
#include "mm-ublox-enums-types.h"
#include "mm-modem-helpers.h"
#include "mm-modem-helpers-ublox.h"

G_DEFINE_TYPE (MMBroadbandBearerUblox, mm_broadband_bearer_ublox, MM_TYPE_BROADBAND_BEARER)

enum {
    PROP_0,
    PROP_USB_PROFILE,
    PROP_NETWORKING_MODE,
    PROP_LAST
};

static GParamSpec *properties[PROP_LAST];

struct _MMBroadbandBearerUbloxPrivate {
    MMUbloxUsbProfile        profile;
    MMUbloxNetworkingMode    mode;
    MMUbloxBearerAllowedAuth allowed_auths;
    FeatureSupport           statistics;
    FeatureSupport           cedata;
};

/*****************************************************************************/
/* Common connection context and task */

typedef struct {
    MMBroadbandBearerUblox *self;
    MMBroadbandModem       *modem;
    MMPortSerialAt         *primary;
    MMPort                 *data;
    guint                   cid;
    gboolean                auth_required;
    /* For IPv4 settings */
    MMBearerIpConfig       *ip_config;
} CommonConnectContext;

static void
common_connect_context_free (CommonConnectContext *ctx)
{
    if (ctx->ip_config)
        g_object_unref (ctx->ip_config);
    if (ctx->data)
        g_object_unref (ctx->data);
    g_object_unref (ctx->self);
    g_object_unref (ctx->modem);
    g_object_unref (ctx->primary);
    g_slice_free (CommonConnectContext, ctx);
}

static GTask *
common_connect_task_new (MMBroadbandBearerUblox  *self,
                         MMBroadbandModem        *modem,
                         MMPortSerialAt          *primary,
                         guint                    cid,
                         MMPort                  *data,
                         GCancellable            *cancellable,
                         GAsyncReadyCallback      callback,
                         gpointer                 user_data)
{
    CommonConnectContext *ctx;
    GTask                *task;

    ctx = g_slice_new0 (CommonConnectContext);
    ctx->self    = g_object_ref (self);
    ctx->modem   = g_object_ref (modem);
    ctx->primary = g_object_ref (primary);
    ctx->cid     = cid;

    task = g_task_new (self, cancellable, callback, user_data);
    g_task_set_task_data (task, ctx, (GDestroyNotify) common_connect_context_free);

    /* We need a net data port */
    if (data)
        ctx->data = g_object_ref (data);
    else {
        ctx->data = mm_base_modem_get_best_data_port (MM_BASE_MODEM (modem), MM_PORT_TYPE_NET);
        if (!ctx->data) {
            g_task_return_new_error (task,
                                     MM_CORE_ERROR,
                                     MM_CORE_ERROR_NOT_FOUND,
                                     "No valid data port found to launch connection");
            g_object_unref (task);
            return NULL;
        }
    }

    return task;
}

/*****************************************************************************/
/* 3GPP IP config (sub-step of the 3GPP Connection sequence) */

static gboolean
get_ip_config_3gpp_finish (MMBroadbandBearer *self,
                           GAsyncResult      *res,
                           MMBearerIpConfig **ipv4_config,
                           MMBearerIpConfig **ipv6_config,
                           GError           **error)
{
    MMBearerConnectResult *configs;
    MMBearerIpConfig *ipv4;

    configs = g_task_propagate_pointer (G_TASK (res), error);
    if (!configs)
        return FALSE;

    /* Just IPv4 for now */
    ipv4 = mm_bearer_connect_result_peek_ipv4_config (configs);
    g_assert (ipv4);
    if (ipv4_config)
        *ipv4_config = g_object_ref (ipv4);
    if (ipv6_config)
        *ipv6_config = NULL;
    mm_bearer_connect_result_unref (configs);
    return TRUE;
}

static void
complete_get_ip_config_3gpp (GTask *task)
{
    CommonConnectContext *ctx;

    ctx = (CommonConnectContext *) g_task_get_task_data (task);
    g_assert (mm_bearer_ip_config_get_method (ctx->ip_config) != MM_BEARER_IP_METHOD_UNKNOWN);
    g_task_return_pointer (task,
                           mm_bearer_connect_result_new (ctx->data, ctx->ip_config, NULL),
                           (GDestroyNotify) mm_bearer_connect_result_unref);
    g_object_unref (task);
}

static void
cgcontrdp_ready (MMBaseModem  *modem,
                 GAsyncResult *res,
                 GTask        *task)
{
    const gchar          *response;
    GError               *error = NULL;
    CommonConnectContext *ctx;
    gchar                *local_address = NULL;
    gchar                *subnet = NULL;
    gchar                *dns_addresses[3] = { NULL, NULL, NULL };

    ctx = (CommonConnectContext *) g_task_get_task_data (task);

    response = mm_base_modem_at_command_finish (modem, res, &error);
    if (!response || !mm_3gpp_parse_cgcontrdp_response (response,
                                                        NULL, /* cid */
                                                        NULL, /* bearer id */
                                                        NULL, /* apn */
                                                        &local_address,
                                                        &subnet,
                                                        NULL, /* gateway_address */
                                                        &dns_addresses[0],
                                                        &dns_addresses[1],
                                                        &error)) {
        g_task_return_error (task, error);
        g_object_unref (task);
        return;
    }

    mm_dbg ("IPv4 address retrieved: %s", local_address);
    mm_bearer_ip_config_set_address (ctx->ip_config, local_address);
    mm_dbg ("IPv4 subnet retrieved: %s", subnet);
    mm_bearer_ip_config_set_prefix (ctx->ip_config, mm_netmask_to_cidr (subnet));
    if (dns_addresses[0])
        mm_dbg ("Primary DNS retrieved: %s", dns_addresses[0]);
    if (dns_addresses[1])
        mm_dbg ("Secondary DNS retrieved: %s", dns_addresses[1]);
    mm_bearer_ip_config_set_dns (ctx->ip_config, (const gchar **) dns_addresses);

    g_free (local_address);
    g_free (subnet);
    g_free (dns_addresses[0]);
    g_free (dns_addresses[1]);

    mm_dbg ("finished IP settings retrieval for PDP context #%u...", ctx->cid);

    complete_get_ip_config_3gpp (task);
}

static void
uipaddr_ready (MMBaseModem  *modem,
               GAsyncResult *res,
               GTask        *task)
{
    const gchar          *response;
    gchar                *cmd;
    GError               *error = NULL;
    CommonConnectContext *ctx;
    gchar                *gw_ipv4_address = NULL;

    ctx = (CommonConnectContext *) g_task_get_task_data (task);

    response = mm_base_modem_at_command_finish (modem, res, &error);
    if (!response || !mm_ublox_parse_uipaddr_response (response,
                                                       NULL, /* cid */
                                                       NULL, /* if_name */
                                                       &gw_ipv4_address,
                                                       NULL, /* ipv4_subnet */
                                                       NULL, /* ipv6_global_address */
                                                       NULL, /* ipv6_link_local_address */
                                                       &error)) {
        g_task_return_error (task, error);
        g_object_unref (task);
        return;
    }

    mm_dbg ("IPv4 gateway address retrieved: %s", gw_ipv4_address);
    mm_bearer_ip_config_set_gateway (ctx->ip_config, gw_ipv4_address);
    g_free (gw_ipv4_address);

    cmd = g_strdup_printf ("+CGCONTRDP=%u", ctx->cid);
    mm_dbg ("gathering IP and DNS information for PDP context #%u...", ctx->cid);
    mm_base_modem_at_command (MM_BASE_MODEM (modem),
                              cmd,
                              10,
                              FALSE,
                              (GAsyncReadyCallback) cgcontrdp_ready,
                              task);
    g_free (cmd);
}

static void
get_ip_config_3gpp (MMBroadbandBearer   *self,
                    MMBroadbandModem    *modem,
                    MMPortSerialAt      *primary,
                    MMPortSerialAt      *secondary,
                    MMPort              *data,
                    guint                cid,
                    MMBearerIpFamily     ip_family,
                    GAsyncReadyCallback  callback,
                    gpointer             user_data)
{
    GTask                *task;
    CommonConnectContext *ctx;

    if (!(task = common_connect_task_new (MM_BROADBAND_BEARER_UBLOX (self),
                                          MM_BROADBAND_MODEM (modem),
                                          primary,
                                          cid,
                                          data,
                                          NULL,
                                          callback,
                                          user_data)))
        return;

    ctx = (CommonConnectContext *) g_task_get_task_data (task);
    ctx->ip_config = mm_bearer_ip_config_new ();

    /* If we're in BRIDGE mode, we need to ask for static IP addressing details:
     *  - AT+UIPADDR=[CID] will give us the default gateway address.
     *  - +CGCONTRDP?[CID] will give us the IP address, subnet and DNS addresses.
     */
    if (ctx->self->priv->mode == MM_UBLOX_NETWORKING_MODE_BRIDGE) {
        gchar *cmd;

        mm_bearer_ip_config_set_method (ctx->ip_config, MM_BEARER_IP_METHOD_STATIC);

        cmd = g_strdup_printf ("+UIPADDR=%u", cid);
        mm_dbg ("gathering gateway information for PDP context #%u...", cid);
        mm_base_modem_at_command (MM_BASE_MODEM (modem),
                                  cmd,
                                  10,
                                  FALSE,
                                  (GAsyncReadyCallback) uipaddr_ready,
                                  task);
        g_free (cmd);
        return;
    }

    /* If we're in ROUTER networking mode, we just need to request DHCP on the
     * network interface. Early return with that result. */
    if (ctx->self->priv->mode == MM_UBLOX_NETWORKING_MODE_ROUTER) {
        mm_bearer_ip_config_set_method (ctx->ip_config, MM_BEARER_IP_METHOD_DHCP);
        complete_get_ip_config_3gpp (task);
        return;
    }

    g_assert_not_reached ();
}

/*****************************************************************************/
/* 3GPP Dialing (sub-step of the 3GPP Connection sequence) */

static MMPort *
dial_3gpp_finish (MMBroadbandBearer  *self,
                  GAsyncResult       *res,
                  GError           **error)
{
    return MM_PORT (g_task_propagate_pointer (G_TASK (res), error));
}

static void
cedata_activate_ready (MMBaseModem            *modem,
                       GAsyncResult           *res,
                       MMBroadbandBearerUblox *self)
{
    const gchar *response;
    GError      *error = NULL;

    response = mm_base_modem_at_command_finish (modem, res, &error);
    if (!response) {
        mm_warn ("ECM data connection attempt failed: %s", error->message);
        mm_base_bearer_report_connection_status (MM_BASE_BEARER (self),
                                                 MM_BEARER_CONNECTION_STATUS_DISCONNECTED);
        g_error_free (error);
    }
    /* we received a full bearer object reference */
    g_object_unref (self);
}

static void
cgact_activate_ready (MMBaseModem  *modem,
                      GAsyncResult *res,
                      GTask        *task)
{
    const gchar          *response;
    GError               *error = NULL;
    CommonConnectContext *ctx;

    ctx = (CommonConnectContext *) g_task_get_task_data (task);

    response = mm_base_modem_at_command_finish (modem, res, &error);
    if (!response)
        g_task_return_error (task, error);
    else
        g_task_return_pointer (task, g_object_ref (ctx->data), g_object_unref);
    g_object_unref (task);
}

static void
activate_3gpp (GTask *task)
{
    CommonConnectContext *ctx;
    gchar                *cmd;

    ctx = (CommonConnectContext *) g_task_get_task_data (task);

    if (ctx->self->priv->profile == MM_UBLOX_USB_PROFILE_ECM &&
        ctx->self->priv->cedata == FEATURE_SUPPORTED) {
        /* SARA-U2xx and LISA-U20x only expose one CDC-ECM interface. Hence,
           the fixed 0 as the interface index here. When we see modems with
           multiple interfaces, this needs to be revisited. */
        cmd = g_strdup_printf ("+UCEDATA=%u,0", ctx->cid);
        mm_dbg ("establishing ECM data connection for PDP context #%u...", ctx->cid);
        mm_base_modem_at_command (MM_BASE_MODEM (ctx->modem),
                                  cmd,
                                  180,
                                  FALSE,
                                  (GAsyncReadyCallback) cedata_activate_ready,
                                  g_object_ref (ctx->self));
        /* We'll mark the task done here since the modem expects the DHCP
           discover packet while +UCEDATA runs. If the command fails, we'll
           mark the bearer disconnected later in the callback. */
        g_task_return_pointer (task, g_object_ref (ctx->data), g_object_unref);
        g_object_unref (task);
   } else {
        cmd = g_strdup_printf ("+CGACT=1,%u", ctx->cid);
        mm_dbg ("activating PDP context #%u...", ctx->cid);
        mm_base_modem_at_command (MM_BASE_MODEM (ctx->modem),
                                  cmd,
                                  120,
                                  FALSE,
                                  (GAsyncReadyCallback) cgact_activate_ready,
                                  task);
    }
    g_free (cmd);
}

static void
test_cedata_ready (MMBaseModem  *modem,
                   GAsyncResult *res,
                   GTask        *task)
{
    CommonConnectContext *ctx;
    const gchar         *response;

    ctx = (CommonConnectContext *) g_task_get_task_data (task);

    response = mm_base_modem_at_command_finish (modem, res, NULL);
    if (response)
        ctx->self->priv->cedata = FEATURE_SUPPORTED;
    else
        ctx->self->priv->cedata = FEATURE_UNSUPPORTED;
    mm_dbg ("u-blox: +UCEDATA command%s available",
        (ctx->self->priv->cedata == FEATURE_SUPPORTED) ? "" : " not");

    activate_3gpp (task);
}

static void
test_cedata (GTask *task)
{
    CommonConnectContext *ctx;

    ctx = (CommonConnectContext *) g_task_get_task_data (task);

    /* We don't need to test for +UCEDATA if we're not using CDC-ECM or if we
       have tested before. Instead, we jump right to the activation. */
    if (ctx->self->priv->profile != MM_UBLOX_USB_PROFILE_ECM ||
        ctx->self->priv->cedata != FEATURE_SUPPORT_UNKNOWN) {
        activate_3gpp (task);
        return;
    }

    mm_dbg ("u-blox: checking availability of +UCEDATA command...");
    mm_base_modem_at_command (MM_BASE_MODEM (ctx->modem),
                              "+UCEDATA=?",
                              3,
                              TRUE, /* allow_cached */
                              (GAsyncReadyCallback) test_cedata_ready,
                              task);
}

static void
uauthreq_ready (MMBaseModem  *modem,
                GAsyncResult *res,
                GTask        *task)
{
    const gchar *response;
    GError      *error = NULL;

    response = mm_base_modem_at_command_finish (modem, res, &error);
    if (!response) {
        CommonConnectContext *ctx;

        ctx = (CommonConnectContext *) g_task_get_task_data (task);
        /* If authentication required and the +UAUTHREQ failed, abort */
        if (ctx->auth_required) {
            g_task_return_error (task, error);
            g_object_unref (task);
            return;
        }
        /* Otherwise, ignore */
        g_error_free (error);
    }

    test_cedata (task);
}

static void
authenticate_3gpp (GTask *task)
{
    MMBroadbandBearerUblox *self;
    CommonConnectContext   *ctx;
    gchar                  *cmd = NULL;
    MMBearerAllowedAuth     allowed_auth;
    gint                    ublox_auth = -1;

    self = g_task_get_source_object (task);
    ctx = (CommonConnectContext *) g_task_get_task_data (task);

    allowed_auth = mm_bearer_properties_get_allowed_auth (mm_base_bearer_peek_config (MM_BASE_BEARER (ctx->self)));

    if (!ctx->auth_required) {
        mm_dbg ("Not using authentication");
        ublox_auth = 0;
        goto out;
    }

    if (allowed_auth == MM_BEARER_ALLOWED_AUTH_UNKNOWN || allowed_auth == (MM_BEARER_ALLOWED_AUTH_PAP | MM_BEARER_ALLOWED_AUTH_CHAP)) {
        mm_dbg ("Using automatic authentication method");
        if (self->priv->allowed_auths & MM_UBLOX_BEARER_ALLOWED_AUTH_AUTO)
            ublox_auth = 3;
        else if (self->priv->allowed_auths & MM_UBLOX_BEARER_ALLOWED_AUTH_PAP)
            ublox_auth = 1;
        else if (self->priv->allowed_auths & MM_UBLOX_BEARER_ALLOWED_AUTH_CHAP)
            ublox_auth = 2;
        else if (self->priv->allowed_auths & MM_UBLOX_BEARER_ALLOWED_AUTH_NONE)
            ublox_auth = 0;
    } else if (allowed_auth & MM_BEARER_ALLOWED_AUTH_PAP) {
        mm_dbg ("Using PAP authentication method");
        ublox_auth = 1;
    } else if (allowed_auth & MM_BEARER_ALLOWED_AUTH_CHAP) {
        mm_dbg ("Using CHAP authentication method");
        ublox_auth = 2;
    }

out:

    if (ublox_auth < 0) {
        gchar *str;

        str = mm_bearer_allowed_auth_build_string_from_mask (allowed_auth);
        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                                 "Cannot use any of the specified authentication methods (%s)", str);
        g_object_unref (task);
        g_free (str);
        return;
    }

    if (ublox_auth > 0) {
        const gchar *user;
        const gchar *password;
        gchar       *quoted_user;
        gchar       *quoted_password;

        user     = mm_bearer_properties_get_user     (mm_base_bearer_peek_config (MM_BASE_BEARER (ctx->self)));
        password = mm_bearer_properties_get_password (mm_base_bearer_peek_config (MM_BASE_BEARER (ctx->self)));

        quoted_user     = mm_port_serial_at_quote_string (user);
        quoted_password = mm_port_serial_at_quote_string (password);

        cmd = g_strdup_printf ("+UAUTHREQ=%u,%u,%s,%s",
                               ctx->cid,
                               ublox_auth,
                               quoted_user,
                               quoted_password);

        g_free (quoted_user);
        g_free (quoted_password);
    } else
        cmd = g_strdup_printf ("+UAUTHREQ=%u,0,\"\",\"\"", ctx->cid);

    mm_dbg ("setting up authentication preferences in PDP context #%u...", ctx->cid);
    mm_base_modem_at_command (MM_BASE_MODEM (ctx->modem),
                              cmd,
                              10,
                              FALSE,
                              (GAsyncReadyCallback) uauthreq_ready,
                              task);
    g_free (cmd);
}

static void
uauthreq_test_ready (MMBaseModem  *modem,
                     GAsyncResult *res,
                     GTask        *task)
{
    MMBroadbandBearerUblox *self;
    const gchar            *response;
    GError                 *error = NULL;

    self = g_task_get_source_object (task);

    response = mm_base_modem_at_command_finish (modem, res, &error);
    if (!response)
        goto out;

    self->priv->allowed_auths = mm_ublox_parse_uauthreq_test (response, &error);
out:
    if (error) {
        CommonConnectContext *ctx;

        ctx = (CommonConnectContext *) g_task_get_task_data (task);
        /* If authentication required and the +UAUTHREQ test failed, abort */
        if (ctx->auth_required) {
            g_task_return_error (task, error);
            g_object_unref (task);
            return;
        }
        /* Otherwise, ignore and jump to test_cedata directly as no auth setup
         * is needed */
        g_error_free (error);
        test_cedata (task);
        return;
    }

    authenticate_3gpp (task);
}

static void
check_supported_authentication_methods (GTask *task)
{
    MMBroadbandBearerUblox *self;
    CommonConnectContext   *ctx;
    const gchar            *user;
    const gchar            *password;
    MMBearerAllowedAuth     allowed_auth;

    self = g_task_get_source_object (task);
    ctx = (CommonConnectContext *) g_task_get_task_data (task);

    user         = mm_bearer_properties_get_user         (mm_base_bearer_peek_config (MM_BASE_BEARER (ctx->self)));
    password     = mm_bearer_properties_get_password     (mm_base_bearer_peek_config (MM_BASE_BEARER (ctx->self)));
    allowed_auth = mm_bearer_properties_get_allowed_auth (mm_base_bearer_peek_config (MM_BASE_BEARER (ctx->self)));

    /* Flag whether authentication is required. If it isn't, we won't fail
     * connection attempt if the +UAUTHREQ command fails */
    ctx->auth_required = (user && password && allowed_auth != MM_BEARER_ALLOWED_AUTH_NONE);

    /* If we already cached the support, not do it again */
    if (self->priv->allowed_auths != MM_UBLOX_BEARER_ALLOWED_AUTH_UNKNOWN) {
        authenticate_3gpp (task);
        return;
    }

    mm_dbg ("checking supported authentication methods...");
    mm_base_modem_at_command (MM_BASE_MODEM (ctx->modem),
                              "+UAUTHREQ=?",
                              10,
                              TRUE, /* allow cached */
                              (GAsyncReadyCallback) uauthreq_test_ready,
                              task);
}

static void
dial_3gpp (MMBroadbandBearer   *self,
           MMBaseModem         *modem,
           MMPortSerialAt      *primary,
           guint                cid,
           GCancellable        *cancellable,
           GAsyncReadyCallback  callback,
           gpointer             user_data)
{
    GTask *task;

    if (!(task = common_connect_task_new (MM_BROADBAND_BEARER_UBLOX (self),
                                          MM_BROADBAND_MODEM (modem),
                                          primary,
                                          cid,
                                          NULL, /* data, unused */
                                          cancellable,
                                          callback,
                                          user_data)))
        return;

    check_supported_authentication_methods (task);
}

/*****************************************************************************/
/* 3GPP disconnection */

static gboolean
disconnect_3gpp_finish (MMBroadbandBearer  *self,
                        GAsyncResult       *res,
                        GError            **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

static void
cgact_deactivate_ready (MMBaseModem  *modem,
                        GAsyncResult *res,
                        GTask        *task)
{
    const gchar *response;
    GError      *error = NULL;

    response = mm_base_modem_at_command_finish (modem, res, &error);
    if (!response) {
        /* TOBY-L4 and TOBY-L2 L2 don't allow to disconnect the last LTE bearer
         * as that would imply de-registration from the LTE network, so we just
         * assume that it's disconnected from the user point of view.
         *
         * TOBY-L4 reports this as a generic unknown Packet Domain Error, which
         * is a bit unfortunate:
         *    AT+CGACT=0,1
         *    +CME ERROR: 148
         *
         * TOBY-L2 reports this as "LAST PDN disconnection not allowed" but using
         * the legacy numeric value before 3GPP Rel 11 (i.e. 151 instead of 171).
         *   AT+CGACT=0,1
         *    +CME ERROR: 151
         */
        if (!g_error_matches (error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_GPRS_UNKNOWN) &&
            !g_error_matches (error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_GPRS_LAST_PDN_DISCONNECTION_NOT_ALLOWED) &&
            !g_error_matches (error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_GPRS_LAST_PDN_DISCONNECTION_NOT_ALLOWED_LEGACY)) {
            g_task_return_error (task, error);
            g_object_unref (task);
            return;
        }

        mm_dbg ("ignored error when disconnecting last LTE bearer: %s", error->message);
        g_clear_error (&error);
    }

    g_task_return_boolean (task, TRUE);
    g_object_unref (task);
}

static void
disconnect_3gpp  (MMBroadbandBearer   *self,
                  MMBroadbandModem    *modem,
                  MMPortSerialAt      *primary,
                  MMPortSerialAt      *secondary,
                  MMPort              *data,
                  guint                cid,
                  GAsyncReadyCallback  callback,
                  gpointer             user_data)
{
    GTask *task;
    gchar *cmd;

    if (!(task = common_connect_task_new (MM_BROADBAND_BEARER_UBLOX (self),
                                          MM_BROADBAND_MODEM (modem),
                                          primary,
                                          cid,
                                          data,
                                          NULL,
                                          callback,
                                          user_data)))
        return;

    cmd = g_strdup_printf ("+CGACT=0,%u", cid);
    mm_dbg ("deactivating PDP context #%u...", cid);
    mm_base_modem_at_command (MM_BASE_MODEM (modem),
                              cmd,
                              120,
                              FALSE,
                              (GAsyncReadyCallback) cgact_deactivate_ready,
                              task);
    g_free (cmd);
}

/*****************************************************************************/
/* Reload statistics */

typedef struct {
    guint64 bytes_rx;
    guint64 bytes_tx;
} StatsResult;

static gboolean
reload_stats_finish (MMBaseBearer  *self,
                     guint64       *bytes_rx,
                     guint64       *bytes_tx,
                     GAsyncResult  *res,
                     GError       **error)
{
    StatsResult *result;

    result = g_task_propagate_pointer (G_TASK (res), error);
    if (!result)
        return FALSE;

    if (bytes_rx)
        *bytes_rx = result->bytes_rx;
    if (bytes_tx)
        *bytes_tx = result->bytes_tx;
    g_free (result);
    return TRUE;
}

static void
ugcntrd_ready (MMBaseModem  *modem,
               GAsyncResult *res,
               GTask        *task)
{
    MMBroadbandBearerUblox *self;
    const gchar            *response;
    GError                 *error = NULL;
    guint64                 tx_bytes = 0;
    guint64                 rx_bytes = 0;
    guint                   cid;

    self = MM_BROADBAND_BEARER_UBLOX (g_task_get_source_object (task));

    cid = mm_broadband_bearer_get_3gpp_cid (MM_BROADBAND_BEARER (self));

    response = mm_base_modem_at_command_finish (modem, res, &error);
    if (response)
        mm_ublox_parse_ugcntrd_response_for_cid (response,
                                                 cid,
                                                 &tx_bytes, &rx_bytes,
                                                 NULL, NULL,
                                                 &error);

    if (error) {
        g_prefix_error (&error, "Couldn't load PDP context %u statistics: ", cid);
        g_task_return_error (task, error);
    } else {
        StatsResult *result;

        result = g_new (StatsResult, 1);
        result->bytes_rx = rx_bytes;
        result->bytes_tx = tx_bytes;
        g_task_return_pointer (task, result, g_free);
    }
    g_object_unref (task);
}

static void
run_reload_stats (MMBroadbandBearerUblox *self,
                  GTask                  *task)
{
    /* Unsupported? */
    if (self->priv->statistics == FEATURE_UNSUPPORTED) {
        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                                 "Loading statistics isn't supported by this device");
        g_object_unref (task);
        return;
    }

    /* Supported */
    if (self->priv->statistics == FEATURE_SUPPORTED) {
        MMBaseModem *modem = NULL;

        g_object_get (MM_BASE_BEARER (self),
                      MM_BASE_BEARER_MODEM, &modem,
                      NULL);
        mm_base_modem_at_command (MM_BASE_MODEM (modem),
                                  "+UGCNTRD",
                                  3,
                                  FALSE,
                                  (GAsyncReadyCallback) ugcntrd_ready,
                                  task);
        g_object_unref (modem);
        return;
    }

    g_assert_not_reached ();
}

static void
ugcntrd_test_ready (MMBaseModem  *modem,
                    GAsyncResult *res,
                    GTask        *task)
{
    MMBroadbandBearerUblox *self;

    self = MM_BROADBAND_BEARER_UBLOX (g_task_get_source_object (task));

    if (!mm_base_modem_at_command_finish (modem, res, NULL))
        self->priv->statistics = FEATURE_UNSUPPORTED;
    else
        self->priv->statistics = FEATURE_SUPPORTED;

    run_reload_stats (self, task);
}

static void
reload_stats (MMBaseBearer        *self,
              GAsyncReadyCallback  callback,
              gpointer             user_data)
{
    GTask *task;

    task = g_task_new (self, NULL, callback, user_data);

    if (MM_BROADBAND_BEARER_UBLOX (self)->priv->statistics == FEATURE_SUPPORT_UNKNOWN) {
        MMBaseModem *modem = NULL;

        g_object_get (MM_BASE_BEARER (self),
                      MM_BASE_BEARER_MODEM, &modem,
                      NULL);

        mm_base_modem_at_command (MM_BASE_MODEM (modem),
                                  "+UGCNTRD=?",
                                  3,
                                  FALSE,
                                  (GAsyncReadyCallback) ugcntrd_test_ready,
                                  task);
        g_object_unref (modem);
        return;
    }

    run_reload_stats (MM_BROADBAND_BEARER_UBLOX (self), task);
}

/*****************************************************************************/

MMBaseBearer *
mm_broadband_bearer_ublox_new_finish (GAsyncResult  *res,
                                      GError       **error)
{
    GObject *source;
    GObject *bearer;

    source = g_async_result_get_source_object (res);
    bearer = g_async_initable_new_finish (G_ASYNC_INITABLE (source), res, error);
    g_object_unref (source);

    if (!bearer)
        return NULL;

    /* Only export valid bearers */
    mm_base_bearer_export (MM_BASE_BEARER (bearer));

    return MM_BASE_BEARER (bearer);
}

void
mm_broadband_bearer_ublox_new (MMBroadbandModem      *modem,
                               MMUbloxUsbProfile      profile,
                               MMUbloxNetworkingMode  mode,
                               MMBearerProperties    *config,
                               GCancellable          *cancellable,
                               GAsyncReadyCallback    callback,
                               gpointer               user_data)
{
    g_assert (mode == MM_UBLOX_NETWORKING_MODE_ROUTER || mode == MM_UBLOX_NETWORKING_MODE_BRIDGE);

    g_async_initable_new_async (
        MM_TYPE_BROADBAND_BEARER_UBLOX,
        G_PRIORITY_DEFAULT,
        cancellable,
        callback,
        user_data,
        MM_BASE_BEARER_MODEM, modem,
        MM_BASE_BEARER_CONFIG, config,
        MM_BROADBAND_BEARER_UBLOX_USB_PROFILE, profile,
        MM_BROADBAND_BEARER_UBLOX_NETWORKING_MODE, mode,
        NULL);
}

static void
set_property (GObject      *object,
              guint         prop_id,
              const GValue *value,
              GParamSpec   *pspec)
{
    MMBroadbandBearerUblox *self = MM_BROADBAND_BEARER_UBLOX (object);

    switch (prop_id) {
    case PROP_USB_PROFILE:
        self->priv->profile = g_value_get_enum (value);
        break;
    case PROP_NETWORKING_MODE:
        self->priv->mode = g_value_get_enum (value);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
get_property (GObject    *object,
              guint       prop_id,
              GValue     *value,
              GParamSpec *pspec)
{
    MMBroadbandBearerUblox *self = MM_BROADBAND_BEARER_UBLOX (object);

    switch (prop_id) {
    case PROP_USB_PROFILE:
        g_value_set_enum (value, self->priv->profile);
        break;
    case PROP_NETWORKING_MODE:
        g_value_set_enum (value, self->priv->mode);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
mm_broadband_bearer_ublox_init (MMBroadbandBearerUblox *self)
{
    /* Initialize private data */
    self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
                                              MM_TYPE_BROADBAND_BEARER_UBLOX,
                                              MMBroadbandBearerUbloxPrivate);

    /* Defaults */
    self->priv->profile       = MM_UBLOX_USB_PROFILE_UNKNOWN;
    self->priv->mode          = MM_UBLOX_NETWORKING_MODE_UNKNOWN;
    self->priv->allowed_auths = MM_UBLOX_BEARER_ALLOWED_AUTH_UNKNOWN;
    self->priv->statistics    = FEATURE_SUPPORT_UNKNOWN;
    self->priv->cedata        = FEATURE_SUPPORT_UNKNOWN;
}

static void
mm_broadband_bearer_ublox_class_init (MMBroadbandBearerUbloxClass *klass)
{
    GObjectClass           *object_class           = G_OBJECT_CLASS (klass);
    MMBaseBearerClass      *base_bearer_class      = MM_BASE_BEARER_CLASS (klass);
    MMBroadbandBearerClass *broadband_bearer_class = MM_BROADBAND_BEARER_CLASS (klass);

    g_type_class_add_private (object_class, sizeof (MMBroadbandBearerUbloxPrivate));

    object_class->get_property = get_property;
    object_class->set_property = set_property;

    /* Note: the ublox plugin uses the generic AT+CGACT? based check to monitor
     * the connection status (i.e. default load_connection_status()) */
    base_bearer_class->reload_stats = reload_stats;
    base_bearer_class->reload_stats_finish = reload_stats_finish;

    broadband_bearer_class->disconnect_3gpp = disconnect_3gpp;
    broadband_bearer_class->disconnect_3gpp_finish = disconnect_3gpp_finish;
    broadband_bearer_class->dial_3gpp = dial_3gpp;
    broadband_bearer_class->dial_3gpp_finish = dial_3gpp_finish;
    broadband_bearer_class->get_ip_config_3gpp = get_ip_config_3gpp;
    broadband_bearer_class->get_ip_config_3gpp_finish = get_ip_config_3gpp_finish;

    properties[PROP_USB_PROFILE] =
        g_param_spec_enum (MM_BROADBAND_BEARER_UBLOX_USB_PROFILE,
                           "USB profile",
                           "USB profile in use",
                           MM_TYPE_UBLOX_USB_PROFILE,
                           MM_UBLOX_USB_PROFILE_UNKNOWN,
                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
    g_object_class_install_property (object_class, PROP_USB_PROFILE, properties[PROP_USB_PROFILE]);

    properties[PROP_NETWORKING_MODE] =
        g_param_spec_enum (MM_BROADBAND_BEARER_UBLOX_NETWORKING_MODE,
                           "Networking mode",
                           "Networking mode in use",
                           MM_TYPE_UBLOX_NETWORKING_MODE,
                           MM_UBLOX_NETWORKING_MODE_UNKNOWN,
                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
    g_object_class_install_property (object_class, PROP_NETWORKING_MODE, properties[PROP_NETWORKING_MODE]);
}
