/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2010-2011 Red Hat, Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "config.h"

#include <stdio.h>
#include <stdlib.h>

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

#include <gdesktop-enums.h>
#include <gnome-bg/gnome-bg.h>

#include "cc-background-enum-types.h"
#include "cc-background-item.h"
#include "gdesktop-enums-types.h"

typedef struct {
    int width;
    int height;
    int scale_factor;
    GdkPixbuf *thumbnail;
} CachedThumbnail;

struct _CcBackgroundItem {
    GObject parent_instance;

    /* properties */
    char *name;
    char *uri;
    char *uri_dark;
    char *size;
    GDesktopBackgroundStyle placement;
    GDesktopBackgroundShading shading;
    char *primary_color;
    char *secondary_color;
    char *source_url; /* Used by the Flickr source */
    char *source_xml; /* Used by the Wallpapers source */
    gboolean is_deleted;
    gboolean needs_download;
    CcBackgroundItemFlags flags;
    guint64 modified;

    /* internal */
    char *mime_type;
    int width;
    int height;

    CachedThumbnail cached_thumbnail;
    CachedThumbnail cached_thumbnail_dark;
};

enum {
    PROP_0,
    PROP_NAME,
    PROP_URI,
    PROP_URI_DARK,
    PROP_PLACEMENT,
    PROP_SHADING,
    PROP_PRIMARY_COLOR,
    PROP_SECONDARY_COLOR,
    PROP_IS_DELETED,
    PROP_SOURCE_URL,
    PROP_SOURCE_XML,
    PROP_FLAGS,
    PROP_SIZE,
    PROP_NEEDS_DOWNLOAD,
    PROP_MODIFIED,
    N_PROPS
};

static GParamSpec *props[N_PROPS];
static GMutex thread_mutex;
static GQueue thread_queue;
static GThread *thread;

static void cc_background_item_finalize (GObject *object);

G_DEFINE_FINAL_TYPE (CcBackgroundItem, cc_background_item, G_TYPE_OBJECT)

typedef struct {
    GList link;
    GTask *task;
    GTaskThreadFunc func;
} RunInThread;

static gpointer
run_in_thread_worker (gpointer data)
{
    gboolean exiting = FALSE;

    while (!exiting) {
        RunInThread *state;

        g_mutex_lock (&thread_mutex);
        if ((state = g_queue_peek_head (&thread_queue))) {
            g_queue_unlink (&thread_queue, &state->link);
        } else {
            exiting = TRUE;
            thread = NULL;
        }

        g_mutex_unlock (&thread_mutex);

        if (state != NULL) {
            state->func (state->task, g_task_get_source_object (state->task), g_task_get_task_data (state->task),
                         g_task_get_cancellable (state->task));
            g_clear_object (&state->task);
            g_free (state);
        }
    }

    return NULL;
}

static void
run_in_thread (GTask *task, GTaskThreadFunc func)
{
    RunInThread *state;

    g_assert (G_IS_TASK (task));
    g_assert (func != NULL);

    state = g_new0 (RunInThread, 1);
    state->link.data = state;
    state->task = g_object_ref (task);
    state->func = func;

    g_mutex_lock (&thread_mutex);
    g_queue_push_tail_link (&thread_queue, &state->link);
    if (thread == NULL)
        thread = g_thread_new ("[cc-background-item]", run_in_thread_worker, NULL);
    g_mutex_unlock (&thread_mutex);
}

static GnomeBG *
item_to_gnome_bg (CcBackgroundItem *item, gboolean dark)
{
    GnomeBG *bg;
    char *uri;
    g_autoptr(GFile) file = NULL;
    g_autofree gchar *filename = NULL;
    GdkRGBA pcolor = { 0, 0, 0, 0 };
    GdkRGBA scolor = { 0, 0, 0, 0 };

    uri = dark ? item->uri_dark : item->uri;

    g_return_val_if_fail (uri != NULL, NULL);

    bg = gnome_bg_new ();

    file = g_file_new_for_commandline_arg (uri);
    filename = g_file_get_path (file);
    gnome_bg_set_filename (bg, filename);

    if (item->primary_color != NULL) {
        gdk_rgba_parse (&pcolor, item->primary_color);
    }
    if (item->secondary_color != NULL) {
        gdk_rgba_parse (&scolor, item->secondary_color);
    }

    gnome_bg_set_rgba (bg, item->shading, &pcolor, &scolor);
    gnome_bg_set_placement (bg, item->placement);

    return bg;
}

gboolean
cc_background_item_changes_with_time (CcBackgroundItem *item)
{
    g_autoptr(GnomeBG) bg = NULL;
    g_autoptr(GnomeBG) bg_dark = NULL;
    gboolean changes = FALSE;

    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), FALSE);

    if (item->uri != NULL) {
        bg = item_to_gnome_bg (item, FALSE);
        changes |= gnome_bg_changes_with_time (bg);
    }
    if (item->uri_dark != NULL && !changes) {
        bg_dark = item_to_gnome_bg (item, TRUE);
        changes |= gnome_bg_changes_with_time (bg_dark);
    }

    return changes;
}

gboolean
cc_background_item_has_dark_version (CcBackgroundItem *item)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), FALSE);

    return item->uri && item->uri_dark;
}

static void
update_size (CcBackgroundItem *item)
{
    g_autoptr(GnomeBG) bg = NULL;

    g_clear_pointer (&item->size, g_free);

    if (item->uri == NULL) {
        item->size = g_strdup ("");
    } else {
        bg = item_to_gnome_bg (item, FALSE);
        if (gnome_bg_has_multiple_sizes (bg) || gnome_bg_changes_with_time (bg)) {
            item->size = g_strdup (_("multiple sizes"));
        } else {
            gdk_pixbuf_get_file_info (gnome_bg_get_filename (bg), &item->width, &item->height);
            /* translators: 100 × 100px
             * Note that this is not an "x", but U+00D7 MULTIPLICATION SIGN */
            item->size = g_strdup_printf (_("%d × %d"), item->width, item->height);
        }
    }
}

typedef struct {
    GnomeDesktopThumbnailFactory *thumbs;
    GnomeBG *bg;
    GdkRectangle monitor_layout;
    int width;
    int height;
    int scale_factor;
    guint dark : 1;
    guint cached_result : 1;
} GetThumbnailAsync;

static void
get_thumbnail_async_free (gpointer data)
{
    GetThumbnailAsync *state = data;

    g_clear_object (&state->thumbs);
    g_clear_object (&state->bg);
    g_free (state);
}

static void
cc_background_item_get_thumbnail_worker (GTask *task, gpointer source_object, gpointer task_data,
                                         GCancellable *cancellable)
{
    GetThumbnailAsync *state = task_data;
    CcBackgroundItem *item = source_object;
    GdkPixbuf *pixbuf;

    g_assert (G_IS_TASK (task));
    g_assert (CC_IS_BACKGROUND_ITEM (item));
    g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
    g_assert (state != NULL);
    g_assert (state->thumbs != NULL);

    pixbuf = gnome_bg_create_thumbnail (state->bg, state->thumbs, &state->monitor_layout,
                                        state->scale_factor * state->width, state->scale_factor * state->height);

    if (pixbuf != NULL)
        g_task_return_pointer (task, pixbuf, g_object_unref);
    else
        g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to load pixbuf");
}

void
cc_background_item_get_thumbnail_async (CcBackgroundItem *item, GnomeDesktopThumbnailFactory *thumbs, int width,
                                        int height, int scale_factor, gboolean dark, GCancellable *cancellable,
                                        GAsyncReadyCallback callback, gpointer user_data)
{
    g_autoptr(GdkMonitor) monitor = NULL;
    g_autoptr(GTask) task = NULL;
    GetThumbnailAsync *state;
    CachedThumbnail *thumbnail;
    GListModel *monitors;
    GdkDisplay *display;

    g_return_if_fail (CC_IS_BACKGROUND_ITEM (item));
    g_return_if_fail (width > 0 && height > 0);
    g_return_if_fail (thumbs != NULL);
    g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));

    task = g_task_new (item, cancellable, callback, user_data);
    g_task_set_source_tag (task, cc_background_item_get_thumbnail_async);

    state = g_new0 (GetThumbnailAsync, 1);
    g_task_set_task_data (task, state, get_thumbnail_async_free);

    thumbnail = dark ? &item->cached_thumbnail_dark : &item->cached_thumbnail;

    /* Use the cached thumbnail if the sizes match */
    if (thumbnail->thumbnail && thumbnail->width == width && thumbnail->height == height
        && thumbnail->scale_factor == scale_factor) {
        state->cached_result = TRUE;
        g_task_return_pointer (task, g_object_ref (thumbnail->thumbnail), g_object_unref);
        return;
    }

    display = gdk_display_get_default ();
    monitors = gdk_display_get_monitors (display);
    monitor = g_list_model_get_item (monitors, 0);

    state->thumbs = g_object_ref (thumbs);
    gdk_monitor_get_geometry (monitor, &state->monitor_layout);
    state->width = width;
    state->height = height;
    state->scale_factor = scale_factor;
    state->dark = !!dark;
    state->bg = item_to_gnome_bg (item, dark);

    /* g_task_run_in_thread() will use a threadpool which may jump the
     * number of parallel workers to around 10. On low-memory systems, that
     * could cause excessive memory use to the point of paging.
     *
     * What we really want is non-blocking more than massive concurrency,
     * so use a single worker thread but retain GTask usage.
     */
    run_in_thread (task, cc_background_item_get_thumbnail_worker);
}

GdkPixbuf *
cc_background_item_get_thumbnail_finish (CcBackgroundItem *item, GAsyncResult *result, GError **error)
{
    GdkPixbuf *pixbuf;
    GetThumbnailAsync *state;
    CachedThumbnail *thumbnail;

    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), NULL);
    g_return_val_if_fail (G_IS_TASK (result), NULL);

    if (!(pixbuf = g_task_propagate_pointer (G_TASK (result), error)))
        return NULL;

    state = g_task_get_task_data (G_TASK (result));

    if (state->cached_result)
        return pixbuf;

    thumbnail = state->dark ? &item->cached_thumbnail_dark : &item->cached_thumbnail;

    /* Cache the new thumbnail */
    if (g_set_object (&thumbnail->thumbnail, pixbuf)) {
        thumbnail->width = state->width;
        thumbnail->height = state->height;
        thumbnail->scale_factor = state->scale_factor;
    }

    return pixbuf;
}

static void
update_info (CcBackgroundItem *item, GFileInfo *_info)
{
    g_autoptr(GFileInfo) info = NULL;

    if (_info == NULL) {
        g_autoptr(GFile) file = NULL;

        file = g_file_new_for_uri (item->uri);

        info = g_file_query_info (file,
                                  G_FILE_ATTRIBUTE_STANDARD_NAME
                                  "," G_FILE_ATTRIBUTE_STANDARD_SIZE "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE
                                  "," G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME "," G_FILE_ATTRIBUTE_TIME_MODIFIED,
                                  G_FILE_QUERY_INFO_NONE, NULL, NULL);
    } else {
        info = g_object_ref (_info);
    }

    g_clear_pointer (&item->mime_type, g_free);

    if (info == NULL || g_file_info_get_content_type (info) == NULL) {
        if (item->uri == NULL) {
            item->mime_type = g_strdup ("image/x-no-data");
            g_set_str (&item->name, _("No Desktop Background"));
        }
    } else {
        if (item->name == NULL)
            item->name = g_strdup (g_file_info_get_display_name (info));

        item->mime_type = g_strdup (g_file_info_get_content_type (info));
        if (item->modified == 0)
            item->modified = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
    }
}

gboolean
cc_background_item_load (CcBackgroundItem *item, GFileInfo *info)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), FALSE);

    if (item->uri == NULL)
        return TRUE;

    update_info (item, info);

    if (item->mime_type == NULL
        || !(g_str_has_prefix (item->mime_type, "image/") || strcmp (item->mime_type, "application/xml") == 0))
        return FALSE;

    /* FIXME we should handle XML files as well */
    if (item->mime_type != NULL && g_str_has_prefix (item->mime_type, "image/")) {
        g_autofree gchar *filename = NULL;

        filename = g_filename_from_uri (item->uri, NULL, NULL);
        gdk_pixbuf_get_file_info (filename, &item->width, &item->height);
    }

    return TRUE;
}

static void
_set_name (CcBackgroundItem *item, const char *value)
{
    g_set_str (&item->name, value);
}

const char *
cc_background_item_get_name (CcBackgroundItem *item)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), NULL);

    return item->name;
}

static void
_add_flag (CcBackgroundItem *item, CcBackgroundItemFlags flag)
{
    item->flags |= flag;
    g_object_notify_by_pspec (G_OBJECT (item), props[PROP_FLAGS]);
}

static void
_set_uri (CcBackgroundItem *item, const char *value)
{
    g_free (item->uri);
    if (value && *value == '\0') {
        item->uri = NULL;
    } else {
        if (value && strstr (value, "://") == NULL)
            g_warning ("URI '%s' is invalid", value);
        item->uri = g_strdup (value);
    }
    _add_flag (item, CC_BACKGROUND_ITEM_HAS_URI);
}

static void
_set_uri_dark (CcBackgroundItem *item, const char *value)
{
    g_free (item->uri_dark);
    if (value && *value == '\0') {
        item->uri_dark = NULL;
    } else {
        if (value && strstr (value, "://") == NULL)
            g_warning ("URI '%s' is invalid", value);
        item->uri_dark = g_strdup (value);
    }
    _add_flag (item, CC_BACKGROUND_ITEM_HAS_URI_DARK);
}

const char *
cc_background_item_get_uri (CcBackgroundItem *item)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), NULL);

    return item->uri;
}

const char *
cc_background_item_get_uri_dark (CcBackgroundItem *item)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), NULL);

    return item->uri_dark;
}

static void
_set_placement (CcBackgroundItem *item, GDesktopBackgroundStyle value)
{
    item->placement = value;
    _add_flag (item, CC_BACKGROUND_ITEM_HAS_PLACEMENT);
}

static void
_set_shading (CcBackgroundItem *item, GDesktopBackgroundShading value)
{
    item->shading = value;
    _add_flag (item, CC_BACKGROUND_ITEM_HAS_SHADING);
}

static void
_set_primary_color (CcBackgroundItem *item, const char *value)
{
    g_set_str (&item->primary_color, value);
    _add_flag (item, CC_BACKGROUND_ITEM_HAS_PCOLOR);
}

const char *
cc_background_item_get_pcolor (CcBackgroundItem *item)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), NULL);

    return item->primary_color;
}

static void
_set_secondary_color (CcBackgroundItem *item, const char *value)
{
    g_set_str (&item->secondary_color, value);
    _add_flag (item, CC_BACKGROUND_ITEM_HAS_SCOLOR);
}

const char *
cc_background_item_get_scolor (CcBackgroundItem *item)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), NULL);

    return item->secondary_color;
}

GDesktopBackgroundStyle
cc_background_item_get_placement (CcBackgroundItem *item)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), G_DESKTOP_BACKGROUND_STYLE_SCALED);

    return item->placement;
}

GDesktopBackgroundShading
cc_background_item_get_shading (CcBackgroundItem *item)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), G_DESKTOP_BACKGROUND_SHADING_SOLID);

    return item->shading;
}

static void
_set_is_deleted (CcBackgroundItem *item, gboolean value)
{
    item->is_deleted = value;
}

static void
_set_source_url (CcBackgroundItem *item, const char *value)
{
    g_set_str (&item->source_url, value);
}

const char *
cc_background_item_get_source_url (CcBackgroundItem *item)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), NULL);

    return item->source_url;
}

static void
_set_source_xml (CcBackgroundItem *item, const char *value)
{
    g_set_str (&item->source_xml, value);
}

const char *
cc_background_item_get_source_xml (CcBackgroundItem *item)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), NULL);

    return item->source_xml;
}

CcBackgroundItemFlags
cc_background_item_get_flags (CcBackgroundItem *item)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), 0);

    return item->flags;
}

const char *
cc_background_item_get_size (CcBackgroundItem *item)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), NULL);

    update_size (item);

    return item->size;
}

static void
_set_needs_download (CcBackgroundItem *item, gboolean value)
{
    item->needs_download = value;
}

gboolean
cc_background_item_get_needs_download (CcBackgroundItem *item)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), 0);

    return item->needs_download;
}

static void
_set_modified (CcBackgroundItem *item, guint64 value)
{
    item->modified = value;
}

guint64
cc_background_item_get_modified (CcBackgroundItem *item)
{
    g_return_val_if_fail (CC_IS_BACKGROUND_ITEM (item), 0);

    return item->modified;
}

static void
cc_background_item_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
    CcBackgroundItem *self;

    self = CC_BACKGROUND_ITEM (object);

    switch (prop_id) {
    case PROP_NAME:
        _set_name (self, g_value_get_string (value));
        break;
    case PROP_URI:
        _set_uri (self, g_value_get_string (value));
        break;
    case PROP_URI_DARK:
        _set_uri_dark (self, g_value_get_string (value));
        break;
    case PROP_PLACEMENT:
        _set_placement (self, g_value_get_enum (value));
        break;
    case PROP_SHADING:
        _set_shading (self, g_value_get_enum (value));
        break;
    case PROP_PRIMARY_COLOR:
        _set_primary_color (self, g_value_get_string (value));
        break;
    case PROP_SECONDARY_COLOR:
        _set_secondary_color (self, g_value_get_string (value));
        break;
    case PROP_IS_DELETED:
        _set_is_deleted (self, g_value_get_boolean (value));
        break;
    case PROP_SOURCE_URL:
        _set_source_url (self, g_value_get_string (value));
        break;
    case PROP_SOURCE_XML:
        _set_source_xml (self, g_value_get_string (value));
        break;
    case PROP_NEEDS_DOWNLOAD:
        _set_needs_download (self, g_value_get_boolean (value));
        break;
    case PROP_MODIFIED:
        _set_modified (self, g_value_get_uint64 (value));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
cc_background_item_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    CcBackgroundItem *self;

    self = CC_BACKGROUND_ITEM (object);

    switch (prop_id) {
    case PROP_NAME:
        g_value_set_string (value, self->name);
        break;
    case PROP_URI:
        g_value_set_string (value, self->uri);
        break;
    case PROP_URI_DARK:
        g_value_set_string (value, self->uri_dark);
        break;
    case PROP_PLACEMENT:
        g_value_set_enum (value, self->placement);
        break;
    case PROP_SHADING:
        g_value_set_enum (value, self->shading);
        break;
    case PROP_PRIMARY_COLOR:
        g_value_set_string (value, self->primary_color);
        break;
    case PROP_SECONDARY_COLOR:
        g_value_set_string (value, self->secondary_color);
        break;
    case PROP_IS_DELETED:
        g_value_set_boolean (value, self->is_deleted);
        break;
    case PROP_SOURCE_URL:
        g_value_set_string (value, self->source_url);
        break;
    case PROP_SOURCE_XML:
        g_value_set_string (value, self->source_xml);
        break;
    case PROP_FLAGS:
        g_value_set_flags (value, self->flags);
        break;
    case PROP_SIZE:
        update_size (self);
        g_value_set_string (value, self->size);
        break;
    case PROP_NEEDS_DOWNLOAD:
        g_value_set_boolean (value, self->needs_download);
        break;
    case PROP_MODIFIED:
        g_value_set_uint64 (value, self->modified);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static GObject *
cc_background_item_constructor (GType type, guint n_construct_properties, GObjectConstructParam *construct_properties)
{
    CcBackgroundItem *background_item;

    background_item = CC_BACKGROUND_ITEM (G_OBJECT_CLASS (cc_background_item_parent_class)
                                              ->constructor (type, n_construct_properties, construct_properties));

    return G_OBJECT (background_item);
}

static void
cc_background_item_class_init (CcBackgroundItemClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    object_class->get_property = cc_background_item_get_property;
    object_class->set_property = cc_background_item_set_property;
    object_class->constructor = cc_background_item_constructor;
    object_class->finalize = cc_background_item_finalize;

    props[PROP_NAME] = g_param_spec_string ("name", NULL, NULL, NULL, G_PARAM_READWRITE);

    props[PROP_URI] = g_param_spec_string ("uri", NULL, NULL, NULL, G_PARAM_READWRITE);

    props[PROP_URI_DARK] = g_param_spec_string ("uri-dark", NULL, NULL, NULL, G_PARAM_READWRITE);

    props[PROP_PLACEMENT] = g_param_spec_enum ("placement", NULL, NULL, G_DESKTOP_TYPE_BACKGROUND_STYLE,
                                               G_DESKTOP_BACKGROUND_STYLE_SCALED, G_PARAM_READWRITE);

    props[PROP_SHADING] = g_param_spec_enum ("shading", NULL, NULL, G_DESKTOP_TYPE_BACKGROUND_SHADING,
                                             G_DESKTOP_BACKGROUND_SHADING_SOLID, G_PARAM_READWRITE);

    props[PROP_PRIMARY_COLOR] = g_param_spec_string ("primary-color", NULL, NULL, "#000000000000", G_PARAM_READWRITE);

    props[PROP_SECONDARY_COLOR] =
        g_param_spec_string ("secondary-color", NULL, NULL, "#000000000000", G_PARAM_READWRITE);

    props[PROP_IS_DELETED] = g_param_spec_boolean ("is-deleted", NULL, NULL, FALSE, G_PARAM_READWRITE);

    props[PROP_SOURCE_URL] = g_param_spec_string ("source-url", NULL, NULL, NULL, G_PARAM_READWRITE);

    props[PROP_SOURCE_XML] = g_param_spec_string ("source-xml", NULL, NULL, NULL, G_PARAM_READWRITE);

    props[PROP_FLAGS] = g_param_spec_flags ("flags", NULL, NULL, CC_TYPE_BACKGROUND_ITEM_FLAGS, 0, G_PARAM_READABLE);

    props[PROP_SIZE] = g_param_spec_string ("size", NULL, NULL, NULL, G_PARAM_READABLE);

    props[PROP_NEEDS_DOWNLOAD] = g_param_spec_boolean ("needs-download", NULL, NULL, TRUE, G_PARAM_READWRITE);

    props[PROP_MODIFIED] = g_param_spec_uint64 ("modified", NULL, NULL, 0, G_MAXUINT64, 0, G_PARAM_READWRITE);

    g_object_class_install_properties (object_class, N_PROPS, props);
}

static void
cc_background_item_init (CcBackgroundItem *item)
{
    item->shading = G_DESKTOP_BACKGROUND_SHADING_SOLID;
    item->placement = G_DESKTOP_BACKGROUND_STYLE_SCALED;
    item->primary_color = g_strdup ("#000000000000");
    item->secondary_color = g_strdup ("#000000000000");
    item->needs_download = TRUE;
    item->flags = 0;
    item->modified = 0;
}

static void
cc_background_item_finalize (GObject *object)
{
    CcBackgroundItem *item;

    g_return_if_fail (object != NULL);
    g_return_if_fail (CC_IS_BACKGROUND_ITEM (object));

    item = CC_BACKGROUND_ITEM (object);

    g_return_if_fail (item != NULL);

    g_clear_object (&item->cached_thumbnail.thumbnail);
    g_clear_object (&item->cached_thumbnail_dark.thumbnail);
    g_free (item->name);
    g_free (item->uri);
    g_free (item->uri_dark);
    g_free (item->primary_color);
    g_free (item->secondary_color);
    g_free (item->mime_type);
    g_free (item->size);
    g_free (item->source_url);
    g_free (item->source_xml);

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

CcBackgroundItem *
cc_background_item_new (const char *uri)
{
    GObject *object;

    object = g_object_new (CC_TYPE_BACKGROUND_ITEM, "uri", uri, NULL);

    return CC_BACKGROUND_ITEM (object);
}

CcBackgroundItem *
cc_background_item_copy (CcBackgroundItem *item)
{
    CcBackgroundItem *ret;

    ret = cc_background_item_new (item->uri);
    ret->name = g_strdup (item->name);
    ret->size = g_strdup (item->size);
    ret->placement = item->placement;
    ret->shading = item->shading;
    ret->primary_color = g_strdup (item->primary_color);
    ret->secondary_color = g_strdup (item->secondary_color);
    ret->source_url = g_strdup (item->source_url);
    ret->source_xml = g_strdup (item->source_xml);
    ret->is_deleted = item->is_deleted;
    ret->needs_download = item->needs_download;
    ret->flags = item->flags;

    return ret;
}

static const char *
flags_to_str (CcBackgroundItemFlags flag)
{
    GFlagsClass *fclass;
    GFlagsValue *value;

    fclass = G_FLAGS_CLASS (g_type_class_peek (CC_TYPE_BACKGROUND_ITEM_FLAGS));
    value = g_flags_get_first_value (fclass, flag);

    g_assert (value);

    return value->value_nick;
}

static const char *
enum_to_str (GType type, int v)
{
    GEnumClass *eclass;
    GEnumValue *value;

    eclass = G_ENUM_CLASS (g_type_class_peek (type));
    value = g_enum_get_value (eclass, v);

    g_assert (value);

    return value->value_nick;
}

void
cc_background_item_dump (CcBackgroundItem *item)
{
    g_autoptr(GString) flags = NULL;
    int i;

    g_return_if_fail (CC_IS_BACKGROUND_ITEM (item));

    update_size (item);

    g_debug ("name:\t\t\t%s", item->name);
    g_debug ("URI:\t\t\t%s", item->uri ? item->uri : "NULL");
    if (item->size)
        g_debug ("size:\t\t\t'%s'", item->size);
    flags = g_string_new (NULL);
    for (i = 0; i < 5; i++) {
        if (item->flags & (1 << i)) {
            g_string_append (flags, flags_to_str (1 << i));
            g_string_append_c (flags, ' ');
        }
    }
    if (flags->len == 0)
        g_string_append (flags, "-none-");
    g_debug ("flags:\t\t\t%s", flags->str);
    if (item->primary_color)
        g_debug ("pcolor:\t\t\t%s", item->primary_color);
    if (item->secondary_color)
        g_debug ("scolor:\t\t\t%s", item->secondary_color);
    g_debug ("placement:\t\t%s", enum_to_str (G_DESKTOP_TYPE_BACKGROUND_STYLE, item->placement));
    g_debug ("shading:\t\t%s", enum_to_str (G_DESKTOP_TYPE_BACKGROUND_SHADING, item->shading));
    if (item->source_url)
        g_debug ("source URL:\t\t%s", item->source_url);
    if (item->source_xml)
        g_debug ("source XML:\t\t%s", item->source_xml);
    g_debug ("deleted:\t\t%s", item->is_deleted ? "yes" : "no");
    if (item->mime_type)
        g_debug ("mime-type:\t\t%s", item->mime_type);
    g_debug ("dimensions:\t\t%d x %d", item->width, item->height);
    g_debug ("modified: %" G_GUINT64_FORMAT, item->modified);
    g_debug (" ");
}

static gboolean
files_equal (const char *a, const char *b)
{
    g_autoptr(GFile) file1 = NULL;
    g_autoptr(GFile) file2 = NULL;

    if (a == NULL && b == NULL)
        return TRUE;

    if (a == NULL || b == NULL)
        return FALSE;

    file1 = g_file_new_for_commandline_arg (a);
    file2 = g_file_new_for_commandline_arg (b);

    return g_file_equal (file1, file2);
}

static gboolean
colors_equal (const char *a, const char *b)
{
    GdkRGBA color1, color2;

    gdk_rgba_parse (&color1, a);
    gdk_rgba_parse (&color2, b);

    return gdk_rgba_equal (&color1, &color2);
}

gboolean
cc_background_item_compare (CcBackgroundItem *saved, CcBackgroundItem *configured)
{
    CcBackgroundItemFlags flags;

    flags = saved->flags;
    if (flags == 0)
        return FALSE;

    if (flags & CC_BACKGROUND_ITEM_HAS_URI) {
        if (files_equal (saved->uri, configured->uri) == FALSE)
            return FALSE;
    }
    if (flags & CC_BACKGROUND_ITEM_HAS_URI_DARK) {
        if (files_equal (saved->uri_dark, configured->uri_dark) == FALSE)
            return FALSE;
    }
    if (flags & CC_BACKGROUND_ITEM_HAS_SHADING) {
        if (saved->shading != configured->shading)
            return FALSE;
    }
    if (flags & CC_BACKGROUND_ITEM_HAS_PLACEMENT) {
        if (saved->placement != configured->placement)
            return FALSE;
    }
    if (flags & CC_BACKGROUND_ITEM_HAS_PCOLOR) {
        if (colors_equal (saved->primary_color, configured->primary_color) == FALSE) {
            return FALSE;
        }
    }
    if (flags & CC_BACKGROUND_ITEM_HAS_SCOLOR) {
        if (colors_equal (saved->secondary_color, configured->secondary_color) == FALSE) {
            return FALSE;
        }
    }

    return TRUE;
}
