/* -*- 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 <glib.h>
#include <string.h>

#include "mm-log.h"
#include "mm-modem-helpers.h"
#include "mm-modem-helpers-ublox.h"

/*****************************************************************************/
/* +UPINCNT response parser */

gboolean
mm_ublox_parse_upincnt_response (const gchar  *response,
                                 guint        *out_pin_attempts,
                                 guint        *out_pin2_attempts,
                                 guint        *out_puk_attempts,
                                 guint        *out_puk2_attempts,
                                 GError      **error)
{
    g_autoptr(GRegex)      r = NULL;
    g_autoptr(GMatchInfo)  match_info = NULL;
    GError                *inner_error = NULL;
    guint                  pin_attempts = 0;
    guint                  pin2_attempts = 0;
    guint                  puk_attempts = 0;
    guint                  puk2_attempts = 0;
    gboolean               success = TRUE;

    g_assert (out_pin_attempts);
    g_assert (out_pin2_attempts);
    g_assert (out_puk_attempts);
    g_assert (out_puk2_attempts);

    /* Response may be e.g.:
     * +UPINCNT: 3,3,10,10
     */
    r = g_regex_new ("\\+UPINCNT: (\\d+),(\\d+),(\\d+),(\\d+)(?:\\r\\n)?", 0, 0, NULL);
    g_assert (r != NULL);

    g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, &inner_error);
    if (!inner_error && g_match_info_matches (match_info)) {
        if (!mm_get_uint_from_match_info (match_info, 1, &pin_attempts)) {
            inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                                       "Couldn't parse PIN attempts");
            goto out;
        }
        if (!mm_get_uint_from_match_info (match_info, 2, &pin2_attempts)) {
            inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                                       "Couldn't parse PIN2 attempts");
            goto out;
        }
        if (!mm_get_uint_from_match_info (match_info, 3, &puk_attempts)) {
            inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                                       "Couldn't parse PUK attempts");
            goto out;
        }
        if (!mm_get_uint_from_match_info (match_info, 4, &puk2_attempts)) {
            inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                                       "Couldn't parse PUK2 attempts");
            goto out;
        }
        success = TRUE;
    }

out:

    if (inner_error) {
        g_propagate_error (error, inner_error);
        return FALSE;
    }

    if (!success) {
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                     "Couldn't parse +UPINCNT response: '%s'", response);
        return FALSE;
    }

    *out_pin_attempts  = pin_attempts;
    *out_pin2_attempts = pin2_attempts;
    *out_puk_attempts  = puk_attempts;
    *out_puk2_attempts = puk2_attempts;
    return TRUE;
}

/*****************************************************************************/
/* UUSBCONF? response parser */

gboolean
mm_ublox_parse_uusbconf_response (const gchar        *response,
                                  MMUbloxUsbProfile  *out_profile,
                                  GError            **error)
{
    g_autoptr(GRegex)      r = NULL;
    g_autoptr(GMatchInfo)  match_info = NULL;
    GError                *inner_error = NULL;
    MMUbloxUsbProfile      profile = MM_UBLOX_USB_PROFILE_UNKNOWN;

    g_assert (out_profile != NULL);

    /* Response may be e.g.:
     * +UUSBCONF: 3,"RNDIS",,"0x1146"
     * +UUSBCONF: 2,"ECM",,"0x1143"
     * +UUSBCONF: 0,"",,"0x1141"
     *
     * Note: we don't rely on the PID; assuming future new modules will
     * have a different PID but they may keep the profile names.
     */
    r = g_regex_new ("\\+UUSBCONF: (\\d+),([^,]*),([^,]*),([^,]*)(?:\\r\\n)?", 0, 0, NULL);
    g_assert (r != NULL);

    g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, &inner_error);
    if (!inner_error && g_match_info_matches (match_info)) {
        g_autofree gchar *profile_name = NULL;

        profile_name = mm_get_string_unquoted_from_match_info (match_info, 2);
        if (profile_name && profile_name[0]) {
            if (g_str_equal (profile_name, "RNDIS"))
                profile = MM_UBLOX_USB_PROFILE_RNDIS;
            else if (g_str_equal (profile_name, "ECM"))
                profile = MM_UBLOX_USB_PROFILE_ECM;
            else
                inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                                           "Unknown USB profile: '%s'", profile_name);
        } else
            profile = MM_UBLOX_USB_PROFILE_BACK_COMPATIBLE;
    }

    if (inner_error) {
        g_propagate_error (error, inner_error);
        return FALSE;
    }

    if (profile == MM_UBLOX_USB_PROFILE_UNKNOWN) {
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                     "Couldn't parse profile response");
        return FALSE;
    }

    *out_profile = profile;
    return TRUE;
}

/*****************************************************************************/
/* UBMCONF? response parser */

gboolean
mm_ublox_parse_ubmconf_response (const gchar            *response,
                                 MMUbloxNetworkingMode  *out_mode,
                                 GError                **error)
{
    g_autoptr(GRegex)      r = NULL;
    g_autoptr(GMatchInfo)  match_info = NULL;
    GError                *inner_error = NULL;
    MMUbloxNetworkingMode  mode = MM_UBLOX_NETWORKING_MODE_UNKNOWN;

    g_assert (out_mode != NULL);

    /* Response may be e.g.:
     * +UBMCONF: 1
     * +UBMCONF: 2
     */
    r = g_regex_new ("\\+UBMCONF: (\\d+)(?:\\r\\n)?", 0, 0, NULL);
    g_assert (r != NULL);

    g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, &inner_error);
    if (!inner_error && g_match_info_matches (match_info)) {
        guint mode_id = 0;

        if (mm_get_uint_from_match_info (match_info, 1, &mode_id)) {
            switch (mode_id) {
            case 1:
                mode = MM_UBLOX_NETWORKING_MODE_ROUTER;
                break;
            case 2:
                mode = MM_UBLOX_NETWORKING_MODE_BRIDGE;
                break;
            default:
                inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                                           "Unknown mode id: '%u'", mode_id);
                break;
            }
        }
    }

    if (inner_error) {
        g_propagate_error (error, inner_error);
        return FALSE;
    }

    if (mode == MM_UBLOX_NETWORKING_MODE_UNKNOWN) {
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                     "Couldn't parse networking mode response");
        return FALSE;
    }

    *out_mode = mode;
    return TRUE;
}

/*****************************************************************************/
/* UIPADDR=N response parser */

gboolean
mm_ublox_parse_uipaddr_response (const gchar  *response,
                                 guint        *out_cid,
                                 gchar       **out_if_name,
                                 gchar       **out_ipv4_address,
                                 gchar       **out_ipv4_subnet,
                                 gchar       **out_ipv6_global_address,
                                 gchar       **out_ipv6_link_local_address,
                                 GError      **error)
{
    g_autoptr(GRegex)      r = NULL;
    g_autoptr(GMatchInfo)  match_info = NULL;
    GError                *inner_error = NULL;
    guint                  cid = 0;
    g_autofree gchar      *if_name = NULL;
    g_autofree gchar      *ipv4_address = NULL;
    g_autofree gchar      *ipv4_subnet = NULL;
    g_autofree gchar      *ipv6_global_address = NULL;
    g_autofree gchar      *ipv6_link_local_address = NULL;

    /* Response may be e.g.:
     * +UIPADDR: 1,"ccinet0","5.168.120.13","255.255.255.0","",""
     * +UIPADDR: 2,"ccinet1","","","2001::2:200:FF:FE00:0/64","FE80::200:FF:FE00:0/64"
     * +UIPADDR: 3,"ccinet2","5.10.100.2","255.255.255.0","2001::1:200:FF:FE00:0/64","FE80::200:FF:FE00:0/64"
     *
     * We assume only ONE line is returned; because we request +UIPADDR with a specific N CID.
     */
    r = g_regex_new ("\\+UIPADDR: (\\d+),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*)(?:\\r\\n)?", 0, 0, NULL);
    g_assert (r != NULL);

    g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, &inner_error);
    if (inner_error) {
        g_propagate_error (error, inner_error);
        return FALSE;
    }

    if (!g_match_info_matches (match_info)) {
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS, "Couldn't match +UIPADDR response");
        return FALSE;
    }

    if (out_cid && !mm_get_uint_from_match_info (match_info, 1, &cid)) {
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "Error parsing cid");
        return FALSE;
    }

    if (out_if_name && !(if_name = mm_get_string_unquoted_from_match_info (match_info, 2))) {
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "Error parsing interface name");
        return FALSE;
    }

    /* Remaining strings are optional */
    ipv4_address = mm_get_string_unquoted_from_match_info (match_info, 3);
    ipv4_subnet = mm_get_string_unquoted_from_match_info (match_info, 4);
    ipv6_global_address = mm_get_string_unquoted_from_match_info (match_info, 5);
    ipv6_link_local_address = mm_get_string_unquoted_from_match_info (match_info, 6);

    if (out_cid)
        *out_cid = cid;
    if (out_if_name)
        *out_if_name = g_steal_pointer (&if_name);
    if (out_ipv4_address)
        *out_ipv4_address = g_steal_pointer (&ipv4_address);
    if (out_ipv4_subnet)
        *out_ipv4_subnet = g_steal_pointer (&ipv4_subnet);
    if (out_ipv6_global_address)
        *out_ipv6_global_address = g_steal_pointer (&ipv6_global_address);
    if (out_ipv6_link_local_address)
        *out_ipv6_link_local_address = g_steal_pointer (&ipv6_link_local_address);
    return TRUE;
}

/*****************************************************************************/
/* CFUN? response parser */

gboolean
mm_ublox_parse_cfun_response (const gchar        *response,
                              MMModemPowerState  *out_state,
                              GError            **error)
{
    guint state;

    if (!mm_3gpp_parse_cfun_query_response (response, &state, error))
        return FALSE;

    switch (state) {
    case 1:
        *out_state = MM_MODEM_POWER_STATE_ON;
        return TRUE;
    case 0:
        /* minimum functionality */
    case 4:
        /* airplane mode */
    case 19:
        /* minimum functionality with SIM deactivated */
        *out_state = MM_MODEM_POWER_STATE_LOW;
        return TRUE;
    default:
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                     "Unknown +CFUN state: %u", state);
        return FALSE;
    }
}

/*****************************************************************************/
/* URAT=? response parser */

/* Index of the array is the ublox-specific value */
static const MMModemMode ublox_combinations[] = {
    ( MM_MODEM_MODE_2G ),
    ( MM_MODEM_MODE_2G | MM_MODEM_MODE_3G ),
    (                    MM_MODEM_MODE_3G ),
    (                                       MM_MODEM_MODE_4G ),
    ( MM_MODEM_MODE_2G | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G ),
    ( MM_MODEM_MODE_2G |                    MM_MODEM_MODE_4G ),
    (                    MM_MODEM_MODE_3G | MM_MODEM_MODE_4G ),
    (                                       MM_MODEM_MODE_4G ),
    (                                       MM_MODEM_MODE_4G ),
};

GArray *
mm_ublox_parse_urat_test_response (const gchar  *response,
                                   gpointer      log_object,
                                   GError      **error)
{
    GArray *combinations = NULL;
    GArray *selected = NULL;
    GArray *preferred = NULL;
    gchar **split;
    guint   split_len;
    GError *inner_error = NULL;
    guint   i;

    /*
     * E.g.:
     *  AT+URAT=?
     *  +URAT: (0-6),(0,2,3)
     */
    response = mm_strip_tag (response, "+URAT:");
    split = mm_split_string_groups (response);
    split_len = g_strv_length (split);
    if (split_len > 2 || split_len < 1) {
        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                   "Unexpected number of groups in +URAT=? response: %u", g_strv_length (split));
        goto out;
    }

    /* The selected list must have values */
    selected = mm_parse_uint_list (split[0], &inner_error);
    if (inner_error)
        goto out;

    if (!selected) {
        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                   "No selected RAT values given in +URAT=? response");
        goto out;
    }

    /* For our purposes, the preferred list may be empty */
    preferred = mm_parse_uint_list (split[1], &inner_error);
    if (inner_error)
        goto out;

    /* Build array of combinations */
    combinations = g_array_new (FALSE, FALSE, sizeof (MMModemModeCombination));

    for (i = 0; i < selected->len; i++) {
        guint                  selected_value;
        MMModemModeCombination combination;
        guint                  j;

        selected_value = g_array_index (selected, guint, i);
        if (selected_value >= G_N_ELEMENTS (ublox_combinations)) {
            mm_obj_warn (log_object, "unexpected AcT value: %u", selected_value);
            continue;
        }

        /* Combination without any preferred */
        combination.allowed = ublox_combinations[selected_value];
        combination.preferred = MM_MODEM_MODE_NONE;
        g_array_append_val (combinations, combination);

        if (mm_count_bits_set (combination.allowed) == 1)
            continue;

        if (!preferred)
            continue;

        for (j = 0; j < preferred->len; j++) {
            guint preferred_value;

            preferred_value = g_array_index (preferred, guint, j);
            if (preferred_value >= G_N_ELEMENTS (ublox_combinations)) {
                mm_obj_warn (log_object, "unexpected AcT preferred value: %u", preferred_value);
                continue;
            }
            combination.preferred = ublox_combinations[preferred_value];
            if (mm_count_bits_set (combination.preferred) != 1) {
                mm_obj_warn (log_object, "AcT preferred value should be a single AcT: %u", preferred_value);
                continue;
            }
            if (!(combination.allowed & combination.preferred))
                continue;
            g_array_append_val (combinations, combination);
        }
    }

    if (combinations->len == 0) {
        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                   "No combinations built from +URAT=? response");
        goto out;
    }

out:
    g_strfreev (split);
    if (selected)
        g_array_unref (selected);
    if (preferred)
        g_array_unref (preferred);

    if (inner_error) {
        if (combinations)
            g_array_unref (combinations);
        g_propagate_error (error, inner_error);
        return NULL;
    }

    return combinations;
}

typedef struct {
    const gchar          *model;
    SettingsUpdateMethod  method;
    FeatureSupport        uact;
    FeatureSupport        ubandsel;
    MMModemMode           mode;
    MMModemBand           bands_2g[4];
    MMModemBand           bands_3g[6];
    MMModemBand           bands_4g[12];
} BandConfiguration;

static const BandConfiguration band_configuration[] = {
    {
        .model    = "SARA-G300",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G,
        .bands_2g = { MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS }
    },
    {
        .model    = "SARA-G310",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS }
    },
     {
        .model    = "SARA-G340",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G,
        .bands_2g = { MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS }
    },
    {
        .model    = "SARA-G350",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS }
    },
    {
        .model    = "SARA-G450",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS }
    },
    {
        .model    = "LISA-U200",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_6, MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_8,
                      MM_MODEM_BAND_UTRAN_4, MM_MODEM_BAND_UTRAN_2, MM_MODEM_BAND_UTRAN_1 }
    },
    {
        .model    = "LISA-U201",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_6, MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_8,
                      MM_MODEM_BAND_UTRAN_2, MM_MODEM_BAND_UTRAN_1 }
    },
    {
        .model    = "LISA-U230",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_6, MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_8,
                      MM_MODEM_BAND_UTRAN_4, MM_MODEM_BAND_UTRAN_2, MM_MODEM_BAND_UTRAN_1 }
    },
    {
        .model    = "LISA-U260",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_2 }
    },
    {
        .model    = "LISA-U270",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UTRAN_1 }
    },
    {
        .model    = "SARA-U201",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_6, MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_8,
                      MM_MODEM_BAND_UTRAN_2, MM_MODEM_BAND_UTRAN_1 }
    },
    {
        .model    = "SARA-U260",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_2 }
    },
    {
        .model    = "SARA-U270",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G,
        .bands_2g = { MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UTRAN_1 }
    },
    {
        .model    = "SARA-U280",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_3G,
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_2 }
    },
    {
        .model    = "MPCI-L201",
        .method   = SETTINGS_UPDATE_METHOD_CFUN,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_2 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_2,  MM_MODEM_BAND_EUTRAN_4,  MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_13, MM_MODEM_BAND_EUTRAN_17 }
    },
    {
        .model    = "MPCI-L200",
        .method   = SETTINGS_UPDATE_METHOD_CFUN,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_8,  MM_MODEM_BAND_UTRAN_4,
                      MM_MODEM_BAND_UTRAN_2, MM_MODEM_BAND_UTRAN_1 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_2, MM_MODEM_BAND_EUTRAN_4,  MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_7, MM_MODEM_BAND_EUTRAN_17 }
    },
    {
        .model    = "MPCI-L210",
        .method   = SETTINGS_UPDATE_METHOD_CFUN,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_5,  MM_MODEM_BAND_UTRAN_8,  MM_MODEM_BAND_UTRAN_2,
                      MM_MODEM_BAND_UTRAN_1 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_1, MM_MODEM_BAND_EUTRAN_3, MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_7, MM_MODEM_BAND_EUTRAN_8, MM_MODEM_BAND_EUTRAN_20 }
    },
    {
        .model    = "MPCI-L220",
        .method   = SETTINGS_UPDATE_METHOD_CFUN,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_3g = { MM_MODEM_BAND_UTRAN_5,  MM_MODEM_BAND_UTRAN_8,  MM_MODEM_BAND_UTRAN_1 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_1, MM_MODEM_BAND_EUTRAN_3, MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_8, MM_MODEM_BAND_EUTRAN_19 }
    },
    {
        .model    = "MPCI-L280",
        .method   = SETTINGS_UPDATE_METHOD_CFUN,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_5,  MM_MODEM_BAND_UTRAN_8,  MM_MODEM_BAND_UTRAN_2,
                      MM_MODEM_BAND_UTRAN_1 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_1, MM_MODEM_BAND_EUTRAN_3, MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_7, MM_MODEM_BAND_EUTRAN_8, MM_MODEM_BAND_EUTRAN_28 }
    },
    {
        .model    = "TOBY-L200",
        .method   = SETTINGS_UPDATE_METHOD_CFUN,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UTRAN_4,
                      MM_MODEM_BAND_UTRAN_2, MM_MODEM_BAND_UTRAN_1 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_2, MM_MODEM_BAND_EUTRAN_4, MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_7, MM_MODEM_BAND_EUTRAN_17 }
    },
    {
        .model    = "TOBY-L201",
        .method   = SETTINGS_UPDATE_METHOD_CFUN,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_2 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_2,  MM_MODEM_BAND_EUTRAN_4,  MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_13, MM_MODEM_BAND_EUTRAN_17 }
    },
    {
        .model    = "TOBY-L210",
        .method   = SETTINGS_UPDATE_METHOD_CFUN,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UTRAN_2,
                      MM_MODEM_BAND_UTRAN_1 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_1,  MM_MODEM_BAND_EUTRAN_3, MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_7, MM_MODEM_BAND_EUTRAN_8, MM_MODEM_BAND_EUTRAN_20 }
    },
    {
        .model    = "TOBY-L220",
        .method   = SETTINGS_UPDATE_METHOD_CFUN,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UTRAN_2,
                      MM_MODEM_BAND_UTRAN_1 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_1,  MM_MODEM_BAND_EUTRAN_3,  MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_8, MM_MODEM_BAND_EUTRAN_19 }
    },
    {
        .model    = "TOBY-L280",
        .method   = SETTINGS_UPDATE_METHOD_CFUN,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UTRAN_2,
                      MM_MODEM_BAND_UTRAN_1 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_1,  MM_MODEM_BAND_EUTRAN_3, MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_7, MM_MODEM_BAND_EUTRAN_8, MM_MODEM_BAND_EUTRAN_28 }
    },
    {
        .model    = "TOBY-L4006",
        .method   = SETTINGS_UPDATE_METHOD_CFUN,
        .uact     = FEATURE_SUPPORTED,
        .ubandsel = FEATURE_UNSUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_4, MM_MODEM_BAND_UTRAN_2 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_2,  MM_MODEM_BAND_EUTRAN_4, MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_7, MM_MODEM_BAND_EUTRAN_12, MM_MODEM_BAND_EUTRAN_13,
                      MM_MODEM_BAND_EUTRAN_29 }
    },
    {
        .model    = "TOBY-L4106",
        .method   = SETTINGS_UPDATE_METHOD_CFUN,
        .uact     = FEATURE_SUPPORTED,
        .ubandsel = FEATURE_UNSUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_2g = { MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UTRAN_1 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_1, MM_MODEM_BAND_EUTRAN_3, MM_MODEM_BAND_EUTRAN_7,
                      MM_MODEM_BAND_EUTRAN_8, MM_MODEM_BAND_EUTRAN_20, MM_MODEM_BAND_EUTRAN_38 }
    },
    {
        .model    = "TOBY-L4206",
        .method   = SETTINGS_UPDATE_METHOD_CFUN,
        .uact     = FEATURE_SUPPORTED,
        .ubandsel = FEATURE_UNSUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UTRAN_1 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_1, MM_MODEM_BAND_EUTRAN_3, MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_7, MM_MODEM_BAND_EUTRAN_8, MM_MODEM_BAND_EUTRAN_9,
                      MM_MODEM_BAND_EUTRAN_19, MM_MODEM_BAND_EUTRAN_28 }
    },
    {
        .model    = "TOBY-L4906",
        .method   = SETTINGS_UPDATE_METHOD_CFUN,
        .uact     = FEATURE_SUPPORTED,
        .ubandsel = FEATURE_UNSUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_2g = { MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UTRAN_1 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_1, MM_MODEM_BAND_EUTRAN_3, MM_MODEM_BAND_EUTRAN_39,
                      MM_MODEM_BAND_EUTRAN_40, MM_MODEM_BAND_EUTRAN_41 }
    },
    {
        .model    = "TOBY-R200",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_8, MM_MODEM_BAND_UTRAN_2,
                      MM_MODEM_BAND_UTRAN_1 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_2, MM_MODEM_BAND_EUTRAN_4, MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_12 }
    },
    {
        .model    = "TOBY-R202",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_2 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_2, MM_MODEM_BAND_EUTRAN_4, MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_12 }
    },
    {
        .model    = "LARA-R202",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_3g = { MM_MODEM_BAND_UTRAN_5, MM_MODEM_BAND_UTRAN_2 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_2, MM_MODEM_BAND_EUTRAN_4, MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_12 }
    },
    {
        .model    = "LARA-R203",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_4G,
        .bands_4g = { MM_MODEM_BAND_EUTRAN_2, MM_MODEM_BAND_EUTRAN_4, MM_MODEM_BAND_EUTRAN_12 }
    },
    {
        .model    = "LARA-R204",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_4G,
        .bands_4g = { MM_MODEM_BAND_EUTRAN_4, MM_MODEM_BAND_EUTRAN_13 }
    },
    {
        .model    = "LARA-R211",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_4G,
        .bands_2g = { MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_3, MM_MODEM_BAND_EUTRAN_7, MM_MODEM_BAND_EUTRAN_20 }
    },
    {
        .model    = "LARA-R280",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_3G | MM_MODEM_MODE_4G,
        .bands_3g = { MM_MODEM_BAND_UTRAN_1 },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_3, MM_MODEM_BAND_EUTRAN_8, MM_MODEM_BAND_EUTRAN_28 }
    },
    {
        .model    = "LARA-R3121",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_4G,
        .bands_4g = { MM_MODEM_BAND_EUTRAN_3, MM_MODEM_BAND_EUTRAN_7, MM_MODEM_BAND_EUTRAN_20 }
    },
    {
        .model    = "SARA-N200",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_4G,
        .bands_4g = { MM_MODEM_BAND_EUTRAN_8 }
    },
    {
        .model    = "SARA-N201",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_4G,
        .bands_4g = { MM_MODEM_BAND_EUTRAN_5 }
    },
    {
        .model    = "SARA-N210",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_4G,
        .bands_4g = { MM_MODEM_BAND_EUTRAN_20 }
    },
    {
        .model    = "SARA-N211",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_4G,
        .bands_4g = { MM_MODEM_BAND_EUTRAN_8, MM_MODEM_BAND_EUTRAN_20 }
    },
    {
        .model    = "SARA-N280",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_SUPPORTED,
        .mode     = MM_MODEM_MODE_4G,
        .bands_4g = { MM_MODEM_BAND_EUTRAN_28 }
    },
    {
        .model    = "SARA-R410M-52B",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_UNSUPPORTED,
        .mode     = MM_MODEM_MODE_4G,
        .bands_4g = { MM_MODEM_BAND_EUTRAN_2,  MM_MODEM_BAND_EUTRAN_4, MM_MODEM_BAND_EUTRAN_5,
                      MM_MODEM_BAND_EUTRAN_12, MM_MODEM_BAND_EUTRAN_13 }
    },
    {
        .model    = "SARA-R410M-02B",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_UNSUPPORTED,
        .mode     = MM_MODEM_MODE_4G,
        .bands_4g = { MM_MODEM_BAND_EUTRAN_1,  MM_MODEM_BAND_EUTRAN_2, MM_MODEM_BAND_EUTRAN_3,
                      MM_MODEM_BAND_EUTRAN_4, MM_MODEM_BAND_EUTRAN_5, MM_MODEM_BAND_EUTRAN_8,
                      MM_MODEM_BAND_EUTRAN_12, MM_MODEM_BAND_EUTRAN_13, MM_MODEM_BAND_EUTRAN_19,
                      MM_MODEM_BAND_EUTRAN_20, MM_MODEM_BAND_EUTRAN_28, MM_MODEM_BAND_EUTRAN_39 }
    },
    {
        .model    = "SARA-R412M-02B",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_UNSUPPORTED,
        .mode     = MM_MODEM_MODE_2G | MM_MODEM_MODE_4G,
        .bands_2g = { MM_MODEM_BAND_G850, MM_MODEM_BAND_EGSM, MM_MODEM_BAND_DCS, MM_MODEM_BAND_PCS },
        .bands_4g = { MM_MODEM_BAND_EUTRAN_1,  MM_MODEM_BAND_EUTRAN_2,  MM_MODEM_BAND_EUTRAN_3,
                      MM_MODEM_BAND_EUTRAN_4,  MM_MODEM_BAND_EUTRAN_5,  MM_MODEM_BAND_EUTRAN_8,
                      MM_MODEM_BAND_EUTRAN_12, MM_MODEM_BAND_EUTRAN_13, MM_MODEM_BAND_EUTRAN_19,
                      MM_MODEM_BAND_EUTRAN_20, MM_MODEM_BAND_EUTRAN_28, MM_MODEM_BAND_EUTRAN_39 }
    },
    {
        .model    = "SARA-N410-02B",
        .method   = SETTINGS_UPDATE_METHOD_COPS,
        .uact     = FEATURE_UNSUPPORTED,
        .ubandsel = FEATURE_UNSUPPORTED,
        .mode     = MM_MODEM_MODE_4G,
        .bands_4g = { MM_MODEM_BAND_EUTRAN_1,  MM_MODEM_BAND_EUTRAN_2,  MM_MODEM_BAND_EUTRAN_3,
                      MM_MODEM_BAND_EUTRAN_4,  MM_MODEM_BAND_EUTRAN_5,  MM_MODEM_BAND_EUTRAN_8,
                      MM_MODEM_BAND_EUTRAN_12, MM_MODEM_BAND_EUTRAN_13, MM_MODEM_BAND_EUTRAN_19,
                      MM_MODEM_BAND_EUTRAN_20, MM_MODEM_BAND_EUTRAN_28 }
    },
};

gboolean
mm_ublox_get_support_config (const gchar         *model,
                             UbloxSupportConfig  *config,
                             GError             **error)
{
    guint i;

    if (!model) {
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                     "Support configuration unknown for unknown model");
        return FALSE;
    }

    for (i = 0; i < G_N_ELEMENTS (band_configuration); i++) {
        /* NOTE: matching by prefix! */
        if (g_str_has_prefix (model, band_configuration[i].model)) {
            config->loaded   = TRUE;
            config->method   = band_configuration[i].method;
            config->uact     = band_configuration[i].uact;
            config->ubandsel = band_configuration[i].ubandsel;
            return TRUE;
        }
    }

    g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                 "No support configuration found for modem: %s", model);
    return FALSE;
}

/*****************************************************************************/
/* Supported modes loading */

static MMModemMode
supported_modes_per_model (const gchar *model)
{
    MMModemMode mode;
    guint       i;

    mode = MM_MODEM_MODE_2G | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G;

    if (model) {
        for (i = 0; i < G_N_ELEMENTS (band_configuration); i++)
            if (g_str_has_prefix (model, band_configuration[i].model)) {
                mode = band_configuration[i].mode;
                return mode;;
            }
    }

    return mode;
}

GArray *
mm_ublox_filter_supported_modes (const gchar  *model,
                                 GArray       *combinations,
                                 gpointer      logger,
                                 GError      **error)
{
    MMModemModeCombination mode;
    GArray *all;
    GArray *filtered;

    /* Model not specified? */
    if (!model)
        return combinations;

    /* AT+URAT=? lies; we need an extra per-device filtering, thanks u-blox.
     * Don't know all PIDs for all devices, so model string based filtering... */

    mode.allowed   = supported_modes_per_model (model);
    mode.preferred = MM_MODEM_MODE_NONE;

    /* Nothing filtered? */
    if (mode.allowed == supported_modes_per_model (NULL))
        return combinations;

    all = g_array_sized_new (FALSE, FALSE, sizeof (MMModemModeCombination), 1);
    g_array_append_val (all, mode);
    filtered = mm_filter_supported_modes (all, combinations, logger);
    g_array_unref (all);
    g_array_unref (combinations);

    /* Error if nothing left */
    if (filtered->len == 0) {
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                     "No valid mode combinations built after filtering (model %s)", model);
        g_array_unref (filtered);
        return NULL;
    }

    return filtered;
}

/*****************************************************************************/
/* Supported bands loading */

GArray *
mm_ublox_get_supported_bands (const gchar  *model,
                              gpointer      log_object,
                              GError      **error)
{
    MMModemMode  mode;
    GArray      *bands;
    guint        i, j;

    mode  = supported_modes_per_model (model);
    bands = g_array_new (FALSE, FALSE, sizeof (MMModemBand));

    for (i = 0; i < G_N_ELEMENTS (band_configuration); i++) {
        if (g_str_has_prefix (model, band_configuration[i].model)) {
            mm_obj_dbg (log_object, "known supported bands found for model: %s", band_configuration[i].model);
            break;
        }
    }

    if (i == G_N_ELEMENTS (band_configuration)) {
        mm_obj_warn (log_object, "unknown model name given when looking for supported bands: %s", model);
        return NULL;
    }

    mode = band_configuration[i].mode;

    if (mode & MM_MODEM_MODE_2G) {
        for (j = 0; j < G_N_ELEMENTS (band_configuration[i].bands_2g) && band_configuration[i].bands_2g[j]; j++) {
            bands = g_array_append_val (bands, band_configuration[i].bands_2g[j]);
        }
    }

    if (mode & MM_MODEM_MODE_3G) {
        for (j = 0; j < G_N_ELEMENTS (band_configuration[i].bands_3g) && band_configuration[i].bands_3g[j]; j++) {
            bands = g_array_append_val (bands, band_configuration[i].bands_3g[j]);
        }
    }

    if (mode & MM_MODEM_MODE_4G) {
        for (j = 0; j < G_N_ELEMENTS (band_configuration[i].bands_4g) && band_configuration[i].bands_4g[j]; j++) {
            bands = g_array_append_val (bands, band_configuration[i].bands_4g[j]);
        }
    }

    if (bands->len == 0) {
        g_array_unref (bands);
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                     "No valid supported bands loaded");
        return NULL;
    }

    return bands;
}

typedef struct {
    guint       num;
    MMModemBand band[4];
} NumToBand;

/* 2G GSM Band Frequencies */
static const NumToBand num_bands_2g [] = {
    { .num =  850, .band = { MM_MODEM_BAND_G850 } },
    { .num =  900, .band = { MM_MODEM_BAND_EGSM } },
    { .num = 1900, .band = { MM_MODEM_BAND_PCS  } },
    { .num = 1800, .band = { MM_MODEM_BAND_DCS  } },
};

/* 3G UMTS Band Frequencies */
static const NumToBand num_bands_3g [] = {
    { .num =  800, .band = { MM_MODEM_BAND_UTRAN_6 } },
    { .num =  850, .band = { MM_MODEM_BAND_UTRAN_5 } },
    { .num =  900, .band = { MM_MODEM_BAND_UTRAN_8 } },
    { .num = 1700, .band = { MM_MODEM_BAND_UTRAN_4 } },
    { .num = 1900, .band = { MM_MODEM_BAND_UTRAN_2 } },
    { .num = 2100, .band = { MM_MODEM_BAND_UTRAN_1 } },
};

/* 4G LTE Band Frequencies */
static const NumToBand num_bands_4g [] = {
    { .num =  700, .band = { MM_MODEM_BAND_EUTRAN_12, MM_MODEM_BAND_EUTRAN_13, MM_MODEM_BAND_EUTRAN_17 } },
    { .num =  800, .band = { MM_MODEM_BAND_EUTRAN_20, MM_MODEM_BAND_EUTRAN_27 } },
    { .num =  850, .band = { MM_MODEM_BAND_EUTRAN_5, MM_MODEM_BAND_EUTRAN_18, MM_MODEM_BAND_EUTRAN_19, MM_MODEM_BAND_EUTRAN_26 } },
    { .num =  900, .band = { MM_MODEM_BAND_EUTRAN_8 } },
    { .num = 1700, .band = { MM_MODEM_BAND_EUTRAN_4, MM_MODEM_BAND_EUTRAN_10 } },
    { .num = 1800, .band = { MM_MODEM_BAND_EUTRAN_3 } },
    { .num = 1900, .band = { MM_MODEM_BAND_EUTRAN_2, MM_MODEM_BAND_EUTRAN_39 } },
    { .num = 2100, .band = { MM_MODEM_BAND_EUTRAN_1 } },
    { .num = 2300, .band = { MM_MODEM_BAND_EUTRAN_40 } },
    { .num = 2500, .band = { MM_MODEM_BAND_EUTRAN_41 } },
    { .num = 2600, .band = { MM_MODEM_BAND_EUTRAN_7, MM_MODEM_BAND_EUTRAN_38 } },
};
/*****************************************************************************/
/* +UBANDSEL? response parser */

static MMModemBand
num_to_band_2g (guint num)
{
    guint i;

    for (i = 0; i < G_N_ELEMENTS (num_bands_2g); i++) {
        if (num == num_bands_2g[i].num)
            return num_bands_2g[i].band[0];
    }
    return MM_MODEM_BAND_UNKNOWN;
}

static MMModemBand
num_to_band_3g (guint num)
{
    guint i;

    for (i = 0; i < G_N_ELEMENTS (num_bands_3g); i++) {
        if (num == num_bands_3g[i].num)
            return num_bands_3g[i].band[0];
    }
    return MM_MODEM_BAND_UNKNOWN;
}

static guint
band_to_num (MMModemBand band)
{
    guint i, j;

    /* Search 2G list */
    for (i = 0; i < G_N_ELEMENTS (num_bands_2g); i++) {
        for (j = 0; j < G_N_ELEMENTS (num_bands_2g[i].band) && num_bands_2g[i].band[j]; j++) {
            if (band == num_bands_2g[i].band[j])
                return num_bands_2g[i].num;
        }
    }

    /* Search 3G list */
    for (i = 0; i < G_N_ELEMENTS (num_bands_3g); i++) {
        for (j = 0; j < G_N_ELEMENTS (num_bands_3g[i].band) && num_bands_3g[i].band[j]; j++) {
            if (band == num_bands_3g[i].band[j])
                return num_bands_3g[i].num;
        }
    }

    /* Search 4G list */
    for (i = 0; i < G_N_ELEMENTS (num_bands_4g); i++) {
        for (j = 0; j < G_N_ELEMENTS (num_bands_4g[i].band) && num_bands_4g[i].band[j]; j++) {
            if (band == num_bands_4g[i].band[j])
                return num_bands_4g[i].num;
        }
    }

    /* Should never happen */
    return 0;
}

static void
append_bands (GArray      *bands,
              guint        ubandsel_value,
              MMModemMode  mode,
              const gchar *model,
              gpointer     log_object)
{
    guint i, j, k, x;
    MMModemBand band;

    /* Find Modem Model Index in band_configuration */
    for (i = 0; i < G_N_ELEMENTS (band_configuration); i++) {
        if (g_str_has_prefix (model, band_configuration[i].model)) {
            mm_obj_dbg (log_object, "known bands found for model: %s", band_configuration[i].model);
            break;
        }
    }

    if (i == G_N_ELEMENTS (band_configuration)) {
        mm_obj_warn (log_object, "unknown model name given when looking for bands: %s", model);
        return;
    }

    if (mode & MM_MODEM_MODE_2G) {
        band = num_to_band_2g (ubandsel_value);
        if (band != MM_MODEM_BAND_UNKNOWN) {
            for (x = 0; x < G_N_ELEMENTS (band_configuration[i].bands_2g); x++) {
                if (band_configuration[i].bands_2g[x] == band) {
                    g_array_append_val (bands, band);
                    break;
                }
            }
        }
    }

    if (mode & MM_MODEM_MODE_3G) {
        band = num_to_band_3g (ubandsel_value);
        if (band != MM_MODEM_BAND_UNKNOWN) {
            for (x = 0; x < G_N_ELEMENTS (band_configuration[i].bands_3g); x++) {
                if (band_configuration[i].bands_3g[x] == band) {
                    g_array_append_val (bands, band);
                    break;
                }
            }
        }
    }

    /* Note: The weird code segment below is to separate out specific LTE bands since
     * UBANDSEL? reports back the frequency of the band and not the band itself.
     */

    if (mode & MM_MODEM_MODE_4G) {
        for (j = 0; j < G_N_ELEMENTS (num_bands_4g); j++) {
            if (ubandsel_value == num_bands_4g[j].num) {
                for (k = 0; k < G_N_ELEMENTS (num_bands_4g[j].band); k++) {
                    band = num_bands_4g[j].band[k];
                    if (band != MM_MODEM_BAND_UNKNOWN) {
                        for (x = 0; x < G_N_ELEMENTS (band_configuration[i].bands_4g); x++) {
                            if (band_configuration[i].bands_4g[x] == band) {
                                g_array_append_val (bands, band);
                                break;
                            }
                        }
                    }
                }
                break;
            }
        }
    }
}

GArray *
mm_ublox_parse_ubandsel_response (const gchar  *response,
                                  const gchar  *model,
                                  gpointer      log_object,
                                  GError      **error)
{
    GArray      *array_values = NULL;
    GArray      *array = NULL;
    gchar       *dupstr = NULL;
    GError      *inner_error = NULL;
    guint        i;
    MMModemMode  mode;

    if (!g_str_has_prefix (response, "+UBANDSEL")) {
        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                   "Couldn't parse +UBANDSEL response: '%s'", response);
        goto out;
    }

    /* Response may be e.g.:
     *  +UBANDSEL: 850,900,1800,1900
     */
    dupstr = g_strchomp (g_strdup (mm_strip_tag (response, "+UBANDSEL:")));

    array_values = mm_parse_uint_list (dupstr, &inner_error);
    if (!array_values)
        goto out;

    /* Convert list of ubandsel numbers to MMModemBand values */
    mode = supported_modes_per_model (model);
    array = g_array_new (FALSE, FALSE, sizeof (MMModemBand));
    for (i = 0; i < array_values->len; i++)
        append_bands (array, g_array_index (array_values, guint, i), mode, model, log_object);

    if (!array->len) {
        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                   "No known band selection values matched in +UBANDSEL response: '%s'", response);
        goto out;
    }

out:
    if (inner_error) {
        g_propagate_error (error, inner_error);
        g_clear_pointer (&array, g_array_unref);
    }
    g_clear_pointer (&array_values, g_array_unref);
    g_free (dupstr);
    return array;
}

/*****************************************************************************/
/* UBANDSEL=X command builder */

static gint
ubandsel_num_cmp (const guint *a, const guint *b)
{
    return (*a - *b);
}

gchar *
mm_ublox_build_ubandsel_set_command (GArray       *bands,
                                     const gchar  *model,
                                     GError      **error)
{
    GString *command = NULL;
    GArray  *ubandsel_nums;
    guint    num;
    guint    i, j, k;

    if (bands->len == 1 && g_array_index (bands, MMModemBand, 0) == MM_MODEM_BAND_ANY)
        return g_strdup ("+UBANDSEL=0");

    for (i = 0; i < G_N_ELEMENTS (band_configuration); i++) {
        if (g_str_has_prefix (model, band_configuration[i].model))
            break;
    }

    if (i == G_N_ELEMENTS (band_configuration)) {
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                     "Unknown modem model %s", model);
        return NULL;
    }

    ubandsel_nums = g_array_sized_new (FALSE, FALSE, sizeof (guint), bands->len);

    for (j = 0; j < bands->len; j++) {
        MMModemBand band;
        gboolean   found = FALSE;

        band = g_array_index (bands, MMModemBand, j);

        /* Check to see if band is supported by the model */
        for (k = 0; !found && k < G_N_ELEMENTS (band_configuration[i].bands_2g) && band_configuration[i].bands_2g[k]; k++) {
            if (band == band_configuration[i].bands_2g[k])
                found = TRUE;
        }

        for (k = 0; !found && k < G_N_ELEMENTS (band_configuration[i].bands_3g) && band_configuration[i].bands_3g[k]; k++) {
            if (band == band_configuration[i].bands_3g[k])
                found = TRUE;
        }

        for (k = 0; !found && k < G_N_ELEMENTS (band_configuration[i].bands_4g) && band_configuration[i].bands_4g[k]; k++) {
            if (band == band_configuration[i].bands_4g[k])
                found = TRUE;
        }

        if (found) {
            num = band_to_num (band);
            g_assert (num != 0);
            g_array_append_val (ubandsel_nums, num);
        }
    }

    if (ubandsel_nums->len == 0) {
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                     "Given band combination is unsupported");
        g_array_unref (ubandsel_nums);
        return NULL;
    }

    if (ubandsel_nums->len > 1)
        g_array_sort (ubandsel_nums, (GCompareFunc) ubandsel_num_cmp);

    /* Build command */
    command = g_string_new ("+UBANDSEL=");
    for (i = 0; i < ubandsel_nums->len; i++)
        g_string_append_printf (command, "%s%u", i == 0 ? "" : ",", g_array_index (ubandsel_nums, guint, i));

    return g_string_free (command, FALSE);
}

/*****************************************************************************/
/* Get mode to apply when ANY */

MMModemMode
mm_ublox_get_modem_mode_any (const GArray *combinations)
{
    guint       i;
    MMModemMode any = MM_MODEM_MODE_NONE;
    guint       any_bits_set = 0;

    for (i = 0; i < combinations->len; i++) {
        MMModemModeCombination *combination;
        guint bits_set;

        combination = &g_array_index (combinations, MMModemModeCombination, i);
        if (combination->preferred != MM_MODEM_MODE_NONE)
            continue;
        bits_set = mm_count_bits_set (combination->allowed);
        if (bits_set > any_bits_set) {
            any_bits_set = bits_set;
            any = combination->allowed;
        }
    }

    /* If combinations were processed via mm_ublox_parse_urat_test_response(),
     * we're sure that there will be at least one combination with preferred
     * 'none', so there must be some valid combination as result */
    g_assert (any != MM_MODEM_MODE_NONE);
    return any;
}

/*****************************************************************************/
/* UACT common config */

typedef struct {
    guint       num;
    MMModemBand band;
} UactBandConfig;

static const UactBandConfig uact_band_config[] = {
    /* GSM bands */
    { .num =  900, .band = MM_MODEM_BAND_EGSM },
    { .num = 1800, .band = MM_MODEM_BAND_DCS  },
    { .num = 1900, .band = MM_MODEM_BAND_PCS  },
    { .num =  850, .band = MM_MODEM_BAND_G850 },
    { .num =  450, .band = MM_MODEM_BAND_G450 },
    { .num =  480, .band = MM_MODEM_BAND_G480 },
    { .num =  750, .band = MM_MODEM_BAND_G750 },
    { .num =  380, .band = MM_MODEM_BAND_G380 },
    { .num =  410, .band = MM_MODEM_BAND_G410 },
    { .num =  710, .band = MM_MODEM_BAND_G710 },
    { .num =  810, .band = MM_MODEM_BAND_G810 },
    /* UMTS bands */
    { .num =    1, .band = MM_MODEM_BAND_UTRAN_1  },
    { .num =    2, .band = MM_MODEM_BAND_UTRAN_2  },
    { .num =    3, .band = MM_MODEM_BAND_UTRAN_3  },
    { .num =    4, .band = MM_MODEM_BAND_UTRAN_4  },
    { .num =    5, .band = MM_MODEM_BAND_UTRAN_5  },
    { .num =    6, .band = MM_MODEM_BAND_UTRAN_6  },
    { .num =    7, .band = MM_MODEM_BAND_UTRAN_7  },
    { .num =    8, .band = MM_MODEM_BAND_UTRAN_8  },
    { .num =    9, .band = MM_MODEM_BAND_UTRAN_9  },
    { .num =   10, .band = MM_MODEM_BAND_UTRAN_10 },
    { .num =   11, .band = MM_MODEM_BAND_UTRAN_11 },
    { .num =   12, .band = MM_MODEM_BAND_UTRAN_12 },
    { .num =   13, .band = MM_MODEM_BAND_UTRAN_13 },
    { .num =   14, .band = MM_MODEM_BAND_UTRAN_14 },
    { .num =   19, .band = MM_MODEM_BAND_UTRAN_19 },
    { .num =   20, .band = MM_MODEM_BAND_UTRAN_20 },
    { .num =   21, .band = MM_MODEM_BAND_UTRAN_21 },
    { .num =   22, .band = MM_MODEM_BAND_UTRAN_22 },
    { .num =   25, .band = MM_MODEM_BAND_UTRAN_25 },
    /* LTE bands */
    { .num =  101, .band = MM_MODEM_BAND_EUTRAN_1  },
    { .num =  102, .band = MM_MODEM_BAND_EUTRAN_2  },
    { .num =  103, .band = MM_MODEM_BAND_EUTRAN_3  },
    { .num =  104, .band = MM_MODEM_BAND_EUTRAN_4  },
    { .num =  105, .band = MM_MODEM_BAND_EUTRAN_5  },
    { .num =  106, .band = MM_MODEM_BAND_EUTRAN_6  },
    { .num =  107, .band = MM_MODEM_BAND_EUTRAN_7  },
    { .num =  108, .band = MM_MODEM_BAND_EUTRAN_8  },
    { .num =  109, .band = MM_MODEM_BAND_EUTRAN_9  },
    { .num =  110, .band = MM_MODEM_BAND_EUTRAN_10 },
    { .num =  111, .band = MM_MODEM_BAND_EUTRAN_11 },
    { .num =  112, .band = MM_MODEM_BAND_EUTRAN_12 },
    { .num =  113, .band = MM_MODEM_BAND_EUTRAN_13 },
    { .num =  114, .band = MM_MODEM_BAND_EUTRAN_14 },
    { .num =  117, .band = MM_MODEM_BAND_EUTRAN_17 },
    { .num =  118, .band = MM_MODEM_BAND_EUTRAN_18 },
    { .num =  119, .band = MM_MODEM_BAND_EUTRAN_19 },
    { .num =  120, .band = MM_MODEM_BAND_EUTRAN_20 },
    { .num =  121, .band = MM_MODEM_BAND_EUTRAN_21 },
    { .num =  122, .band = MM_MODEM_BAND_EUTRAN_22 },
    { .num =  123, .band = MM_MODEM_BAND_EUTRAN_23 },
    { .num =  124, .band = MM_MODEM_BAND_EUTRAN_24 },
    { .num =  125, .band = MM_MODEM_BAND_EUTRAN_25 },
    { .num =  126, .band = MM_MODEM_BAND_EUTRAN_26 },
    { .num =  127, .band = MM_MODEM_BAND_EUTRAN_27 },
    { .num =  128, .band = MM_MODEM_BAND_EUTRAN_28 },
    { .num =  129, .band = MM_MODEM_BAND_EUTRAN_29 },
    { .num =  130, .band = MM_MODEM_BAND_EUTRAN_30 },
    { .num =  131, .band = MM_MODEM_BAND_EUTRAN_31 },
    { .num =  132, .band = MM_MODEM_BAND_EUTRAN_32 },
    { .num =  133, .band = MM_MODEM_BAND_EUTRAN_33 },
    { .num =  134, .band = MM_MODEM_BAND_EUTRAN_34 },
    { .num =  135, .band = MM_MODEM_BAND_EUTRAN_35 },
    { .num =  136, .band = MM_MODEM_BAND_EUTRAN_36 },
    { .num =  137, .band = MM_MODEM_BAND_EUTRAN_37 },
    { .num =  138, .band = MM_MODEM_BAND_EUTRAN_38 },
    { .num =  139, .band = MM_MODEM_BAND_EUTRAN_39 },
    { .num =  140, .band = MM_MODEM_BAND_EUTRAN_40 },
    { .num =  141, .band = MM_MODEM_BAND_EUTRAN_41 },
    { .num =  142, .band = MM_MODEM_BAND_EUTRAN_42 },
    { .num =  143, .band = MM_MODEM_BAND_EUTRAN_43 },
    { .num =  144, .band = MM_MODEM_BAND_EUTRAN_44 },
    { .num =  145, .band = MM_MODEM_BAND_EUTRAN_45 },
    { .num =  146, .band = MM_MODEM_BAND_EUTRAN_46 },
    { .num =  147, .band = MM_MODEM_BAND_EUTRAN_47 },
    { .num =  148, .band = MM_MODEM_BAND_EUTRAN_48 },
};

static MMModemBand
uact_num_to_band (guint num)
{
    guint i;

    for (i = 0; i < G_N_ELEMENTS (uact_band_config); i++) {
        if (num == uact_band_config[i].num)
            return uact_band_config[i].band;
    }
    return MM_MODEM_BAND_UNKNOWN;
}

static guint
uact_band_to_num (MMModemBand band)
{
    guint i;

    for (i = 0; i < G_N_ELEMENTS (uact_band_config); i++) {
        if (band == uact_band_config[i].band)
            return uact_band_config[i].num;
    }
    return 0;
}

/*****************************************************************************/
/* UACT? response parser */

static GArray *
uact_num_array_to_band_array (GArray *nums)
{
    GArray *bands = NULL;
    guint   i;

    if (!nums)
        return NULL;

    bands = g_array_sized_new (FALSE, FALSE, sizeof (MMModemBand), nums->len);
    for (i = 0; i < nums->len; i++) {
        MMModemBand band;

        band = uact_num_to_band (g_array_index (nums, guint, i));
        g_array_append_val (bands, band);
    }

    return bands;
}

GArray *
mm_ublox_parse_uact_response (const gchar  *response,
                              GError      **error)
{
    g_autoptr(GRegex)      r = NULL;
    g_autoptr(GMatchInfo)  match_info = NULL;
    GError                *inner_error = NULL;
    GArray                *nums = NULL;
    GArray                *bands = NULL;

    /*
     * AT+UACT?
     * +UACT: ,,,900,1800,1,8,101,103,107,108,120,138
     */
    r = g_regex_new ("\\+UACT: ([^,]*),([^,]*),([^,]*),(.*)(?:\\r\\n)?",
                     G_REGEX_DOLLAR_ENDONLY | G_REGEX_RAW, 0, NULL);
    g_assert (r != NULL);

    g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, &inner_error);
    if (!inner_error && g_match_info_matches (match_info)) {
        g_autofree gchar *bandstr = NULL;

        bandstr = mm_get_string_unquoted_from_match_info (match_info, 4);
        nums = mm_parse_uint_list (bandstr, &inner_error);
    }

    if (inner_error) {
        g_propagate_error (error, inner_error);
        return NULL;
    }

    /* Convert to MMModemBand values */
    if (nums) {
        bands = uact_num_array_to_band_array (nums);
        g_array_unref (nums);
    }

    if (!bands)
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                     "No known band selection values matched in +UACT response: '%s'", response);

    return bands;
}

/*****************************************************************************/
/* UACT=? response parser */

static GArray *
parse_bands_from_string (const gchar *str,
                         const gchar *group,
                         gpointer     log_object)
{
    GArray *bands = NULL;
    GError *inner_error = NULL;
    GArray *nums;

    nums = mm_parse_uint_list (str, &inner_error);
    if (nums) {
        gchar *tmpstr;

        bands = uact_num_array_to_band_array (nums);
        tmpstr = mm_common_build_bands_string ((MMModemBand *)(gpointer)(bands->data), bands->len);
        mm_obj_dbg (log_object, "modem reports support for %s bands: %s", group, tmpstr);
        g_free (tmpstr);

        g_array_unref (nums);
    } else if (inner_error) {
        mm_obj_warn (log_object, "couldn't parse list of supported %s bands: %s", group, inner_error->message);
        g_clear_error (&inner_error);
    }

    return bands;
}

gboolean
mm_ublox_parse_uact_test (const gchar  *response,
                          gpointer      log_object,
                          GArray      **bands2g_out,
                          GArray      **bands3g_out,
                          GArray      **bands4g_out,
                          GError      **error)
{
    g_autoptr(GRegex)      r = NULL;
    g_autoptr(GMatchInfo)  match_info = NULL;
    g_auto(GStrv)          split = NULL;
    GError                *inner_error = NULL;
    const gchar           *bands2g_str = NULL;
    const gchar           *bands3g_str = NULL;
    const gchar           *bands4g_str = NULL;
    GArray                *bands2g = NULL;
    GArray                *bands3g = NULL;
    GArray                *bands4g = NULL;

    g_assert (bands2g_out && bands3g_out && bands4g_out);

    /*
     * AT+UACT=?
     * +UACT: ,,,(900,1800),(1,8),(101,103,107,108,120),(138)
     */
    r = g_regex_new ("\\+UACT: ([^,]*),([^,]*),([^,]*),(.*)(?:\\r\\n)?",
                     G_REGEX_DOLLAR_ENDONLY | G_REGEX_RAW, 0, NULL);
    g_assert (r != NULL);

    g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, &inner_error);
    if (inner_error)
        goto out;

    if (g_match_info_matches (match_info)) {
        g_autofree gchar *aux = NULL;
        guint             n_groups;

        aux  = mm_get_string_unquoted_from_match_info (match_info, 4);
        split = mm_split_string_groups (aux);
        n_groups = g_strv_length (split);
        if (n_groups >= 1)
            bands2g_str = split[0];
        if (n_groups >= 2)
            bands3g_str = split[1];
        if (n_groups >= 3)
            bands4g_str = split[2];
    }

    if (!bands2g_str && !bands3g_str && !bands4g_str) {
        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                   "frequency groups not found: %s", response);
        goto out;
    }

    bands2g = parse_bands_from_string (bands2g_str, "2G", log_object);
    bands3g = parse_bands_from_string (bands3g_str, "3G", log_object);
    bands4g = parse_bands_from_string (bands4g_str, "4G", log_object);

    if (!bands2g->len && !bands3g->len && !bands4g->len) {
        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                   "no supported frequencies reported: %s", response);
        goto out;
    }

    /* success */

out:
    if (inner_error) {
        if (bands2g)
            g_array_unref (bands2g);
        if (bands3g)
            g_array_unref (bands3g);
        if (bands4g)
            g_array_unref (bands4g);
        g_propagate_error (error, inner_error);
        return FALSE;
    }

    *bands2g_out = bands2g;
    *bands3g_out = bands3g;
    *bands4g_out = bands4g;
    return TRUE;
}

/*****************************************************************************/
/* UACT=X command builder */

gchar *
mm_ublox_build_uact_set_command (GArray  *bands,
                                 GError **error)
{
    GString *command;

    /* Build command */
    command = g_string_new ("+UACT=,,,");

    if (bands->len == 1 && g_array_index (bands, MMModemBand, 0) == MM_MODEM_BAND_ANY)
        g_string_append (command, "0");
    else {
        guint i;

        for (i = 0; i < bands->len; i++) {
            MMModemBand band;
            guint       num;

            band = g_array_index (bands, MMModemBand, i);
            num = uact_band_to_num (band);
            if (!num) {
                g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                             "Band unsupported by this plugin: %s", mm_modem_band_get_string (band));
                g_string_free (command, TRUE);
                return NULL;
            }

            g_string_append_printf (command, "%s%u", i == 0 ? "" : ",", num);
        }
    }

    return g_string_free (command, FALSE);
}

/*****************************************************************************/
/* URAT? response parser */

gboolean
mm_ublox_parse_urat_read_response (const gchar  *response,
                                   gpointer      log_object,
                                   MMModemMode  *out_allowed,
                                   MMModemMode  *out_preferred,
                                   GError      **error)
{
    g_autoptr(GRegex)      r = NULL;
    g_autoptr(GMatchInfo)  match_info = NULL;
    GError                *inner_error = NULL;
    MMModemMode            allowed = MM_MODEM_MODE_NONE;
    MMModemMode            preferred = MM_MODEM_MODE_NONE;
    g_autofree gchar      *allowed_str = NULL;
    g_autofree gchar      *preferred_str = NULL;

    g_assert (out_allowed != NULL && out_preferred != NULL);

    /* Response may be e.g.:
     * +URAT: 1,2
     * +URAT: 1
     */
    r = g_regex_new ("\\+URAT: (\\d+)(?:,(\\d+))?(?:\\r\\n)?", 0, 0, NULL);
    g_assert (r != NULL);

    g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, &inner_error);
    if (!inner_error && g_match_info_matches (match_info)) {
        guint  value = 0;

        /* Selected item is mandatory */
        if (!mm_get_uint_from_match_info (match_info, 1, &value)) {
            inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                       "Couldn't read AcT selected value");
            goto out;
        }
        if (value >= G_N_ELEMENTS (ublox_combinations)) {
            inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                       "Unexpected AcT selected value: %u", value);
            goto out;
        }
        allowed = ublox_combinations[value];
        allowed_str = mm_modem_mode_build_string_from_mask (allowed);
        mm_obj_dbg (log_object, "current allowed modes retrieved: %s", allowed_str);

        /* Preferred item is optional */
        if (mm_get_uint_from_match_info (match_info, 2, &value)) {
            if (value >= G_N_ELEMENTS (ublox_combinations)) {
                inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                           "Unexpected AcT preferred value: %u", value);
                goto out;
            }
            preferred = ublox_combinations[value];
            preferred_str = mm_modem_mode_build_string_from_mask (preferred);
            mm_obj_dbg (log_object, "current preferred modes retrieved: %s", preferred_str);
            if (mm_count_bits_set (preferred) != 1) {
                inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                           "AcT preferred value should be a single AcT: %s", preferred_str);
                goto out;
            }
            if (!(allowed & preferred)) {
                inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                           "AcT preferred value (%s) not a subset of the allowed value (%s)",
                                           preferred_str, allowed_str);
                goto out;
            }
        }
    }

out:
    if (inner_error) {
        g_propagate_error (error, inner_error);
        return FALSE;
    }

    if (allowed == MM_MODEM_MODE_NONE) {
        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                   "Couldn't parse +URAT response: %s", response);
        return FALSE;
    }

    *out_allowed = allowed;
    *out_preferred = preferred;
    return TRUE;
}

/*****************************************************************************/
/* URAT=X command builder */

static gboolean
append_rat_value (GString      *str,
                  MMModemMode   mode,
                  GError      **error)
{
    guint i;

    for (i = 0; i < G_N_ELEMENTS (ublox_combinations); i++) {
        if (ublox_combinations[i] == mode) {
            g_string_append_printf (str, "%u", i);
            return TRUE;
        }
    }

    g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                 "No AcT value matches requested mode");
    return FALSE;
}

gchar *
mm_ublox_build_urat_set_command (MMModemMode   allowed,
                                 MMModemMode   preferred,
                                 GError      **error)
{
    GString *command;

    command = g_string_new ("+URAT=");
    if (!append_rat_value (command, allowed, error)) {
        g_string_free (command, TRUE);
        return NULL;
    }

    if (preferred != MM_MODEM_MODE_NONE) {
        g_string_append (command, ",");
        if (!append_rat_value (command, preferred, error)) {
            g_string_free (command, TRUE);
            return NULL;
        }
    }

    return g_string_free (command, FALSE);
}

/*****************************************************************************/
/* +UAUTHREQ=? test parser */

MMUbloxBearerAllowedAuth
mm_ublox_parse_uauthreq_test (const char  *response,
                              gpointer     log_object,
                              GError     **error)
{
    MMUbloxBearerAllowedAuth   mask = MM_UBLOX_BEARER_ALLOWED_AUTH_UNKNOWN;
    GError                    *inner_error = NULL;
    GArray                    *allowed_auths = NULL;
    gchar                    **split;
    guint                      split_len;

    /*
     * Response may be like:
     *   AT+UAUTHREQ=?
     *   +UAUTHREQ: (1-4),(0-2),,
     */
    response = mm_strip_tag (response, "+UAUTHREQ:");
    split = mm_split_string_groups (response);
    split_len = g_strv_length (split);
    if (split_len < 2) {
        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                   "Unexpected number of groups in +UAUTHREQ=? response: %u", g_strv_length (split));
        goto out;
    }

    allowed_auths = mm_parse_uint_list (split[1], &inner_error);
    if (inner_error)
        goto out;

    if (allowed_auths) {
        guint i;

        for (i = 0; i < allowed_auths->len; i++) {
            guint val;

            val = g_array_index (allowed_auths, guint, i);
            switch (val) {
                case 0:
                    mask |= MM_UBLOX_BEARER_ALLOWED_AUTH_NONE;
                    break;
                case 1:
                    mask |= MM_UBLOX_BEARER_ALLOWED_AUTH_PAP;
                    break;
                case 2:
                    mask |= MM_UBLOX_BEARER_ALLOWED_AUTH_CHAP;
                    break;
                case 3:
                    mask |= MM_UBLOX_BEARER_ALLOWED_AUTH_AUTO;
                    break;
                default:
                    mm_obj_warn (log_object, "unexpected +UAUTHREQ value: %u", val);
                    break;
            }
        }
    }

    if (!mask) {
        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                   "No supported authentication methods in +UAUTHREQ=? response");
        goto out;
    }

out:
    g_strfreev (split);

    if (allowed_auths)
        g_array_unref (allowed_auths);

    if (inner_error) {
        g_propagate_error (error, inner_error);
        return MM_UBLOX_BEARER_ALLOWED_AUTH_UNKNOWN;
    }

    return mask;
}

/*****************************************************************************/
/* +UGCNTRD response parser */

gboolean
mm_ublox_parse_ugcntrd_response_for_cid (const gchar  *response,
                                         guint         in_cid,
                                         guint64      *out_session_tx_bytes,
                                         guint64      *out_session_rx_bytes,
                                         guint64      *out_total_tx_bytes,
                                         guint64      *out_total_rx_bytes,
                                         GError      **error)
{
    g_autoptr(GRegex)      r = NULL;
    g_autoptr(GMatchInfo)  match_info = NULL;
    GError                *inner_error = NULL;
    guint64                session_tx_bytes = 0;
    guint64                session_rx_bytes = 0;
    guint64                total_tx_bytes   = 0;
    guint64                total_rx_bytes   = 0;
    gboolean               matched = FALSE;

    /* Response may be e.g.:
     *  +UGCNTRD: 31,2704,1819,2724,1839
     * We assume only ONE line is returned.
     */
    r = g_regex_new ("\\+UGCNTRD:\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),\\s*(\\d+)",
                     G_REGEX_DOLLAR_ENDONLY | G_REGEX_RAW, 0, NULL);
    g_assert (r != NULL);

    /* Report invalid CID given */
    if (!in_cid) {
        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "Invalid CID given");
        goto out;
    }

    g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, &inner_error);
    while (!inner_error && g_match_info_matches (match_info)) {
        guint cid = 0;

        /* Matched CID? */
        if (!mm_get_uint_from_match_info (match_info, 1, &cid) || cid != in_cid) {
            g_match_info_next (match_info, &inner_error);
            continue;
        }

        if (out_session_tx_bytes && !mm_get_u64_from_match_info (match_info, 2, &session_tx_bytes)) {
            inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "Error parsing session TX bytes");
            goto out;
        }

        if (out_session_rx_bytes && !mm_get_u64_from_match_info (match_info, 3, &session_rx_bytes)) {
            inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "Error parsing session RX bytes");
            goto out;
        }

        if (out_total_tx_bytes && !mm_get_u64_from_match_info (match_info, 4, &total_tx_bytes)) {
            inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "Error parsing total TX bytes");
            goto out;
        }

        if (out_total_rx_bytes && !mm_get_u64_from_match_info (match_info, 5, &total_rx_bytes)) {
            inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "Error parsing total RX bytes");
            goto out;
        }

        matched = TRUE;
        break;
    }

    if (!matched) {
        inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "No statistics found for CID %u", in_cid);
        goto out;
    }

out:
    if (inner_error) {
        g_propagate_error (error, inner_error);
        return FALSE;
    }

    if (out_session_tx_bytes)
        *out_session_tx_bytes = session_tx_bytes;
    if (out_session_rx_bytes)
        *out_session_rx_bytes = session_rx_bytes;
    if (out_total_tx_bytes)
        *out_total_tx_bytes = total_tx_bytes;
    if (out_total_rx_bytes)
        *out_total_rx_bytes = total_rx_bytes;
    return TRUE;
}
