/* -*- 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) 2008 - 2009 Novell, Inc.
 * Copyright (C) 2009 - 2010 Red Hat, Inc.
 */

#define _GNU_SOURCE  /* for strcasestr */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <string.h>

#define G_UDEV_API_IS_SUBJECT_TO_CHANGE
#include <gudev/gudev.h>

#include "mm-plugin-base.h"
#include "mm-at-serial-port.h"
#include "mm-qcdm-serial-port.h"
#include "mm-serial-parsers.h"
#include "mm-errors.h"
#include "mm-marshal.h"
#include "libqcdm/src/commands.h"
#include "libqcdm/src/utils.h"

static void plugin_init (MMPlugin *plugin_class);

G_DEFINE_TYPE_EXTENDED (MMPluginBase, mm_plugin_base, G_TYPE_OBJECT,
                        0, G_IMPLEMENT_INTERFACE (MM_TYPE_PLUGIN, plugin_init))

#define MM_PLUGIN_BASE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), MM_TYPE_PLUGIN_BASE, MMPluginBasePrivate))

/* A hash table shared between all instances of the plugin base that
 * caches the probed capabilities so that only one plugin has to actually
 * probe a port.
 */
static GHashTable *cached_caps = NULL;


typedef struct {
    char *name;
    GUdevClient *client;
    GHashTable *tasks;
} MMPluginBasePrivate;

enum {
    PROP_0,
    PROP_NAME,
    LAST_PROP
};

enum {
    PROBE_RESULT,
    LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };


typedef enum {
    PROBE_STATE_GCAP_TRY1 = 0,
    PROBE_STATE_GCAP_TRY2,
    PROBE_STATE_GCAP_TRY3,
    PROBE_STATE_ATI,
    PROBE_STATE_CPIN,
    PROBE_STATE_CGMM,
    PROBE_STATE_LAST
} ProbeState;

static void probe_complete (MMPluginBaseSupportsTask *task);

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

G_DEFINE_TYPE (MMPluginBaseSupportsTask, mm_plugin_base_supports_task, G_TYPE_OBJECT)

#define MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), MM_TYPE_PLUGIN_BASE_SUPPORTS_TASK, MMPluginBaseSupportsTaskPrivate))

typedef struct {
    MMPluginBase *plugin;
    GUdevDevice *port;
    char *physdev_path;
    char *driver;

    guint open_id;
    guint32 open_tries;
    guint full_id;

    MMAtSerialPort *probe_port;
    MMQcdmSerialPort *qcdm_port;
    guint32 probed_caps;
    ProbeState probe_state;
    guint probe_id;
    char *probe_resp;
    GError *probe_error;

    char *custom_init;
    guint32 custom_init_max_tries;
    guint32 custom_init_tries;
    guint32 custom_init_delay_seconds;
    gboolean custom_init_fail_if_timeout;

    MMSupportsPortResultFunc callback;
    gpointer callback_data;
}  MMPluginBaseSupportsTaskPrivate;

static MMPluginBaseSupportsTask *
supports_task_new (MMPluginBase *self,
                   GUdevDevice *port,
                   const char *physdev_path,
                   const char *driver,
                   MMSupportsPortResultFunc callback,
                   gpointer callback_data)
{
    MMPluginBaseSupportsTask *task;
    MMPluginBaseSupportsTaskPrivate *priv;

    g_return_val_if_fail (self != NULL, NULL);
    g_return_val_if_fail (MM_IS_PLUGIN_BASE (self), NULL);
    g_return_val_if_fail (port != NULL, NULL);
    g_return_val_if_fail (physdev_path != NULL, NULL);
    g_return_val_if_fail (driver != NULL, NULL);
    g_return_val_if_fail (callback != NULL, NULL);

    task = (MMPluginBaseSupportsTask *) g_object_new (MM_TYPE_PLUGIN_BASE_SUPPORTS_TASK, NULL);

    priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task);
    priv->plugin = self;
    priv->port = g_object_ref (port);
    priv->physdev_path = g_strdup (physdev_path);
    priv->driver = g_strdup (driver);
    priv->callback = callback;
    priv->callback_data = callback_data;

    return task;
}

MMPlugin *
mm_plugin_base_supports_task_get_plugin (MMPluginBaseSupportsTask *task)
{
    g_return_val_if_fail (task != NULL, NULL);
    g_return_val_if_fail (MM_IS_PLUGIN_BASE_SUPPORTS_TASK (task), NULL);

    return MM_PLUGIN (MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task)->plugin);
}

GUdevDevice *
mm_plugin_base_supports_task_get_port (MMPluginBaseSupportsTask *task)
{
    g_return_val_if_fail (task != NULL, NULL);
    g_return_val_if_fail (MM_IS_PLUGIN_BASE_SUPPORTS_TASK (task), NULL);

    return MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task)->port;
}

const char *
mm_plugin_base_supports_task_get_physdev_path (MMPluginBaseSupportsTask *task)
{
    g_return_val_if_fail (task != NULL, NULL);
    g_return_val_if_fail (MM_IS_PLUGIN_BASE_SUPPORTS_TASK (task), NULL);

    return MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task)->physdev_path;
}

const char *
mm_plugin_base_supports_task_get_driver (MMPluginBaseSupportsTask *task)
{
    g_return_val_if_fail (task != NULL, NULL);
    g_return_val_if_fail (MM_IS_PLUGIN_BASE_SUPPORTS_TASK (task), NULL);

    return MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task)->driver;
}

guint32
mm_plugin_base_supports_task_get_probed_capabilities (MMPluginBaseSupportsTask *task)
{
    g_return_val_if_fail (task != NULL, 0);
    g_return_val_if_fail (MM_IS_PLUGIN_BASE_SUPPORTS_TASK (task), 0);

    return MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task)->probed_caps;
}

void
mm_plugin_base_supports_task_complete (MMPluginBaseSupportsTask *task,
                                       guint32 level)
{
    MMPluginBaseSupportsTaskPrivate *priv;
    const char *subsys, *name;

    g_return_if_fail (task != NULL);
    g_return_if_fail (MM_IS_PLUGIN_BASE_SUPPORTS_TASK (task));

    priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task);
    g_return_if_fail (priv->callback != NULL);

    if (priv->full_id) {
        g_source_remove (priv->full_id);
        priv->full_id = 0;
    }

    subsys = g_udev_device_get_subsystem (priv->port);
    name = g_udev_device_get_name (priv->port);

    priv->callback (MM_PLUGIN (priv->plugin), subsys, name, level, priv->callback_data);

    /* Clear out the callback, it shouldn't be called more than once */
    priv->callback = NULL;
    priv->callback_data = NULL;
}

void
mm_plugin_base_supports_task_set_custom_init_command (MMPluginBaseSupportsTask *task,
                                                      const char *cmd,
                                                      guint32 delay_seconds,
                                                      guint32 max_tries,
                                                      gboolean fail_if_timeout)
{
    MMPluginBaseSupportsTaskPrivate *priv;

    g_return_if_fail (task != NULL);
    g_return_if_fail (MM_IS_PLUGIN_BASE_SUPPORTS_TASK (task));

    priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task);

    g_free (priv->custom_init);
    priv->custom_init = g_strdup (cmd);
    priv->custom_init_max_tries = max_tries;
    priv->custom_init_delay_seconds = delay_seconds;
    priv->custom_init_fail_if_timeout = fail_if_timeout;
}

static void
mm_plugin_base_supports_task_init (MMPluginBaseSupportsTask *self)
{
}

static void
supports_task_dispose (GObject *object)
{
    MMPluginBaseSupportsTaskPrivate *priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (object);

    if (MM_IS_SERIAL_PORT (priv->probe_port))
        mm_serial_port_flash_cancel (MM_SERIAL_PORT (priv->probe_port));

    g_object_unref (priv->port);
    g_free (priv->physdev_path);
    g_free (priv->driver);
    g_free (priv->probe_resp);
    g_clear_error (&(priv->probe_error));
    g_free (priv->custom_init);

    if (priv->open_id)
        g_source_remove (priv->open_id);
    if (priv->full_id)
        g_source_remove (priv->full_id);

    if (priv->probe_id)
        g_source_remove (priv->probe_id);
    if (priv->probe_port) {
        mm_serial_port_close (MM_SERIAL_PORT (priv->probe_port));
        g_object_unref (priv->probe_port);
    }
    if (priv->qcdm_port) {
        mm_serial_port_close (MM_SERIAL_PORT (priv->qcdm_port));
        g_object_unref (priv->qcdm_port);
    }

    G_OBJECT_CLASS (mm_plugin_base_supports_task_parent_class)->dispose (object);
}

static void
mm_plugin_base_supports_task_class_init (MMPluginBaseSupportsTaskClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    g_type_class_add_private (object_class, sizeof (MMPluginBaseSupportsTaskPrivate));

    /* Virtual methods */
    object_class->dispose = supports_task_dispose;
}

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

#define MM_PLUGIN_BASE_PORT_CAP_CDMA (MM_PLUGIN_BASE_PORT_CAP_IS707_A | \
                                      MM_PLUGIN_BASE_PORT_CAP_IS707_P | \
                                      MM_PLUGIN_BASE_PORT_CAP_IS856 | \
                                      MM_PLUGIN_BASE_PORT_CAP_IS856_A)

#define CAP_GSM_OR_CDMA (MM_PLUGIN_BASE_PORT_CAP_CDMA | MM_PLUGIN_BASE_PORT_CAP_GSM)

struct modem_caps {
	char *name;
	guint32 bits;
};

static struct modem_caps modem_caps[] = {
	{"+CGSM",     MM_PLUGIN_BASE_PORT_CAP_GSM},
	{"+CIS707-A", MM_PLUGIN_BASE_PORT_CAP_IS707_A},
	{"+CIS707A",  MM_PLUGIN_BASE_PORT_CAP_IS707_A}, /* Cmotech */
	{"+CIS707",   MM_PLUGIN_BASE_PORT_CAP_IS707_A},
	{"CIS707",    MM_PLUGIN_BASE_PORT_CAP_IS707_A}, /* Qualcomm Gobi */
	{"+CIS707P",  MM_PLUGIN_BASE_PORT_CAP_IS707_P},
	{"CIS-856",   MM_PLUGIN_BASE_PORT_CAP_IS856},
	{"+IS-856",   MM_PLUGIN_BASE_PORT_CAP_IS856},   /* Cmotech */
	{"CIS-856-A", MM_PLUGIN_BASE_PORT_CAP_IS856_A},
	{"CIS-856A",  MM_PLUGIN_BASE_PORT_CAP_IS856_A}, /* Kyocera KPC680 */
	{"+DS",       MM_PLUGIN_BASE_PORT_CAP_DS},
	{"+ES",       MM_PLUGIN_BASE_PORT_CAP_ES},
	{"+MS",       MM_PLUGIN_BASE_PORT_CAP_MS},
	{"+FCLASS",   MM_PLUGIN_BASE_PORT_CAP_FCLASS},
	{NULL}
};

static guint32
parse_gcap (const char *buf)
{
    struct modem_caps *cap = modem_caps;
    guint32 ret = 0;

    while (cap->name) {
        if (strstr (buf, cap->name))
            ret |= cap->bits;
        cap++;
    }
    return ret;
}

static guint32
parse_cpin (const char *buf)
{
    if (   strcasestr (buf, "SIM PIN")
        || strcasestr (buf, "SIM PUK")
        || strcasestr (buf, "PH-SIM PIN")
        || strcasestr (buf, "PH-FSIM PIN")
        || strcasestr (buf, "PH-FSIM PUK")
        || strcasestr (buf, "SIM PIN2")
        || strcasestr (buf, "SIM PUK2")
        || strcasestr (buf, "PH-NET PIN")
        || strcasestr (buf, "PH-NET PUK")
        || strcasestr (buf, "PH-NETSUB PIN")
        || strcasestr (buf, "PH-NETSUB PUK")
        || strcasestr (buf, "PH-SP PIN")
        || strcasestr (buf, "PH-SP PUK")
        || strcasestr (buf, "PH-CORP PIN")
        || strcasestr (buf, "PH-CORP PUK")
        || strcasestr (buf, "READY"))
        return MM_PLUGIN_BASE_PORT_CAP_GSM;

    return 0;
}

static guint32
parse_cgmm (const char *buf)
{
    if (strstr (buf, "GSM900") || strstr (buf, "GSM1800") ||
        strstr (buf, "GSM1900") || strstr (buf, "GSM850"))
        return MM_PLUGIN_BASE_PORT_CAP_GSM;
    return 0;
}

static const char *dq_strings[] = {
    /* Option Icera-based devices */
    "option/faema_",
    "os_logids.h",
    /* Sierra CnS port */
    "NETWORK SERVICE CHANGE",
    NULL
};

static void
port_buffer_full (MMSerialPort *port, GByteArray *buffer, gpointer user_data)
{
    MMPluginBaseSupportsTask *task = MM_PLUGIN_BASE_SUPPORTS_TASK (user_data);
    MMPluginBaseSupportsTaskPrivate *priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (user_data);
    const char **iter;
    size_t iter_len;
    int i;

    /* Check for an immediate disqualification response.  There are some
     * ports (Option Icera-based chipsets have them, as do Qualcomm Gobi
     * devices before their firmware is loaded) that just shouldn't be
     * probed if we get a certain response because we know they can't be
     * used.  Kernel bugs (at least with 2.6.31 and 2.6.32) also trigger port
     * flow control kernel oopses if we read too much data for these ports.
     */

    for (iter = &dq_strings[0]; iter && *iter; iter++) {
        /* Search in the response for the item; the response could have embedded
         * nulls so we can't use memcmp() or strstr() on the whole response.
         */
        iter_len = strlen (*iter);
        for (i = 0; i < buffer->len - iter_len; i++) {
            if (!memcmp (&buffer->data[i], *iter, iter_len)) {
                /* Immediately close the port and complete probing */
                priv->probed_caps = 0;
                mm_serial_port_close (MM_SERIAL_PORT (priv->probe_port));
                probe_complete (task);
                return;
            }
        }
    }
}

static gboolean
emit_probe_result (gpointer user_data)
{
    MMPluginBaseSupportsTask *task = MM_PLUGIN_BASE_SUPPORTS_TASK (user_data);
    MMPluginBaseSupportsTaskPrivate *task_priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task);
    MMPlugin *self = mm_plugin_base_supports_task_get_plugin (task);

    /* Close the serial ports */
    if (task_priv->probe_port) {
        g_object_unref (task_priv->probe_port);
        task_priv->probe_port = NULL;
    }
    if (task_priv->qcdm_port) {
        g_object_unref (task_priv->qcdm_port);
        task_priv->qcdm_port = NULL;
    }

    task_priv->probe_id = 0;
    g_signal_emit (self, signals[PROBE_RESULT], 0, task, task_priv->probed_caps);
    return FALSE;
}

static void
probe_complete (MMPluginBaseSupportsTask *task)
{
    MMPluginBaseSupportsTaskPrivate *priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task);
    MMPort *port;

    port = priv->probe_port ? MM_PORT (priv->probe_port) : MM_PORT (priv->qcdm_port);
    g_assert (port);
    g_hash_table_insert (cached_caps,
                         g_strdup (mm_port_get_device (port)),
                         GUINT_TO_POINTER (priv->probed_caps));

    priv->probe_id = g_idle_add (emit_probe_result, task);
}

static void
qcdm_verinfo_cb (MMQcdmSerialPort *port,
                 GByteArray *response,
                 GError *error,
                 gpointer user_data)
{
    MMPluginBaseSupportsTask *task;
    MMPluginBaseSupportsTaskPrivate *priv;
    QCDMResult *result;
    GError *dm_error = NULL;

    /* Just the initial poke; ignore it */
    if (!user_data)
        return;

    task = MM_PLUGIN_BASE_SUPPORTS_TASK (user_data);
    priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task);

    if (error) {
        /* Probably not a QCDM port */
        goto done;
    }

    /* Parse the response */
    result = qcdm_cmd_version_info_result ((const char *) response->data, response->len, &dm_error);
    if (!result) {
        g_warning ("(%s) failed to parse QCDM version info command result: (%d) %s.",
                   g_udev_device_get_name (priv->port),
                   dm_error ? dm_error->code : -1,
                   dm_error && dm_error->message ? dm_error->message : "(unknown)");
        g_clear_error (&dm_error);
        goto done;
    }

    /* yay, probably a QCDM port */
    qcdm_result_unref (result);
    priv->probed_caps |= MM_PLUGIN_BASE_PORT_CAP_QCDM;

done:
    probe_complete (task);
}

static void
try_qcdm_probe (MMPluginBaseSupportsTask *task)
{
    MMPluginBaseSupportsTaskPrivate *priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task);
    const char *name;
    GError *error = NULL;
    GByteArray *verinfo = NULL, *verinfo2;
    gint len;

    /* Close the AT port */
    if (priv->probe_port) {
        mm_serial_port_close (MM_SERIAL_PORT (priv->probe_port));
        g_object_unref (priv->probe_port);
        priv->probe_port = NULL;
    }

    /* Open the QCDM port */
    name = g_udev_device_get_name (priv->port);
    g_assert (name);
    priv->qcdm_port = mm_qcdm_serial_port_new (name, MM_PORT_TYPE_PRIMARY);
    if (priv->qcdm_port == NULL) {
        g_warning ("(%s) failed to create new QCDM serial port.", name);
        probe_complete (task);
        return;
    }

    if (!mm_serial_port_open (MM_SERIAL_PORT (priv->qcdm_port), &error)) {
        g_warning ("(%s) failed to open new QCDM serial port: (%d) %s.",
                   name,
                   error ? error->code : -1,
                   error && error->message ? error->message : "(unknown)");
        g_clear_error (&error);
        probe_complete (task);
        return;
    }

    /* Build up the probe command */
    verinfo = g_byte_array_sized_new (50);
    len = qcdm_cmd_version_info_new ((char *) verinfo->data, 50, &error);
    if (len <= 0) {
        g_byte_array_free (verinfo, TRUE);
        g_warning ("(%s) failed to create QCDM version info command: (%d) %s.",
                   name,
                   error ? error->code : -1,
                   error && error->message ? error->message : "(unknown)");
        g_clear_error (&error);
        probe_complete (task);
        return;
    }
    verinfo->len = len;

    /* Queuing the command takes ownership over it; copy it for the second try */
    verinfo2 = g_byte_array_sized_new (verinfo->len);
    g_byte_array_append (verinfo2, verinfo->data, verinfo->len);

    /* Send the command twice; the ports often need to be woken up */
    mm_qcdm_serial_port_queue_command (priv->qcdm_port, verinfo, 3, qcdm_verinfo_cb, NULL);
    mm_qcdm_serial_port_queue_command (priv->qcdm_port, verinfo2, 3, qcdm_verinfo_cb, task);
}

static void
parse_response (MMAtSerialPort *port,
                GString *response,
                GError *error,
                gpointer user_data);

static void
real_handle_probe_response (MMPluginBase *self,
                            MMPluginBaseSupportsTask *task,
                            const char *cmd,
                            const char *response,
                            const GError *error)
{
    MMPluginBaseSupportsTaskPrivate *task_priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task);
    MMAtSerialPort *port = task_priv->probe_port;
    gboolean ignore_error = FALSE;

    /* Some modems (Huawei E160g) won't respond to +GCAP with no SIM, but
     * will respond to ATI.
     */
    if (response && strstr (response, "+CME ERROR:"))
        ignore_error = TRUE;

    if (error && !ignore_error) {
        if (g_error_matches (error, MM_SERIAL_ERROR, MM_SERIAL_ERROR_RESPONSE_TIMEOUT)) {
            /* Try GCAP again */
            if (task_priv->probe_state < PROBE_STATE_GCAP_TRY3) {
                task_priv->probe_state++;
                mm_at_serial_port_queue_command (port, "+GCAP", 3, parse_response, task);
            } else {
               /* Otherwise, if all the GCAP tries timed out, ignore the port
                * as it's probably not an AT-capable port.  Try QCDM.
                */
               try_qcdm_probe (task);
            }
            return;
        }

        /* Otherwise proceed to the next command */
    } else if (response) {
        /* Parse the response */

        switch (task_priv->probe_state) {
        case PROBE_STATE_GCAP_TRY1:
        case PROBE_STATE_GCAP_TRY2:
        case PROBE_STATE_GCAP_TRY3:
        case PROBE_STATE_ATI:
            /* Some modems don't respond to AT+GCAP, but often they put a
             * GCAP-style response as a line in the ATI response.
             */
            task_priv->probed_caps = parse_gcap (response);
            break;
        case PROBE_STATE_CPIN:
            /* Some devices (ZTE MF628/ONDA MT503HS for example) reply to
             * anything but AT+CPIN? with ERROR if the device has a PIN set.
             * Since no known CDMA modems support AT+CPIN? we can consider the
             * device a GSM device if it returns a non-error response to AT+CPIN?.
             */
            task_priv->probed_caps = parse_cpin (response);
            break;
        case PROBE_STATE_CGMM:
            /* Some models (BUSlink SCWi275u) stick stupid stuff in the CGMM
             * response but at least it allows us to identify them.
             */
            task_priv->probed_caps = parse_cgmm (response);
            break;
        default:
            break;
        }

        if (task_priv->probed_caps & CAP_GSM_OR_CDMA) {
            probe_complete (task);
            return;
        }
    }

    task_priv->probe_state++;

    /* Try a different command */
    switch (task_priv->probe_state) {
    case PROBE_STATE_GCAP_TRY2:
    case PROBE_STATE_GCAP_TRY3:
        mm_at_serial_port_queue_command (port, "+GCAP", 3, parse_response, task);
        break;
    case PROBE_STATE_ATI:
        /* After the last GCAP attempt, try ATI */
        mm_at_serial_port_queue_command (port, "I", 3, parse_response, task);
        break;
    case PROBE_STATE_CPIN:
        /* After the ATI attempt, try CPIN */
        mm_at_serial_port_queue_command (port, "+CPIN?", 3, parse_response, task);
        break;
    case PROBE_STATE_CGMM:
        /* After the CPIN attempt, try CGMM */
        mm_at_serial_port_queue_command (port, "+CGMM", 3, parse_response, task);
        break;
    default:
        /* Probably not GSM or CDMA */
        probe_complete (task);
        break;
    }
}

static gboolean
handle_probe_response (gpointer user_data)
{
    MMPluginBaseSupportsTask *task = MM_PLUGIN_BASE_SUPPORTS_TASK (user_data);
    MMPluginBaseSupportsTaskPrivate *task_priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task);
    MMPluginBase *self = MM_PLUGIN_BASE (mm_plugin_base_supports_task_get_plugin (task));
    const char *cmd = NULL;

    switch (task_priv->probe_state) {
    case PROBE_STATE_GCAP_TRY1:
    case PROBE_STATE_GCAP_TRY2:
    case PROBE_STATE_GCAP_TRY3:
        cmd = "+GCAP";
        break;
    case PROBE_STATE_ATI:
        cmd = "I";
        break;
    case PROBE_STATE_CPIN:
        cmd = "+CPIN?";
        break;
    case PROBE_STATE_CGMM:
    default:
        cmd = "+CGMM";
        break;
    }

    MM_PLUGIN_BASE_GET_CLASS (self)->handle_probe_response (self,
                                                            task,
                                                            cmd,
                                                            task_priv->probe_resp,
                                                            task_priv->probe_error);
    return FALSE;
}

static void
parse_response (MMAtSerialPort *port,
                GString *response,
                GError *error,
                gpointer user_data)
{
    MMPluginBaseSupportsTask *task = MM_PLUGIN_BASE_SUPPORTS_TASK (user_data);
    MMPluginBaseSupportsTaskPrivate *task_priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task);

    if (task_priv->probe_id)
        g_source_remove (task_priv->probe_id);
    g_free (task_priv->probe_resp);
    task_priv->probe_resp = NULL;
    g_clear_error (&(task_priv->probe_error));

    if (response && response->len)
        task_priv->probe_resp = g_strdup (response->str);
    if (error)
        task_priv->probe_error = g_error_copy (error);

    /* Schedule the response handler in an idle, since we can't emit the
     * PROBE_RESULT signal from the serial port response handler without
     * potentially destroying the serial port in the middle of its response
     * handler, which it understandably doesn't like.
     */
    task_priv->probe_id = g_idle_add (handle_probe_response, task);
}

static void flash_done (MMSerialPort *port, GError *error, gpointer user_data);

static void
custom_init_response (MMAtSerialPort *port,
                      GString *response,
                      GError *error,
                      gpointer user_data)
{
    MMPluginBaseSupportsTask *task = MM_PLUGIN_BASE_SUPPORTS_TASK (user_data);
    MMPluginBaseSupportsTaskPrivate *task_priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task);

    if (error) {
        task_priv->custom_init_tries++;
        if (task_priv->custom_init_tries < task_priv->custom_init_max_tries) {
            /* Try the custom command again */
            flash_done (MM_SERIAL_PORT (port), NULL, user_data);
            return;
        } else if (task_priv->custom_init_fail_if_timeout) {
            /* Fail the probe if the plugin wanted it and the command timed out */
            if (g_error_matches (error, MM_SERIAL_ERROR, MM_SERIAL_ERROR_RESPONSE_TIMEOUT)) {
                probe_complete (task);
                return;
            }
        }
    }

    /* Otherwise proceed to probing */
    mm_at_serial_port_queue_command (port, "+GCAP", 3, parse_response, user_data);
}

static void
flash_done (MMSerialPort *port, GError *error, gpointer user_data)
{
    MMPluginBaseSupportsTask *task = MM_PLUGIN_BASE_SUPPORTS_TASK (user_data);
    MMPluginBaseSupportsTaskPrivate *task_priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task);
    guint32 delay_secs = task_priv->custom_init_delay_seconds;

    /* Send the custom init command if any */
    if (task_priv->custom_init) {
        if (!delay_secs)
            delay_secs = 3;
        mm_at_serial_port_queue_command (MM_AT_SERIAL_PORT (port),
                                         task_priv->custom_init,
                                         delay_secs,
                                         custom_init_response,
                                         user_data);
    } else {
        /* Otherwise start normal probing */
        custom_init_response (MM_AT_SERIAL_PORT (port), NULL, NULL, user_data);
    }
}

static gboolean
try_open (gpointer user_data)
{
    MMPluginBaseSupportsTask *task = MM_PLUGIN_BASE_SUPPORTS_TASK (user_data);
    MMPluginBaseSupportsTaskPrivate *task_priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task);
    GError *error = NULL;

    task_priv->open_id = 0;

    if (!mm_serial_port_open (MM_SERIAL_PORT (task_priv->probe_port), &error)) {
        if (++task_priv->open_tries > 4) {
            /* took too long to open the port; give up */
            g_warning ("(%s): failed to open after 4 tries.",
                       mm_port_get_device (MM_PORT (task_priv->probe_port)));
            probe_complete (task);
        } else if (g_error_matches (error,
                                    MM_SERIAL_ERROR,
                                    MM_SERIAL_ERROR_OPEN_FAILED_NO_DEVICE)) {
            /* this is nozomi being dumb; try again */
            task_priv->open_id = g_timeout_add_seconds (1, try_open, task);
        } else {
            /* some other hard error */
            probe_complete (task);
        }
        g_clear_error (&error);
    } else {
        /* success, start probing */
        GUdevDevice *port;

        port = mm_plugin_base_supports_task_get_port (task);
        g_assert (port);

        task_priv->full_id = g_signal_connect (task_priv->probe_port, "buffer-full",
                                               G_CALLBACK (port_buffer_full), task);

        g_debug ("(%s): probe requested by plugin '%s'",
                 g_udev_device_get_name (port),
                 mm_plugin_get_name (MM_PLUGIN (task_priv->plugin)));
        mm_serial_port_flash (MM_SERIAL_PORT (task_priv->probe_port), 100, TRUE, flash_done, task);
    }

    return FALSE;
}

gboolean
mm_plugin_base_probe_port (MMPluginBase *self,
                           MMPluginBaseSupportsTask *task,
                           GError **error)
{
    MMPluginBaseSupportsTaskPrivate *task_priv = MM_PLUGIN_BASE_SUPPORTS_TASK_GET_PRIVATE (task);
    MMAtSerialPort *serial;
    const char *name;
    GUdevDevice *port;

    g_return_val_if_fail (self != NULL, FALSE);
    g_return_val_if_fail (MM_IS_PLUGIN_BASE (self), FALSE);
    g_return_val_if_fail (task != NULL, FALSE);

    port = mm_plugin_base_supports_task_get_port (task);
    g_assert (port);
    name = g_udev_device_get_name (port);
    g_assert (name);

    serial = mm_at_serial_port_new (name, MM_PORT_TYPE_PRIMARY);
    if (serial == NULL) {
        g_set_error_literal (error, MM_MODEM_ERROR, MM_MODEM_ERROR_GENERAL,
                             "Failed to create new serial port.");
        return FALSE;
    }

    g_object_set (serial,
                  MM_SERIAL_PORT_SEND_DELAY, (guint64) 100000,
                  MM_PORT_CARRIER_DETECT, FALSE,
                  NULL);

    mm_at_serial_port_set_response_parser (serial,
                                           mm_serial_parser_v1_parse,
                                           mm_serial_parser_v1_new (),
                                           mm_serial_parser_v1_destroy);

    /* Open the port */
    task_priv->probe_port = serial;
    task_priv->open_id = g_idle_add (try_open, task);
    return TRUE;
}

gboolean
mm_plugin_base_get_cached_port_capabilities (MMPluginBase *self,
                                             GUdevDevice *port,
                                             guint32 *capabilities)
{
    gpointer tmp = NULL;
    gboolean found;

    found = g_hash_table_lookup_extended (cached_caps, g_udev_device_get_name (port), NULL, tmp);
    *capabilities = GPOINTER_TO_UINT (tmp);
    return found;
}

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

static void
modem_destroyed (gpointer data, GObject *modem)
{
    /* Since we don't track port cached capabilities on a per-modem basis,
     * we just have to live with blowing away the cached capabilities whenever
     * a modem gets removed.  Could do better here by storing a structure
     * in the cached capabilities table that includes { caps, modem device }
     * or something and then only removing cached capabilities for ports
     * that the modem that was just removed owned, but whatever.
     */
    g_hash_table_remove_all (cached_caps);
}

/* From hostap, Copyright (c) 2002-2005, Jouni Malinen <jkmaline@cc.hut.fi> */

static int hex2num (char c)
{
	if (c >= '0' && c <= '9')
		return c - '0';
	if (c >= 'a' && c <= 'f')
		return c - 'a' + 10;
	if (c >= 'A' && c <= 'F')
		return c - 'A' + 10;
	return -1;
}

static int hex2byte (const char *hex)
{
	int a, b;
	a = hex2num(*hex++);
	if (a < 0)
		return -1;
	b = hex2num(*hex++);
	if (b < 0)
		return -1;
	return (a << 4) | b;
}

/* End from hostap */

gboolean
mm_plugin_base_get_device_ids (MMPluginBase *self,
                               const char *subsys,
                               const char *name,
                               guint16 *vendor,
                               guint16 *product)
{
    MMPluginBasePrivate *priv;
    GUdevDevice *device = NULL;
    const char *vid, *pid;
    gboolean success = FALSE;

    g_return_val_if_fail (self != NULL, FALSE);
    g_return_val_if_fail (MM_IS_PLUGIN_BASE (self), FALSE);
    g_return_val_if_fail (subsys != NULL, FALSE);
    g_return_val_if_fail (name != NULL, FALSE);
    if (vendor)
        g_return_val_if_fail (*vendor == 0, FALSE);
    if (product)
        g_return_val_if_fail (*product == 0, FALSE);

    priv = MM_PLUGIN_BASE_GET_PRIVATE (self);

    device = g_udev_client_query_by_subsystem_and_name (priv->client, subsys, name);
    if (!device)
        goto out;

    vid = g_udev_device_get_property (device, "ID_VENDOR_ID");
    if (!vid || (strlen (vid) != 4))
        goto out;

    if (vendor) {
        *vendor = (guint16) (hex2byte (vid + 2) & 0xFF);
        *vendor |= (guint16) ((hex2byte (vid) & 0xFF) << 8);
    }

    pid = g_udev_device_get_property (device, "ID_MODEL_ID");
    if (!pid || (strlen (pid) != 4)) {
        *vendor = 0;
        goto out;
    }

    if (product) {
        *product = (guint16) (hex2byte (pid + 2) & 0xFF);
        *product |= (guint16) ((hex2byte (pid) & 0xFF) << 8);
    }

    success = TRUE;

out:
    if (device)
        g_object_unref (device);
    return success;
}

static char *
get_key (const char *subsys, const char *name)
{
    return g_strdup_printf ("%s%s", subsys, name);
}

static const char *
get_name (MMPlugin *plugin)
{
    return MM_PLUGIN_BASE_GET_PRIVATE (plugin)->name;
}

static char *
get_driver_name (GUdevDevice *device)
{
    GUdevDevice *parent = NULL;
    const char *driver, *subsys;
    char *ret = NULL;

    driver = g_udev_device_get_driver (device);
    if (!driver) {
        parent = g_udev_device_get_parent (device);
        if (parent)
            driver = g_udev_device_get_driver (parent);

        /* Check for bluetooth; it's driver is a bunch of levels up so we
         * just check for the subsystem of the parent being bluetooth.
         */
        if (!driver && parent) {
            subsys = g_udev_device_get_subsystem (parent);
            if (subsys && !strcmp (subsys, "bluetooth"))
                driver = "bluetooth";
        }
    }

    if (driver)
        ret = g_strdup (driver);
    if (parent)
        g_object_unref (parent);

    return ret;
}

static MMPluginSupportsResult
supports_port (MMPlugin *plugin,
               const char *subsys,
               const char *name,
               const char *physdev_path,
               MMModem *existing,
               MMSupportsPortResultFunc callback,
               gpointer callback_data)
{
    MMPluginBase *self = MM_PLUGIN_BASE (plugin);
    MMPluginBasePrivate *priv = MM_PLUGIN_BASE_GET_PRIVATE (self);
    GUdevDevice *port = NULL;
    char *driver = NULL, *key = NULL;
    MMPluginBaseSupportsTask *task;
    MMPluginSupportsResult result = MM_PLUGIN_SUPPORTS_PORT_UNSUPPORTED;

    key = get_key (subsys, name);
    task = g_hash_table_lookup (priv->tasks, key);
    if (task) {
        g_free (key);
        g_return_val_if_fail (task == NULL, MM_PLUGIN_SUPPORTS_PORT_UNSUPPORTED);
    }

    port = g_udev_client_query_by_subsystem_and_name (priv->client, subsys, name);
    if (!port)
        goto out;

    driver = get_driver_name (port);
    if (!driver)
        goto out;

    task = supports_task_new (self, port, physdev_path, driver, callback, callback_data);
    g_assert (task);
    g_hash_table_insert (priv->tasks, g_strdup (key), g_object_ref (task));

    result = MM_PLUGIN_BASE_GET_CLASS (self)->supports_port (self, existing, task);
    if (result != MM_PLUGIN_SUPPORTS_PORT_IN_PROGRESS) {
        /* If the plugin doesn't support the port at all, the supports task is
         * not needed.
         */
        g_hash_table_remove (priv->tasks, key);
    }
    g_object_unref (task);

out:
    if (port)
        g_object_unref (port);
    g_free (key);
    g_free (driver);
    return result;
}

static void
cancel_supports_port (MMPlugin *plugin,
                      const char *subsys,
                      const char *name)
{
    MMPluginBase *self = MM_PLUGIN_BASE (plugin);
    MMPluginBasePrivate *priv = MM_PLUGIN_BASE_GET_PRIVATE (self);
    MMPluginBaseSupportsTask *task;
    char *key;

    key = get_key (subsys, name);
    task = g_hash_table_lookup (priv->tasks, key);
    if (task) {
        /* Let the plugin cancel any ongoing tasks */
        if (MM_PLUGIN_BASE_GET_CLASS (self)->cancel_task)
            MM_PLUGIN_BASE_GET_CLASS (self)->cancel_task (self, task);
        g_hash_table_remove (priv->tasks, key);
    }

    g_free (key);
}

static MMModem *
grab_port (MMPlugin *plugin,
           const char *subsys,
           const char *name,
           MMModem *existing,
           GError **error)
{
    MMPluginBase *self = MM_PLUGIN_BASE (plugin);
    MMPluginBasePrivate *priv = MM_PLUGIN_BASE_GET_PRIVATE (self);
    MMPluginBaseSupportsTask *task;
    MMModem *modem = NULL;
    char *key;

    key = get_key (subsys, name);
    task = g_hash_table_lookup (priv->tasks, key);
    if (!task) {
        g_free (key);
        g_return_val_if_fail (task != NULL, FALSE);
    }

    /* Let the modem grab the port */
    modem = MM_PLUGIN_BASE_GET_CLASS (self)->grab_port (self, existing, task, error);
    if (modem && !existing)
        g_object_weak_ref (G_OBJECT (modem), modem_destroyed, self);

    g_hash_table_remove (priv->tasks, key);
    g_free (key);
    return modem;
}

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

static void
plugin_init (MMPlugin *plugin_class)
{
    /* interface implementation */
    plugin_class->get_name = get_name;
    plugin_class->supports_port = supports_port;
    plugin_class->cancel_supports_port = cancel_supports_port;
    plugin_class->grab_port = grab_port;
}

static void
mm_plugin_base_init (MMPluginBase *self)
{
    MMPluginBasePrivate *priv = MM_PLUGIN_BASE_GET_PRIVATE (self);
    const char *subsys[] = { "tty", "net", NULL };

    if (!cached_caps)
        cached_caps = g_hash_table_new (g_str_hash, g_str_equal);

    priv->client = g_udev_client_new (subsys);

    priv->tasks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
                                         (GDestroyNotify) g_object_unref);
}

static void
set_property (GObject *object, guint prop_id,
              const GValue *value, GParamSpec *pspec)
{
    MMPluginBasePrivate *priv = MM_PLUGIN_BASE_GET_PRIVATE (object);

    switch (prop_id) {
    case PROP_NAME:
        /* Construct only */
        priv->name = g_value_dup_string (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)
{
    MMPluginBasePrivate *priv = MM_PLUGIN_BASE_GET_PRIVATE (object);

    switch (prop_id) {
    case PROP_NAME:
        g_value_set_string (value, priv->name);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
finalize (GObject *object)
{
    MMPluginBasePrivate *priv = MM_PLUGIN_BASE_GET_PRIVATE (object);

    g_free (priv->name);

    g_object_unref (priv->client);

    g_hash_table_destroy (priv->tasks);

    G_OBJECT_CLASS (mm_plugin_base_parent_class)->finalize (object);
}

static void
mm_plugin_base_class_init (MMPluginBaseClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    g_type_class_add_private (object_class, sizeof (MMPluginBasePrivate));

    klass->handle_probe_response = real_handle_probe_response;

    /* Virtual methods */
    object_class->get_property = get_property;
    object_class->set_property = set_property;
    object_class->finalize = finalize;

    g_object_class_install_property
        (object_class, PROP_NAME,
         g_param_spec_string (MM_PLUGIN_BASE_NAME,
                              "Name",
                              "Name",
                              NULL,
                              G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));

    signals[PROBE_RESULT] =
        g_signal_new ("probe-result",
                      G_OBJECT_CLASS_TYPE (object_class),
                      G_SIGNAL_RUN_FIRST,
                      G_STRUCT_OFFSET (MMPluginBaseClass, probe_result),
                      NULL, NULL,
                      mm_marshal_VOID__OBJECT_UINT,
                      G_TYPE_NONE, 2, G_TYPE_OBJECT, G_TYPE_UINT);
}
