/*
 * e-mail-formatter.c
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) version 3.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with the program; if not, see <http://www.gnu.org/licenses/>
 *
 */

#include "e-mail-formatter.h"

#include "e-mail-formatter-extension.h"
#include "e-mail-formatter-utils.h"
#include "e-mail-part.h"

#include <e-util/e-util.h>
#include <libebackend/libebackend.h>
#include <gdk/gdk.h>
#include <glib/gi18n.h>

#include "libemail-engine/e-mail-enumtypes.h"

#define d(x)

#define E_MAIL_FORMATTER_GET_PRIVATE(obj) \
	(G_TYPE_INSTANCE_GET_PRIVATE \
	((obj), E_TYPE_MAIL_FORMATTER, EMailFormatterPrivate))\

#define STYLESHEET_URI \
	"evo-file://" EVOLUTION_PRIVDATADIR "/theme/webview.css"

typedef struct _AsyncContext AsyncContext;

struct _EMailFormatterPrivate {
	EMailImageLoadingPolicy image_loading_policy;

	gboolean show_sender_photo;
	gboolean show_real_date;
	gboolean animate_images;

	GMutex property_lock;

	gchar *charset;
	gchar *default_charset;
};

struct _AsyncContext {
	CamelStream *stream;
	EMailPartList *part_list;
	EMailFormatterHeaderFlags flags;
	EMailFormatterMode mode;
};

/* internal formatter extensions */
GType e_mail_formatter_attachment_get_type (void);
GType e_mail_formatter_attachment_bar_get_type (void);
GType e_mail_formatter_error_get_type (void);
GType e_mail_formatter_headers_get_type (void);
GType e_mail_formatter_image_get_type (void);
GType e_mail_formatter_message_rfc822_get_type (void);
GType e_mail_formatter_secure_button_get_type (void);
GType e_mail_formatter_source_get_type (void);
GType e_mail_formatter_text_enriched_get_type (void);
GType e_mail_formatter_text_html_get_type (void);
GType e_mail_formatter_text_plain_get_type (void);

void e_mail_formatter_internal_extensions_load (EMailExtensionRegistry *ereg);

static gpointer e_mail_formatter_parent_class = 0;

enum {
	PROP_0,
	PROP_ANIMATE_IMAGES,
	PROP_BODY_COLOR,
	PROP_CHARSET,
	PROP_CITATION_COLOR,
	PROP_CONTENT_COLOR,
	PROP_DEFAULT_CHARSET,
	PROP_FRAME_COLOR,
	PROP_HEADER_COLOR,
	PROP_IMAGE_LOADING_POLICY,
	PROP_MARK_CITATIONS,
	PROP_SHOW_REAL_DATE,
	PROP_SHOW_SENDER_PHOTO,
	PROP_TEXT_COLOR
};

enum {
	NEED_REDRAW,
	LAST_SIGNAL
};

static gint signals[LAST_SIGNAL];

static void
async_context_free (AsyncContext *async_context)
{
	g_clear_object (&async_context->part_list);
	g_clear_object (&async_context->stream);

	g_slice_free (AsyncContext, async_context);
}

static EMailFormatterContext *
mail_formatter_create_context (EMailFormatter *formatter,
                               EMailPartList *part_list,
                               EMailFormatterMode mode,
                               EMailFormatterHeaderFlags flags)
{
	EMailFormatterClass *class;
	EMailFormatterContext *context;

	class = E_MAIL_FORMATTER_GET_CLASS (formatter);

	g_warn_if_fail (class->context_size >= sizeof (EMailFormatterContext));

	context = g_malloc0 (class->context_size);
	context->part_list = g_object_ref (part_list);
	context->mode = mode;
	context->flags = flags;

	return context;
}

static void
mail_formatter_free_context (EMailFormatterContext *context)
{
	if (context->part_list != NULL)
		g_object_unref (context->part_list);

	g_free (context);
}

static void
e_mail_formatter_set_property (GObject *object,
                               guint property_id,
                               const GValue *value,
                               GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_ANIMATE_IMAGES:
			e_mail_formatter_set_animate_images (
				E_MAIL_FORMATTER (object),
				g_value_get_boolean (value));
			return;

		case PROP_BODY_COLOR:
			e_mail_formatter_set_color (
				E_MAIL_FORMATTER (object),
				E_MAIL_FORMATTER_COLOR_BODY,
				g_value_get_boxed (value));
			return;

		case PROP_CHARSET:
			e_mail_formatter_set_charset (
				E_MAIL_FORMATTER (object),
				g_value_get_string (value));
			return;

		case PROP_CITATION_COLOR:
			e_mail_formatter_set_color (
				E_MAIL_FORMATTER (object),
				E_MAIL_FORMATTER_COLOR_CITATION,
				g_value_get_boxed (value));
			return;

		case PROP_CONTENT_COLOR:
			e_mail_formatter_set_color (
				E_MAIL_FORMATTER (object),
				E_MAIL_FORMATTER_COLOR_CONTENT,
				g_value_get_boxed (value));
			return;

		case PROP_DEFAULT_CHARSET:
			e_mail_formatter_set_default_charset (
				E_MAIL_FORMATTER (object),
				g_value_get_string (value));
			return;

		case PROP_FRAME_COLOR:
			e_mail_formatter_set_color (
				E_MAIL_FORMATTER (object),
				E_MAIL_FORMATTER_COLOR_FRAME,
				g_value_get_boxed (value));
			return;

		case PROP_HEADER_COLOR:
			e_mail_formatter_set_color (
				E_MAIL_FORMATTER (object),
				E_MAIL_FORMATTER_COLOR_HEADER,
				g_value_get_boxed (value));
			return;

		case PROP_IMAGE_LOADING_POLICY:
			e_mail_formatter_set_image_loading_policy (
				E_MAIL_FORMATTER (object),
				g_value_get_enum (value));
			return;

		case PROP_MARK_CITATIONS:
			e_mail_formatter_set_mark_citations (
				E_MAIL_FORMATTER (object),
				g_value_get_boolean (value));
			return;

		case PROP_SHOW_REAL_DATE:
			e_mail_formatter_set_show_real_date (
				E_MAIL_FORMATTER (object),
				g_value_get_boolean (value));
			return;

		case PROP_SHOW_SENDER_PHOTO:
			e_mail_formatter_set_show_sender_photo (
				E_MAIL_FORMATTER (object),
				g_value_get_boolean (value));
			return;

		case PROP_TEXT_COLOR:
			e_mail_formatter_set_color (
				E_MAIL_FORMATTER (object),
				E_MAIL_FORMATTER_COLOR_TEXT,
				g_value_get_boxed (value));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
e_mail_formatter_get_property (GObject *object,
                             guint property_id,
                             GValue *value,
                             GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_ANIMATE_IMAGES:
			g_value_set_boolean (
				value,
				e_mail_formatter_get_animate_images (
				E_MAIL_FORMATTER (object)));
			return;

		case PROP_BODY_COLOR:
			g_value_set_boxed (
				value,
				e_mail_formatter_get_color (
				E_MAIL_FORMATTER (object),
				E_MAIL_FORMATTER_COLOR_BODY));
			return;

		case PROP_CHARSET:
			g_value_take_string (
				value,
				e_mail_formatter_dup_charset (
				E_MAIL_FORMATTER (object)));
			return;

		case PROP_CITATION_COLOR:
			g_value_set_boxed (
				value,
				e_mail_formatter_get_color (
				E_MAIL_FORMATTER (object),
				E_MAIL_FORMATTER_COLOR_CITATION));
			return;

		case PROP_CONTENT_COLOR:
			g_value_set_boxed (
				value,
				e_mail_formatter_get_color (
				E_MAIL_FORMATTER (object),
				E_MAIL_FORMATTER_COLOR_CONTENT));
			return;

		case PROP_DEFAULT_CHARSET:
			g_value_take_string (
				value,
				e_mail_formatter_dup_default_charset (
				E_MAIL_FORMATTER (object)));
			return;

		case PROP_FRAME_COLOR:
			g_value_set_boxed (
				value,
				e_mail_formatter_get_color (
				E_MAIL_FORMATTER (object),
				E_MAIL_FORMATTER_COLOR_FRAME));
			return;

		case PROP_HEADER_COLOR:
			g_value_set_boxed (
				value,
				e_mail_formatter_get_color (
				E_MAIL_FORMATTER (object),
				E_MAIL_FORMATTER_COLOR_HEADER));
			return;

		case PROP_IMAGE_LOADING_POLICY:
			g_value_set_enum (
				value,
				e_mail_formatter_get_image_loading_policy (
				E_MAIL_FORMATTER (object)));
			return;

		case PROP_MARK_CITATIONS:
			g_value_set_boolean (
				value,
				e_mail_formatter_get_mark_citations (
				E_MAIL_FORMATTER (object)));
			return;

		case PROP_SHOW_REAL_DATE:
			g_value_set_boolean (
				value,
				e_mail_formatter_get_show_real_date (
				E_MAIL_FORMATTER (object)));
			return;

		case PROP_SHOW_SENDER_PHOTO:
			g_value_set_boolean (
				value,
				e_mail_formatter_get_show_sender_photo (
				E_MAIL_FORMATTER (object)));
			return;

		case PROP_TEXT_COLOR:
			g_value_set_boxed (
				value,
				e_mail_formatter_get_color (
				E_MAIL_FORMATTER (object),
				E_MAIL_FORMATTER_COLOR_TEXT));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
e_mail_formatter_finalize (GObject *object)
{
	EMailFormatterPrivate *priv;

	priv = E_MAIL_FORMATTER_GET_PRIVATE (object);

	g_free (priv->charset);
	g_free (priv->default_charset);

	g_mutex_clear (&priv->property_lock);

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

static void
e_mail_formatter_constructed (GObject *object)
{
	/* Chain up to parent's constructed() method. */
	G_OBJECT_CLASS (e_mail_formatter_parent_class)->constructed (object);

	e_extensible_load_extensions (E_EXTENSIBLE (object));
}

static void
mail_formatter_run (EMailFormatter *formatter,
                    EMailFormatterContext *context,
                    CamelStream *stream,
                    GCancellable *cancellable)
{
	GQueue queue = G_QUEUE_INIT;
	GList *head, *link;
	gchar *hdr;

	hdr = e_mail_formatter_get_html_header (formatter);
	camel_stream_write_string (stream, hdr, cancellable, NULL);
	g_free (hdr);

	e_mail_part_list_queue_parts (context->part_list, NULL, &queue);

	head = g_queue_peek_head_link (&queue);

	for (link = head; link != NULL; link = g_list_next (link)) {
		EMailPart *part = link->data;
		const gchar *part_id;
		gboolean ok;

		part_id = e_mail_part_get_id (part);

		if (g_cancellable_is_cancelled (cancellable))
			break;

		if (part->is_hidden && !part->is_error) {
			if (e_mail_part_id_has_suffix (part, ".rfc822")) {
				link = e_mail_formatter_find_rfc822_end_iter (link);
			}

			if (link == NULL)
				break;

			continue;
		}

		/* Force formatting as source if needed */
		if (context->mode != E_MAIL_FORMATTER_MODE_SOURCE) {
			const gchar *mime_type;

			mime_type = e_mail_part_get_mime_type (part);
			if (mime_type == NULL)
				continue;

			ok = e_mail_formatter_format_as (
				formatter, context, part, stream,
				mime_type, cancellable);

			/* If the written part was message/rfc822 then
			 * jump to the end of the message, because content
			 * of the whole message has been formatted by
			 * message_rfc822 formatter */
			if (ok && e_mail_part_id_has_suffix (part, ".rfc822")) {
				link = e_mail_formatter_find_rfc822_end_iter (link);

				if (link == NULL)
					break;

				continue;
			}

		} else {
			ok = FALSE;
		}

		if (!ok) {
			/* We don't want to source these */
			if (e_mail_part_id_has_suffix (part, ".headers") ||
			    e_mail_part_id_has_suffix (part, "attachment-bar"))
				continue;

			e_mail_formatter_format_as (
				formatter, context, part, stream,
				"application/vnd.evolution.source", cancellable);

			/* .message is the entire message. There's nothing more
			 * to be written. */
			if (g_strcmp0 (part_id, ".message") == 0)
				break;

			/* If we just wrote source of a rfc822 message, then jump
			 * behind the message (otherwise source of all parts
			 * would be rendered twice) */
			if (e_mail_part_id_has_suffix (part, ".rfc822")) {

				do {
					part = link->data;
					if (e_mail_part_id_has_suffix (part, ".rfc822.end"))
						break;

					link = g_list_next (link);
				} while (link != NULL);

				if (link == NULL)
					break;
			}
		}
	}

	while (!g_queue_is_empty (&queue))
		g_object_unref (g_queue_pop_head (&queue));

	camel_stream_write_string (stream, "</body></html>", cancellable, NULL);
}

static void
mail_formatter_update_style (EMailFormatter *formatter,
                             GtkStateFlags state)
{
	GtkStyleContext *style_context;
	GtkWidgetPath *widget_path;
	GdkRGBA rgba;

	g_object_freeze_notify (G_OBJECT (formatter));

	/* derive colors from top-level window */
	style_context = gtk_style_context_new ();
	widget_path = gtk_widget_path_new ();
	gtk_widget_path_append_type (widget_path, GTK_TYPE_WINDOW);
	gtk_style_context_set_path (style_context, widget_path);
	gtk_style_context_invalidate (style_context);

	gtk_style_context_save (style_context);
	gtk_style_context_add_class (style_context, GTK_STYLE_CLASS_TOOLBAR);

	gtk_style_context_get_background_color (style_context, state, &rgba);
	e_mail_formatter_set_color (
		formatter, E_MAIL_FORMATTER_COLOR_BODY, &rgba);

	rgba.red *= 0.8;
	rgba.green *= 0.8;
	rgba.blue *= 0.8;
	e_mail_formatter_set_color (
		formatter, E_MAIL_FORMATTER_COLOR_FRAME, &rgba);

	gtk_style_context_restore (style_context);
	gtk_style_context_add_class (style_context, GTK_STYLE_CLASS_ENTRY);

	gtk_style_context_get_color (style_context, state, &rgba);
	e_mail_formatter_set_color (
		formatter, E_MAIL_FORMATTER_COLOR_HEADER, &rgba);

	gtk_style_context_get_background_color (
		style_context, state | GTK_STATE_FLAG_FOCUSED, &rgba);
	e_mail_formatter_set_color  (
		formatter, E_MAIL_FORMATTER_COLOR_CONTENT, &rgba);

	gtk_style_context_get_color (
		style_context, state | GTK_STATE_FLAG_FOCUSED, &rgba);
	e_mail_formatter_set_color (
		formatter, E_MAIL_FORMATTER_COLOR_TEXT, &rgba);

	gtk_widget_path_free (widget_path);
	g_object_unref (style_context);

	g_object_thaw_notify (G_OBJECT (formatter));
}

static void
e_mail_formatter_base_init (EMailFormatterClass *class)
{
	/* Register internal extensions. */
	g_type_ensure (e_mail_formatter_attachment_get_type ());
	g_type_ensure (e_mail_formatter_attachment_bar_get_type ());
	g_type_ensure (e_mail_formatter_error_get_type ());
	g_type_ensure (e_mail_formatter_headers_get_type ());
	g_type_ensure (e_mail_formatter_image_get_type ());
	g_type_ensure (e_mail_formatter_message_rfc822_get_type ());
	g_type_ensure (e_mail_formatter_secure_button_get_type ());
	g_type_ensure (e_mail_formatter_source_get_type ());
	g_type_ensure (e_mail_formatter_text_enriched_get_type ());
	g_type_ensure (e_mail_formatter_text_html_get_type ());
	g_type_ensure (e_mail_formatter_text_plain_get_type ());

	class->extension_registry = g_object_new (
		E_TYPE_MAIL_FORMATTER_EXTENSION_REGISTRY, NULL);

	e_mail_formatter_extension_registry_load (
		class->extension_registry,
		E_TYPE_MAIL_FORMATTER_EXTENSION);

	e_extensible_load_extensions (
		E_EXTENSIBLE (class->extension_registry));

	class->text_html_flags =
		CAMEL_MIME_FILTER_TOHTML_CONVERT_URLS |
		CAMEL_MIME_FILTER_TOHTML_CONVERT_NL |
		CAMEL_MIME_FILTER_TOHTML_CONVERT_SPACES |
		CAMEL_MIME_FILTER_TOHTML_CONVERT_ADDRESSES |
		CAMEL_MIME_FILTER_TOHTML_MARK_CITATION;
}

static void
e_mail_formatter_base_finalize (EMailFormatterClass *class)
{
	g_object_unref (class->extension_registry);
}

static void
e_mail_formatter_class_init (EMailFormatterClass *class)
{
	GObjectClass *object_class;
	GdkRGBA *rgba;

	e_mail_formatter_parent_class = g_type_class_peek_parent (class);
	g_type_class_add_private (class, sizeof (EMailFormatterPrivate));

	object_class = G_OBJECT_CLASS (class);
	object_class->set_property = e_mail_formatter_set_property;
	object_class->get_property = e_mail_formatter_get_property;
	object_class->finalize = e_mail_formatter_finalize;
	object_class->constructed = e_mail_formatter_constructed;

	class->context_size = sizeof (EMailFormatterContext);
	class->run = mail_formatter_run;
	class->update_style = mail_formatter_update_style;

	rgba = &class->colors[E_MAIL_FORMATTER_COLOR_BODY];
	gdk_rgba_parse (rgba, "#eeeeee");

	rgba = &class->colors[E_MAIL_FORMATTER_COLOR_CONTENT];
	gdk_rgba_parse (rgba, "#ffffff");

	rgba = &class->colors[E_MAIL_FORMATTER_COLOR_FRAME];
	gdk_rgba_parse (rgba, "#3f3f3f");

	rgba = &class->colors[E_MAIL_FORMATTER_COLOR_HEADER];
	gdk_rgba_parse (rgba, "#eeeeee");

	rgba = &class->colors[E_MAIL_FORMATTER_COLOR_TEXT];
	gdk_rgba_parse (rgba, "#000000");

	g_object_class_install_property (
		object_class,
		PROP_ANIMATE_IMAGES,
		g_param_spec_boolean (
			"animate-images",
			"Animate images",
			NULL,
			FALSE,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (
		object_class,
		PROP_BODY_COLOR,
		g_param_spec_boxed (
			"body-color",
			"Body Color",
			NULL,
			GDK_TYPE_RGBA,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (
		object_class,
		PROP_CHARSET,
		g_param_spec_string (
			"charset",
			NULL,
			NULL,
			NULL,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (
		object_class,
		PROP_CITATION_COLOR,
		g_param_spec_boxed (
			"citation-color",
			"Citation Color",
			NULL,
			GDK_TYPE_RGBA,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (
		object_class,
		PROP_CONTENT_COLOR,
		g_param_spec_boxed (
			"content-color",
			"Content Color",
			NULL,
			GDK_TYPE_RGBA,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (
		object_class,
		PROP_DEFAULT_CHARSET,
		g_param_spec_string (
			"default-charset",
			NULL,
			NULL,
			NULL,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (
		object_class,
		PROP_FRAME_COLOR,
		g_param_spec_boxed (
			"frame-color",
			"Frame Color",
			NULL,
			GDK_TYPE_RGBA,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (
		object_class,
		PROP_HEADER_COLOR,
		g_param_spec_boxed (
			"header-color",
			"Header Color",
			NULL,
			GDK_TYPE_RGBA,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (
		object_class,
		PROP_IMAGE_LOADING_POLICY,
		g_param_spec_enum (
			"image-loading-policy",
			"Image Loading Policy",
			NULL,
			E_TYPE_MAIL_IMAGE_LOADING_POLICY,
			E_MAIL_IMAGE_LOADING_POLICY_NEVER,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (
		object_class,
		PROP_MARK_CITATIONS,
		g_param_spec_boolean (
			"mark-citations",
			"Mark Citations",
			NULL,
			TRUE,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (
		object_class,
		PROP_SHOW_REAL_DATE,
		g_param_spec_boolean (
			"show-real-date",
			"Show real Date header value",
			NULL,
			TRUE,
			G_PARAM_READWRITE |
			G_PARAM_CONSTRUCT |
			G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (
		object_class,
		PROP_SHOW_SENDER_PHOTO,
		g_param_spec_boolean (
			"show-sender-photo",
			"Show Sender Photo",
			NULL,
			FALSE,
			G_PARAM_READWRITE |
			G_PARAM_CONSTRUCT |
			G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (
		object_class,
		PROP_TEXT_COLOR,
		g_param_spec_boxed (
			"text-color",
			"Text Color",
			NULL,
			GDK_TYPE_COLOR,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	signals[NEED_REDRAW] = g_signal_new (
		"need-redraw",
		E_TYPE_MAIL_FORMATTER,
		G_SIGNAL_RUN_FIRST,
		G_STRUCT_OFFSET (EMailFormatterClass, need_redraw),
		NULL, NULL, NULL,
		G_TYPE_NONE, 0);
}

static void
e_mail_formatter_init (EMailFormatter *formatter)
{
	formatter->priv = E_MAIL_FORMATTER_GET_PRIVATE (formatter);

	g_mutex_init (&formatter->priv->property_lock);
}

static void
e_mail_formatter_extensible_interface_init (EExtensibleInterface *interface)
{

}

EMailFormatter *
e_mail_formatter_new (void)
{
	return g_object_new (E_TYPE_MAIL_FORMATTER, NULL);
}

GType
e_mail_formatter_get_type (void)
{
	static GType type = 0;

	if (G_UNLIKELY (type == 0)) {
		const GTypeInfo type_info = {
			sizeof (EMailFormatterClass),
			(GBaseInitFunc) e_mail_formatter_base_init,
			(GBaseFinalizeFunc) e_mail_formatter_base_finalize,
			(GClassInitFunc) e_mail_formatter_class_init,
			(GClassFinalizeFunc) NULL,
			NULL,	/* class_data */
			sizeof (EMailFormatter),
			0,	/* n_preallocs */
			(GInstanceInitFunc) e_mail_formatter_init,
			NULL	/* value_table */
		};

		const GInterfaceInfo e_extensible_interface_info = {
			(GInterfaceInitFunc) e_mail_formatter_extensible_interface_init
		};

		type = g_type_register_static (
			G_TYPE_OBJECT,
			"EMailFormatter", &type_info, 0);

		g_type_add_interface_static (
			type, E_TYPE_EXTENSIBLE, &e_extensible_interface_info);
	}

	return type;
}

void
e_mail_formatter_format_sync (EMailFormatter *formatter,
                              EMailPartList *part_list,
                              CamelStream *stream,
                              EMailFormatterHeaderFlags flags,
                              EMailFormatterMode mode,
                              GCancellable *cancellable)
{
	EMailFormatterContext *context;
	EMailFormatterClass *class;

	g_return_if_fail (E_IS_MAIL_FORMATTER (formatter));
	/* EMailPartList can be NULL. */
	g_return_if_fail (CAMEL_IS_STREAM (stream));

	class = E_MAIL_FORMATTER_GET_CLASS (formatter);
	g_return_if_fail (class->run != NULL);

	context = mail_formatter_create_context (
		formatter, part_list, mode, flags);

	class->run (formatter, context, stream, cancellable);

	mail_formatter_free_context (context);
}

static void
mail_formatter_format_thread (GSimpleAsyncResult *simple,
                              GObject *source_object,
                              GCancellable *cancellable)
{
	AsyncContext *async_context;

	async_context = g_simple_async_result_get_op_res_gpointer (simple);

	e_mail_formatter_format_sync (
		E_MAIL_FORMATTER (source_object),
		async_context->part_list,
		async_context->stream,
		async_context->flags,
		async_context->mode,
		cancellable);
}

void
e_mail_formatter_format (EMailFormatter *formatter,
                         EMailPartList *part_list,
                         CamelStream *stream,
                         EMailFormatterHeaderFlags flags,
                         EMailFormatterMode mode,
                         GAsyncReadyCallback callback,
                         GCancellable *cancellable,
                         gpointer user_data)
{
	GSimpleAsyncResult *simple;
	AsyncContext *async_context;
	EMailFormatterClass *class;

	g_return_if_fail (E_IS_MAIL_FORMATTER (formatter));
	/* EMailPartList can be NULL. */
	g_return_if_fail (CAMEL_IS_STREAM (stream));

	class = E_MAIL_FORMATTER_GET_CLASS (formatter);
	g_return_if_fail (class->run != NULL);

	async_context = g_slice_new0 (AsyncContext);
	async_context->stream = g_object_ref (stream);
	async_context->flags = flags;
	async_context->mode = mode;

	simple = g_simple_async_result_new (
		G_OBJECT (formatter), callback,
		user_data, e_mail_formatter_format);

	g_simple_async_result_set_check_cancellable (simple, cancellable);

	g_simple_async_result_set_op_res_gpointer (
		simple, async_context, (GDestroyNotify) async_context_free);

	if (part_list != NULL) {
		async_context->part_list = g_object_ref (part_list);

		g_simple_async_result_run_in_thread (
			simple, mail_formatter_format_thread,
			G_PRIORITY_DEFAULT, cancellable);
	} else {
		g_simple_async_result_complete_in_idle (simple);
	}

	g_object_unref (simple);
}

gboolean
e_mail_formatter_format_finish (EMailFormatter *formatter,
                                GAsyncResult *result,
                                GError **error)
{
	GSimpleAsyncResult *simple;

	g_return_val_if_fail (
		g_simple_async_result_is_valid (
		result, G_OBJECT (formatter),
		e_mail_formatter_format), FALSE);

	simple = G_SIMPLE_ASYNC_RESULT (result);

	/* Assume success unless a GError is set. */
	return !g_simple_async_result_propagate_error (simple, error);
}

/**
 * e_mail_formatter_format_as:
 * @formatter: an #EMailFormatter
 * @context: an #EMailFormatterContext
 * @part: an #EMailPart
 * @stream: a #CamelStream
 * @as_mime_type: (allow-none) mime-type to use for formatting, or %NULL
 * @cancellable: (allow-none) an optional #GCancellable
 *
 * Formats given @part using a @formatter extension for given mime type. When
 * the mime type is %NULL, the function will try to lookup the best formatter
 * for given @part by it's default mime type.
 *
 * Return Value: %TRUE on success, %FALSE when no suitable formatter is found or
 * when it fails to format the part. 
 */
gboolean
e_mail_formatter_format_as (EMailFormatter *formatter,
                            EMailFormatterContext *context,
                            EMailPart *part,
                            CamelStream *stream,
                            const gchar *as_mime_type,
                            GCancellable *cancellable)
{
	EMailExtensionRegistry *extension_registry;
	GQueue *formatters;
	gboolean ok;
	d (
		gint _call_i;
		static gint _call = 0;
		G_LOCK_DEFINE_STATIC (_call);
		G_LOCK (_call);
		_call++;
		_call_i = _call;
		G_UNLOCK (_call)
	);

	g_return_val_if_fail (E_IS_MAIL_FORMATTER (formatter), FALSE);
	g_return_val_if_fail (part != NULL, FALSE);
	g_return_val_if_fail (CAMEL_IS_STREAM (stream), FALSE);

	if (as_mime_type == NULL || *as_mime_type == '\0')
		as_mime_type = e_mail_part_get_mime_type (part);

	if (as_mime_type == NULL || *as_mime_type == '\0')
		return FALSE;

	extension_registry =
		e_mail_formatter_get_extension_registry (formatter);
	formatters = e_mail_extension_registry_get_for_mime_type (
		extension_registry, as_mime_type);
	if (formatters == NULL)
		formatters = e_mail_extension_registry_get_fallback (
			extension_registry, as_mime_type);

	ok = FALSE;

	d (
		printf ("(%d) Formatting for part %s of type %s (found %d formatters)\n",
		_call_i, part->id, as_mime_type,
		formatters ? g_queue_get_length (formatters) : 0));

	if (formatters != NULL) {
		GList *head, *link;

		head = g_queue_peek_head_link (formatters);

		for (link = head; link != NULL; link = g_list_next (link)) {
			EMailFormatterExtension *extension;

			extension = link->data;
			if (extension == NULL)
				continue;

			ok = e_mail_formatter_extension_format (
				extension, formatter, context,
				part, stream, cancellable);

			d (
				printf (
					"\t(%d) trying %s...%s\n", _call_i,
					G_OBJECT_TYPE_NAME (extension),
					ok ? "OK" : "failed"));

			if (ok)
				break;
		}
	}

	return ok;
}

/**
 * em_format_format_text:
 * @part: an #EMailPart to decode
 * @formatter: an #EMailFormatter
 * @stream: Where to write the converted text
 * @cancellable: optional #GCancellable object, or %NULL
 *
 * Decode/output a part's content to @stream.
 **/
void
e_mail_formatter_format_text (EMailFormatter *formatter,
                              EMailPart *part,
                              CamelStream *stream,
                              GCancellable *cancellable)
{
	CamelStream *filter_stream;
	CamelMimeFilter *filter;
	const gchar *charset = NULL;
	CamelMimeFilter *windows = NULL;
	CamelStream *mem_stream = NULL;
	CamelMimePart *mime_part;
	CamelContentType *mime_type;

	if (g_cancellable_is_cancelled (cancellable))
		return;

	mime_part = e_mail_part_ref_mime_part (part);
	mime_type = CAMEL_DATA_WRAPPER (mime_part)->mime_type;

	if (formatter->priv->charset != NULL) {
		charset = formatter->priv->charset;
	} else if (mime_type != NULL
		   && (charset = camel_content_type_param (mime_type, "charset"))
		   && g_ascii_strncasecmp (charset, "iso-8859-", 9) == 0) {
		CamelStream *null;

		/* Since a few Windows mailers like to claim they sent
		 * out iso-8859-# encoded text when they really sent
		 * out windows-cp125#, do some simple sanity checking
		 * before we move on... */

		null = camel_stream_null_new ();
		filter_stream = camel_stream_filter_new (null);
		g_object_unref (null);

		windows = camel_mime_filter_windows_new (charset);
		camel_stream_filter_add (
			CAMEL_STREAM_FILTER (filter_stream), windows);

		camel_data_wrapper_decode_to_stream_sync (
			CAMEL_DATA_WRAPPER (mime_part),
			filter_stream, cancellable, NULL);
		camel_stream_flush (filter_stream, cancellable, NULL);
		g_object_unref (filter_stream);

		charset = camel_mime_filter_windows_real_charset (
			CAMEL_MIME_FILTER_WINDOWS (windows));
	} else if (charset == NULL) {
		charset = formatter->priv->default_charset;
	}

	mem_stream = (CamelStream *) camel_stream_mem_new ();
	filter_stream = camel_stream_filter_new (mem_stream);

	filter = camel_mime_filter_charset_new (charset, "UTF-8");
	if (filter != NULL) {
		camel_stream_filter_add (
			CAMEL_STREAM_FILTER (filter_stream), filter);
		g_object_unref (filter);
	}

	camel_data_wrapper_decode_to_stream_sync (
		camel_medium_get_content (CAMEL_MEDIUM (mime_part)),
		filter_stream, cancellable, NULL);
	camel_stream_flush (filter_stream, cancellable, NULL);
	g_object_unref (filter_stream);

	g_seekable_seek (G_SEEKABLE (mem_stream), 0, G_SEEK_SET, NULL, NULL);

	camel_stream_write_to_stream (
		mem_stream, stream, cancellable, NULL);
	camel_stream_flush (mem_stream, cancellable, NULL);

	if (windows != NULL)
		g_object_unref (windows);

	g_object_unref (mem_stream);

	g_object_unref (mime_part);
}

gchar *
e_mail_formatter_get_html_header (EMailFormatter *formatter)
{
	return g_strdup_printf (
		"<!DOCTYPE HTML>\n"
		"<html>\n"
		"<head>\n"
		"<meta name=\"generator\" content=\"Evolution Mail\"/>\n"
		"<title>Evolution Mail Display</title>\n"
		"<link type=\"text/css\" rel=\"stylesheet\" "
		"      href=\"" STYLESHEET_URI "\"/>\n"
		"<style type=\"text/css\">\n"
		"  table th { color: #%06x; font-weight: bold; }\n"
		"</style>\n"
		"</head>"
		"<body bgcolor=\"#%06x\" text=\"#%06x\">",
		e_rgba_to_value (
			e_mail_formatter_get_color (
				formatter, E_MAIL_FORMATTER_COLOR_HEADER)),
		e_rgba_to_value (
			e_mail_formatter_get_color (
				formatter, E_MAIL_FORMATTER_COLOR_BODY)),
		e_rgba_to_value (
			e_mail_formatter_get_color (
				formatter, E_MAIL_FORMATTER_COLOR_TEXT)));
}

EMailExtensionRegistry *
e_mail_formatter_get_extension_registry (EMailFormatter *formatter)
{
	EMailFormatterClass * class;

	g_return_val_if_fail (E_IS_MAIL_FORMATTER (formatter), NULL);

	class = E_MAIL_FORMATTER_GET_CLASS (formatter);
	return E_MAIL_EXTENSION_REGISTRY (class->extension_registry);
}

CamelMimeFilterToHTMLFlags
e_mail_formatter_get_text_format_flags (EMailFormatter *formatter)
{
	g_return_val_if_fail (E_IS_MAIL_FORMATTER (formatter), 0);

	return E_MAIL_FORMATTER_GET_CLASS (formatter)->text_html_flags;
}

const GdkRGBA *
e_mail_formatter_get_color (EMailFormatter *formatter,
                            EMailFormatterColor type)
{
	g_return_val_if_fail (E_IS_MAIL_FORMATTER (formatter), NULL);
	g_return_val_if_fail (type < E_MAIL_FORMATTER_NUM_COLOR_TYPES, NULL);

	return &E_MAIL_FORMATTER_GET_CLASS (formatter)->colors[type];
}

void
e_mail_formatter_set_color (EMailFormatter *formatter,
                            EMailFormatterColor type,
                            const GdkRGBA *color)
{
	GdkRGBA *format_color;
	const gchar *property_name;

	g_return_if_fail (E_IS_MAIL_FORMATTER (formatter));
	g_return_if_fail (type < E_MAIL_FORMATTER_NUM_COLOR_TYPES);
	g_return_if_fail (color != NULL);

	format_color = &E_MAIL_FORMATTER_GET_CLASS (formatter)->colors[type];

	if (gdk_rgba_equal (color, format_color))
		return;

	format_color->red   = color->red;
	format_color->green = color->green;
	format_color->blue  = color->blue;

	switch (type) {
		case E_MAIL_FORMATTER_COLOR_BODY:
			property_name = "body-color";
			break;
		case E_MAIL_FORMATTER_COLOR_CITATION:
			property_name = "citation-color";
			break;
		case E_MAIL_FORMATTER_COLOR_CONTENT:
			property_name = "content-color";
			break;
		case E_MAIL_FORMATTER_COLOR_FRAME:
			property_name = "frame-color";
			break;
		case E_MAIL_FORMATTER_COLOR_HEADER:
			property_name = "header-color";
			break;
		case E_MAIL_FORMATTER_COLOR_TEXT:
			property_name = "text-color";
			break;
		default:
			g_return_if_reached ();
	}

	g_object_notify (G_OBJECT (formatter), property_name);
}

void
e_mail_formatter_update_style (EMailFormatter *formatter,
                               GtkStateFlags state)
{
	EMailFormatterClass *class;

	g_return_if_fail (E_IS_MAIL_FORMATTER (formatter));

	class = E_MAIL_FORMATTER_GET_CLASS (formatter);
	g_return_if_fail (class->update_style != NULL);

	class->update_style (formatter, state);
}

EMailImageLoadingPolicy
e_mail_formatter_get_image_loading_policy (EMailFormatter *formatter)
{
	g_return_val_if_fail (E_IS_MAIL_FORMATTER (formatter), 0);

	return formatter->priv->image_loading_policy;
}

void
e_mail_formatter_set_image_loading_policy (EMailFormatter *formatter,
                                           EMailImageLoadingPolicy policy)
{
	g_return_if_fail (E_IS_MAIL_FORMATTER (formatter));

	if (policy == formatter->priv->image_loading_policy)
		return;

	formatter->priv->image_loading_policy = policy;

	g_object_notify (G_OBJECT (formatter), "image-loading-policy");
}

gboolean
e_mail_formatter_get_mark_citations (EMailFormatter *formatter)
{
	guint32 flags;

	g_return_val_if_fail (E_IS_MAIL_FORMATTER (formatter), FALSE);

	flags = E_MAIL_FORMATTER_GET_CLASS (formatter)->text_html_flags;

	return ((flags & CAMEL_MIME_FILTER_TOHTML_MARK_CITATION) != 0);
}

void
e_mail_formatter_set_mark_citations (EMailFormatter *formatter,
                                     gboolean mark_citations)
{
	g_return_if_fail (E_IS_MAIL_FORMATTER (formatter));

	if (mark_citations)
		E_MAIL_FORMATTER_GET_CLASS (formatter)->text_html_flags |=
			CAMEL_MIME_FILTER_TOHTML_MARK_CITATION;
	else
		E_MAIL_FORMATTER_GET_CLASS (formatter)->text_html_flags &=
			~CAMEL_MIME_FILTER_TOHTML_MARK_CITATION;

	g_object_notify (G_OBJECT (formatter), "mark-citations");
}

gboolean
e_mail_formatter_get_show_sender_photo (EMailFormatter *formatter)
{
	g_return_val_if_fail (E_IS_MAIL_FORMATTER (formatter), FALSE);

	return formatter->priv->show_sender_photo;
}

void
e_mail_formatter_set_show_sender_photo (EMailFormatter *formatter,
                                        gboolean show_sender_photo)
{
	g_return_if_fail (E_IS_MAIL_FORMATTER (formatter));

	if (formatter->priv->show_sender_photo == show_sender_photo)
		return;

	formatter->priv->show_sender_photo = show_sender_photo;

	g_object_notify (G_OBJECT (formatter), "show-sender-photo");
}

gboolean
e_mail_formatter_get_show_real_date (EMailFormatter *formatter)
{
	g_return_val_if_fail (E_IS_MAIL_FORMATTER (formatter), FALSE);

	return formatter->priv->show_real_date;
}

void
e_mail_formatter_set_show_real_date (EMailFormatter *formatter,
                                     gboolean show_real_date)
{
	g_return_if_fail (E_IS_MAIL_FORMATTER (formatter));

	if (formatter->priv->show_real_date == show_real_date)
		return;

	formatter->priv->show_real_date = show_real_date;

	g_object_notify (G_OBJECT (formatter), "show-real-date");
}

gboolean
e_mail_formatter_get_animate_images (EMailFormatter *formatter)
{
	g_return_val_if_fail (E_IS_MAIL_FORMATTER (formatter), FALSE);

	return formatter->priv->animate_images;
}

void
e_mail_formatter_set_animate_images (EMailFormatter *formatter,
                                     gboolean animate_images)
{
	g_return_if_fail (E_IS_MAIL_FORMATTER (formatter));

	if (formatter->priv->animate_images == animate_images)
		return;

	formatter->priv->animate_images = animate_images;

	g_object_notify (G_OBJECT (formatter), "animate-images");
}

const gchar *
e_mail_formatter_get_charset (EMailFormatter *formatter)
{
	g_return_val_if_fail (E_IS_MAIL_FORMATTER (formatter), NULL);

	return formatter->priv->charset;
}

gchar *
e_mail_formatter_dup_charset (EMailFormatter *formatter)
{
	const gchar *protected;
	gchar *duplicate;

	g_return_val_if_fail (E_IS_MAIL_FORMATTER (formatter), NULL);

	g_mutex_lock (&formatter->priv->property_lock);

	protected = e_mail_formatter_get_charset (formatter);
	duplicate = g_strdup (protected);

	g_mutex_unlock (&formatter->priv->property_lock);

	return duplicate;
}

void
e_mail_formatter_set_charset (EMailFormatter *formatter,
                              const gchar *charset)
{
	g_return_if_fail (E_IS_MAIL_FORMATTER (formatter));

	g_mutex_lock (&formatter->priv->property_lock);

	if (g_strcmp0 (formatter->priv->charset, charset) == 0) {
		g_mutex_unlock (&formatter->priv->property_lock);
		return;
	}

	g_free (formatter->priv->charset);
	formatter->priv->charset = g_strdup (charset);

	g_mutex_unlock (&formatter->priv->property_lock);

	g_object_notify (G_OBJECT (formatter), "charset");
}

const gchar *
e_mail_formatter_get_default_charset (EMailFormatter *formatter)
{
	g_return_val_if_fail (E_IS_MAIL_FORMATTER (formatter), NULL);

	return formatter->priv->default_charset;
}

gchar *
e_mail_formatter_dup_default_charset (EMailFormatter *formatter)
{
	const gchar *protected;
	gchar *duplicate;

	g_return_val_if_fail (E_IS_MAIL_FORMATTER (formatter), NULL);

	g_mutex_lock (&formatter->priv->property_lock);

	protected = e_mail_formatter_get_default_charset (formatter);
	duplicate = g_strdup (protected);

	g_mutex_unlock (&formatter->priv->property_lock);

	return duplicate;
}

void
e_mail_formatter_set_default_charset (EMailFormatter *formatter,
                                      const gchar *default_charset)
{
	g_return_if_fail (E_IS_MAIL_FORMATTER (formatter));
	g_return_if_fail (default_charset && *default_charset);

	g_mutex_lock (&formatter->priv->property_lock);

	if (g_strcmp0 (formatter->priv->default_charset, default_charset) == 0) {
		g_mutex_unlock (&formatter->priv->property_lock);
		return;
	}

	g_free (formatter->priv->default_charset);
	formatter->priv->default_charset = g_strdup (default_charset);

	g_mutex_unlock (&formatter->priv->property_lock);

	g_object_notify (G_OBJECT (formatter), "default-charset");
}