/*
 * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

/**
 * SECTION: e-gdata-session
 * @include: libedataserver/libedataserver.h
 * @short_description: A GData (Google Data) session
 *
 * The #EGDataSession is a class to work with Google's GData API.
 **/

#include "evolution-data-server-config.h"

#include <glib.h>
#include <glib/gi18n-lib.h>

#include "camel/camel.h"

#include "e-flag.h"
#include "e-json-utils.h"
#include "e-gdata-query.h"
#include "e-soup-session.h"

#include "e-gdata-session.h"

#define LOCK(x) g_rec_mutex_lock (&(x->priv->property_lock))
#define UNLOCK(x) g_rec_mutex_unlock (&(x->priv->property_lock))

#define E_GDATA_RETRY_IO_ERROR_SECONDS 3

#define TASKS_API_URL		"https://tasks.googleapis.com"
#define TASKLISTS_TOP_URL	TASKS_API_URL "/tasks/v1/users/@me/lists"
#define TASKS_TOP_URL		TASKS_API_URL "/tasks/v1/lists"

struct _EGDataSessionPrivate {
	GRecMutex property_lock;
	gint64 backoff_for_usec;
};

G_DEFINE_TYPE_WITH_PRIVATE (EGDataSession, e_gdata_session, E_TYPE_SOUP_SESSION)

static void
e_gdata_session_finalize (GObject *object)
{
	EGDataSession *self = E_GDATA_SESSION (object);

	g_rec_mutex_clear (&self->priv->property_lock);

	/* Chain up to parent's method. */
	G_OBJECT_CLASS (e_gdata_session_parent_class)->finalize (object);
}

static void
e_gdata_session_class_init (EGDataSessionClass *klass)
{
	GObjectClass *object_class;

	object_class = G_OBJECT_CLASS (klass);
	object_class->finalize = e_gdata_session_finalize;
}

static void
e_gdata_session_init (EGDataSession *self)
{
	self->priv = e_gdata_session_get_instance_private (self);

	g_rec_mutex_init (&self->priv->property_lock);

	e_soup_session_setup_logging (E_SOUP_SESSION (self), g_getenv ("GDATA_DEBUG"));
}

/**
 * e_gdata_session_new:
 * @source: an #ESource
 *
 * Creates a new #EGDataSession associated with the given @source.
 *
 * Returns: (transfer full): a new #EGDataSession; free it with g_object_unref(),
 *    when no longer needed.
 *
 * Since: 3.46
 **/
EGDataSession *
e_gdata_session_new (ESource *source)
{
	g_return_val_if_fail (E_IS_SOURCE (source), NULL);

	return g_object_new (E_TYPE_GDATA_SESSION,
		"source", source,
		NULL);
}

static void
e_gdata_session_request_cancelled_cb (GCancellable *cancellable,
				      gpointer user_data)
{
	EFlag *flag = user_data;

	g_return_if_fail (flag != NULL);

	e_flag_set (flag);
}

/* (transfer full) (nullable): Free the *out_node with json_node_unref(), if not NULL;
   It can return 'success', even when the *out_node is NULL. */
static gboolean
e_gdata_session_json_node_from_message (SoupMessage *message,
					GByteArray *bytes,
					JsonNode **out_node,
					GCancellable *cancellable,
					GError **error)
{
	const gchar *content_type;
	gboolean success = TRUE;
	GError *local_error = NULL;

	g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
	g_return_val_if_fail (out_node != NULL, FALSE);

	*out_node = NULL;

	content_type = soup_message_get_response_headers (message) ?
		soup_message_headers_get_content_type (soup_message_get_response_headers (message), NULL) : NULL;

	if (content_type && g_ascii_strcasecmp (content_type, "application/json") == 0) {
		JsonParser *json_parser;

		json_parser = json_parser_new_immutable ();

		if (bytes) {
			success = json_parser_load_from_data (json_parser, (const gchar *) bytes->data, bytes->len, error);
		} else {
			/* This should not happen, it's for safety check only, thus the string is not localized */
			success = FALSE;
			g_set_error_literal (&local_error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "No JSON data found");
		}

		if (success)
			*out_node = json_parser_steal_root (json_parser);

		g_object_unref (json_parser);
	}

	if (!success && *out_node)
		g_clear_pointer (out_node, json_node_unref);

	if (local_error)
		g_propagate_error (error, local_error);

	return success;
}

static gboolean
e_gdata_session_send_request_sync (EGDataSession *self,
				   SoupMessage *message,
				   JsonNode **out_node,
				   GByteArray **out_bytes,
				   GCancellable *cancellable,
				   GError **error)
{
	gint need_retry_seconds = 5;
	gboolean did_io_error_retry = FALSE;
	gboolean success = FALSE, need_retry = TRUE;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
	g_return_val_if_fail (out_node == NULL || out_bytes == NULL, FALSE);

	while (need_retry && !g_cancellable_is_cancelled (cancellable)) {
		gboolean is_io_error = FALSE;
		GByteArray *bytes;
		GError *local_error = NULL;

		need_retry = FALSE;

		LOCK (self);

		if (self->priv->backoff_for_usec) {
			EFlag *flag;
			gint64 wait_ms;
			gulong handler_id = 0;

			wait_ms = self->priv->backoff_for_usec / G_TIME_SPAN_MILLISECOND;

			UNLOCK (self);

			flag = e_flag_new ();

			if (cancellable) {
				handler_id = g_cancellable_connect (cancellable, G_CALLBACK (e_gdata_session_request_cancelled_cb),
					flag, NULL);
			}

			while (wait_ms > 0 && !g_cancellable_is_cancelled (cancellable)) {
				gint64 now = g_get_monotonic_time ();
				gint left_minutes, left_seconds;

				left_minutes = wait_ms / 60000;
				left_seconds = (wait_ms / 1000) % 60;

				if (left_minutes > 0) {
					camel_operation_push_message (cancellable,
						g_dngettext (GETTEXT_PACKAGE,
							"Google server is busy, waiting to retry (%d:%02d minute)",
							"Google server is busy, waiting to retry (%d:%02d minutes)", left_minutes),
						left_minutes, left_seconds);
				} else {
					camel_operation_push_message (cancellable,
						g_dngettext (GETTEXT_PACKAGE,
							"Google server is busy, waiting to retry (%d second)",
							"Google server is busy, waiting to retry (%d seconds)", left_seconds),
						left_seconds);
				}

				e_flag_wait_until (flag, now + (G_TIME_SPAN_MILLISECOND * (wait_ms > 1000 ? 1000 : wait_ms)));
				e_flag_clear (flag);

				now = g_get_monotonic_time () - now;
				now = now / G_TIME_SPAN_MILLISECOND;

				if (now >= wait_ms)
					wait_ms = 0;
				wait_ms -= now;

				camel_operation_pop_message (cancellable);
			}

			if (handler_id)
				g_cancellable_disconnect (cancellable, handler_id);

			e_flag_free (flag);

			LOCK (self);

			self->priv->backoff_for_usec = 0;
		}

		if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
			UNLOCK (self);
			return FALSE;
		}

		bytes = e_soup_session_send_message_simple_sync (E_SOUP_SESSION (self), message, cancellable, &local_error);

		success = bytes != NULL;
		is_io_error = !did_io_error_retry && g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_PARTIAL_INPUT);

		if (local_error) {
			g_propagate_error (error, local_error);
			local_error = NULL;
		}

		if (is_io_error ||
		    /* Throttling */
		    soup_message_get_status (message) == 429 ||
		    /* https://docs.microsoft.com/en-us/graph/best-practices-concept#handling-expected-errors */
		    soup_message_get_status (message) == SOUP_STATUS_SERVICE_UNAVAILABLE) {
			did_io_error_retry = did_io_error_retry || is_io_error;
			need_retry = TRUE;
		}

		if (need_retry) {
			const gchar *retry_after_str;
			gint64 retry_after;

			retry_after_str = soup_message_get_response_headers (message) ?
				soup_message_headers_get_one (soup_message_get_response_headers (message), "Retry-After") : NULL;

			if (retry_after_str && *retry_after_str)
				retry_after = g_ascii_strtoll (retry_after_str, NULL, 10);
			else if (is_io_error)
				retry_after = E_GDATA_RETRY_IO_ERROR_SECONDS;
			else
				retry_after = 0;

			if (retry_after > 0)
				need_retry_seconds = retry_after;
			else if (need_retry_seconds < 120)
				need_retry_seconds *= 2;

			if (self->priv->backoff_for_usec < need_retry_seconds * G_USEC_PER_SEC)
				self->priv->backoff_for_usec = need_retry_seconds * G_USEC_PER_SEC;

			if (soup_message_get_status (message) == SOUP_STATUS_SERVICE_UNAVAILABLE)
				soup_session_abort (SOUP_SESSION (self));

			success = FALSE;
		} else if (success && out_bytes && SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message))) {
			*out_bytes = bytes;
			bytes = NULL;
		} else if (success && out_node) {
			JsonNode *node = NULL;

			success = e_gdata_session_json_node_from_message (message, bytes, &node, cancellable, error);

			if (success) {
				*out_node = node;
				node = NULL;
			} else if (error && !*error && !SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message))) {
				g_set_error_literal (error, E_SOUP_SESSION_ERROR, soup_message_get_status (message),
					soup_message_get_reason_phrase (message) ? soup_message_get_reason_phrase (message) :
					soup_status_get_phrase (soup_message_get_status (message)));
			}

			g_clear_pointer (&node, json_node_unref);
		}

		g_clear_pointer (&bytes, g_byte_array_unref);

		UNLOCK (self);

		if (need_retry) {
			success = FALSE;
			g_clear_error (error);
		}
	}

	return success;
}

static void
e_gdata_session_set_json_body (SoupMessage *message,
			       JsonBuilder *builder)
{
	JsonGenerator *generator;
	JsonNode *node;
	gchar *data;
	gsize data_length = 0;

	g_return_if_fail (SOUP_IS_MESSAGE (message));
	g_return_if_fail (builder != NULL);

	node = json_builder_get_root (builder);

	generator = json_generator_new ();
	json_generator_set_root (generator, node);

	data = json_generator_to_data (generator, &data_length);

	soup_message_headers_set_content_type (soup_message_get_request_headers (message), "application/json", NULL);

	if (data)
		e_soup_session_util_set_message_request_body_from_data (message, FALSE, "application/json", data, data_length, g_free);

	g_object_unref (generator);
	json_node_unref (node);
}

static SoupMessage *
e_gdata_session_new_message (EGDataSession *self,
			     const gchar *method,
			     const gchar *base_url,
			     const gchar *append_path,
			     gboolean base_url_has_params,
			     EGDataQuery *query,
			     JsonBuilder *body,
			     GError **error,
			     ...) G_GNUC_NULL_TERMINATED;

/* base URI followed by param name, GType of the value and the value, terminated by NULL */
static SoupMessage *
e_gdata_session_new_message (EGDataSession *self,
			     const gchar *method,
			     const gchar *base_url,
			     const gchar *append_path,
			     gboolean base_url_has_params,
			     EGDataQuery *query,
			     JsonBuilder *body,
			     GError **error,
			     ...)
{
	GString *uri = NULL;
	GUri *guri;
	SoupMessage *message = NULL;
	va_list ap;
	const gchar *name;
	gchar separator;

	g_return_val_if_fail (base_url != NULL, NULL);

	va_start (ap, error);
	name = va_arg (ap, gchar *);
	separator = base_url_has_params ? '&' : '?';
	while (name) {
		gint type = va_arg (ap, gint);

		if (!uri) {
			uri = g_string_new (base_url);

			if (append_path) {
				g_string_append_c (uri, '/');
				g_string_append (uri, append_path);
			}
		}

		switch (type) {
			case G_TYPE_INT:
			case G_TYPE_BOOLEAN: {
				gint val = va_arg (ap, gint);
				g_string_append_printf (uri, "%c%s=%d", separator, name, val);
				break;
			}
			case G_TYPE_FLOAT:
			case G_TYPE_DOUBLE: {
				gdouble val = va_arg (ap, double);
				g_string_append_printf (uri, "%c%s=%f", separator, name, val);
				break;
			}
			case G_TYPE_STRING: {
				gchar *val = va_arg (ap, gchar *);
				gchar *escaped = g_uri_escape_string (val, NULL, FALSE);
				g_string_append_printf (uri, "%c%s=%s", separator, name, escaped);
				g_free (escaped);
				break;
			}
			case G_TYPE_POINTER: {
				gpointer val = va_arg (ap, gpointer);
				g_string_append_printf (uri, "%c%s=%p", separator, name, val);
				break;
			}
			default:
				g_warning ("%s: Unexpected param type %s", G_STRFUNC, g_type_name (type));
				g_string_free (uri, TRUE);
				va_end (ap);
				return NULL;
		}

		if (separator == '?')
			separator = '&';

		name = va_arg (ap, gchar *);
	}
	va_end (ap);

	if (!uri && append_path) {
		uri = g_string_new (base_url);
		g_string_append_c (uri, '/');
		g_string_append (uri, append_path);
	}

	if (query) {
		gchar *str;

		str = e_gdata_query_to_string (query);

		if (str && *str) {
			if (!uri)
				uri = g_string_new (base_url);
			g_string_append_c (uri, separator);
			g_string_append (uri, str);
		}

		g_free (str);
	}

	guri = g_uri_parse (uri ? uri->str : base_url, G_URI_FLAGS_PARSE_RELAXED | G_URI_FLAGS_ENCODED, error);
	if (guri) {
		message = e_soup_session_new_message_from_uri (E_SOUP_SESSION (self), method, guri, error);

		if (message && body)
			e_gdata_session_set_json_body (message, body);

		g_uri_unref (guri);
	}

	if (uri)
		g_string_free (uri, TRUE);

	return message;
}

static gboolean
e_gdata_session_handle_resource_items (EGDataSession *self,
				       const gchar *method,
				       const gchar *base_url,
				       gboolean base_url_has_params,
				       EGDataQuery *query,
				       EGDataObjectCallback cb,
				       gpointer user_data,
				       GCancellable *cancellable,
				       GError **error)
{
	gchar *next_page_token = NULL;
	gboolean success = TRUE, done = FALSE;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (cb != NULL, FALSE);

	while (success && !done) {
		SoupMessage *message;
		JsonNode *node = NULL;

		done = TRUE;

		message = e_gdata_session_new_message (self, method, base_url, NULL, base_url_has_params, query, NULL, error,
			next_page_token ? "pageToken" : NULL, G_TYPE_STRING, next_page_token,
			NULL);

		if (!message) {
			success = FALSE;
			break;
		}

		success = e_gdata_session_send_request_sync (self, message, &node, NULL, cancellable, error);

		if (success && node) {
			JsonObject *object;

			object = json_node_get_object (node);
			if (object) {
				const gchar *str = e_json_get_string_member (object, "nextPageToken", NULL);
				JsonArray *array;

				if (str) {
					g_free (next_page_token);
					next_page_token = g_strdup (str);
					done = FALSE;
				}

				array = e_json_get_array_member (object, "items");

				if (array) {
					guint ii, len;

					len = json_array_get_length (array);

					for (ii = 0; ii < len && !g_cancellable_is_cancelled (cancellable); ii++) {
						JsonNode *elem = json_array_get_element (array, ii);

						if (JSON_NODE_HOLDS_OBJECT (elem)) {
							JsonObject *elem_object = json_node_get_object (elem);

							if (elem_object && !cb (self, elem_object, user_data)) {
								done = TRUE;
								break;
							}
						}
					}
				} else {
					done = TRUE;
				}

				success = !g_cancellable_set_error_if_cancelled (cancellable, error);
			} else {
				g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, _("No JSON object returned by the server"));
				success = FALSE;
			}
		}

		g_clear_pointer (&node, json_node_unref);
		g_clear_object (&message);
	}

	g_free (next_page_token);

	return success;
}

static gboolean
e_gdata_object_is_kind (JsonObject *object,
			const gchar *kind)
{
	return object != NULL && kind != NULL &&
		g_strcmp0 (e_json_get_string_member (object, "kind", NULL), kind) == 0;
}

/**
 * e_gdata_tasklist_get_id:
 * @tasklist: a GData TaskList
 *
 * Returns TaskList::id property.
 *
 * Returns: (transfer none) (nullable): TaskList::id property or %NULL, when not found
 *
 * Since: 3.46
 **/
const gchar *
e_gdata_tasklist_get_id (JsonObject *tasklist)
{
	g_return_val_if_fail (e_gdata_object_is_kind (tasklist, "tasks#taskList"), NULL);

	return e_json_get_string_member (tasklist, "id", NULL);
}

/**
 * e_gdata_tasklist_add_id:
 * @builder: a #JsonBuilder with a started object member
 * @value: a TaskList::id property value
 *
 * Adds a TaskList::id property @value into the @builder, which
 * should have started an object member.
 *
 * Since: 3.46
 **/
void
e_gdata_tasklist_add_id (JsonBuilder *builder,
			 const gchar *value)
{
	g_return_if_fail (builder != NULL);
	g_return_if_fail (value != NULL);

	e_json_add_string_member (builder, "id", value);
}

/**
 * e_gdata_tasklist_get_etag:
 * @tasklist: a GData TaskList
 *
 * Returns TaskList::etag property.
 *
 * Returns: (transfer none) (nullable): TaskList::etag property or %NULL, when not found
 *
 * Since: 3.46
 **/
const gchar *
e_gdata_tasklist_get_etag (JsonObject *tasklist)
{
	g_return_val_if_fail (e_gdata_object_is_kind (tasklist, "tasks#taskList"), NULL);

	return e_json_get_string_member (tasklist, "etag", NULL);
}

/**
 * e_gdata_tasklist_get_title:
 * @tasklist: a GData TaskList
 *
 * Returns TaskList::title property.
 *
 * Returns: (transfer none) (nullable): TaskList::title property or %NULL, when not found
 *
 * Since: 3.46
 **/
const gchar *
e_gdata_tasklist_get_title (JsonObject *tasklist)
{
	g_return_val_if_fail (e_gdata_object_is_kind (tasklist, "tasks#taskList"), NULL);

	return e_json_get_string_member (tasklist, "title", NULL);
}

/**
 * e_gdata_tasklist_add_title:
 * @builder: a #JsonBuilder with a started object member
 * @value: a TaskList::title property value
 *
 * Adds a TaskList::title property @value into the @builder, which
 * should have started an object member.
 *
 * Since: 3.46
 **/
void
e_gdata_tasklist_add_title (JsonBuilder *builder,
			    const gchar *value)
{
	g_return_if_fail (builder != NULL);
	g_return_if_fail (value != NULL);

	e_json_add_string_member (builder, "title", value);
}

/**
 * e_gdata_tasklist_get_self_link:
 * @tasklist: a GData TaskList
 *
 * Returns TaskList::selfLink property.
 *
 * Returns: (transfer none) (nullable): TaskList::selfLink property or %NULL, when not found
 *
 * Since: 3.46
 **/
const gchar *
e_gdata_tasklist_get_self_link (JsonObject *tasklist)
{
	g_return_val_if_fail (e_gdata_object_is_kind (tasklist, "tasks#taskList"), NULL);

	return e_json_get_string_member (tasklist, "selfLink", NULL);
}

/**
 * e_gdata_tasklist_get_updated:
 * @tasklist: a GData TaskList
 *
 * Returns TaskList::updated property, as Unix time.
 *
 * Returns: TaskList::updated property or 0, when not found
 *
 * Since: 3.46
 **/
gint64
e_gdata_tasklist_get_updated (JsonObject *tasklist)
{
	g_return_val_if_fail (e_gdata_object_is_kind (tasklist, "tasks#taskList"), 0);

	return e_json_get_iso8601_member (tasklist, "updated", 0);
}

/**
 * e_gdata_session_tasklists_delete_sync:
 * @self: an #EGDataSession
 * @tasklist_id: id of a task list
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Deletes a task list @tasklist_id.
 *
 * Returns: whether succeeded
 *
 * Since: 3.46
 **/
gboolean
e_gdata_session_tasklists_delete_sync (EGDataSession *self,
				       const gchar *tasklist_id,
				       GCancellable *cancellable,
				       GError **error)
{
	SoupMessage *message;
	gboolean success;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (tasklist_id != NULL, FALSE);

	message = e_gdata_session_new_message (self, SOUP_METHOD_DELETE, TASKLISTS_TOP_URL, tasklist_id, FALSE, NULL, NULL, error, NULL);

	if (!message)
		return FALSE;

	success = e_gdata_session_send_request_sync (self, message, NULL, NULL, cancellable, error);

	g_clear_object (&message);

	g_prefix_error (error, _("Failed to call %s: "), "tasklists::delete");

	return success;
}

/**
 * e_gdata_session_tasklists_get_sync:
 * @self: an #EGDataSession
 * @tasklist_id: id of a task list
 * @out_tasklist: (out) (transfer full): tasklist object
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Gets a task list @tasklist_id and returns it as a #JsonObject,
 * which should be freed with json_object_unref(), when no longer needed.
 *
 * There can be used e_gdata_tasklist_get_id(), e_gdata_tasklist_get_etag(),
 * e_gdata_tasklist_get_title(), e_gdata_tasklist_get_self_link(),
 * e_gdata_tasklist_get_updated() to read the properties of the task list.
 *
 * Returns: whether succeeded
 *
 * Since: 3.46
 **/
gboolean
e_gdata_session_tasklists_get_sync (EGDataSession *self,
				    const gchar *tasklist_id,
				    JsonObject **out_tasklist,
				    GCancellable *cancellable,
				    GError **error)
{
	SoupMessage *message;
	JsonNode *node = NULL;
	gboolean success;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (tasklist_id != NULL, FALSE);
	g_return_val_if_fail (out_tasklist != NULL, FALSE);

	*out_tasklist = NULL;

	message = e_gdata_session_new_message (self, SOUP_METHOD_GET, TASKLISTS_TOP_URL, tasklist_id, FALSE, NULL, NULL, error, NULL);

	if (!message)
		return FALSE;

	success = e_gdata_session_send_request_sync (self, message, &node, NULL, cancellable, error);

	if (success && node) {
		JsonObject *object;

		object = json_node_get_object (node);
		if (object)
			*out_tasklist = json_object_ref (object);
	}

	g_clear_object (&message);
	g_clear_pointer (&node, json_node_unref);

	g_prefix_error (error, _("Failed to call %s: "), "tasklists::get");

	return success;
}

/**
 * e_gdata_session_tasklists_insert_sync:
 * @self: an #EGDataSession
 * @title: title to set
 * @out_inserted_tasklist: (out) (transfer full): the created task list
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Creates a new task list, titled @title. The @out_inserted_tasklist should
 * be freed with json_object_unref(), when no longer needed.
 *
 * Returns: whether succeeded
 *
 * Since: 3.46
 **/
gboolean
e_gdata_session_tasklists_insert_sync (EGDataSession *self,
				       const gchar *title,
				       JsonObject **out_inserted_tasklist,
				       GCancellable *cancellable,
				       GError **error)
{
	SoupMessage *message;
	JsonBuilder *builder;
	JsonNode *node = NULL;
	gboolean success;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (title != NULL, FALSE);
	g_return_val_if_fail (out_inserted_tasklist != NULL, FALSE);

	*out_inserted_tasklist = NULL;

	builder = json_builder_new_immutable ();

	e_json_begin_object_member (builder, NULL);
	e_json_add_string_member (builder, "title", title);
	e_json_end_object_member (builder);

	message = e_gdata_session_new_message (self, SOUP_METHOD_POST, TASKLISTS_TOP_URL, NULL, FALSE, NULL, builder, error, NULL);

	g_clear_object (&builder);

	if (!message)
		return FALSE;

	success = e_gdata_session_send_request_sync (self, message, &node, NULL, cancellable, error);

	if (success && node) {
		JsonObject *object;

		object = json_node_get_object (node);
		if (object)
			*out_inserted_tasklist = json_object_ref (object);
	}

	g_clear_pointer (&node, json_node_unref);
	g_clear_object (&message);

	g_prefix_error (error, _("Failed to call %s: "), "tasklists::insert");

	return success;
}

/**
 * e_gdata_session_tasklists_list_sync:
 * @self: an #EGDataSession
 * @query: (nullable): an #EGDataQuery to limit returned task lists, or %NULL
 * @cb: (scope call) (closure user_data): an #EGDataObjectCallback to call for each found task list
 * @user_data: user data passed to the @cb
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Lists all configured task lists for the user, calling the @cb for each of them.
 *
 * Returns: whether succeeded
 *
 * Since: 3.46
 **/
gboolean
e_gdata_session_tasklists_list_sync (EGDataSession *self,
				     EGDataQuery *query,
				     EGDataObjectCallback cb,
				     gpointer user_data,
				     GCancellable *cancellable,
				     GError **error)
{
	gboolean success;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (cb != NULL, FALSE);

	success = e_gdata_session_handle_resource_items (self, SOUP_METHOD_GET, TASKLISTS_TOP_URL, FALSE,
		query, cb, user_data, cancellable, error);

	g_prefix_error (error, _("Failed to call %s: "), "tasklists::list");

	return success;
}

/**
 * e_gdata_session_tasklists_patch_sync:
 * @self: an #EGDataSession
 * @tasklist_id: id of a task list
 * @tasklist_properties: task list properties to change
 * @out_patched_tasklist: (out) (optional) (transfer full): where to store patched task list, or %NULL
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Changes properties of a task list @tasklist_id.
 *
 * If not %NULL, free the @out_patched_tasklist with json_object_unref(),
 * when no longer needed.
 *
 * Returns: whether succeeded
 *
 * Since: 3.46
 **/
gboolean
e_gdata_session_tasklists_patch_sync (EGDataSession *self,
				      const gchar *tasklist_id,
				      JsonBuilder *tasklist_properties,
				      JsonObject **out_patched_tasklist,
				      GCancellable *cancellable,
				      GError **error)
{
	SoupMessage *message;
	JsonNode *node = NULL;
	gboolean success;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (tasklist_id != NULL, FALSE);
	g_return_val_if_fail (tasklist_properties != NULL, FALSE);

	message = e_gdata_session_new_message (self, "PATCH", TASKLISTS_TOP_URL, tasklist_id, FALSE, NULL, tasklist_properties, error, NULL);

	if (!message)
		return FALSE;

	success = e_gdata_session_send_request_sync (self, message, out_patched_tasklist ? &node : NULL, NULL, cancellable, error);

	if (success && node && out_patched_tasklist) {
		JsonObject *object;

		object = json_node_get_object (node);
		if (object)
			*out_patched_tasklist = json_object_ref (object);
	}

	g_clear_pointer (&node, json_node_unref);
	g_clear_object (&message);

	g_prefix_error (error, _("Failed to call %s: "), "tasklists::patch");

	return success;
}

/**
 * e_gdata_session_tasklists_update_sync:
 * @self: an #EGDataSession
 * @tasklist_id: id of a task list
 * @tasklist: task list object to update the task list with
 * @out_updated_tasklist: (out) (optional) (transfer full): where to store updated task list, or %NULL
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Updates a task list @tasklist_id with values from the @tasklist.
 *
 * Returns: whether succeeded
 *
 * Since: 3.46
 **/
gboolean
e_gdata_session_tasklists_update_sync (EGDataSession *self,
				       const gchar *tasklist_id,
				       JsonBuilder *tasklist,
				       JsonObject **out_updated_tasklist,
				       GCancellable *cancellable,
				       GError **error)
{
	SoupMessage *message;
	JsonNode *node = NULL;
	gboolean success;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (tasklist_id != NULL, FALSE);
	g_return_val_if_fail (tasklist != NULL, FALSE);

	message = e_gdata_session_new_message (self, SOUP_METHOD_PUT, TASKLISTS_TOP_URL, tasklist_id, FALSE, NULL, tasklist, error, NULL);

	if (!message)
		return FALSE;

	success = e_gdata_session_send_request_sync (self, message, out_updated_tasklist ? &node : NULL, NULL, cancellable, error);

	if (success && node && out_updated_tasklist) {
		JsonObject *object;

		object = json_node_get_object (node);
		if (object)
			*out_updated_tasklist = json_object_ref (object);
	}

	g_clear_pointer (&node, json_node_unref);
	g_clear_object (&message);

	g_prefix_error (error, _("Failed to call %s: "), "tasklists::update");

	return success;
}

/**
 * e_gdata_task_get_id:
 * @task: a GData Task
 *
 * Returns Task::id property.
 *
 * Returns: (transfer none) (nullable): Task::id property or %NULL, when not found
 *
 * Since: 3.46
 **/
const gchar *
e_gdata_task_get_id (JsonObject *task)
{
	g_return_val_if_fail (e_gdata_object_is_kind (task, "tasks#task"), NULL);

	return e_json_get_string_member (task, "id", NULL);
}

/**
 * e_gdata_task_add_id:
 * @builder: a #JsonBuilder with a started object member
 * @value: a Task::id property value
 *
 * Adds a Task::id property @value into the @builder, which
 * should have started an object member.
 *
 * Since: 3.46
 **/
void
e_gdata_task_add_id (JsonBuilder *builder,
		     const gchar *value)
{
	g_return_if_fail (builder != NULL);
	g_return_if_fail (value != NULL);

	e_json_add_string_member (builder, "id", value);
}

/**
 * e_gdata_task_get_etag:
 * @task: a GData Task
 *
 * Returns Task::etag property.
 *
 * Returns: (transfer none) (nullable): Task::etag property or %NULL, when not found
 *
 * Since: 3.46
 **/
const gchar *
e_gdata_task_get_etag (JsonObject *task)
{
	g_return_val_if_fail (e_gdata_object_is_kind (task, "tasks#task"), NULL);

	return e_json_get_string_member (task, "etag", NULL);
}

/**
 * e_gdata_task_get_title:
 * @task: a GData Task
 *
 * Returns Task::title property.
 *
 * Returns: (transfer none) (nullable): Task::title property or %NULL, when not found
 *
 * Since: 3.46
 **/
const gchar *
e_gdata_task_get_title (JsonObject *task)
{
	g_return_val_if_fail (e_gdata_object_is_kind (task, "tasks#task"), NULL);

	return e_json_get_string_member (task, "title", NULL);
}

/**
 * e_gdata_task_add_title:
 * @builder: a #JsonBuilder with a started object member
 * @value: a Task::title property value
 *
 * Adds a Task::title property @value into the @builder, which
 * should have started an object member.
 *
 * Since: 3.46
 **/
void
e_gdata_task_add_title (JsonBuilder *builder,
			const gchar *value)
{
	g_return_if_fail (builder != NULL);
	g_return_if_fail (value != NULL);

	e_json_add_string_member (builder, "title", value);
}

/**
 * e_gdata_task_get_updated:
 * @task: a GData Task
 *
 * Returns Task::updated property, as Unix time.
 *
 * Returns: Task::updated property or 0, when not found
 *
 * Since: 3.46
 **/
gint64
e_gdata_task_get_updated (JsonObject *task)
{
	g_return_val_if_fail (e_gdata_object_is_kind (task, "tasks#task"), 0);

	return e_json_get_iso8601_member (task, "updated", 0);
}

/**
 * e_gdata_task_get_self_link:
 * @task: a GData TaskList
 *
 * Returns Task::selfLink property.
 *
 * Returns: (transfer none) (nullable): Task::selfLink property or %NULL, when not found
 *
 * Since: 3.46
 **/
const gchar *
e_gdata_task_get_self_link (JsonObject *task)
{
	g_return_val_if_fail (e_gdata_object_is_kind (task, "tasks#task"), NULL);

	return e_json_get_string_member (task, "selfLink", NULL);
}

/**
 * e_gdata_task_get_parent:
 * @task: a GData Task
 *
 * Returns Task::parent property.
 *
 * Returns: (transfer none) (nullable): Task::parent property or %NULL, when not found
 *
 * Since: 3.46
 **/
const gchar *
e_gdata_task_get_parent (JsonObject *task)
{
	g_return_val_if_fail (e_gdata_object_is_kind (task, "tasks#task"), NULL);

	return e_json_get_string_member (task, "parent", NULL);
}

/**
 * e_gdata_task_get_position:
 * @task: a GData Task
 *
 * Returns Task::position property.
 *
 * Returns: (transfer none) (nullable): Task::position property or %NULL, when not found
 *
 * Since: 3.46
 **/
const gchar *
e_gdata_task_get_position (JsonObject *task)
{
	g_return_val_if_fail (e_gdata_object_is_kind (task, "tasks#task"), NULL);

	return e_json_get_string_member (task, "position", NULL);
}

/**
 * e_gdata_task_get_notes:
 * @task: a GData Task
 *
 * Returns Task::notes property.
 *
 * Returns: (transfer none) (nullable): Task::notes property or %NULL, when not found
 *
 * Since: 3.46
 **/
const gchar *
e_gdata_task_get_notes (JsonObject *task)
{
	g_return_val_if_fail (e_gdata_object_is_kind (task, "tasks#task"), NULL);

	return e_json_get_string_member (task, "notes", NULL);
}

/**
 * e_gdata_task_add_notes:
 * @builder: a #JsonBuilder with a started object member
 * @value: (nullable): a Task::notes property value
 *
 * Adds a Task::notes property @value into the @builder, which
 * should have started an object member.
 *
 * When the @value is %NULL, then adds a NULL-object, to indicate removal
 * of the property.
 *
 * Since: 3.46
 **/
void
e_gdata_task_add_notes (JsonBuilder *builder,
			const gchar *value)
{
	g_return_if_fail (builder != NULL);

	if (value)
		e_json_add_string_member (builder, "notes", value);
	else
		e_json_add_null_member (builder, "notes");
}

/**
 * e_gdata_task_get_status:
 * @task: a GData Task
 *
 * Returns Task::status property.
 *
 * Returns: Task::status property as #EGDataTaskStatus or %E_GDATA_TASK_STATUS_UNKNOWN,
 *    when not found or has set an unknown value.
 *
 * Since: 3.46
 **/
EGDataTaskStatus
e_gdata_task_get_status (JsonObject *task)
{
	const gchar *str;

	g_return_val_if_fail (e_gdata_object_is_kind (task, "tasks#task"), E_GDATA_TASK_STATUS_UNKNOWN);

	str = e_json_get_string_member (task, "status", NULL);

	if (g_strcmp0 (str, "needsAction") == 0)
		return E_GDATA_TASK_STATUS_NEEDS_ACTION;

	if (g_strcmp0 (str, "completed") == 0)
		return E_GDATA_TASK_STATUS_COMPLETED;

	return E_GDATA_TASK_STATUS_UNKNOWN;
}

/**
 * e_gdata_task_add_status:
 * @builder: a #JsonBuilder with a started object member
 * @value: a Task::status property value
 *
 * Adds a Task::status property @value into the @builder, which
 * should have started an object member.
 *
 * When the @value is %E_GDATA_TASK_STATUS_UNKNOWN, then adds a NULL-object,
 * to indicate removal of the property.
 *
 * Since: 3.46
 **/
void
e_gdata_task_add_status (JsonBuilder *builder,
			 EGDataTaskStatus value)
{
	g_return_if_fail (builder != NULL);

	switch (value) {
	case E_GDATA_TASK_STATUS_UNKNOWN:
		e_json_add_null_member (builder, "status");
		break;
	case E_GDATA_TASK_STATUS_NEEDS_ACTION:
		e_json_add_string_member (builder, "status", "needsAction");
		break;
	case E_GDATA_TASK_STATUS_COMPLETED:
		e_json_add_string_member (builder, "status", "completed");
		break;
	}
}

/**
 * e_gdata_task_get_due:
 * @task: a GData Task
 *
 * Returns Task::due property, as Unix time.
 *
 * Returns: Task::due property or 0, when not found
 *
 * Since: 3.46
 **/
gint64
e_gdata_task_get_due (JsonObject *task)
{
	g_return_val_if_fail (e_gdata_object_is_kind (task, "tasks#task"), 0);

	return e_json_get_iso8601_member (task, "due", 0);
}

/**
 * e_gdata_task_add_due:
 * @builder: a #JsonBuilder with a started object member
 * @value: a Task::due property value, as Unix time
 *
 * Adds a Task::due property @value into the @builder, which
 * should have started an object member.
 *
 * When the @value is 0, then adds a NULL-object, to indicate
 * removal of the property.
 *
 * Since: 3.46
 **/
void
e_gdata_task_add_due (JsonBuilder *builder,
		      gint64 value)
{
	g_return_if_fail (builder != NULL);

	if (value != 0)
		e_json_add_iso8601_member (builder, "due", value);
	else
		e_json_add_null_member (builder, "due");
}

/**
 * e_gdata_task_get_completed:
 * @task: a GData Task
 *
 * Returns Task::completed property, as Unix time.
 *
 * Returns: Task::completed property or 0, when not found
 *
 * Since: 3.46
 **/
gint64
e_gdata_task_get_completed (JsonObject *task)
{
	g_return_val_if_fail (e_gdata_object_is_kind (task, "tasks#task"), 0);

	return e_json_get_iso8601_member (task, "completed", 0);
}

/**
 * e_gdata_task_add_completed:
 * @builder: a #JsonBuilder with a started object member
 * @value: a Task::completed property value, as Unix time
 *
 * Adds a Task:completed property @value into the @builder, which
 * should have started an object member.
 *
 * When the @value is 0, then adds a NULL-object, to indicate
 * removal of the property.
 *
 * Since: 3.46
 **/
void
e_gdata_task_add_completed (JsonBuilder *builder,
			    gint64 value)
{
	g_return_if_fail (builder != NULL);

	if (value != 0)
		e_json_add_iso8601_member (builder, "completed", value);
	else
		e_json_add_null_member (builder, "completed");
}

/**
 * e_gdata_task_get_deleted:
 * @task: a GData Task
 *
 * Returns Task::deleted property, as Unix time.
 *
 * Returns: Task::deleted property or %FALSE, when not found
 *
 * Since: 3.46
 **/
gboolean
e_gdata_task_get_deleted (JsonObject *task)
{
	g_return_val_if_fail (e_gdata_object_is_kind (task, "tasks#task"), FALSE);

	return e_json_get_boolean_member (task, "deleted", FALSE);
}

/**
 * e_gdata_task_get_hidden:
 * @task: a GData Task
 *
 * Returns Task::hidden property, as Unix time.
 *
 * Returns: Task::hidden property or %FALSE, when not found
 *
 * Since: 3.46
 **/
gboolean
e_gdata_task_get_hidden (JsonObject *task)
{
	g_return_val_if_fail (e_gdata_object_is_kind (task, "tasks#task"), FALSE);

	return e_json_get_boolean_member (task, "hidden", FALSE);
}

/**
 * e_gdata_session_tasks_clear_sync:
 * @self: an #EGDataSession
 * @tasklist_id: id of a task list
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Clears all completed tasks from the task list @tasklist_id. The affected tasks
 * will be marked as 'hidden' and no longer be returned by default when retrieving
 * all tasks for a task list.
 *
 * Returns: whether succeeded
 *
 * Since: 3.46
 **/
gboolean
e_gdata_session_tasks_clear_sync (EGDataSession *self,
				  const gchar *tasklist_id,
				  GCancellable *cancellable,
				  GError **error)
{
	SoupMessage *message;
	gboolean success;
	gchar *path;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (tasklist_id != NULL, FALSE);

	path = g_strconcat (tasklist_id, "/clear", NULL);
	message = e_gdata_session_new_message (self, SOUP_METHOD_POST, TASKS_TOP_URL, path, FALSE, NULL, NULL, error, NULL);
	g_free (path);

	if (!message)
		return FALSE;

	success = e_gdata_session_send_request_sync (self, message, NULL, NULL, cancellable, error);

	g_clear_object (&message);

	g_prefix_error (error, _("Failed to call %s: "), "tasks::clear");

	return success;
}

/**
 * e_gdata_session_tasks_delete_sync:
 * @self: an #EGDataSession
 * @tasklist_id: id of a task list
 * @task_id: id of a task
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Deletes a task @task_id from a task list @tasklist_id.
 *
 * Returns: whether succeeded
 *
 * Since: 3.46
 **/
gboolean
e_gdata_session_tasks_delete_sync (EGDataSession *self,
				   const gchar *tasklist_id,
				   const gchar *task_id,
				   GCancellable *cancellable,
				   GError **error)
{
	SoupMessage *message;
	gboolean success;
	gchar *path;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (tasklist_id != NULL, FALSE);

	path = g_strconcat (tasklist_id, "/tasks/", task_id, NULL);
	message = e_gdata_session_new_message (self, SOUP_METHOD_DELETE, TASKS_TOP_URL, path, FALSE, NULL, NULL, error, NULL);
	g_free (path);

	if (!message)
		return FALSE;

	success = e_gdata_session_send_request_sync (self, message, NULL, NULL, cancellable, error);

	g_clear_object (&message);

	g_prefix_error (error, _("Failed to call %s: "), "tasks::delete");

	return success;
}

/**
 * e_gdata_session_tasks_get_sync:
 * @self: an #EGDataSession
 * @tasklist_id: id of a task list
 * @task_id: id of a task
 * @out_task: (out) (transfer full): task object
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Gets a task @task_id from a task list @tasklist_id and returns it as a #JsonObject,
 * which should be freed with json_object_unref(), when no longer needed.
 *
 * There can be used e_gdata_task_get_id(), e_gdata_task_get_etag(),
 * e_gdata_task_get_title() and other e_gdata_task_... functions
 * to read the properties of the task.
 *
 * Returns: whether succeeded
 *
 * Since: 3.46
 **/
gboolean
e_gdata_session_tasks_get_sync (EGDataSession *self,
				const gchar *tasklist_id,
				const gchar *task_id,
				JsonObject **out_task,
				GCancellable *cancellable,
				GError **error)
{
	SoupMessage *message;
	JsonNode *node = NULL;
	gboolean success;
	gchar *path;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (tasklist_id != NULL, FALSE);
	g_return_val_if_fail (task_id != NULL, FALSE);
	g_return_val_if_fail (out_task != NULL, FALSE);

	*out_task = NULL;

	path = g_strconcat (tasklist_id, "/tasks/", task_id, NULL);
	message = e_gdata_session_new_message (self, SOUP_METHOD_GET, TASKS_TOP_URL, path, FALSE, NULL, NULL, error, NULL);
	g_free (path);

	if (!message)
		return FALSE;

	success = e_gdata_session_send_request_sync (self, message, &node, NULL, cancellable, error);

	if (success && node) {
		JsonObject *object;

		object = json_node_get_object (node);
		if (object)
			*out_task = json_object_ref (object);
	}

	g_clear_object (&message);
	g_clear_pointer (&node, json_node_unref);

	g_prefix_error (error, _("Failed to call %s: "), "tasks::get");

	return success;
}

/**
 * e_gdata_session_tasks_insert_sync:
 * @self: an #EGDataSession
 * @tasklist_id: id of a task list
 * @task: a #JsonBuilder with the task object
 * @parent_task_id: (nullable): parent task identifier, or %NULL to create at the top-level
 * @previous_task_id: (nullable): previous sibling task identifier, or %NULL to create at the first position among its siblings
 * @out_inserted_task: (out) (transfer full): the created task
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Creates a new task @task in the task list @tasklist_id. The @out_inserted_task should
 * be freed with json_object_unref(), when no longer needed.
 *
 * Returns: whether succeeded
 *
 * Since: 3.46
 **/
gboolean
e_gdata_session_tasks_insert_sync (EGDataSession *self,
				   const gchar *tasklist_id,
				   JsonBuilder *task,
				   const gchar *parent_task_id,
				   const gchar *previous_task_id,
				   JsonObject **out_inserted_task,
				   GCancellable *cancellable,
				   GError **error)
{
	SoupMessage *message;
	JsonNode *node = NULL;
	gboolean success;
	gchar *path;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (tasklist_id != NULL, FALSE);
	g_return_val_if_fail (task != NULL, FALSE);
	g_return_val_if_fail (out_inserted_task != NULL, FALSE);

	*out_inserted_task = NULL;

	path = g_strconcat (tasklist_id, "/tasks", NULL);
	message = e_gdata_session_new_message (self, SOUP_METHOD_POST, TASKS_TOP_URL, path, FALSE, NULL, task, error,
		parent_task_id ? "parent" : previous_task_id ? "previous" : NULL,
		G_TYPE_STRING,
		parent_task_id ? parent_task_id : previous_task_id ? previous_task_id : NULL,
		parent_task_id && previous_task_id ? "previous" : NULL,
		G_TYPE_STRING,
		previous_task_id,
		NULL);
	g_free (path);

	if (!message)
		return FALSE;

	success = e_gdata_session_send_request_sync (self, message, &node, NULL, cancellable, error);

	if (success && node) {
		JsonObject *object;

		object = json_node_get_object (node);
		if (object)
			*out_inserted_task = json_object_ref (object);
	}

	g_clear_object (&message);
	g_clear_pointer (&node, json_node_unref);

	g_prefix_error (error, _("Failed to call %s: "), "tasks::insert");

	return success;
}

/**
 * e_gdata_session_tasks_list_sync:
 * @self: an #EGDataSession
 * @tasklist_id: id of a task list
 * @query: (nullable): an #EGDataQuery to limit returned tasks, or %NULL
 * @cb: (scope call) (closure user_data): an #EGDataObjectCallback to call for each found task
 * @user_data: user data passed to the @cb
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Lists all tasks in the task list @tasklist_id, calling the @cb for each of them.
 *
 * Returns: whether succeeded
 *
 * Since: 3.46
 **/
gboolean
e_gdata_session_tasks_list_sync (EGDataSession *self,
				 const gchar *tasklist_id,
				 EGDataQuery *query,
				 EGDataObjectCallback cb,
				 gpointer user_data,
				 GCancellable *cancellable,
				 GError **error)
{
	gboolean success;
	gchar *base_url;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (cb != NULL, FALSE);

	base_url = g_strconcat (TASKS_TOP_URL, "/", tasklist_id, "/tasks", NULL);
	success = e_gdata_session_handle_resource_items (self, SOUP_METHOD_GET, base_url, FALSE,
		query, cb, user_data, cancellable, error);
	g_free (base_url);

	g_prefix_error (error, _("Failed to call %s: "), "tasks::list");

	return success;
}

/**
 * e_gdata_session_tasks_move_sync:
 * @self: an #EGDataSession
 * @tasklist_id: id of a task list
 * @task_id: id of a task
 * @parent_task_id: (nullable): parent task identifier, or %NULL to move at the top-level
 * @previous_task_id: (nullable): previous sibling task identifier, or %NULL to move at the first position among its siblings
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Moves the specified task @task_id to another position in the task
 * list @tasklist_id. This can include putting it as a child task under
 * a new parent and/or move it to a different position among its sibling tasks.
 *
 * Returns: whether succeeded
 *
 * Since: 3.46
 **/
gboolean
e_gdata_session_tasks_move_sync (EGDataSession *self,
				 const gchar *tasklist_id,
				 const gchar *task_id,
				 const gchar *parent_task_id,
				 const gchar *previous_task_id,
				 GCancellable *cancellable,
				 GError **error)
{
	SoupMessage *message;
	gboolean success;
	gchar *path;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (tasklist_id != NULL, FALSE);
	g_return_val_if_fail (task_id != NULL, FALSE);

	path = g_strconcat (tasklist_id, "/tasks/", task_id, "/move", NULL);
	message = e_gdata_session_new_message (self, SOUP_METHOD_POST, TASKS_TOP_URL, path, FALSE, NULL, NULL, error,
		parent_task_id ? "parent" : previous_task_id ? "previous" : NULL,
		G_TYPE_STRING,
		parent_task_id ? parent_task_id : previous_task_id ? previous_task_id : NULL,
		parent_task_id && previous_task_id ? "previous" : NULL,
		G_TYPE_STRING,
		previous_task_id,
		NULL);
	g_free (path);

	if (!message)
		return FALSE;

	success = e_gdata_session_send_request_sync (self, message, NULL, NULL, cancellable, error);

	g_clear_object (&message);

	g_prefix_error (error, _("Failed to call %s: "), "tasks::move");

	return success;
}

/**
 * e_gdata_session_tasks_patch_sync:
 * @self: an #EGDataSession
 * @tasklist_id: id of a task list
 * @task_id: id of a task
 * @task_properties: task properties to change
 * @out_patched_task: (out) (optional) (transfer full): where to set patches task, or %NULL
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Changes properties @task_properties of a task @task_id in the task list @tasklist_id.
 *
 * If not %NULL, free the @out_patched_task with json_object_unref(),
 * when no longer needed.
 *
 * Returns: whether succeeded
 *
 * Since: 3.46
 **/
gboolean
e_gdata_session_tasks_patch_sync (EGDataSession *self,
				  const gchar *tasklist_id,
				  const gchar *task_id,
				  JsonBuilder *task_properties,
				  JsonObject **out_patched_task,
				  GCancellable *cancellable,
				  GError **error)
{
	SoupMessage *message;
	JsonNode *node = NULL;
	gboolean success;
	gchar *path;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (tasklist_id != NULL, FALSE);
	g_return_val_if_fail (task_id != NULL, FALSE);
	g_return_val_if_fail (task_properties != NULL, FALSE);

	path = g_strconcat (tasklist_id, "/tasks/", task_id, NULL);
	message = e_gdata_session_new_message (self, "PATCH", TASKS_TOP_URL, path, FALSE, NULL, task_properties, error, NULL);
	g_free (path);

	if (!message)
		return FALSE;

	success = e_gdata_session_send_request_sync (self, message, out_patched_task ? &node : NULL, NULL, cancellable, error);

	if (success && node && out_patched_task) {
		JsonObject *object;

		object = json_node_get_object (node);
		if (object)
			*out_patched_task = json_object_ref (object);
	}

	g_clear_pointer (&node, json_node_unref);
	g_clear_object (&message);

	g_prefix_error (error, _("Failed to call %s: "), "tasks::patch");

	return success;
}

/**
 * e_gdata_session_tasks_update_sync:
 * @self: an #EGDataSession
 * @tasklist_id: id of a task list
 * @task_id: id of a task
 * @task: task object to update the task with
 * @out_updated_task: (out) (optional) (transfer full): where to store updated task, or %NULL
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Updates a task @task_id in a task list @tasklist_id to the values from the @task.
 *
 * Returns: whether succeeded
 *
 * Since: 3.46
 **/
gboolean
e_gdata_session_tasks_update_sync (EGDataSession *self,
				   const gchar *tasklist_id,
				   const gchar *task_id,
				   JsonBuilder *task,
				   JsonObject **out_updated_task,
				   GCancellable *cancellable,
				   GError **error)
{
	SoupMessage *message;
	JsonNode *node = NULL;
	gboolean success;
	gchar *path;

	g_return_val_if_fail (E_IS_GDATA_SESSION (self), FALSE);
	g_return_val_if_fail (tasklist_id != NULL, FALSE);
	g_return_val_if_fail (task_id != NULL, FALSE);
	g_return_val_if_fail (task != NULL, FALSE);

	path = g_strconcat (tasklist_id, "/tasks/", task_id, NULL);
	message = e_gdata_session_new_message (self, SOUP_METHOD_PUT, TASKS_TOP_URL, path, FALSE, NULL, task, error, NULL);
	g_free (path);

	if (!message)
		return FALSE;

	success = e_gdata_session_send_request_sync (self, message, out_updated_task ? &node : NULL, NULL, cancellable, error);

	if (success && node && out_updated_task) {
		JsonObject *object;

		object = json_node_get_object (node);
		if (object)
			*out_updated_task = json_object_ref (object);
	}

	g_clear_pointer (&node, json_node_unref);
	g_clear_object (&message);

	g_prefix_error (error, _("Failed to call %s: "), "tasks::update");

	return success;
}
