/* -*- 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) 2012 Google, Inc.
 */

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

#include <libqmi-glib.h>

#include <ModemManager.h>
#include <mm-errors-types.h>

#include "mm-port-qmi.h"
#include "mm-log.h"

G_DEFINE_TYPE (MMPortQmi, mm_port_qmi, MM_TYPE_PORT)

typedef struct {
    QmiService service;
    QmiClient *client;
    MMPortQmiFlag flag;
} ServiceInfo;

struct _MMPortQmiPrivate {
    gboolean opening;
    QmiDevice *qmi_device;
    GList *services;
    gboolean llp_is_raw_ip;
};

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

QmiClient *
mm_port_qmi_peek_client (MMPortQmi *self,
                         QmiService service,
                         MMPortQmiFlag flag)
{
    GList *l;

    for (l = self->priv->services; l; l = g_list_next (l)) {
        ServiceInfo *info = l->data;

        if (info->service == service &&
            info->flag == flag)
            return info->client;
    }

    return NULL;
}

QmiClient *
mm_port_qmi_get_client (MMPortQmi *self,
                        QmiService service,
                        MMPortQmiFlag flag)
{
    QmiClient *client;

    client = mm_port_qmi_peek_client (self, service, flag);
    return (client ? g_object_ref (client) : NULL);
}

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

QmiDevice *
mm_port_qmi_peek_device (MMPortQmi *self)
{
    g_return_val_if_fail (MM_IS_PORT_QMI (self), NULL);

    return self->priv->qmi_device;
}

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

typedef struct {
    ServiceInfo *info;
} AllocateClientContext;

static void
allocate_client_context_free (AllocateClientContext *ctx)
{
    if (ctx->info) {
        g_assert (ctx->info->client == NULL);
        g_free (ctx->info);
    }
    g_free (ctx);
}

gboolean
mm_port_qmi_allocate_client_finish (MMPortQmi *self,
                                    GAsyncResult *res,
                                    GError **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

static void
allocate_client_ready (QmiDevice *qmi_device,
                       GAsyncResult *res,
                       GTask *task)
{
    MMPortQmi *self;
    AllocateClientContext *ctx;
    GError *error = NULL;

    self = g_task_get_source_object (task);
    ctx = g_task_get_task_data (task);
    ctx->info->client = qmi_device_allocate_client_finish (qmi_device, res, &error);
    if (!ctx->info->client) {
        g_prefix_error (&error,
                        "Couldn't create client for service '%s': ",
                        qmi_service_get_string (ctx->info->service));
        g_task_return_error (task, error);
    } else {
        /* Move the service info to our internal list */
        self->priv->services = g_list_prepend (self->priv->services, ctx->info);
        ctx->info = NULL;
        g_task_return_boolean (task, TRUE);
    }

    g_object_unref (task);
}

void
mm_port_qmi_allocate_client (MMPortQmi *self,
                             QmiService service,
                             MMPortQmiFlag flag,
                             GCancellable *cancellable,
                             GAsyncReadyCallback callback,
                             gpointer user_data)
{
    AllocateClientContext *ctx;
    GTask *task;

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

    if (!mm_port_qmi_is_open (self)) {
        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_WRONG_STATE,
                                 "Port is closed");
        g_object_unref (task);
        return;
    }

    if (!!mm_port_qmi_peek_client (self, service, flag)) {
        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_EXISTS,
                                 "Client for service '%s' already allocated",
                                 qmi_service_get_string (service));
        g_object_unref (task);
        return;
    }

    ctx = g_new0 (AllocateClientContext, 1);
    ctx->info = g_new0 (ServiceInfo, 1);
    ctx->info->service = service;
    ctx->info->flag = flag;
    g_task_set_task_data (task, ctx, (GDestroyNotify)allocate_client_context_free);

    qmi_device_allocate_client (self->priv->qmi_device,
                                service,
                                QMI_CID_NONE,
                                10,
                                cancellable,
                                (GAsyncReadyCallback)allocate_client_ready,
                                task);
}

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

gboolean
mm_port_qmi_llp_is_raw_ip (MMPortQmi *self)
{
    return self->priv->llp_is_raw_ip;
}

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

typedef enum {
    PORT_OPEN_STEP_FIRST,
    PORT_OPEN_STEP_CHECK_OPENING,
    PORT_OPEN_STEP_CHECK_ALREADY_OPEN,
    PORT_OPEN_STEP_DEVICE_NEW,
    PORT_OPEN_STEP_OPEN_WITHOUT_DATA_FORMAT,
    PORT_OPEN_STEP_GET_KERNEL_DATA_FORMAT,
    PORT_OPEN_STEP_ALLOCATE_WDA_CLIENT,
    PORT_OPEN_STEP_GET_WDA_DATA_FORMAT,
    PORT_OPEN_STEP_CHECK_DATA_FORMAT,
    PORT_OPEN_STEP_SET_KERNEL_DATA_FORMAT,
    PORT_OPEN_STEP_OPEN_WITH_DATA_FORMAT,
    PORT_OPEN_STEP_LAST
} PortOpenStep;

typedef struct {
    QmiDevice *device;
    QmiClient *wda;
    GError *error;
    PortOpenStep step;
    gboolean set_data_format;
    QmiDeviceExpectedDataFormat kernel_data_format;
    QmiWdaLinkLayerProtocol llp;
} PortOpenContext;

static void
port_open_context_free (PortOpenContext *ctx)
{
    if (ctx->wda) {
        g_assert (ctx->device);
        qmi_device_release_client (ctx->device,
                                   ctx->wda,
                                   QMI_DEVICE_RELEASE_CLIENT_FLAGS_RELEASE_CID,
                                   3, NULL, NULL, NULL);
        g_object_unref (ctx->wda);
    }
    if (ctx->device)
        g_object_unref (ctx->device);
    g_slice_free (PortOpenContext, ctx);
}

gboolean
mm_port_qmi_open_finish (MMPortQmi *self,
                         GAsyncResult *res,
                         GError **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

static void port_open_step (GTask *task);

static void
qmi_device_open_second_ready (QmiDevice *qmi_device,
                              GAsyncResult *res,
                              GTask *task)
{
    PortOpenContext *ctx;

    ctx = g_task_get_task_data (task);

    qmi_device_open_finish (qmi_device, res, &ctx->error);

    /* In both error and success, we go to last step */
    ctx->step = PORT_OPEN_STEP_LAST;
    port_open_step (task);
}

static void
get_data_format_ready (QmiClientWda *client,
                       GAsyncResult *res,
                       GTask *task)
{
    PortOpenContext *ctx;
    QmiMessageWdaGetDataFormatOutput *output;

    ctx = g_task_get_task_data (task);
    output = qmi_client_wda_get_data_format_finish (client, res, NULL);
    if (!output ||
        !qmi_message_wda_get_data_format_output_get_result (output, NULL) ||
        !qmi_message_wda_get_data_format_output_get_link_layer_protocol (output, &ctx->llp, NULL))
        /* If loading WDA data format fails, fallback to 802.3 requested via CTL */
        ctx->step = PORT_OPEN_STEP_OPEN_WITH_DATA_FORMAT;
    else
        /* Go on to next step */
        ctx->step++;

    if (output)
        qmi_message_wda_get_data_format_output_unref (output);

    port_open_step (task);
}

static void
allocate_client_wda_ready (QmiDevice *device,
                           GAsyncResult *res,
                           GTask *task)
{
    PortOpenContext *ctx;

    ctx = g_task_get_task_data (task);
    ctx->wda = qmi_device_allocate_client_finish (device, res, NULL);
    if (!ctx->wda) {
        /* If no WDA supported, then we just fallback to reopening explicitly
         * requesting 802.3 in the CTL service. */
        ctx->step = PORT_OPEN_STEP_OPEN_WITH_DATA_FORMAT;
        port_open_step (task);
        return;
    }

    /* Go on to next step */
    ctx->step++;
    port_open_step (task);
}

static void
qmi_device_open_first_ready (QmiDevice *qmi_device,
                             GAsyncResult *res,
                             GTask *task)
{
    PortOpenContext *ctx;

    ctx = g_task_get_task_data (task);
    if (!qmi_device_open_finish (qmi_device, res, &ctx->error))
        /* Error opening the device */
        ctx->step = PORT_OPEN_STEP_LAST;
    else if (!ctx->set_data_format)
        /* If not setting data format, we're done */
        ctx->step = PORT_OPEN_STEP_LAST;
    else
        /* Go on to next step */
        ctx->step++;
    port_open_step (task);
}

static void
qmi_device_new_ready (GObject *unused,
                      GAsyncResult *res,
                      GTask *task)
{
    PortOpenContext *ctx;

    ctx = g_task_get_task_data (task);
    /* Store the device in the context until the operation is fully done,
     * so that we return IN_PROGRESS errors until we finish this async
     * operation. */
    ctx->device = qmi_device_new_finish (res, &ctx->error);
    if (!ctx->device)
        /* Error creating the device */
        ctx->step = PORT_OPEN_STEP_LAST;
    else
        /* Go on to next step */
        ctx->step++;
    port_open_step (task);
}

static void
port_open_step (GTask *task)
{
    MMPortQmi *self;
    PortOpenContext *ctx;

    self = g_task_get_source_object (task);
    ctx = g_task_get_task_data (task);
    switch (ctx->step) {
    case PORT_OPEN_STEP_FIRST:
        mm_dbg ("Opening QMI device...");
        ctx->step++;
        /* Fall down to next step */

    case PORT_OPEN_STEP_CHECK_OPENING:
        mm_dbg ("Checking if QMI device already opening...");
        if (self->priv->opening) {
            g_task_return_new_error (task,
                                     MM_CORE_ERROR,
                                     MM_CORE_ERROR_IN_PROGRESS,
                                     "QMI device already being opened");
            g_object_unref (task);
            return;
        }
        ctx->step++;
        /* Fall down to next step */

    case PORT_OPEN_STEP_CHECK_ALREADY_OPEN:
        mm_dbg ("Checking if QMI device already open...");
        if (self->priv->qmi_device) {
            g_task_return_boolean (task, TRUE);
            g_object_unref (task);
            return;
        }
        ctx->step++;
        /* Fall down to next step */

    case PORT_OPEN_STEP_DEVICE_NEW: {
        GFile *file;
        gchar *fullpath;

        fullpath = g_strdup_printf ("/dev/%s", mm_port_get_device (MM_PORT (self)));
        file = g_file_new_for_path (fullpath);

        /* We flag in this point that we're opening. From now on, if we stop
         * for whatever reason, we should clear this flag. We do this by ensuring
         * that all callbacks go through the LAST step for completing. */
        self->priv->opening = TRUE;

        mm_dbg ("Creating QMI device...");
        qmi_device_new (file,
                        g_task_get_cancellable (task),
                        (GAsyncReadyCallback) qmi_device_new_ready,
                        task);

        g_free (fullpath);
        g_object_unref (file);
        return;
    }

    case PORT_OPEN_STEP_OPEN_WITHOUT_DATA_FORMAT:
        /* Now open the QMI device without any data format CTL flag */
        mm_dbg ("Opening device without data format update...");
        qmi_device_open (ctx->device,
                         (QMI_DEVICE_OPEN_FLAGS_VERSION_INFO |
                          QMI_DEVICE_OPEN_FLAGS_PROXY),
                         20,
                         g_task_get_cancellable (task),
                         (GAsyncReadyCallback) qmi_device_open_first_ready,
                         task);
        return;

    case PORT_OPEN_STEP_GET_KERNEL_DATA_FORMAT:
        mm_dbg ("Querying kernel data format...");
        /* Try to gather expected data format from the sysfs file */
        ctx->kernel_data_format = qmi_device_get_expected_data_format (ctx->device, NULL);
        /* If data format cannot be retrieved, we fallback to 802.3 via CTL */
        if (ctx->kernel_data_format == QMI_DEVICE_EXPECTED_DATA_FORMAT_UNKNOWN) {
            ctx->step = PORT_OPEN_STEP_OPEN_WITH_DATA_FORMAT;
            port_open_step (task);
            return;
        }
        ctx->step++;
        /* Fall down to next step */

    case PORT_OPEN_STEP_ALLOCATE_WDA_CLIENT:
        /* Allocate WDA client */
        mm_dbg ("Allocating WDA client...");
        qmi_device_allocate_client (ctx->device,
                                    QMI_SERVICE_WDA,
                                    QMI_CID_NONE,
                                    10,
                                    g_task_get_cancellable (task),
                                    (GAsyncReadyCallback) allocate_client_wda_ready,
                                    task);
        return;

    case PORT_OPEN_STEP_GET_WDA_DATA_FORMAT:
        /* If we have WDA client, query current data format */
        g_assert (ctx->wda);
        mm_dbg ("Querying device data format...");
        qmi_client_wda_get_data_format (QMI_CLIENT_WDA (ctx->wda),
                                        NULL,
                                        10,
                                        g_task_get_cancellable (task),
                                        (GAsyncReadyCallback) get_data_format_ready,
                                        task);
        return;

    case PORT_OPEN_STEP_CHECK_DATA_FORMAT:
        /* We now have the WDA data format and the kernel data format, if they're
         * equal, we're done */
        mm_dbg ("Checking data format: kernel %s, device %s",
                qmi_device_expected_data_format_get_string (ctx->kernel_data_format),
                qmi_wda_link_layer_protocol_get_string (ctx->llp));

        if (ctx->kernel_data_format == QMI_DEVICE_EXPECTED_DATA_FORMAT_802_3 &&
            ctx->llp == QMI_WDA_LINK_LAYER_PROTOCOL_802_3) {
            self->priv->llp_is_raw_ip = FALSE;
            ctx->step = PORT_OPEN_STEP_LAST;
            port_open_step (task);
            return;
        }

        if (ctx->kernel_data_format == QMI_DEVICE_EXPECTED_DATA_FORMAT_RAW_IP &&
            ctx->llp == QMI_WDA_LINK_LAYER_PROTOCOL_RAW_IP) {
            self->priv->llp_is_raw_ip = TRUE;
            ctx->step = PORT_OPEN_STEP_LAST;
            port_open_step (task);
            return;
        }

        ctx->step++;
        /* Fall down to next step */

    case PORT_OPEN_STEP_SET_KERNEL_DATA_FORMAT:
        /* Update the data format to be expected by the kernel */
        mm_dbg ("Updating kernel data format: %s", qmi_wda_link_layer_protocol_get_string (ctx->llp));
        if (ctx->llp == QMI_WDA_LINK_LAYER_PROTOCOL_802_3) {
            ctx->kernel_data_format = QMI_DEVICE_EXPECTED_DATA_FORMAT_802_3;
            self->priv->llp_is_raw_ip = FALSE;
        } else if (ctx->llp == QMI_WDA_LINK_LAYER_PROTOCOL_RAW_IP) {
            ctx->kernel_data_format = QMI_DEVICE_EXPECTED_DATA_FORMAT_RAW_IP;
            self->priv->llp_is_raw_ip = TRUE;
        } else
            g_assert_not_reached ();

        /* Regardless of the output, we're done after this action */
        qmi_device_set_expected_data_format (ctx->device,
                                             ctx->kernel_data_format,
                                             &ctx->error);
        ctx->step = PORT_OPEN_STEP_LAST;
        port_open_step (task);
        return;

    case PORT_OPEN_STEP_OPEN_WITH_DATA_FORMAT:
        /* Need to reopen setting 802.3 using CTL */
        mm_dbg ("Closing device to reopen it right away...");
        if (!qmi_device_close (ctx->device, &ctx->error)) {
            mm_warn ("Couldn't close QMI device to reopen it");
            ctx->step = PORT_OPEN_STEP_LAST;
            port_open_step (task);
            return;
        }

        mm_dbg ("Reopening device with data format...");
        qmi_device_open (ctx->device,
                         (QMI_DEVICE_OPEN_FLAGS_VERSION_INFO |
                          QMI_DEVICE_OPEN_FLAGS_PROXY        |
                          QMI_DEVICE_OPEN_FLAGS_NET_802_3    |
                          QMI_DEVICE_OPEN_FLAGS_NET_NO_QOS_HEADER),
                         10,
                         g_task_get_cancellable (task),
                         (GAsyncReadyCallback) qmi_device_open_second_ready,
                         task);
        return;

    case PORT_OPEN_STEP_LAST:
        mm_dbg ("QMI port open operation finished");

        /* Reset opening flag */
        self->priv->opening = FALSE;

        if (ctx->error) {
            /* Propagate error */
            if (ctx->device)
                qmi_device_close (ctx->device, NULL);
            g_task_return_error (task, ctx->error);
            ctx->error = NULL;
        } else {
            /* Store device in private info */
            g_assert (ctx->device);
            g_assert (!self->priv->qmi_device);
            self->priv->qmi_device = g_object_ref (ctx->device);
            g_task_return_boolean (task, TRUE);
        }
        g_object_unref (task);
        return;
    }
}

void
mm_port_qmi_open (MMPortQmi *self,
                  gboolean set_data_format,
                  GCancellable *cancellable,
                  GAsyncReadyCallback callback,
                  gpointer user_data)
{
    PortOpenContext *ctx;
    GTask *task;

    g_return_if_fail (MM_IS_PORT_QMI (self));

    ctx = g_slice_new0 (PortOpenContext);
    ctx->step = PORT_OPEN_STEP_FIRST;
    ctx->set_data_format = set_data_format;
    ctx->kernel_data_format = QMI_DEVICE_EXPECTED_DATA_FORMAT_UNKNOWN;
    ctx->llp = QMI_WDA_LINK_LAYER_PROTOCOL_UNKNOWN;

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

    port_open_step (task);
}

gboolean
mm_port_qmi_is_open (MMPortQmi *self)
{
    g_return_val_if_fail (MM_IS_PORT_QMI (self), FALSE);

    return !!self->priv->qmi_device;
}

void
mm_port_qmi_close (MMPortQmi *self)
{
    GList *l;
    GError *error = NULL;

    g_return_if_fail (MM_IS_PORT_QMI (self));

    if (!self->priv->qmi_device)
        return;

    /* Release all allocated clients */
    for (l = self->priv->services; l; l = g_list_next (l)) {
        ServiceInfo *info = l->data;

        mm_dbg ("Releasing client for service '%s'...", qmi_service_get_string (info->service));
        qmi_device_release_client (self->priv->qmi_device,
                                   info->client,
                                   QMI_DEVICE_RELEASE_CLIENT_FLAGS_RELEASE_CID,
                                   3, NULL, NULL, NULL);
        g_clear_object (&info->client);
    }
    g_list_free_full (self->priv->services, g_free);
    self->priv->services = NULL;

    /* Close and release the device */
    if (!qmi_device_close (self->priv->qmi_device, &error)) {
        mm_warn ("Couldn't properly close QMI device: %s",
                 error->message);
        g_error_free (error);
    }

    g_clear_object (&self->priv->qmi_device);
}

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

MMPortQmi *
mm_port_qmi_new (const gchar *name)
{
    return MM_PORT_QMI (g_object_new (MM_TYPE_PORT_QMI,
                                      MM_PORT_DEVICE, name,
                                      MM_PORT_SUBSYS, MM_PORT_SUBSYS_USB,
                                      MM_PORT_TYPE, MM_PORT_TYPE_QMI,
                                      NULL));
}

static void
mm_port_qmi_init (MMPortQmi *self)
{
    self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_PORT_QMI, MMPortQmiPrivate);
}

static void
dispose (GObject *object)
{
    MMPortQmi *self = MM_PORT_QMI (object);
    GList *l;

    /* Deallocate all clients */
    for (l = self->priv->services; l; l = g_list_next (l)) {
        ServiceInfo *info = l->data;

        if (info->client)
            g_object_unref (info->client);
    }
    g_list_free_full (self->priv->services, g_free);
    self->priv->services = NULL;

    /* Clear device object */
    g_clear_object (&self->priv->qmi_device);

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

static void
mm_port_qmi_class_init (MMPortQmiClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    g_type_class_add_private (object_class, sizeof (MMPortQmiPrivate));

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