/* -*- 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) 2011 Aleksander Morgado <aleksander@gnu.org>
 */

#include <glib.h>
#include <glib-object.h>

#include <ModemManager.h>

#include "mm-base-modem-at.h"
#include "mm-errors-types.h"

static gboolean
abort_async_if_port_unusable (MMBaseModem *self,
                              MMPortSerialAt *port,
                              GAsyncReadyCallback callback,
                              gpointer user_data)
{
    GError *error = NULL;
    gboolean init_sequence_enabled = FALSE;

    /* If no port given, probably the port disappeared */
    if (!port) {
        g_simple_async_report_error_in_idle (
            G_OBJECT (self),
            callback,
            user_data,
            MM_CORE_ERROR,
            MM_CORE_ERROR_NOT_FOUND,
            "Cannot run sequence: port not given");
        return FALSE;
    }

    /* Ensure we don't try to use a connected port */
    if (mm_port_get_connected (MM_PORT (port))) {
        g_simple_async_report_error_in_idle (
            G_OBJECT (self),
            callback,
            user_data,
            MM_CORE_ERROR,
            MM_CORE_ERROR_CONNECTED,
            "Cannot run sequence: port is connected");
        return FALSE;
    }

    /* Temporarily disable init sequence if we're just sending a
     * command to a just opened port */
    g_object_get (port, MM_PORT_SERIAL_AT_INIT_SEQUENCE_ENABLED, &init_sequence_enabled, NULL);
    g_object_set (port, MM_PORT_SERIAL_AT_INIT_SEQUENCE_ENABLED, FALSE, NULL);

    /* Ensure we have a port open during the sequence */
    if (!mm_port_serial_open (MM_PORT_SERIAL (port), &error)) {
        g_simple_async_report_error_in_idle (
            G_OBJECT (self),
            callback,
            user_data,
            MM_CORE_ERROR,
            MM_CORE_ERROR_CONNECTED,
            "Cannot run sequence: '%s'",
            error->message);
        g_error_free (error);
        return FALSE;
    }

    /* Reset previous init sequence state */
    g_object_set (port, MM_PORT_SERIAL_AT_INIT_SEQUENCE_ENABLED, init_sequence_enabled, NULL);

    return TRUE;
}

static void
modem_cancellable_cancelled (GCancellable *modem_cancellable,
                             GCancellable *user_cancellable)
{
    g_cancellable_cancel (user_cancellable);
}

/*****************************************************************************/
/* AT sequence handling */

typedef struct {
    MMBaseModem *self;
    MMPortSerialAt *port;
    GCancellable *cancellable;
    gulong cancelled_id;
    GCancellable *modem_cancellable;
    GCancellable *user_cancellable;
    const MMBaseModemAtCommand *current;
    const MMBaseModemAtCommand *sequence;
    GSimpleAsyncResult *simple;
    gpointer response_processor_context;
    GDestroyNotify response_processor_context_free;
    GVariant *result;
} AtSequenceContext;

static void
at_sequence_context_free (AtSequenceContext *ctx)
{
    mm_port_serial_close (MM_PORT_SERIAL (ctx->port));
    g_object_unref (ctx->port);
    g_object_unref (ctx->self);

    if (ctx->response_processor_context &&
        ctx->response_processor_context_free)
        ctx->response_processor_context_free (ctx->response_processor_context);

    if (ctx->cancelled_id)
        g_cancellable_disconnect (ctx->modem_cancellable,
                                  ctx->cancelled_id);
    if (ctx->user_cancellable)
        g_object_unref (ctx->user_cancellable);
    g_object_unref (ctx->modem_cancellable);
    g_object_unref (ctx->cancellable);

    if (ctx->result)
        g_variant_unref (ctx->result);
    if (ctx->simple)
        g_object_unref (ctx->simple);
    g_free (ctx);
}

GVariant *
mm_base_modem_at_sequence_full_finish (MMBaseModem *self,
                                       GAsyncResult *res,
                                       gpointer *response_processor_context,
                                       GError **error)
{
    AtSequenceContext *ctx;

    if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (res),
                                               error))
        return NULL;

    ctx = g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (res));
    if (response_processor_context)
        /* transfer none, no need to free the context ourselves, if
         * we gave a response_processor_context_free callback */
        *response_processor_context = ctx->response_processor_context;

    /* transfer-none! (so that we can ignore it) */
    return ctx->result;
}

static void
at_sequence_parse_response (MMPortSerialAt *port,
                            GAsyncResult *res,
                            AtSequenceContext *ctx)
{
    GVariant *result = NULL;
    GError *result_error = NULL;
    gboolean continue_sequence;
    GSimpleAsyncResult *simple;
    const gchar *response;
    GError *error = NULL;

    response = mm_port_serial_at_command_finish (port, res, &error);

    /* Cancelled? */
    if (g_cancellable_is_cancelled (ctx->cancellable)) {
        g_simple_async_result_set_error (ctx->simple, G_IO_ERROR, G_IO_ERROR_CANCELLED,
                                         "AT sequence was cancelled");
        if (error)
            g_error_free (error);
        g_simple_async_result_complete (ctx->simple);
        at_sequence_context_free (ctx);
        return;
    }

    if (!ctx->current->response_processor)
        /* No need to process response, go on to next command */
        continue_sequence = TRUE;
    else {
        const MMBaseModemAtCommand *next = ctx->current + 1;

        /* Response processor will tell us if we need to keep on the sequence */
        continue_sequence = !ctx->current->response_processor (
            ctx->self,
            ctx->response_processor_context,
            ctx->current->command,
            response,
            next->command ? FALSE : TRUE,  /* Last command in sequence? */
            error,
            &result,
            &result_error);
        /* Were we told to abort the sequence? */
        if (result_error) {
            g_assert (result == NULL);
            g_simple_async_result_take_error (ctx->simple, result_error);
            g_simple_async_result_complete (ctx->simple);
            at_sequence_context_free (ctx);
            if (error)
                g_error_free (error);
            return;
        }
    }

    if (error)
        g_error_free (error);

    if (continue_sequence) {
        g_assert (result == NULL);
        ctx->current++;
        if (ctx->current->command) {
            /* Schedule the next command in the probing group */
            mm_port_serial_at_command (
                ctx->port,
                ctx->current->command,
                ctx->current->timeout,
                FALSE,
                ctx->current->allow_cached,
                ctx->cancellable,
                (GAsyncReadyCallback)at_sequence_parse_response,
                ctx);
            return;
        }

        /* On last command, end. */
    }

    /* If we got a response, set it as result */
    if (result)
        /* transfer-full */
        ctx->result = result;

    /* Set the whole context as result, in order to pass the response
     * processor context during finish(). We do remove the simple async result
     * from the context as well, so that we control its last unref. */
    simple = ctx->simple;
    ctx->simple = NULL;
    g_simple_async_result_set_op_res_gpointer (
        simple,
        ctx,
        (GDestroyNotify)at_sequence_context_free);

    /* And complete. The whole context is owned by the result, and it will
     * be freed when completed. */
    g_simple_async_result_complete (simple);
    g_object_unref (simple);
}

void
mm_base_modem_at_sequence_full (MMBaseModem *self,
                                MMPortSerialAt *port,
                                const MMBaseModemAtCommand *sequence,
                                gpointer response_processor_context,
                                GDestroyNotify response_processor_context_free,
                                GCancellable *cancellable,
                                GAsyncReadyCallback callback,
                                gpointer user_data)
{
    AtSequenceContext *ctx;

    /* Ensure that we have an open port */
    if (!abort_async_if_port_unusable (self, port, callback, user_data))
        return;

    /* Setup context */
    ctx = g_new0 (AtSequenceContext, 1);
    ctx->self = g_object_ref (self);
    ctx->port = g_object_ref (port);
    ctx->simple = g_simple_async_result_new (G_OBJECT (self),
                                             callback,
                                             user_data,
                                             mm_base_modem_at_sequence_full);
    ctx->current = ctx->sequence = sequence;
    ctx->response_processor_context = response_processor_context;
    ctx->response_processor_context_free = response_processor_context_free;

    /* Setup cancellables */
    ctx->modem_cancellable = mm_base_modem_get_cancellable (self);
    ctx->user_cancellable = cancellable ? g_object_ref (cancellable) : NULL;
    if (!ctx->user_cancellable)
        /* Just the modem-wide one, use it directly */
        ctx->cancellable = g_object_ref (ctx->modem_cancellable);
    else {
        /* Use the user provided one, which will also get cancelled if the modem
         * wide-one gets cancelled */
        ctx->cancellable = g_object_ref (ctx->user_cancellable);
        ctx->cancelled_id = g_cancellable_connect (ctx->modem_cancellable,
                                                   G_CALLBACK (modem_cancellable_cancelled),
                                                   ctx->user_cancellable,
                                                   NULL);
    }

    /* Go on with the first one in the sequence */
    mm_port_serial_at_command (
        ctx->port,
        ctx->current->command,
        ctx->current->timeout,
        FALSE,
        ctx->current->allow_cached,
        ctx->cancellable,
        (GAsyncReadyCallback)at_sequence_parse_response,
        ctx);
}

GVariant *
mm_base_modem_at_sequence_finish (MMBaseModem *self,
                                  GAsyncResult *res,
                                  gpointer *response_processor_context,
                                  GError **error)
{
    return (mm_base_modem_at_sequence_full_finish (
                self,
                res,
                response_processor_context,
                error));
}

void
mm_base_modem_at_sequence (MMBaseModem *self,
                           const MMBaseModemAtCommand *sequence,
                           gpointer response_processor_context,
                           GDestroyNotify response_processor_context_free,
                           GAsyncReadyCallback callback,
                           gpointer user_data)
{
    MMPortSerialAt *port;
    GError *error = NULL;

    /* No port given, so we'll try to guess which is best */
    port = mm_base_modem_peek_best_at_port (self, &error);
    if (!port) {
        g_assert (error != NULL);
        g_simple_async_report_take_gerror_in_idle (G_OBJECT (self),
                                                   callback,
                                                   user_data,
                                                   error);
        return;
    }

    mm_base_modem_at_sequence_full (
        self,
        port,
        sequence,
        response_processor_context,
        response_processor_context_free,
        NULL,
        callback,
        user_data);
}

/*****************************************************************************/
/* Response processor helpers */

gboolean
mm_base_modem_response_processor_string (MMBaseModem *self,
                                         gpointer none,
                                         const gchar *command,
                                         const gchar *response,
                                         gboolean last_command,
                                         const GError *error,
                                         GVariant **result,
                                         GError **result_error)
{
    if (error) {
        *result_error = g_error_copy (error);
        return FALSE;
    }

    *result = g_variant_new_string (response);
    return TRUE;
}

gboolean
mm_base_modem_response_processor_no_result (MMBaseModem *self,
                                            gpointer none,
                                            const gchar *command,
                                            const gchar *response,
                                            gboolean last_command,
                                            const GError *error,
                                            GVariant **result,
                                            GError **result_error)
{
    if (error) {
        *result_error = g_error_copy (error);
        return FALSE;
    }

    *result = NULL;
    return TRUE;
}

gboolean
mm_base_modem_response_processor_no_result_continue (MMBaseModem *self,
                                                     gpointer none,
                                                     const gchar *command,
                                                     const gchar *response,
                                                     gboolean last_command,
                                                     const GError *error,
                                                     GVariant **result,
                                                     GError **result_error)
{
    if (error)
        *result_error = g_error_copy (error);

    /* Return FALSE so that we keep on with the next steps in the sequence */
    return FALSE;
}

gboolean
mm_base_modem_response_processor_continue_on_error (MMBaseModem *self,
                                                    gpointer none,
                                                    const gchar *command,
                                                    const gchar *response,
                                                    gboolean last_command,
                                                    const GError *error,
                                                    GVariant **result,
                                                    GError **result_error)
{
    if (error)
        return FALSE;

    *result = NULL;
    return TRUE;
}

/*****************************************************************************/
/* Single AT command handling */

typedef struct {
    MMBaseModem *self;
    MMPortSerialAt *port;
    GCancellable *cancellable;
    gulong cancelled_id;
    GCancellable *modem_cancellable;
    GCancellable *user_cancellable;
    GSimpleAsyncResult *result;
} AtCommandContext;

static void
at_command_context_free (AtCommandContext *ctx)
{
    mm_port_serial_close (MM_PORT_SERIAL (ctx->port));

    if (ctx->cancelled_id)
        g_cancellable_disconnect (ctx->modem_cancellable,
                                  ctx->cancelled_id);
    if (ctx->user_cancellable)
        g_object_unref (ctx->user_cancellable);
    g_object_unref (ctx->modem_cancellable);
    g_object_unref (ctx->cancellable);

    g_object_unref (ctx->port);
    g_object_unref (ctx->result);
    g_object_unref (ctx->self);
    g_free (ctx);
}

const gchar *
mm_base_modem_at_command_full_finish (MMBaseModem *self,
                                      GAsyncResult *res,
                                      GError **error)
{
    if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (res), error))
        return NULL;

    return g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (res));
}

static void
at_command_ready (MMPortSerialAt *port,
                  GAsyncResult *res,
                  AtCommandContext *ctx)
{
    const gchar *response;
    GError *error = NULL;

    response = mm_port_serial_at_command_finish (port, res, &error);

    /* Cancelled? */
    if (g_cancellable_is_cancelled (ctx->cancellable)) {
        g_simple_async_result_set_error (ctx->result, G_IO_ERROR, G_IO_ERROR_CANCELLED,
                                         "AT command was cancelled");
        if (error)
            g_error_free (error);
    }
    /* Error coming from the serial port? */
    else if (error)
        g_simple_async_result_take_error (ctx->result, error);
    /* Valid string response */
    else if (response)
        g_simple_async_result_set_op_res_gpointer (ctx->result, (gchar *)response, NULL);
    else
        g_assert_not_reached ();

    /* Never in idle! */
    g_simple_async_result_complete (ctx->result);
    at_command_context_free (ctx);
}

void
mm_base_modem_at_command_full (MMBaseModem *self,
                               MMPortSerialAt *port,
                               const gchar *command,
                               guint timeout,
                               gboolean allow_cached,
                               gboolean is_raw,
                               GCancellable *cancellable,
                               GAsyncReadyCallback callback,
                               gpointer user_data)
{
    AtCommandContext *ctx;

    /* Ensure that we have an open port */
    if (!abort_async_if_port_unusable (self, port, callback, user_data))
        return;

    ctx = g_new0 (AtCommandContext, 1);
    ctx->self = g_object_ref (self);
    ctx->port = g_object_ref (port);
    ctx->result = g_simple_async_result_new (G_OBJECT (self),
                                             callback,
                                             user_data,
                                             mm_base_modem_at_command_full);

    /* Setup cancellables */
    ctx->modem_cancellable = mm_base_modem_get_cancellable (self);
    ctx->user_cancellable = cancellable ? g_object_ref (cancellable) : NULL;
    if (!ctx->user_cancellable)
        /* Just the modem-wide one, use it directly */
        ctx->cancellable = g_object_ref (ctx->modem_cancellable);
    else {
        /* Use the user provided one, which will also get cancelled if the modem
         * wide-one gets cancelled */
        ctx->cancellable = g_object_ref (ctx->user_cancellable);
        ctx->cancelled_id = g_cancellable_connect (ctx->modem_cancellable,
                                                   G_CALLBACK (modem_cancellable_cancelled),
                                                   ctx->user_cancellable,
                                                   NULL);
    }

    /* Go on with the command */
    mm_port_serial_at_command (
        port,
        command,
        timeout,
        is_raw,
        allow_cached,
        ctx->cancellable,
        (GAsyncReadyCallback)at_command_ready,
        ctx);
}

const gchar *
mm_base_modem_at_command_finish (MMBaseModem *self,
                                 GAsyncResult *res,
                                 GError **error)
{
    return mm_base_modem_at_command_full_finish (self, res, error);
}

static void
_at_command (MMBaseModem *self,
             const gchar *command,
             guint timeout,
             gboolean allow_cached,
             gboolean is_raw,
             GAsyncReadyCallback callback,
             gpointer user_data)
{
    MMPortSerialAt *port;
    GError *error = NULL;

    /* No port given, so we'll try to guess which is best */
    port = mm_base_modem_peek_best_at_port (self, &error);
    if (!port) {
        g_assert (error != NULL);
        g_simple_async_report_take_gerror_in_idle (G_OBJECT (self),
                                                   callback,
                                                   user_data,
                                                   error);
        return;
    }

    mm_base_modem_at_command_full (self,
                                   port,
                                   command,
                                   timeout,
                                   allow_cached,
                                   is_raw,
                                   NULL,
                                   callback,
                                   user_data);
}

void
mm_base_modem_at_command (MMBaseModem *self,
                          const gchar *command,
                          guint timeout,
                          gboolean allow_cached,
                          GAsyncReadyCallback callback,
                          gpointer user_data)
{
    _at_command (self, command, timeout, allow_cached, FALSE, callback, user_data);
}

void
mm_base_modem_at_command_raw (MMBaseModem *self,
                              const gchar *command,
                              guint timeout,
                              gboolean allow_cached,
                              GAsyncReadyCallback callback,
                              gpointer user_data)
{
    _at_command (self, command, timeout, allow_cached, TRUE, callback, user_data);
}
