/*
 * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd.
 *
 * Author: Jorn Baayen <jorn@openedhand.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

/**
 * SECTION:gupnp-service-info
 * @short_description: Base abstract class for querying service information.
 *
 * The #GUPnPDeviceInfo base abstract class provides methods for querying
 * service information.
 */

#include <libsoup/soup.h>
#include <string.h>

#include "gupnp-service-info.h"
#include "gupnp-service-introspection-private.h"
#include "gupnp-context-private.h"
#include "gupnp-error.h"
#include "gupnp-error-private.h"
#include "gupnp-xml-doc.h"
#include "http-headers.h"
#include "xml-util.h"

G_DEFINE_ABSTRACT_TYPE (GUPnPServiceInfo,
                        gupnp_service_info,
                        G_TYPE_OBJECT);

struct _GUPnPServiceInfoPrivate {
        GUPnPContext *context;

        char *location;
        char *udn;
        char *service_type;

        SoupURI *url_base;

        GUPnPXMLDoc *doc;

        xmlNode *element;

        /* For async downloads */
        GList *pending_gets;
};

enum {
        PROP_0,
        PROP_CONTEXT,
        PROP_LOCATION,
        PROP_UDN,
        PROP_SERVICE_TYPE,
        PROP_URL_BASE,
        PROP_DOCUMENT,
        PROP_ELEMENT
};

typedef struct {
        GUPnPServiceInfo                 *info;

        GUPnPServiceIntrospectionCallback callback;
        gpointer                          user_data;

        GCancellable                     *cancellable;
        gulong                            cancelled_id;

        SoupMessage                      *message;
} GetSCPDURLData;

static void
get_scpd_url_data_free (GetSCPDURLData *data)
{
        if (data->cancellable)
                g_object_unref (data->cancellable);

        g_slice_free (GetSCPDURLData, data);
}

static void
gupnp_service_info_init (GUPnPServiceInfo *info)
{
        info->priv = G_TYPE_INSTANCE_GET_PRIVATE (info,
                                                  GUPNP_TYPE_SERVICE_INFO,
                                                  GUPnPServiceInfoPrivate);
}

static void
gupnp_service_info_set_property (GObject      *object,
                                 guint         property_id,
                                 const GValue *value,
                                 GParamSpec   *pspec)
{
        GUPnPServiceInfo *info;

        info = GUPNP_SERVICE_INFO (object);

        switch (property_id) {
        case PROP_CONTEXT:
                info->priv->context = g_object_ref (g_value_get_object (value));
                break;
        case PROP_LOCATION:
                info->priv->location = g_value_dup_string (value);
                break;
        case PROP_UDN:
                info->priv->udn = g_value_dup_string (value);
                break;
        case PROP_SERVICE_TYPE:
                info->priv->service_type = g_value_dup_string (value);
                break;
        case PROP_URL_BASE:
                info->priv->url_base = g_value_dup_boxed (value);
                break;
        case PROP_DOCUMENT:
                info->priv->doc = g_value_dup_object (value);
                break;
        case PROP_ELEMENT:
                info->priv->element = g_value_get_pointer (value);
                break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
                break;
        }
}

static void
gupnp_service_info_get_property (GObject    *object,
                                 guint       property_id,
                                 GValue     *value,
                                 GParamSpec *pspec)
{
        GUPnPServiceInfo *info;

        info = GUPNP_SERVICE_INFO (object);

        switch (property_id) {
        case PROP_CONTEXT:
                g_value_set_object (value,
                                    info->priv->context);
                break;
        case PROP_LOCATION:
                g_value_set_string (value,
                                    info->priv->location);
                break;
        case PROP_UDN:
                g_value_set_string (value,
                                    info->priv->udn);
                break;
        case PROP_SERVICE_TYPE:
                g_value_set_string (value,
                                    gupnp_service_info_get_service_type (info));
                break;
        case PROP_URL_BASE:
                g_value_set_boxed (value,
                                   info->priv->url_base);
                break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
                break;
        }
}

static void
gupnp_service_info_dispose (GObject *object)
{
        GUPnPServiceInfo *info;

        info = GUPNP_SERVICE_INFO (object);

        /* Cancel any pending SCPD GETs */
        if (info->priv->context) {
                SoupSession *session;

                session = gupnp_context_get_session (info->priv->context);

                while (info->priv->pending_gets) {
                        GetSCPDURLData *data;

                        data = info->priv->pending_gets->data;

                        if (data->cancellable)
                                g_cancellable_disconnect (data->cancellable,
                                                          data->cancelled_id);

                        soup_session_cancel_message (session,
                                                     data->message,
                                                     SOUP_STATUS_CANCELLED);

                        get_scpd_url_data_free (data);

                        info->priv->pending_gets =
                                g_list_delete_link (info->priv->pending_gets,
                                                    info->priv->pending_gets);
                }

                /* Unref context */
                g_object_unref (info->priv->context);
                info->priv->context = NULL;
        }

        if (info->priv->doc) {
                g_object_unref (info->priv->doc);
                info->priv->doc = NULL;
        }

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

static void
gupnp_service_info_finalize (GObject *object)
{
        GUPnPServiceInfo *info;

        info = GUPNP_SERVICE_INFO (object);

        g_free (info->priv->location);
        g_free (info->priv->udn);
        g_free (info->priv->service_type);

        soup_uri_free (info->priv->url_base);

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

static void
gupnp_service_info_class_init (GUPnPServiceInfoClass *klass)
{
        GObjectClass *object_class;

        object_class = G_OBJECT_CLASS (klass);

        object_class->set_property = gupnp_service_info_set_property;
        object_class->get_property = gupnp_service_info_get_property;
        object_class->dispose      = gupnp_service_info_dispose;
        object_class->finalize     = gupnp_service_info_finalize;

        g_type_class_add_private (klass, sizeof (GUPnPServiceInfoPrivate));

        /**
         * GUPnPServiceInfo:context:
         *
         * The #GUPnPContext to use.
         **/
        g_object_class_install_property
                (object_class,
                 PROP_CONTEXT,
                 g_param_spec_object ("context",
                                      "Context",
                                      "The GUPnPContext.",
                                      GUPNP_TYPE_CONTEXT,
                                      G_PARAM_READWRITE |
                                      G_PARAM_CONSTRUCT_ONLY |
                                      G_PARAM_STATIC_NAME |
                                      G_PARAM_STATIC_NICK |
                                      G_PARAM_STATIC_BLURB));

        /**
         * GUPnPServiceInfo:location:
         *
         * The location of the device description file.
         **/
        g_object_class_install_property
                (object_class,
                 PROP_LOCATION,
                 g_param_spec_string ("location",
                                      "Location",
                                      "The location of the device description "
                                      "file",
                                      NULL,
                                      G_PARAM_READWRITE |
                                      G_PARAM_CONSTRUCT_ONLY |
                                      G_PARAM_STATIC_NAME |
                                      G_PARAM_STATIC_NICK |
                                      G_PARAM_STATIC_BLURB));

        /**
         * GUPnPServiceInfo:udn:
         *
         * The UDN of the containing device.
         **/
        g_object_class_install_property
                (object_class,
                 PROP_UDN,
                 g_param_spec_string ("udn",
                                      "UDN",
                                      "The UDN of the containing device",
                                      NULL,
                                      G_PARAM_READWRITE |
                                      G_PARAM_CONSTRUCT_ONLY |
                                      G_PARAM_STATIC_NAME |
                                      G_PARAM_STATIC_NICK |
                                      G_PARAM_STATIC_BLURB));

        /**
         * GUPnPServiceInfo:service-type:
         *
         * The service type.
         **/
        g_object_class_install_property
                (object_class,
                 PROP_SERVICE_TYPE,
                 g_param_spec_string ("service-type",
                                      "Service type",
                                      "The service type",
                                      NULL,
                                      G_PARAM_READWRITE |
                                      G_PARAM_CONSTRUCT_ONLY |
                                      G_PARAM_STATIC_NAME |
                                      G_PARAM_STATIC_NICK |
                                      G_PARAM_STATIC_BLURB));

        /**
         * GUPnPServiceInfo:url-base:
         *
         * The URL base (#SoupURI).
         **/
        g_object_class_install_property
                (object_class,
                 PROP_URL_BASE,
                 g_param_spec_boxed ("url-base",
                                     "URL base",
                                     "The URL base",
                                     SOUP_TYPE_URI,
                                     G_PARAM_READWRITE |
                                     G_PARAM_CONSTRUCT_ONLY |
                                     G_PARAM_STATIC_NAME |
                                     G_PARAM_STATIC_NICK |
                                     G_PARAM_STATIC_BLURB));

        /**
         * GUPnPServiceInfo:document:
         *
         * Private property.
         *
         * Stability: Private
         **/
        g_object_class_install_property
                (object_class,
                 PROP_DOCUMENT,
                 g_param_spec_object ("document",
                                      "Document",
                                      "The XML document related to this "
                                      "service",
                                      GUPNP_TYPE_XML_DOC,
                                      G_PARAM_WRITABLE |
                                      G_PARAM_CONSTRUCT_ONLY |
                                      G_PARAM_STATIC_NAME |
                                      G_PARAM_STATIC_NICK |
                                      G_PARAM_STATIC_BLURB));

        /**
         * GUPnPServiceInfo:element:
         *
         * Private property.
         *
         * Stability: Private
         **/
        g_object_class_install_property
                (object_class,
                 PROP_ELEMENT,
                 g_param_spec_pointer ("element",
                                       "Element",
                                       "The XML element related to this "
                                       "device",
                                       G_PARAM_WRITABLE |
                                       G_PARAM_CONSTRUCT_ONLY |
                                       G_PARAM_STATIC_NAME |
                                       G_PARAM_STATIC_NICK |
                                       G_PARAM_STATIC_BLURB));
}

/**
 * gupnp_service_info_get_context:
 * @info: A #GUPnPServiceInfo
 *
 * Get the #GUPnPContext associated with @info.
 *
 * Returns: (transfer none): A #GUPnPContext.
 **/
GUPnPContext *
gupnp_service_info_get_context (GUPnPServiceInfo *info)
{
        g_return_val_if_fail (GUPNP_IS_SERVICE_INFO (info), NULL);

        return info->priv->context;
}

/**
 * gupnp_service_info_get_location:
 * @info: A #GUPnPServiceInfo
 *
 * Get the location of the device description file.
 *
 * Returns: A constant string.
 **/
const char *
gupnp_service_info_get_location (GUPnPServiceInfo *info)
{
        g_return_val_if_fail (GUPNP_IS_SERVICE_INFO (info), NULL);

        return info->priv->location;
}

/**
 * gupnp_service_info_get_url_base:
 * @info: A #GUPnPServiceInfo
 *
 * Get the URL base of this service.
 *
 * Returns: A constant #SoupURI.
 **/
const SoupURI *
gupnp_service_info_get_url_base (GUPnPServiceInfo *info)
{
        g_return_val_if_fail (GUPNP_IS_SERVICE_INFO (info), NULL);

        return info->priv->url_base;
}

/**
 * gupnp_service_info_get_udn:
 * @info: A #GUPnPServiceInfo
 *
 * Get the Unique Device Name of the containing device.
 *
 * Returns: A constant string.
 **/
const char *
gupnp_service_info_get_udn (GUPnPServiceInfo *info)
{
        g_return_val_if_fail (GUPNP_IS_SERVICE_INFO (info), NULL);

        return info->priv->udn;
}

/**
 * gupnp_service_info_get_service_type:
 * @info: A #GUPnPServiceInfo
 *
 * Get the UPnP service type, or %NULL.
 *
 * Returns: A constant string.
 **/
const char *
gupnp_service_info_get_service_type (GUPnPServiceInfo *info)
{
        g_return_val_if_fail (GUPNP_IS_SERVICE_INFO (info), NULL);

        if (!info->priv->service_type) {
                info->priv->service_type =
                        xml_util_get_child_element_content_glib
                                (info->priv->element, "serviceType");
        }

        return info->priv->service_type;
}

/**
 * gupnp_service_info_get_id:
 * @info: A #GUPnPServiceInfo
 *
 * Get the ID of this service, or %NULL if there is no ID.
 *
 * Return value: A string. This string should be freed with g_free() after use.
 **/
char *
gupnp_service_info_get_id (GUPnPServiceInfo *info)
{
        g_return_val_if_fail (GUPNP_IS_SERVICE_INFO (info), NULL);

        return xml_util_get_child_element_content_glib (info->priv->element,
                                                        "serviceId");
}

/**
 * gupnp_service_info_get_scpd_url:
 * @info: A #GUPnPServiceInfo
 *
 * Get the SCPD URL for this service, or %NULL if there is no SCPD.
 *
 * Return value: A string. This string should be freed with g_free() after use.
 **/
char *
gupnp_service_info_get_scpd_url (GUPnPServiceInfo *info)
{
        g_return_val_if_fail (GUPNP_IS_SERVICE_INFO (info), NULL);

        return xml_util_get_child_element_content_url (info->priv->element,
                                                       "SCPDURL",
                                                       info->priv->url_base);
}

/**
 * gupnp_service_info_get_control_url:
 * @info: A #GUPnPServiceInfo
 *
 * Get the control URL for this service, or %NULL..
 *
 * Return value: A string. This string should be freed with g_free() after use.
 **/
char *
gupnp_service_info_get_control_url (GUPnPServiceInfo *info)
{
        g_return_val_if_fail (GUPNP_IS_SERVICE_INFO (info), NULL);

        return xml_util_get_child_element_content_url (info->priv->element,
                                                       "controlURL",
                                                       info->priv->url_base);
}

/**
 * gupnp_service_info_get_event_subscription_url:
 * @info: A #GUPnPServiceInfo
 *
 * Get the event subscription URL for this service, or %NULL.
 *
 * Return value: A string. This string should be freed with g_free() after use.
 **/
char *
gupnp_service_info_get_event_subscription_url (GUPnPServiceInfo *info)
{
        g_return_val_if_fail (GUPNP_IS_SERVICE_INFO (info), NULL);

        return xml_util_get_child_element_content_url (info->priv->element,
                                                       "eventSubURL",
                                                       info->priv->url_base);
}

/**
 * gupnp_service_info_get_introspection:
 * @info: A #GUPnPServiceInfo
 * @error: return location for a #GError, or %NULL
 *
 * Note that introspection object is created from the information in service
 * description document (SCPD) provided by the service so it can not be created
 * if the service does not provide an SCPD.
 *
 * Warning: You  should use gupnp_service_info_get_introspection_async()
 * instead, this function re-enter the GMainloop before returning or might
 * even block.
 *
 * Return value: (transfer full):  A new #GUPnPServiceIntrospection for this
 * service or %NULL. Unref after use.
 *
 * Deprecated: 0.20.15. Use gupnp_service_info_get_introspection_async() or
 * gupnp_service_info_get_introspection_async_full() instead.
 **/
G_DEPRECATED GUPnPServiceIntrospection *
gupnp_service_info_get_introspection (GUPnPServiceInfo *info,
                                      GError          **error)
{
        GUPnPServiceIntrospection *introspection;
        SoupSession *session;
        SoupMessage *msg;
        int status;
        char *scpd_url;
        xmlDoc *scpd;

        g_return_val_if_fail (GUPNP_IS_SERVICE_INFO (info), NULL);

        introspection = NULL;

        scpd_url = gupnp_service_info_get_scpd_url (info);

        msg = NULL;
        if (scpd_url != NULL) {
                msg = soup_message_new (SOUP_METHOD_GET, scpd_url);

                g_free (scpd_url);
        }

        if (msg == NULL) {
                g_set_error (error,
                             GUPNP_SERVER_ERROR,
                             GUPNP_SERVER_ERROR_INVALID_URL,
                             "No valid SCPD URL defined");

                return NULL;
        }

        /* Send off the message */
        session = gupnp_context_get_session (info->priv->context);

        status = soup_session_send_message (session, msg);
        if (!SOUP_STATUS_IS_SUCCESSFUL (status)) {
                _gupnp_error_set_server_error (error, msg);

                g_object_unref (msg);

                return NULL;
        }

        scpd = xmlRecoverMemory (msg->response_body->data,
                                 msg->response_body->length);

        g_object_unref (msg);

        if (scpd) {
                introspection = gupnp_service_introspection_new (scpd);

                xmlFreeDoc (scpd);
        }

        if (!introspection) {
                g_set_error (error,
                             GUPNP_SERVER_ERROR,
                             GUPNP_SERVER_ERROR_INVALID_RESPONSE,
                             "Could not parse SCPD");
        }

        return introspection;
}

/*
 * SCPD URL downloaded.
 */
static void
got_scpd_url (G_GNUC_UNUSED SoupSession *session,
              SoupMessage               *msg,
              GetSCPDURLData            *data)
{
        GUPnPServiceIntrospection *introspection;
        GError *error;

        introspection = NULL;
        error = NULL;

        if (msg->status_code == SOUP_STATUS_CANCELLED)
                return;

        if (SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
                xmlDoc *scpd;

                scpd = xmlRecoverMemory (msg->response_body->data,
                                         msg->response_body->length);
                if (scpd) {
                        introspection = gupnp_service_introspection_new (scpd);

                        xmlFreeDoc (scpd);
                }

                if (!introspection) {
                        error = g_error_new
                                        (GUPNP_SERVER_ERROR,
                                         GUPNP_SERVER_ERROR_INVALID_RESPONSE,
                                         "Could not parse SCPD");
                }
        } else
                error = _gupnp_error_new_server_error (msg);

        /* prevent the callback from canceling the cancellable
         * (and so freeing data just before we do) */
        if (data->cancellable)
                g_cancellable_disconnect (data->cancellable,
                                          data->cancelled_id);

        data->info->priv->pending_gets =
                g_list_remove (data->info->priv->pending_gets, data);

        data->callback (data->info,
                        introspection,
                        error,
                        data->user_data);

        if (error)
                g_error_free (error);

        get_scpd_url_data_free (data);
}

static void
cancellable_cancelled_cb (GCancellable *cancellable,
                          gpointer user_data)
{
        GUPnPServiceInfo *info;
        GetSCPDURLData *data;
        SoupSession *session;
        GError *error;

        data = user_data;
        info = data->info;

        session = gupnp_context_get_session (info->priv->context);
        soup_session_cancel_message (session,
                                     data->message,
                                     SOUP_STATUS_CANCELLED);

        info->priv->pending_gets =
                g_list_remove (info->priv->pending_gets, data);

        error = g_error_new (G_IO_ERROR,
                             G_IO_ERROR_CANCELLED,
                             "The call was canceled");

        data->callback (data->info,
                        NULL,
                        error,
                        data->user_data);

        get_scpd_url_data_free (data);
}

/**
 * gupnp_service_info_get_introspection_async:
 * @info: A #GUPnPServiceInfo
 * @callback: (scope async) : callback to be called when introspection object is ready.
 * @user_data: user_data to be passed to the callback.
 *
 * Note that introspection object is created from the information in service
 * description document (SCPD) provided by the service so it can not be created
 * if the service does not provide an SCPD.
 **/
void
gupnp_service_info_get_introspection_async
                                (GUPnPServiceInfo                 *info,
                                 GUPnPServiceIntrospectionCallback callback,
                                 gpointer                          user_data)
{
        gupnp_service_info_get_introspection_async_full (info,
                                                         callback,
                                                         NULL,
                                                         user_data);
}

/**
 * gupnp_service_info_get_introspection_async_full:
 * @info: A #GUPnPServiceInfo
 * @callback: (scope async) : callback to be called when introspection object is ready.
 * @cancellable: GCancellable that can be used to cancel the call, or %NULL.
 * @user_data: user_data to be passed to the callback.
 *
 * Note that introspection object is created from the information in service
 * description document (SCPD) provided by the service so it can not be created
 * if the service does not provide an SCPD.
 *
 * If @cancellable is used to cancel the call, @callback will be called with
 * error code %G_IO_ERROR_CANCELLED.
 *
 * Since: 0.20.9
 **/
void
gupnp_service_info_get_introspection_async_full
                                (GUPnPServiceInfo                 *info,
                                 GUPnPServiceIntrospectionCallback callback,
                                 GCancellable                     *cancellable,
                                 gpointer                          user_data)
{
        GetSCPDURLData *data;
        char *scpd_url;
        SoupSession *session;

        g_return_if_fail (GUPNP_IS_SERVICE_INFO (info));
        g_return_if_fail (callback != NULL);

        data = g_slice_new (GetSCPDURLData);

        scpd_url = gupnp_service_info_get_scpd_url (info);

        data->message = NULL;
        if (scpd_url != NULL) {
                data->message = soup_message_new (SOUP_METHOD_GET, scpd_url);

                g_free (scpd_url);
        }

        if (data->message == NULL) {
                GError *error;

                error = g_error_new
                                (GUPNP_SERVER_ERROR,
                                 GUPNP_SERVER_ERROR_INVALID_URL,
                                 "No valid SCPD URL defined");

                callback (info, NULL, error, user_data);

                g_error_free (error);

                g_slice_free (GetSCPDURLData, data);

                return;
        }

        data->info      = info;
        data->callback  = callback;
        data->user_data = user_data;

        /* Send off the message */
        info->priv->pending_gets =
                g_list_prepend (info->priv->pending_gets,
                                data);

        session = gupnp_context_get_session (info->priv->context);

        soup_session_queue_message (session,
                                    data->message,
                                    (SoupSessionCallback) got_scpd_url,
                                    data);

        data->cancellable = cancellable;
        if (data->cancellable) {
                g_object_ref (cancellable);
                data->cancelled_id = g_cancellable_connect
                                (data->cancellable,
                                 G_CALLBACK (cancellable_cancelled_cb),
                                 data,
                                 NULL);
        }
}
