/*
 * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd.
 *
 * Author: Jorn Baayen <jorn@openedhand.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 */

#include <config.h>

#include "gupnp-error-private.h"
#include "gupnp-service-proxy.h"
#include "gupnp-service-proxy-private.h"
#include "gupnp-service-proxy-action-private.h"
#include "gvalue-util.h"
#include "xml-util.h"

struct _ActionArgument {
        char *name;
        GValue value;
};
typedef struct _ActionArgument ActionArgument;

void
action_argument_free (ActionArgument* arg)
{
        g_free (arg->name);
        g_value_unset (&arg->value);
        g_free (arg);
}

/* Reads a value into the parameter name and initialised GValue pair
 * from @response */
static void
read_out_parameter (const char *arg_name,
                    GValue     *value,
                    xmlNode    *params)
{
        xmlNode *param;

        /* Try to find a matching parameter in the response*/
        param = xml_util_get_element (params,
                                      arg_name,
                                      NULL);
        if (!param) {
                g_warning ("Could not find variable \"%s\" in response",
                           arg_name);

                return;
        }

        gvalue_util_set_value_from_xml_node (value, param);
}


GUPnPServiceProxyAction *
gupnp_service_proxy_action_new_internal (const char *action) {
        GUPnPServiceProxyAction *ret;

        g_return_val_if_fail (action != NULL, NULL);

        ret = g_atomic_rc_box_new0 (GUPnPServiceProxyAction);
        ret->name = g_strdup (action);
        ret->args = g_ptr_array_new_with_free_func((GDestroyNotify)action_argument_free);
        ret->arg_map = g_hash_table_new (g_str_hash, g_str_equal);

        return ret;
}


/**
 * gupnp_service_proxy_action_ref:
 * @action: an action
 *
 * Increases reference count of `action`
 *
 * Returns: (nullable): @action with an increased reference count
 * Since: 1.2.0
 */
GUPnPServiceProxyAction *
gupnp_service_proxy_action_ref (GUPnPServiceProxyAction *action)
{
        g_return_val_if_fail (action, NULL);

        return g_atomic_rc_box_acquire (action);
}

static void
action_dispose (GUPnPServiceProxyAction *action)
{
        if (action->proxy != NULL) {
                g_object_remove_weak_pointer (G_OBJECT (action->proxy),
                                              (gpointer *) &(action->proxy));
        }
        g_clear_error (&action->error);
        g_clear_object (&action->msg);

        if (action->msg_str != NULL) {
                g_string_free (action->msg_str, TRUE);
                action->msg_str = NULL;
        }
        g_hash_table_destroy (action->arg_map);
        g_ptr_array_unref (action->args);
        g_clear_pointer (&action->response, g_bytes_unref);
        g_clear_pointer (&action->doc, xmlFreeDoc);

        g_free (action->name);
}

/**
 * gupnp_service_proxy_action_unref:
 * @action: an action
 *
 * Decreases reference count of `action`. If reference count drops to 0,
 * the action and its contents will be freed.
 *
 * Since: 1.2.0
 */
void
gupnp_service_proxy_action_unref (GUPnPServiceProxyAction *action)
{
        g_return_if_fail (action);

        g_atomic_rc_box_release_full (action, (GDestroyNotify) action_dispose);
}

G_DEFINE_BOXED_TYPE (GUPnPServiceProxyAction,
                     gupnp_service_proxy_action,
                     gupnp_service_proxy_action_ref,
                     gupnp_service_proxy_action_unref)

/* Writes a parameter name and GValue pair to @msg */
static void
write_in_parameter (ActionArgument *arg,
                    GString    *msg_str)
{
        /* Write parameter pair */
        xml_util_start_element (msg_str, arg->name);
        gvalue_util_value_append_to_xml_string (&arg->value, msg_str);
        xml_util_end_element (msg_str, arg->name);
}

static void
write_footer (GUPnPServiceProxyAction *action)
{
        /* Finish message */
        g_string_append (action->msg_str, "</u:");
        g_string_append (action->msg_str, action->name);
        g_string_append_c (action->msg_str, '>');

        g_string_append (action->msg_str,
                         "</s:Body>"
                         "</s:Envelope>");
}

/**
 * gupnp_service_proxy_action_new:
 * @action: The name of a remote action to call
 * @...: tuples of in parameter name, in parameter type, and in parameter
 * value, terminated with %NULL
 *
 * Prepares action @action with parameters @Varargs to be sent off to
 * a remote service later with gupnp_service_proxy_call_action() or
 * gupnp_service_proxy_call_action_async().
 *
 * After the action call has finished, the results of the call may be
 * retrived from the #GUPnPServiceProxyAction by using
 * gupnp_service_proxy_action_get_result(),
 * gupnp_service_proxy_action_get_result_list() or
 * gupnp_service_proxy_action_get_result_hash()
 *
 * ```c
 * GUPnPServiceProxyAction *action =
 *         gupnp_service_proxy_action_new ("GetVolume",
 *                                         // Parameters
 *                                         "InstanceID", G_TYPE_INT, 0,
 *                                         "Channel", G_TYPE_STRING, "Master",
 *                                         NULL);
 *
 * GError *error = NULL;
 * gupnp_service_proxy_call_action (proxy, action, NULL, &error);
 * if (error != NULL) {
 *         g_warning ("Failed to call GetVolume: %s", error->message);
 *         g_clear_error (&error);
 *
 *         return;
 * }
 *
 * guint16 volume = 0;
 * if (!gupnp_service_proxy_action_get_result (action,
 *                                             &error,
 *                                             "CurrentVolume", G_TYPE_UINT, &volume,
 *                                             NULL)) {
 *         g_message ("Current Volume: %u", volume);
 * }
 *
 * gupnp_service_proxy_action_unref (action);
 * ```
 *
 * Returns: A newly created #GUPnPServiceProxyAction
 */
GUPnPServiceProxyAction *
gupnp_service_proxy_action_new (const char *action,
                                ...)
{
        GUPnPServiceProxyAction *result = NULL;
        va_list var_args;

        g_return_val_if_fail (action != NULL, NULL);

        va_start (var_args, action);
        const char *arg_name = va_arg (var_args, const char *);

        result = gupnp_service_proxy_action_new_internal (action);

        gint position = 0;
        while (arg_name != NULL) {
                ActionArgument *arg = g_new0 (ActionArgument, 1);
                arg->name = g_strdup (arg_name);

                GType type = va_arg (var_args, GType);
                char *error = NULL;

                G_VALUE_COLLECT_INIT (&arg->value, type, var_args, 0, &error);
                if (error == NULL) {
                        g_hash_table_insert (result->arg_map,
                                             arg->name,
                                             GUINT_TO_POINTER (position));
                        g_ptr_array_add (result->args, arg);
                } else {
                        g_warning (
                                "Failed to collect value of type %s for %s: %s",
                                g_type_name (type),
                                arg_name,
                                error);
                        g_free (error);
                }
                arg_name = va_arg (var_args, const char *);
        }
        va_end (var_args);

        return result;
}

/**
 * gupnp_service_proxy_action_new_from_list:
 * @action: An action
 * @in_names: (element-type utf8) (transfer none): #GList of 'in' parameter
 * names (as strings)
 * @in_values: (element-type GValue) (transfer none): #GList of values (as
 * #GValue) that line up with @in_names
 *
 * Prepares action @action with parameters @in_names and @in_values to be
 * sent off to a remote service later with gupnp_service_proxy_call_action() or
 * gupnp_service_proxy_call_action_async(). This is mainly useful for language
 * bindings.
 *
 * After the action call has finished, the results of the call may be
 * retrived from the #GUPnPServiceProxyAction by using
 * gupnp_service_proxy_action_get_result(),
 * gupnp_service_proxy_action_get_result_list() or
 * gupnp_service_proxy_action_get_result_hash()
 * ```c
 * GList *in_args = NULL;
 * in_args = g_list_append (in_args, "InstanceID");
 * in_args = g_list_append (in_args, "Unit");
 * in_args = g_list_append (in_args, "Target");
 *
 * GValue instance = G_VALUE_INIT;
 * g_value_set_int (&instance, 0);
 * GValue unit = G_VALUE_INIT;
 * g_value_set_static_string (&unit, "ABS_TIME");
 * GValue target = G_VALUE_INIT;
 * g_value_set_static_string (&target, "00:00:00.000");
 *
 * GList *in_values = NULL;
 * in_values = g_list_append (in_values, &instance);
 * in_values = g_list_append (in_values, &unit);
 * in_values = g_list_append (in_values, &target);
 *
 * GUPnPServiceProxyAction *action =
 *         gunp_service_proxy_action_new_from_list ("Seek", in_args, in_values);
 *
 * GError *error = NULL;
 * gupnp_service_proxy_call_action_async (proxy, action, NULL, on_action_finished, NULL);
 * gupnp_service_proxy_action_unref (action);
 * ```
 *
 * Returns: A newly created #GUPnPServiceProxyAction
 */
GUPnPServiceProxyAction *
gupnp_service_proxy_action_new_from_list (const char *action_name,
                                          GList      *in_names,
                                          GList      *in_values)
{
        GUPnPServiceProxyAction *action;
        GList *names, *values;
        guint position;

        action = gupnp_service_proxy_action_new_internal (action_name);

        /* Arguments */
        for (names = in_names, values = in_values, position = 0;
             names && values;
             names = names->next, values = values->next, position++) {
                GValue *val = values->data;

                ActionArgument *arg = g_new0 (ActionArgument, 1);
                arg->name = g_strdup (names->data);
                g_value_init (&arg->value, G_VALUE_TYPE (val));
                g_value_copy (val, &arg->value);
                g_hash_table_insert (action->arg_map,
                                     arg->name,
                                     GUINT_TO_POINTER (position));
                g_ptr_array_add (action->args, arg);
        }

        return action;
}

void
gupnp_service_proxy_action_serialize (GUPnPServiceProxyAction *action,
                                      const char *service_type)
{
        if (action->msg_str != NULL) {
                g_string_free (action->msg_str, TRUE);
        }
        action->msg_str = xml_util_new_string ();

        g_string_append (action->msg_str,
                         "<?xml version=\"1.0\"?>"
                         "<s:Envelope xmlns:s="
                         "\"http://schemas.xmlsoap.org/soap/envelope/\" "
                         "s:encodingStyle="
                         "\"http://schemas.xmlsoap.org/soap/encoding/\">"
                         "<s:Body>");
        action->header_pos = action->msg_str->len;

        g_ptr_array_foreach (action->args,
                             (GFunc) write_in_parameter,
                             action->msg_str);

        write_footer (action);

        g_string_insert (action->msg_str, action->header_pos, "<u:");
        action->header_pos += strlen ("<u:");
        g_string_insert (action->msg_str, action->header_pos, action->name);
        action->header_pos += strlen (action->name);
        g_string_insert (action->msg_str, action->header_pos, " xmlns:u=\"");
        action->header_pos += strlen (" xmlns:u=\"");
        g_string_insert (action->msg_str, action->header_pos, service_type);
        action->header_pos += strlen (service_type);
        g_string_insert (action->msg_str, action->header_pos, "\">");
}

/* Checks an action response for errors and returns the parsed
 * xmlDoc object. */
void
gupnp_service_proxy_action_check_response (GUPnPServiceProxyAction *action)
{
        xmlDoc *response;
        int code;

        if (action->doc != NULL) {
                return;
        }

        if (action->error != NULL) {
                return;
        }

        if (action->msg == NULL || action->response == NULL) {
                g_set_error_literal (&action->error,
                                     GUPNP_SERVER_ERROR,
                                     GUPNP_SERVER_ERROR_INVALID_RESPONSE,
                                     "No message, the action was not sent?");

                return;
        }

        SoupStatus status = soup_message_get_status (action->msg);

        if (status != SOUP_STATUS_OK &&
            status != SOUP_STATUS_INTERNAL_SERVER_ERROR) {
                _gupnp_error_set_server_error (&action->error, action->msg);
                return;
        }

        /* Parse response */
        gconstpointer data;
        gsize length;
        data = g_bytes_get_data (action->response, &length);
        response = xmlReadMemory (data,
                                  length,
                                  NULL,
                                  NULL,
                                  XML_PARSE_NONET | XML_PARSE_RECOVER);

        g_clear_pointer (&action->response, g_bytes_unref);

        if (!response) {
                if (status == SOUP_STATUS_OK) {
                        g_set_error_literal (
                                &action->error,
                                GUPNP_SERVER_ERROR,
                                GUPNP_SERVER_ERROR_INVALID_RESPONSE,
                                "Could not parse SOAP response");
                } else {
                        g_set_error_literal (
                                &action->error,
                                GUPNP_SERVER_ERROR,
                                GUPNP_SERVER_ERROR_INTERNAL_SERVER_ERROR,
                                soup_message_get_reason_phrase (action->msg));
                }

                return;
        }

        xmlNodePtr params = NULL;
        /* Get parameter list */
        params = xml_util_get_element ((xmlNode *) response, "Envelope", NULL);
        if (params != NULL)
                params = xml_util_real_node (params->children);

        if (params != NULL) {
                if (strcmp ((const char *) params->name, "Header") == 0)
                        params = xml_util_real_node (params->next);

                if (params != NULL)
                        if (strcmp ((const char *) params->name, "Body") != 0)
                                params = NULL;
        }

        if (params != NULL)
                params = xml_util_real_node (params->children);

        if (params == NULL) {
                g_set_error (&action->error,
                             GUPNP_SERVER_ERROR,
                             GUPNP_SERVER_ERROR_INVALID_RESPONSE,
                             "Invalid Envelope");

                xmlFreeDoc (response);

                return;
        }

        /* Check whether we have a Fault */
        if (status == SOUP_STATUS_INTERNAL_SERVER_ERROR) {
                xmlNode *param;
                char *desc;

                param = xml_util_get_element (params,
                                              "detail",
                                              "UPnPError",
                                              NULL);

                if (!param) {
                        g_set_error (&action->error,
                                     GUPNP_SERVER_ERROR,
                                     GUPNP_SERVER_ERROR_INVALID_RESPONSE,
                                     "Invalid Fault");

                        xmlFreeDoc (response);

                        return;
                }

                /* Code */
                code = xml_util_get_child_element_content_int (param,
                                                               "errorCode");
                if (code == -1) {
                        g_set_error (&action->error,
                                     GUPNP_SERVER_ERROR,
                                     GUPNP_SERVER_ERROR_INVALID_RESPONSE,
                                     "Invalid Fault");

                        xmlFreeDoc (response);

                        return;
                }

                /* Description */
                desc = xml_util_get_child_element_content_glib (
                        param,
                        "errorDescription");
                if (desc == NULL)
                        desc = g_strdup (
                                soup_message_get_reason_phrase (action->msg));

                g_set_error_literal (&action->error,
                                     GUPNP_CONTROL_ERROR,
                                     code,
                                     desc);

                g_free (desc);

                xmlFreeDoc (response);

                return;
        }

        action->params = params;
        action->doc = response;
}

/**
 * gupnp_service_proxy_action_get_result_list:
 * @action: A #GUPnPServiceProxyAction handle
 * @out_names: (element-type utf8) (transfer none): #GList of 'out' parameter
 * names (as strings)
 * @out_types: (element-type GType) (transfer none): #GList of types (as #GType)
 * that line up with @out_names
 * @out_values: (element-type GValue) (transfer full) (out): #GList of values
 * (as #GValue) that line up with @out_names and @out_types.
 * @error:(inout)(optional)(nullable): The location where to store any error, or %NULL
 *
 * A variant of gupnp_service_proxy_action_get_result() that takes lists of
 * out-parameter names, types and place-holders for values.
 *
 * The returned list in @out_values must be freed using `g_list_free` and each element
 * in it using `g_value_unset` and `g_free`.
 * ```c
 * void on_action_finished(GObject *object, GAsyncResult *res, gpointer user_data)
 * {
 *     GUPnPServiceProxyAction *action;
 *     GError *error;
 *
 *     action = gupnp_service_proxy_call_action_finish (GUPNP_SERVICE_PROXY (object),
 *                                                      res,
 *                                                      &error);
 *
 *     if (error != NULL) {
 *              g_print ("Call failed: %s", error->message);
 *              g_clear_error (&error);
 *              return;
 *     }
 *
 *     GList *out_args = NULL;
 *     out_args = g_list_append (out_args, "PlayMode");
 *     out_args = g_list_append (out_args, "RecQualityMode");
 *     GList *out_types = NULL;
 *     out_types = g_list_append (out_types, GSIZE_TO_POINTER (G_TYPE_STRING));
 *     out_types = g_list_append (out_types, GSIZE_TO_POINTER (G_TYPE_STRING));
 *     GList *out_values = NULL;
 *
 *     if (!gupnp_service_proxy_action_get_result_list (action,
 *                                                      out_args,
 *                                                      out_types,
 *                                                      &out_values,
 *                                                      &error)) {
 *              g_print ("Getting results failed: %s", error->message);
 *              g_clear_error (&error);
 *              return;
 *     }
 *
 *     GList *iter = out_values;
 *     while (iter != NULL) {
 *         GValue *value = iter->data;
 *         g_print ("Result: %s\n", g_value_get_string (value));
 *         g_value_unset (value);
 *         g_free (value);
 *         iter = g_list_remove_link (iter, iter);
 *     }
 *     g_list_free (out_values);
 * }
 *```
 *
 * Return value : %TRUE on success.
 *
 * Since: 1.2.0
 *
 **/
gboolean
gupnp_service_proxy_action_get_result_list (GUPnPServiceProxyAction *action,
                                            GList                   *out_names,
                                            GList                   *out_types,
                                            GList                  **out_values,
                                            GError                 **error)
{
        GList *names;
        GList *types;
        GList *out_values_list;

        out_values_list = NULL;

        g_return_val_if_fail (action, FALSE);

        /* Check for saved error from begin_action() */
        if (action->error) {
                g_propagate_error (error, g_error_copy (action->error));

                return FALSE;
        }

        /* Check response for errors and do initial parsing */
        gupnp_service_proxy_action_check_response (action);
        if (action->error != NULL) {
                g_propagate_error (error, g_error_copy (action->error));

                return FALSE;
        }

        /* Read arguments */
        types = out_types;
        for (names = out_names; names; names=names->next) {
                GValue *val;

                val = g_new0 (GValue, 1);
                g_value_init (val, (GType) types->data);

                read_out_parameter (names->data, val, action->params);

                out_values_list = g_list_append (out_values_list, val);

                types = types->next;
        }

        *out_values = out_values_list;

        return TRUE;

}

/**
 * gupnp_service_proxy_action_get_result_hash:
 * @action: A #GUPnPServiceProxyAction handle
 * @out_hash: (element-type utf8 GValue) (inout) (transfer none): A #GHashTable of
 * out parameter name and initialised #GValue pairs
 * @error:(inout)(optional)(nullable): The location where to store any error, or %NULL
 *
 * See gupnp_service_proxy_action_get_result(); this version takes a #GHashTable for
 * runtime generated parameter lists.
 *
 * The @out_hash needs to be pre-initialized with key value pairs denoting the argument
 * to retrieve and an empty #GValue initialized to hold the wanted type with g_value_init().
 *
 *```c
 * void on_action_finished(GObject *object, GAsyncResult *res, gpointer user_data)
 * {
 *     GUPnPServiceProxyAction *action;
 *     GError *error;
 *
 *     action = gupnp_service_proxy_call_action_finish (GUPNP_SERVICE_PROXY (object),
 *                                                      res,
 *                                                      &error);
 *
 *     if (error != NULL) {
 *              g_print ("Call failed: %s", error->message);
 *              g_clear_error (&error);
 *              return;
 *     }
 *
 *     GValue play_mode = G_VALUE_INIT;
 *     g_value_init(&play_mode, G_TYPE_STRING);
 *     GValue rec_quality_mode = G_VALUE_INIT;
 *     g_value_init(&rec_quality_mode, G_TYPE_STRING);
 *
 *     GHashTable *out_args = g_hash_table_new (g_str_hash, g_str_equal);
 *     g_hash_table_insert(out_args, "PlayMode", &play_mode);
 *     g_hash_table_insert(out_args, "RecQualityMode", &rec_quality_mode);
 *
 *     if (!gupnp_service_proxy_action_get_result_hash (action,
 *                                                      out_args,
 *                                                      &error)) {
 *              g_print ("Getting results failed: %s", error->message);
 *              g_clear_error (&error);
 *              return;
 *     }
 *
 *     g_value_unset (&play_mode);
 *     g_value_unset (&rec_quality_mode);
 *
 *     g_hash_table_unref (out_args);
 * }
 * ```
 *
 * Return value: %TRUE on success.
 *
 * Since: 1.2.0
 **/
gboolean
gupnp_service_proxy_action_get_result_hash (GUPnPServiceProxyAction *action,
                                            GHashTable              *hash,
                                            GError                 **error)
{
        g_return_val_if_fail (action, FALSE);

        /* Check for saved error from begin_action() */
        if (action->error) {
                g_propagate_error (error, g_error_copy (action->error));

                return FALSE;
        }

        /* Check response for errors and do initial parsing */
        gupnp_service_proxy_action_check_response (action);
        if (action->error != NULL) {
                g_propagate_error (error, g_error_copy (action->error));

                return FALSE;
        }

        /* Read arguments */
        g_hash_table_foreach (hash,
                              (GHFunc) read_out_parameter,
                              action->params);

        return TRUE;

}

/**
 * gupnp_service_proxy_action_get_result:
 * @action: A #GUPnPServiceProxyAction handle
 * @error: (inout)(optional)(nullable): The location where to store any error, or %NULL
 * @...: tuples of out parameter name, out parameter type, and out parameter
 * value location, terminated with %NULL. The out parameter values should be
 * freed after use
 *
 * Retrieves the result of @action. The out parameters in @Varargs will be
 * filled in, and if an error occurred, @error will be set. In case of
 * an UPnP error the error code will be the same in @error.
 *
 * Return value: %TRUE on success.
 **/
gboolean
gupnp_service_proxy_action_get_result (GUPnPServiceProxyAction *action,
                                       GError                 **error,
                                       ...)
{
        g_return_val_if_fail (action, FALSE);

        va_list var_args;

        va_start (var_args, error);
        GHashTable *out_hash;
        va_list var_args_copy;
        gboolean result;
        GError *local_error;


        out_hash = g_hash_table_new_full (g_str_hash,
                                          g_str_equal,
                                          g_free,
                                          gvalue_free);
        G_VA_COPY (var_args_copy, var_args);
        VAR_ARGS_TO_OUT_HASH_TABLE (var_args, out_hash);
        local_error = NULL;
        result = gupnp_service_proxy_action_get_result_hash (action,
                                                             out_hash,
                                                             &local_error);

        if (local_error == NULL) {
                OUT_HASH_TABLE_TO_VAR_ARGS (out_hash, var_args_copy);
        } else {
                g_propagate_error (error, local_error);
        }
        va_end (var_args_copy);
        g_hash_table_unref (out_hash);
        va_end (var_args);

        return result;
}

/**
 * gupnp_service_proxy_action_set:
 * @action: the action to modify
 * @key: the name of the value to modify
 * @value: the new value of @key
 * @error: (nullable): a return location for an #GError
 *
 * Update the value of @key to @value.
 *
 * @key needs to already exist in @action.
 *
 * Returns: true if successfully modified, false otherwise
 * Since: 1.4.0
 */
gboolean
gupnp_service_proxy_action_set (GUPnPServiceProxyAction *action,
                                const char *key,
                                const GValue *value,
                                GError **error)
{
        g_return_val_if_fail (key != NULL, FALSE);
        g_return_val_if_fail (value != NULL, FALSE);
        g_return_val_if_fail (error != NULL && *error == NULL, FALSE);
        gpointer position;

        if (!g_hash_table_lookup_extended (action->arg_map,
                                           key,
                                           NULL,
                                           &position)) {
                g_propagate_error (error,
                                   g_error_new (GUPNP_SERVER_ERROR,
                                                GUPNP_SERVER_ERROR_OTHER,
                                                "Unknown argument: %s",
                                                key));

                return FALSE;
        }

        ActionArgument *arg =
                g_ptr_array_index (action->args, GPOINTER_TO_UINT (position));

        if (G_VALUE_TYPE (value) != G_VALUE_TYPE (&arg->value)) {
                g_propagate_error (
                        error,
                        g_error_new (
                                GUPNP_SERVER_ERROR,
                                GUPNP_SERVER_ERROR_OTHER,
                                "Type mismatch for %s. Expected %s, got %s",
                                key,
                                G_VALUE_TYPE_NAME (&arg->value),
                                G_VALUE_TYPE_NAME (value)));

                return FALSE;
        }

        g_value_copy (value, &arg->value);

        return TRUE;
}
