/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
 * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2020 NVIDIA CORPORATION
 *
 * 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 <ctype.h>
#include <glib/gi18n.h>
#include <colord.h>
#include <gdk/gdk.h>
#include <stdlib.h>
#include <lcms2.h>
#include <canberra-gtk.h>
#include <math.h>

#define GNOME_DESKTOP_USE_UNSTABLE_API
#include <libgnome-desktop/gnome-rr.h>

#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif

#include "gnome-settings-bus.h"

#include "gsd-color-manager.h"
#include "gsd-color-state.h"
#include "gcm-edid.h"

#define GSD_DBUS_NAME "org.gnome.SettingsDaemon"
#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon"
#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon"

static void gcm_session_set_gamma_for_all_devices (GsdColorState *state);

static guint gcm_session_get_output_percentage (void);

typedef struct {
  unsigned short vendor;
  unsigned short product;
} EdidInfo;

typedef struct {
  unsigned long pattern;
  EdidInfo info;
} EdidHeader;

static EdidInfo
known_oled[] = {
  // SDC OLED panel in S76 Adder WS and XPS 7590
  { 19587, 41001 },
  // SDC OLED panel in S76 Oryx Pro 10
  { 19587, 16707 },
};

static gboolean
has_oled_builtin_display (void)
{
  gboolean has_oled = FALSE;

  // TODO: The Rust code does a dir walk to find all outputs. Needed?
  FILE *f = fopen("/sys/class/drm/card0-eDP-1/edid", "rb");
  if (f == NULL) {
    return FALSE;
  }

  // Only need the first 12 bytes of the header
  char *edid = malloc(12);
  size_t read_bytes = fread(edid, 1, 12, f);
  fclose(f);

  if (read_bytes == 12) {
    EdidHeader header = { };
    memcpy(&header.pattern, edid, 8);
    memcpy(&header.info.vendor, edid + 8, 2);
    memcpy(&header.info.product, edid + 10, 2);

    // TODO: Endianess swap. Swap here or change the value in the array?
    unsigned short vendor = header.info.vendor;
    header.info.vendor = (vendor >> 8) | (vendor << 8);
    
    if (header.pattern == 0xFFFFFFFFFFFF00) {
      unsigned long known_oled_len = sizeof(known_oled) / sizeof(known_oled[0]);
      for (int i = 0; i < known_oled_len; i++) {
        EdidInfo panel = known_oled[i];
        // TODO: It's also possible to do a `memcmp` here. Requires endianess swap on vendor in `known_oled`
        if (panel.vendor == header.info.vendor && panel.product == header.info.product) {
          has_oled = TRUE;
          break;
        }
      }
    }
  }

  free(edid);

  return has_oled;
}


struct _GsdColorState
{
        GObject          parent;

        GCancellable    *cancellable;
        GsdSessionManager *session;
        CdClient        *client;
        GnomeRRScreen   *state_screen;
        GHashTable      *edid_cache;
        GdkWindow       *gdk_window;
        gboolean         session_is_active;
        GHashTable      *device_assign_hash;
        guint            color_temperature;
        gboolean         oled_builtin_display;
        guint            power_watch_id;
        GDBusProxy      *power_proxy;
};

static void     gsd_color_state_class_init  (GsdColorStateClass *klass);
static void     gsd_color_state_init        (GsdColorState      *color_state);
static void     gsd_color_state_finalize    (GObject            *object);

G_DEFINE_TYPE (GsdColorState, gsd_color_state, G_TYPE_OBJECT)

/* see http://www.oyranos.org/wiki/index.php?title=ICC_Profiles_in_X_Specification_0.3 */
#define GCM_ICC_PROFILE_IN_X_VERSION_MAJOR      0
#define GCM_ICC_PROFILE_IN_X_VERSION_MINOR      3

typedef struct {
        guint32          red;
        guint32          green;
        guint32          blue;
} GnomeRROutputClutItem;

GQuark
gsd_color_state_error_quark (void)
{
        static GQuark quark = 0;
        if (!quark)
                quark = g_quark_from_static_string ("gsd_color_state_error");
        return quark;
}

static GcmEdid *
gcm_session_get_output_edid (GsdColorState *state, GnomeRROutput *output, GError **error)
{
        const guint8 *data;
        gsize size;
        GcmEdid *edid = NULL;
        gboolean ret;

        /* can we find it in the cache */
        edid = g_hash_table_lookup (state->edid_cache,
                                    gnome_rr_output_get_name (output));
        if (edid != NULL) {
                g_object_ref (edid);
                return edid;
        }

        /* parse edid */
        data = gnome_rr_output_get_edid_data (output, &size);
        if (data == NULL || size == 0) {
                g_set_error_literal (error,
                                     GNOME_RR_ERROR,
                                     GNOME_RR_ERROR_UNKNOWN,
                                     "unable to get EDID for output");
                return NULL;
        }
        edid = gcm_edid_new ();
        ret = gcm_edid_parse (edid, data, size, error);
        if (!ret) {
                g_object_unref (edid);
                return NULL;
        }

        /* add to cache */
        g_hash_table_insert (state->edid_cache,
                             g_strdup (gnome_rr_output_get_name (output)),
                             g_object_ref (edid));

        return edid;
}

static CdIcc *
gcm_get_srgb_icc (GError **error)
{
        /* load the reference sRGB profile */
        CdIcc *srgb_icc = cd_icc_new ();
        if (!srgb_icc)
                return NULL;

        if (!cd_icc_create_default_full (srgb_icc,
                                         CD_ICC_LOAD_FLAGS_PRIMARIES,
                                         error)) {
                g_object_unref (srgb_icc);
                return NULL;
        }

        return srgb_icc;
}

static char *
gcm_get_srgb_bytes (gsize *length,
                    GError **error)
{
        g_autoptr(CdIcc) srgb_icc = NULL;
        GBytes *bytes;

        srgb_icc = gcm_get_srgb_icc (error);
        if (srgb_icc == NULL)
                return NULL;

        bytes = cd_icc_save_data (srgb_icc,
                                  CD_ICC_SAVE_FLAGS_NONE,
                                  error);
        if (bytes == NULL)
                return NULL;

        return g_bytes_unref_to_data (bytes, length);
}

static gboolean
gcm_session_screen_set_icc_profile (GsdColorState *state,
                                    GnomeRROutput *output,
                                    const gchar *filename,
                                    GError **error)
{
        gchar *data = NULL;
        gsize length;
        guint version_data;

        g_return_val_if_fail (filename != NULL, FALSE);

        /* wayland */
        if (state->gdk_window == NULL) {
                g_debug ("not setting atom as running under wayland");
                return TRUE;
        }

        g_debug ("setting root window ICC profile atom from %s", filename);

        if (gnome_rr_output_supports_color_transform (output)) {
                /*
                 * If the window system supports color transforms, then
                 * applications should use the standard sRGB color profile and
                 * the window system will take care of converting colors to
                 * match the output device's measured color profile.
                 */
                data = gcm_get_srgb_bytes (&length, error);
                if (data == NULL)
                        return FALSE;
        } else {
                /* get contents of file */
                if (!g_file_get_contents (filename, &data, &length, error))
                        return FALSE;
        }

        /* set profile property */
        gdk_property_change (state->gdk_window,
                             gdk_atom_intern_static_string ("_ICC_PROFILE"),
                             gdk_atom_intern_static_string ("CARDINAL"),
                             8,
                             GDK_PROP_MODE_REPLACE,
                             (const guchar *) data, length);

        /* set version property */
        version_data = GCM_ICC_PROFILE_IN_X_VERSION_MAJOR * 100 +
                        GCM_ICC_PROFILE_IN_X_VERSION_MINOR * 1;
        gdk_property_change (state->gdk_window,
                             gdk_atom_intern_static_string ("_ICC_PROFILE_IN_X_VERSION"),
                             gdk_atom_intern_static_string ("CARDINAL"),
                             8,
                             GDK_PROP_MODE_REPLACE,
                             (const guchar *) &version_data, 1);

        g_free (data);
        return TRUE;
}

void
gsd_color_state_set_temperature (GsdColorState *state, guint temperature)
{
        g_return_if_fail (GSD_IS_COLOR_STATE (state));

        if (state->color_temperature == temperature)
                return;

        state->color_temperature = temperature;
        gcm_session_set_gamma_for_all_devices (state);
}

guint
gsd_color_state_get_temperature (GsdColorState *state)
{
        g_return_val_if_fail (GSD_IS_COLOR_STATE (state), 0);
        return state->color_temperature;
}

static gchar *
gcm_session_get_output_id (GsdColorState *state, GnomeRROutput *output)
{
        const gchar *name;
        const gchar *serial;
        const gchar *vendor;
        GcmEdid *edid = NULL;
        GString *device_id;
        GError *error = NULL;

        /* all output devices are prefixed with this */
        device_id = g_string_new ("xrandr");

        /* get the output EDID if possible */
        edid = gcm_session_get_output_edid (state, output, &error);
        if (edid == NULL) {
                g_debug ("no edid for %s [%s], falling back to connection name",
                         gnome_rr_output_get_name (output),
                         error->message);
                g_error_free (error);
                g_string_append_printf (device_id,
                                        "-%s",
                                        gnome_rr_output_get_name (output));
                goto out;
        }

        /* check EDID data is okay to use */
        vendor = gcm_edid_get_vendor_name (edid);
        name = gcm_edid_get_monitor_name (edid);
        serial = gcm_edid_get_serial_number (edid);
        if (vendor == NULL && name == NULL && serial == NULL) {
                g_debug ("edid invalid for %s, falling back to connection name",
                         gnome_rr_output_get_name (output));
                g_string_append_printf (device_id,
                                        "-%s",
                                        gnome_rr_output_get_name (output));
                goto out;
        }

        /* use EDID data */
        if (vendor != NULL)
                g_string_append_printf (device_id, "-%s", vendor);
        if (name != NULL)
                g_string_append_printf (device_id, "-%s", name);
        if (serial != NULL)
                g_string_append_printf (device_id, "-%s", serial);
out:
        if (edid != NULL)
                g_object_unref (edid);
        return g_string_free (device_id, FALSE);
}

typedef struct {
        GsdColorState         *state;
        CdProfile               *profile;
        CdDevice                *device;
        guint32                  output_id;
} GcmSessionAsyncHelper;

static void
gcm_session_async_helper_free (GcmSessionAsyncHelper *helper)
{
        if (helper->state != NULL)
                g_object_unref (helper->state);
        if (helper->profile != NULL)
                g_object_unref (helper->profile);
        if (helper->device != NULL)
                g_object_unref (helper->device);
        g_free (helper);
}

static gboolean
gcm_utils_mkdir_for_filename (GFile *file, GError **error)
{
        gboolean ret = FALSE;
        GFile *parent_dir = NULL;

        /* get parent directory */
        parent_dir = g_file_get_parent (file);
        if (parent_dir == NULL) {
                g_set_error_literal (error,
                                     GSD_COLOR_MANAGER_ERROR,
                                     GSD_COLOR_MANAGER_ERROR_FAILED,
                                     "could not get parent dir");
                goto out;
        }

        /* ensure desination does not already exist */
        ret = g_file_query_exists (parent_dir, NULL);
        if (ret)
                goto out;
        ret = g_file_make_directory_with_parents (parent_dir, NULL, error);
        if (!ret)
                goto out;
out:
        if (parent_dir != NULL)
                g_object_unref (parent_dir);
        return ret;
}

static gboolean
gcm_get_system_icc_profile (GsdColorState *state,
                            GFile *file)
{
        const char efi_path[] = "/sys/firmware/efi/efivars/INTERNAL_PANEL_COLOR_INFO-01e1ada1-79f2-46b3-8d3e-71fc0996ca6b";
        /* efi variables have a 4-byte header */
        const int efi_var_header_length = 4;
        g_autoptr(GFile) efi_file = g_file_new_for_path (efi_path);
        gboolean ret;
        g_autofree char *data = NULL;
        gsize length;
        g_autoptr(GError) error = NULL;

        ret = g_file_query_exists (efi_file, NULL);
        if (!ret)
                return FALSE;

        ret = g_file_load_contents (efi_file,
                                    NULL /* cancellable */,
                                    &data,
                                    &length,
                                    NULL /* etag_out */,
                                    &error);

        if (!ret) {
                g_warning ("failed to read EFI system color profile: %s",
                           error->message);
                return FALSE;
        }

        if (length <= efi_var_header_length) {
                g_warning ("EFI system color profile was too short");
                return FALSE;
        }

        ret = g_file_replace_contents (file,
                                       data + efi_var_header_length,
                                       length - efi_var_header_length,
                                       NULL /* etag */,
                                       FALSE /* make_backup */,
                                       G_FILE_CREATE_NONE,
                                       NULL /* new_etag */,
                                       NULL /* cancellable */,
                                       &error);
        if (!ret) {
                g_warning ("failed to write system color profile: %s",
                           error->message);
                return FALSE;
        }

        return TRUE;
}

static gboolean
gcm_apply_create_icc_profile_for_edid (GsdColorState *state,
                                       CdDevice *device,
                                       GcmEdid *edid,
                                       GFile *file,
                                       GError **error)
{
        CdIcc *icc = NULL;
        const gchar *data;
        gboolean ret = FALSE;

        /* ensure the per-user directory exists */
        ret = gcm_utils_mkdir_for_filename (file, error);
        if (!ret)
                goto out;

        /* create our generated profile */
        icc = cd_icc_new ();
        ret = cd_icc_create_from_edid (icc,
                                       gcm_edid_get_gamma (edid),
                                       gcm_edid_get_red (edid),
                                       gcm_edid_get_green (edid),
                                       gcm_edid_get_blue (edid),
                                       gcm_edid_get_white (edid),
                                       error);
        if (!ret)
                goto out;

        /* set copyright */
        cd_icc_set_copyright (icc, NULL,
                              /* deliberately not translated */
                              "This profile is free of known copyright restrictions.");

        /* set model and title */
        data = gcm_edid_get_monitor_name (edid);
        if (data == NULL)
                data = cd_client_get_system_model (state->client);
        if (data == NULL)
                data = "Unknown monitor";
        cd_icc_set_model (icc, NULL, data);
        cd_icc_set_description (icc, NULL, data);

        /* get manufacturer */
        data = gcm_edid_get_vendor_name (edid);
        if (data == NULL)
                data = cd_client_get_system_vendor (state->client);
        if (data == NULL)
                data = "Unknown vendor";
        cd_icc_set_manufacturer (icc, NULL, data);

        /* set the framework creator metadata */
        cd_icc_add_metadata (icc,
                             CD_PROFILE_METADATA_CMF_PRODUCT,
                             PACKAGE_NAME);
        cd_icc_add_metadata (icc,
                             CD_PROFILE_METADATA_CMF_BINARY,
                             PACKAGE_NAME);
        cd_icc_add_metadata (icc,
                             CD_PROFILE_METADATA_CMF_VERSION,
                             PACKAGE_VERSION);
        cd_icc_add_metadata (icc,
                             CD_PROFILE_METADATA_MAPPING_DEVICE_ID,
                             cd_device_get_id (device));

        /* set 'ICC meta Tag for Monitor Profiles' data */
        cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_MD5, gcm_edid_get_checksum (edid));
        data = gcm_edid_get_monitor_name (edid);
        if (data != NULL)
                cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_MODEL, data);
        data = gcm_edid_get_serial_number (edid);
        if (data != NULL)
                cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_SERIAL, data);
        data = gcm_edid_get_pnp_id (edid);
        if (data != NULL)
                cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_MNFT, data);
        data = gcm_edid_get_vendor_name (edid);
        if (data != NULL)
                cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_VENDOR, data);

        /* save */
        ret = cd_icc_save_file (icc, file, CD_ICC_SAVE_FLAGS_NONE, NULL, error);
        if (!ret)
                goto out;
out:
        if (icc != NULL)
                g_object_unref (icc);
        return ret;
}

static uint64_t
gcm_double_to_ctmval (double value)
{
        uint64_t sign = value < 0;
        double integer, fractional;

        if (sign)
                value = -value;

        fractional = modf (value, &integer);

        return sign << 63 |
               (uint64_t) integer << 32 |
               (uint64_t) (fractional * 0xffffffffUL);
}

static GnomeRRCTM
gcm_mat33_to_ctm (CdMat3x3 matrix)
{
        GnomeRRCTM ctm;

        /*
         * libcolord generates a matrix containing double values. RandR's CTM
         * property expects values in S31.32 fixed-point sign-magnitude format
         */
        ctm.matrix[0] = gcm_double_to_ctmval (matrix.m00);
        ctm.matrix[1] = gcm_double_to_ctmval (matrix.m01);
        ctm.matrix[2] = gcm_double_to_ctmval (matrix.m02);
        ctm.matrix[3] = gcm_double_to_ctmval (matrix.m10);
        ctm.matrix[4] = gcm_double_to_ctmval (matrix.m11);
        ctm.matrix[5] = gcm_double_to_ctmval (matrix.m12);
        ctm.matrix[6] = gcm_double_to_ctmval (matrix.m20);
        ctm.matrix[7] = gcm_double_to_ctmval (matrix.m21);
        ctm.matrix[8] = gcm_double_to_ctmval (matrix.m22);

        return ctm;
}

static gboolean
gcm_set_csc_matrix (GnomeRROutput *output,
                    CdProfile *profile,
                    GError **error)
{
        g_autoptr(CdIcc) icc = NULL;
        g_autoptr(CdIcc) srgb_icc = NULL;
        CdMat3x3 csc;
        GnomeRRCTM ctm;

        /* open file */
        icc = cd_profile_load_icc (profile, CD_ICC_LOAD_FLAGS_PRIMARIES, NULL,
                                   error);
        if (icc == NULL)
                return FALSE;

        srgb_icc = gcm_get_srgb_icc (error);
        if (srgb_icc == NULL)
                return FALSE;

        if (!cd_icc_utils_get_adaptation_matrix (icc, srgb_icc, &csc, error))
                return FALSE;

        ctm = gcm_mat33_to_ctm (csc);
        return gnome_rr_output_set_color_transform (output, ctm, error);
}

static GPtrArray *
gcm_session_generate_vcgt (CdProfile *profile, guint color_temperature, guint size)
{
        GnomeRROutputClutItem *tmp;
        GPtrArray *array = NULL;
        const cmsToneCurve **vcgt;
        cmsFloat32Number in;
        guint i;
        cmsHPROFILE lcms_profile;
        CdIcc *icc = NULL;
        CdColorRGB temp;

        /* invalid size */
        if (size == 0)
                goto out;

        /* open file */
        icc = cd_profile_load_icc (profile, CD_ICC_LOAD_FLAGS_NONE, NULL, NULL);
        if (icc == NULL)
                goto out;

        /* get tone curves from profile */
        lcms_profile = cd_icc_get_handle (icc);
        vcgt = cmsReadTag (lcms_profile, cmsSigVcgtTag);
        if (vcgt == NULL || vcgt[0] == NULL) {
                g_debug ("profile does not have any VCGT data");
                goto out;
        }

        /* get the color temperature */
        if (!cd_color_get_blackbody_rgb_full (color_temperature,
                                              &temp,
                                              CD_COLOR_BLACKBODY_FLAG_USE_PLANCKIAN)) {
                g_warning ("failed to get blackbody for %uK", color_temperature);
                cd_color_rgb_set (&temp, 1.0, 1.0, 1.0);
        } else {
                g_debug ("using VCGT gamma of %uK = %.1f,%.1f,%.1f",
                         color_temperature, temp.R, temp.G, temp.B);
        }

        /* create array */
        array = g_ptr_array_new_with_free_func (g_free);
        for (i = 0; i < size; i++) {
                in = (gdouble) i / (gdouble) (size - 1);
                tmp = g_new0 (GnomeRROutputClutItem, 1);
                tmp->red = cmsEvalToneCurveFloat(vcgt[0], in) * temp.R * (gdouble) 0xffff;
                tmp->green = cmsEvalToneCurveFloat(vcgt[1], in) * temp.G * (gdouble) 0xffff;
                tmp->blue = cmsEvalToneCurveFloat(vcgt[2], in) * temp.B * (gdouble) 0xffff;
                g_ptr_array_add (array, tmp);
        }
out:
        if (icc != NULL)
                g_object_unref (icc);
        return array;
}

static guint
gnome_rr_output_get_gamma_size (GnomeRROutput *output)
{
        GnomeRRCrtc *crtc;
        gint len = 0;

        crtc = gnome_rr_output_get_crtc (output);
        if (crtc == NULL)
                return 0;
        gnome_rr_crtc_get_gamma (crtc,
                                 &len,
                                 NULL, NULL, NULL);
        return (guint) len;
}

static gboolean
gcm_session_output_set_gamma (GnomeRROutput *output,
                              GPtrArray *array,
                              gboolean oled_display,
                              GError **error)
{
        gboolean ret = TRUE;
        guint16 *red = NULL;
        guint16 *green = NULL;
        guint16 *blue = NULL;
        guint i;
        GnomeRROutputClutItem *data;
        GnomeRRCrtc *crtc;

        /* no length? */
        if (array->len == 0) {
                ret = FALSE;
                g_set_error_literal (error,
                                     GSD_COLOR_MANAGER_ERROR,
                                     GSD_COLOR_MANAGER_ERROR_FAILED,
                                     "no data in the CLUT array");
                goto out;
        }

        g_debug ("oled_display %d", oled_display);
        gdouble oled_brightness = 1.0;
        if (oled_display) {
            double ratio = (double)gcm_session_get_output_percentage() / 100.0;
            oled_brightness = 0.6 * ratio + 0.4;
        }
        g_debug ("oled_brightness %f", oled_brightness);

        /* convert to a type X understands */
        red = g_new (guint16, array->len);
        green = g_new (guint16, array->len);
        blue = g_new (guint16, array->len);
        for (i = 0; i < array->len; i++) {
                data = g_ptr_array_index (array, i);
                red[i] = (guint16)((gdouble)data->red * oled_brightness);
                green[i] = (guint16)((gdouble)data->green * oled_brightness);
                blue[i] = (guint16)((gdouble)data->blue * oled_brightness);
        }

        /* send to LUT */
        crtc = gnome_rr_output_get_crtc (output);
        if (crtc == NULL) {
                ret = FALSE;
                g_set_error (error,
                             GSD_COLOR_MANAGER_ERROR,
                             GSD_COLOR_MANAGER_ERROR_FAILED,
                             "failed to get ctrc for %s",
                             gnome_rr_output_get_name (output));
                goto out;
        }
        gnome_rr_crtc_set_gamma (crtc, array->len,
                                 red, green, blue);
out:
        g_free (red);
        g_free (green);
        g_free (blue);
        return ret;
}

static gboolean
gcm_session_device_set_gamma (GnomeRROutput *output,
                              CdProfile *profile,
                              guint color_temperature,
                              gboolean oled_display,
                              GError **error)
{
        gboolean ret = FALSE;
        guint size;
        GPtrArray *clut = NULL;

        /* create a lookup table */
        size = gnome_rr_output_get_gamma_size (output);
        if (size == 0) {
                ret = TRUE;
                goto out;
        }
        clut = gcm_session_generate_vcgt (profile, color_temperature, size);
        if (clut == NULL) {
                g_set_error_literal (error,
                                     GSD_COLOR_MANAGER_ERROR,
                                     GSD_COLOR_MANAGER_ERROR_FAILED,
                                     "failed to generate vcgt");
                goto out;
        }

        /* apply the vcgt to this output */
        ret = gcm_session_output_set_gamma (output, clut, oled_display, error);
        if (!ret)
                goto out;
out:
        if (clut != NULL)
                g_ptr_array_unref (clut);
        return ret;
}

static gboolean
gcm_session_device_reset_gamma (GnomeRROutput *output,
                                guint color_temperature,
                                gboolean oled_display,
                                GError **error)
{
        gboolean ret;
        guint i;
        guint size;
        guint32 value;
        GPtrArray *clut;
        GnomeRROutputClutItem *data;
        CdColorRGB temp;

        /* create a linear ramp */
        g_debug ("falling back to dummy ramp");
        clut = g_ptr_array_new_with_free_func (g_free);
        size = gnome_rr_output_get_gamma_size (output);
        if (size == 0) {
                ret = TRUE;
                goto out;
        }

        /* get the color temperature */
        if (!cd_color_get_blackbody_rgb_full (color_temperature,
                                              &temp,
                                              CD_COLOR_BLACKBODY_FLAG_USE_PLANCKIAN)) {
                g_warning ("failed to get blackbody for %uK", color_temperature);
                cd_color_rgb_set (&temp, 1.0, 1.0, 1.0);
        } else {
                g_debug ("using reset gamma of %uK = %.1f,%.1f,%.1f",
                         color_temperature, temp.R, temp.G, temp.B);
        }

        for (i = 0; i < size; i++) {
                value = (i * 0xffff) / (size - 1);
                data = g_new0 (GnomeRROutputClutItem, 1);
                data->red = value * temp.R;
                data->green = value * temp.G;
                data->blue = value * temp.B;
                g_ptr_array_add (clut, data);
        }

        /* apply the vcgt to this output */
        ret = gcm_session_output_set_gamma (output, clut, oled_display, error);
        if (!ret)
                goto out;
out:
        g_ptr_array_unref (clut);
        return ret;
}

static GnomeRROutput *
gcm_session_get_state_output_by_id (GsdColorState *state,
                                    const gchar *device_id,
                                    GError **error)
{
        gchar *output_id;
        GnomeRROutput *output = NULL;
        GnomeRROutput **outputs = NULL;
        guint i;

        /* search all STATE outputs for the device id */
        outputs = gnome_rr_screen_list_outputs (state->state_screen);
        if (outputs == NULL) {
                g_set_error_literal (error,
                                     GSD_COLOR_MANAGER_ERROR,
                                     GSD_COLOR_MANAGER_ERROR_FAILED,
                                     "Failed to get outputs");
                goto out;
        }
        for (i = 0; outputs[i] != NULL && output == NULL; i++) {
                output_id = gcm_session_get_output_id (state, outputs[i]);
                if (g_strcmp0 (output_id, device_id) == 0)
                        output = outputs[i];
                g_free (output_id);
        }
        if (output == NULL) {
                g_set_error (error,
                             GSD_COLOR_MANAGER_ERROR,
                             GSD_COLOR_MANAGER_ERROR_FAILED,
                             "Failed to find output %s",
                             device_id);
        }
out:
        return output;
}

/* this function is more complicated than it should be, due to the
 * fact that XOrg sometimes assigns no primary devices when using
 * "xrandr --auto" or when the version of RANDR is < 1.3 */
static gboolean
gcm_session_use_output_profile_for_screen (GsdColorState *state,
                                           GnomeRROutput *output)
{
        gboolean has_laptop = FALSE;
        gboolean has_primary = FALSE;
        GnomeRROutput **outputs;
        GnomeRROutput *connected = NULL;
        guint i;

        /* do we have any screens marked as primary */
        outputs = gnome_rr_screen_list_outputs (state->state_screen);
        if (outputs == NULL || outputs[0] == NULL) {
                g_warning ("failed to get outputs");
                return FALSE;
        }
        for (i = 0; outputs[i] != NULL; i++) {
                if (connected == NULL)
                        connected = outputs[i];
                if (gnome_rr_output_get_is_primary (outputs[i]))
                        has_primary = TRUE;
                if (gnome_rr_output_is_builtin_display (outputs[i]))
                        has_laptop = TRUE;
        }

        /* we have an assigned primary device, are we that? */
        if (has_primary)
                return gnome_rr_output_get_is_primary (output);

        /* choosing the internal panel is probably sane */
        if (has_laptop)
                return gnome_rr_output_is_builtin_display (output);

        /* we have to choose one, so go for the first connected device */
        if (connected != NULL)
                return gnome_rr_output_get_id (connected) == gnome_rr_output_get_id (output);

        return FALSE;
}

/* TODO: remove when we can dep on a released version of colord */
#ifndef CD_PROFILE_METADATA_SCREEN_BRIGHTNESS
#define CD_PROFILE_METADATA_SCREEN_BRIGHTNESS		"SCREEN_brightness"
#endif

#define GSD_DBUS_NAME_POWER		GSD_DBUS_NAME ".Power"
#define GSD_DBUS_INTERFACE_POWER_SCREEN	GSD_DBUS_BASE_INTERFACE ".Power.Screen"
#define GSD_DBUS_PATH_POWER		GSD_DBUS_PATH "/Power"

static guint
gcm_session_get_output_percentage (void) {
    guint percentage = 100;

    GDBusConnection * connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
    if (connection != NULL) {
        GVariant * variant = g_dbus_connection_call_sync (connection,
            GSD_DBUS_NAME_POWER,
            GSD_DBUS_PATH_POWER,
            "org.freedesktop.DBus.Properties",
            "Get",
            g_variant_new_parsed ("('" GSD_DBUS_INTERFACE_POWER_SCREEN "',"
                                  "'Brightness')"),
            NULL,
            G_DBUS_CALL_FLAGS_NONE,
            -1, NULL, NULL
        );
        if (variant != NULL) {
            GVariant * inner = NULL;
            g_variant_get (variant, "(v)", &inner);
            if (inner != NULL) {
                gint32 value = g_variant_get_int32(inner);
                if (value >= 0) {
                    percentage = (guint)value;
                }
                g_variant_unref (inner);
            }
            g_variant_unref(variant);
        }
        g_object_unref (connection);
    }

    return percentage;
}

static void
gcm_session_set_output_percentage (guint percentage)
{
        GDBusConnection *connection;

        /* get a ref to the existing bus connection */
        connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
        if (connection == NULL)
                return;
        g_dbus_connection_call (connection,
                                GSD_DBUS_NAME_POWER,
                                GSD_DBUS_PATH_POWER,
                                "org.freedesktop.DBus.Properties",
                                "Set",
                                g_variant_new_parsed ("('" GSD_DBUS_INTERFACE_POWER_SCREEN "',"
                                                      "'Brightness', %v)",
                                                      g_variant_new_int32 (percentage)),
                                NULL,
                                G_DBUS_CALL_FLAGS_NONE,
                                -1, NULL, NULL, NULL);
        g_object_unref (connection);
}

static void
gcm_session_device_assign_profile_connect_cb (GObject *object,
                                              GAsyncResult *res,
                                              gpointer user_data)
{
        CdProfile *profile = CD_PROFILE (object);
        const gchar *brightness_profile;
        const gchar *filename;
        gboolean ret;
        GError *error = NULL;
        GnomeRROutput *output;
        guint brightness_percentage;
        GcmSessionAsyncHelper *helper = (GcmSessionAsyncHelper *) user_data;
        GsdColorState *state = GSD_COLOR_STATE (helper->state);

        /* get properties */
        ret = cd_profile_connect_finish (profile, res, &error);
        if (!ret) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        g_warning ("failed to connect to profile: %s", error->message);
                g_error_free (error);
                goto out;
        }

        /* get the filename */
        filename = cd_profile_get_filename (profile);
        g_assert (filename != NULL);

        /* get the output (can't save in helper as GnomeRROutput isn't
         * a GObject, just a pointer */
        output = gnome_rr_screen_get_output_by_id (state->state_screen,
                                                   helper->output_id);
        if (output == NULL)
                goto out;

        /* if output is a laptop screen and the profile has a
         * calibration brightness then set this new brightness */
        brightness_profile = cd_profile_get_metadata_item (profile,
                                                           CD_PROFILE_METADATA_SCREEN_BRIGHTNESS);
        gboolean oled_display = FALSE;
        if (gnome_rr_output_is_builtin_display (output)) {
            if (brightness_profile != NULL) {
                /* the percentage is stored in the profile metadata as
                 * a string, not ideal, but it's all we have... */
                brightness_percentage = atoi (brightness_profile);
                gcm_session_set_output_percentage (brightness_percentage);
            }
            oled_display = state->oled_builtin_display;
        }

        /* set the _ICC_PROFILE atom */
        ret = gcm_session_use_output_profile_for_screen (state, output);
        if (ret) {
                ret = gcm_session_screen_set_icc_profile (state,
                                                          output,
                                                          filename,
                                                          &error);
                if (!ret) {
                        g_warning ("failed to set screen _ICC_PROFILE: %s",
                                   error->message);
                        g_clear_error (&error);
                }
        }

        /* create a vcgt for this icc file */
        ret = cd_profile_get_has_vcgt (profile);
        if (ret) {
                ret = gcm_session_device_set_gamma (output,
                                                    profile,
                                                    state->color_temperature,
                                                    oled_display,
                                                    &error);
                if (!ret) {
                        g_warning ("failed to set %s gamma tables: %s",
                                   cd_device_get_id (helper->device),
                                   error->message);
                        g_error_free (error);
                        goto out;
                }
        } else {
                ret = gcm_session_device_reset_gamma (output,
                                                      state->color_temperature,
                                                      oled_display,
                                                      &error);
                if (!ret) {
                        g_warning ("failed to reset %s gamma tables: %s",
                                   cd_device_get_id (helper->device),
                                   error->message);
                        g_error_free (error);
                        goto out;
                }
        }

        if (gnome_rr_output_supports_color_transform (output)) {
                ret = gcm_set_csc_matrix (output,
                                          profile,
                                          &error);
                if (!ret) {
                        g_warning ("failed to set %s color transform matrix: %s",
                                   cd_device_get_id (helper->device),
                                   error->message);
                        g_error_free (error);
                        goto out;
                }
        }

out:
        gcm_session_async_helper_free (helper);
}

/*
 * Check to see if the on-disk profile has the MAPPING_device_id
 * metadata, and if not, we should delete the profile and re-create it
 * so that it gets mapped by the daemon.
 */
static gboolean
gcm_session_check_profile_device_md (GFile *file)
{
        const gchar *key_we_need = CD_PROFILE_METADATA_MAPPING_DEVICE_ID;
        CdIcc *icc;
        gboolean ret;

        icc = cd_icc_new ();
        ret = cd_icc_load_file (icc, file, CD_ICC_LOAD_FLAGS_METADATA, NULL, NULL);
        if (!ret)
                goto out;
        ret = cd_icc_get_metadata_item (icc, key_we_need) != NULL;
        if (!ret) {
                g_debug ("auto-edid profile is old, and contains no %s data",
                         key_we_need);
        }
out:
        g_object_unref (icc);
        return ret;
}

static void
gcm_session_device_assign_connect_cb (GObject *object,
                                      GAsyncResult *res,
                                      gpointer user_data)
{
        CdDeviceKind kind;
        CdProfile *profile = NULL;
        gboolean ret;
        gchar *autogen_filename = NULL;
        gchar *autogen_path = NULL;
        GcmEdid *edid = NULL;
        GnomeRROutput *output = NULL;
        GError *error = NULL;
        GFile *file = NULL;
        const gchar *xrandr_id;
        GcmSessionAsyncHelper *helper;
        CdDevice *device = CD_DEVICE (object);
        GsdColorState *state = GSD_COLOR_STATE (user_data);

        /* remove from assign array */
        g_hash_table_remove (state->device_assign_hash,
                             cd_device_get_object_path (device));

        /* get properties */
        ret = cd_device_connect_finish (device, res, &error);
        if (!ret) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        g_warning ("failed to connect to device: %s", error->message);
                g_error_free (error);
                goto out;
        }

        /* check we care */
        kind = cd_device_get_kind (device);
        if (kind != CD_DEVICE_KIND_DISPLAY)
                goto out;

        g_debug ("need to assign display device %s",
                 cd_device_get_id (device));

        /* get the GnomeRROutput for the device id */
        xrandr_id = cd_device_get_id (device);
        output = gcm_session_get_state_output_by_id (state,
                                                   xrandr_id,
                                                   &error);
        if (output == NULL) {
                g_warning ("no %s device found: %s",
                           cd_device_get_id (device),
                           error->message);
                g_error_free (error);
                goto out;
        }

        /* create profile from device edid if it exists */
        edid = gcm_session_get_output_edid (state, output, &error);
        if (edid == NULL) {
                g_warning ("unable to get EDID for %s: %s",
                           cd_device_get_id (device),
                           error->message);
                g_clear_error (&error);

        } else {
                autogen_filename = g_strdup_printf ("edid-%s.icc",
                                                    gcm_edid_get_checksum (edid));
                autogen_path = g_build_filename (g_get_user_data_dir (),
                                                 "icc", autogen_filename, NULL);

                /* check if auto-profile has up-to-date metadata */
                file = g_file_new_for_path (autogen_path);
                if (gcm_session_check_profile_device_md (file)) {
                        g_debug ("auto-profile edid %s exists with md", autogen_path);
                } else {
                        g_debug ("auto-profile edid does not exist, creating as %s",
                                 autogen_path);

                        /* check if the system has a built-in profile */
                        ret = gnome_rr_output_is_builtin_display (output) &&
                              gcm_get_system_icc_profile (state, file);

                        /* try creating one from the EDID */
                        if (!ret) {
                                ret = gcm_apply_create_icc_profile_for_edid (state,
                                                                             device,
                                                                             edid,
                                                                             file,
                                                                             &error);
                        }

                        if (!ret) {
                                g_warning ("failed to create profile from EDID data: %s",
                                             error->message);
                                g_clear_error (&error);
                        }
                }
        }

        /* get the default profile for the device */
        profile = cd_device_get_default_profile (device);
        if (profile == NULL) {
                g_debug ("%s has no default profile to set",
                         cd_device_get_id (device));

                /* the default output? */
                if (gnome_rr_output_get_is_primary (output) &&
                    state->gdk_window != NULL) {
                        gdk_property_delete (state->gdk_window,
                                             gdk_atom_intern_static_string ("_ICC_PROFILE"));
                        gdk_property_delete (state->gdk_window,
                                             gdk_atom_intern_static_string ("_ICC_PROFILE_IN_X_VERSION"));
                }

                gboolean oled_display = FALSE;
                if (gnome_rr_output_is_builtin_display (output)) {
                    oled_display = state->oled_builtin_display;
                }

                /* reset, as we want linear profiles for profiling */
                ret = gcm_session_device_reset_gamma (output,
                                                      state->color_temperature,
                                                      oled_display,
                                                      &error);
                if (!ret) {
                        g_warning ("failed to reset %s gamma tables: %s",
                                   cd_device_get_id (device),
                                   error->message);
                        g_error_free (error);
                        goto out;
                }
                goto out;
        }

        /* get properties */
        helper = g_new0 (GcmSessionAsyncHelper, 1);
        helper->output_id = gnome_rr_output_get_id (output);
        helper->state = g_object_ref (state);
        helper->device = g_object_ref (device);
        cd_profile_connect (profile,
                            state->cancellable,
                            gcm_session_device_assign_profile_connect_cb,
                            helper);
out:
        g_free (autogen_filename);
        g_free (autogen_path);
        if (file != NULL)
                g_object_unref (file);
        if (edid != NULL)
                g_object_unref (edid);
        if (profile != NULL)
                g_object_unref (profile);
}

static void
gcm_session_device_assign (GsdColorState *state, CdDevice *device)
{
        const gchar *key;
        gpointer found;

        /* are we already assigning this device */
        key = cd_device_get_object_path (device);
        found = g_hash_table_lookup (state->device_assign_hash, key);
        if (found != NULL) {
                g_debug ("assign for %s already in progress", key);
                return;
        }
        g_hash_table_insert (state->device_assign_hash,
                             g_strdup (key),
                             GINT_TO_POINTER (TRUE));
        cd_device_connect (device,
                           state->cancellable,
                           gcm_session_device_assign_connect_cb,
                           state);
}

static void
gcm_session_device_added_assign_cb (CdClient *client,
                                    CdDevice *device,
                                    GsdColorState *state)
{
        gcm_session_device_assign (state, device);
}

static void
gcm_session_device_changed_assign_cb (CdClient *client,
                                      CdDevice *device,
                                      GsdColorState *state)
{
        g_debug ("%s changed", cd_device_get_object_path (device));
        gcm_session_device_assign (state, device);
}

static void
gcm_session_create_device_cb (GObject *object,
                              GAsyncResult *res,
                              gpointer user_data)
{
        CdDevice *device;
        GError *error = NULL;

        device = cd_client_create_device_finish (CD_CLIENT (object),
                                                 res,
                                                 &error);
        if (device == NULL) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
                    !g_error_matches (error, CD_CLIENT_ERROR, CD_CLIENT_ERROR_ALREADY_EXISTS))
                        g_warning ("failed to create device: %s", error->message);
                g_error_free (error);
                return;
        }
        g_object_unref (device);
}

static void
gcm_session_add_state_output (GsdColorState *state, GnomeRROutput *output)
{
        const gchar *edid_checksum = NULL;
        const gchar *model = NULL;
        const gchar *output_name = NULL;
        const gchar *serial = NULL;
        const gchar *vendor = NULL;
        gboolean ret;
        gchar *device_id = NULL;
        GcmEdid *edid;
        GError *error = NULL;
        GHashTable *device_props = NULL;

        /* VNC creates a fake device that cannot be color managed */
        output_name = gnome_rr_output_get_name (output);
        if (output_name != NULL && g_str_has_prefix (output_name, "VNC-")) {
                g_debug ("ignoring %s as fake VNC device detected", output_name);
                return;
        }

        /* try to get edid */
        edid = gcm_session_get_output_edid (state, output, &error);
        if (edid == NULL) {
                g_warning ("failed to get edid: %s",
                           error->message);
                g_clear_error (&error);
        }

        /* prefer DMI data for the internal output */
        ret = gnome_rr_output_is_builtin_display (output);
        if (ret) {
                model = cd_client_get_system_model (state->client);
                vendor = cd_client_get_system_vendor (state->client);
        }

        /* use EDID data if we have it */
        if (edid != NULL) {
                edid_checksum = gcm_edid_get_checksum (edid);
                if (model == NULL)
                        model = gcm_edid_get_monitor_name (edid);
                if (vendor == NULL)
                        vendor = gcm_edid_get_vendor_name (edid);
                if (serial == NULL)
                        serial = gcm_edid_get_serial_number (edid);
        }

        /* ensure mandatory fields are set */
        if (model == NULL)
                model = gnome_rr_output_get_name (output);
        if (vendor == NULL)
                vendor = "unknown";
        if (serial == NULL)
                serial = "unknown";

        device_id = gcm_session_get_output_id (state, output);
        g_debug ("output %s added", device_id);
        device_props = g_hash_table_new_full (g_str_hash, g_str_equal,
                                              NULL, NULL);
        g_hash_table_insert (device_props,
                             (gpointer) CD_DEVICE_PROPERTY_KIND,
                             (gpointer) cd_device_kind_to_string (CD_DEVICE_KIND_DISPLAY));
        g_hash_table_insert (device_props,
                             (gpointer) CD_DEVICE_PROPERTY_MODE,
                             (gpointer) cd_device_mode_to_string (CD_DEVICE_MODE_PHYSICAL));
        g_hash_table_insert (device_props,
                             (gpointer) CD_DEVICE_PROPERTY_COLORSPACE,
                             (gpointer) cd_colorspace_to_string (CD_COLORSPACE_RGB));
        g_hash_table_insert (device_props,
                             (gpointer) CD_DEVICE_PROPERTY_VENDOR,
                             (gpointer) vendor);
        g_hash_table_insert (device_props,
                             (gpointer) CD_DEVICE_PROPERTY_MODEL,
                             (gpointer) model);
        g_hash_table_insert (device_props,
                             (gpointer) CD_DEVICE_PROPERTY_SERIAL,
                             (gpointer) serial);
        g_hash_table_insert (device_props,
                             (gpointer) CD_DEVICE_METADATA_XRANDR_NAME,
                             (gpointer) gnome_rr_output_get_name (output));
        g_hash_table_insert (device_props,
                             (gpointer) CD_DEVICE_METADATA_OUTPUT_PRIORITY,
                             gnome_rr_output_get_is_primary (output) ?
                             (gpointer) CD_DEVICE_METADATA_OUTPUT_PRIORITY_PRIMARY :
                             (gpointer) CD_DEVICE_METADATA_OUTPUT_PRIORITY_SECONDARY);
        if (edid_checksum != NULL) {
                g_hash_table_insert (device_props,
                                     (gpointer) CD_DEVICE_METADATA_OUTPUT_EDID_MD5,
                                     (gpointer) edid_checksum);
        }
        /* set this so we can call the device a 'Laptop Screen' in the
         * control center main panel */
        if (gnome_rr_output_is_builtin_display (output)) {
                g_hash_table_insert (device_props,
                                     (gpointer) CD_DEVICE_PROPERTY_EMBEDDED,
                                     NULL);
        }
        cd_client_create_device (state->client,
                                 device_id,
                                 CD_OBJECT_SCOPE_TEMP,
                                 device_props,
                                 state->cancellable,
                                 gcm_session_create_device_cb,
                                 state);
        g_free (device_id);
        if (device_props != NULL)
                g_hash_table_unref (device_props);
        if (edid != NULL)
                g_object_unref (edid);
}


static void
gnome_rr_screen_output_added_cb (GnomeRRScreen *screen,
                                GnomeRROutput *output,
                                GsdColorState *state)
{
        gcm_session_add_state_output (state, output);
}

static void
gcm_session_screen_removed_delete_device_cb (GObject *object, GAsyncResult *res, gpointer user_data)
{
        gboolean ret;
        GError *error = NULL;

        /* deleted device */
        ret = cd_client_delete_device_finish (CD_CLIENT (object),
                                              res,
                                              &error);
        if (!ret) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        g_warning ("failed to delete device: %s", error->message);
                g_error_free (error);
        }
}

static void
gcm_session_screen_removed_find_device_cb (GObject *object, GAsyncResult *res, gpointer user_data)
{
        GError *error = NULL;
        CdDevice *device;
        GsdColorState *state = GSD_COLOR_STATE (user_data);

        device = cd_client_find_device_finish (state->client,
                                               res,
                                               &error);
        if (device == NULL) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        g_warning ("failed to find device: %s", error->message);
                g_error_free (error);
                return;
        }
        g_debug ("output %s found, and will be removed",
                 cd_device_get_object_path (device));
        cd_client_delete_device (state->client,
                                 device,
                                 state->cancellable,
                                 gcm_session_screen_removed_delete_device_cb,
                                 state);
        g_object_unref (device);
}

static void
gnome_rr_screen_output_removed_cb (GnomeRRScreen *screen,
                                   GnomeRROutput *output,
                                   GsdColorState *state)
{
        g_debug ("output %s removed",
                 gnome_rr_output_get_name (output));
        g_hash_table_remove (state->edid_cache,
                             gnome_rr_output_get_name (output));
        cd_client_find_device_by_property (state->client,
                                           CD_DEVICE_METADATA_XRANDR_NAME,
                                           gnome_rr_output_get_name (output),
                                           state->cancellable,
                                           gcm_session_screen_removed_find_device_cb,
                                           state);
}

static void
gcm_session_get_devices_cb (GObject *object, GAsyncResult *res, gpointer user_data)
{
        CdDevice *device;
        GError *error = NULL;
        GPtrArray *array;
        guint i;
        GsdColorState *state = GSD_COLOR_STATE (user_data);

        array = cd_client_get_devices_finish (CD_CLIENT (object), res, &error);
        if (array == NULL) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        g_warning ("failed to get devices: %s", error->message);
                g_error_free (error);
                return;
        }
        for (i = 0; i < array->len; i++) {
                device = g_ptr_array_index (array, i);
                gcm_session_device_assign (state, device);
        }

        if (array != NULL)
                g_ptr_array_unref (array);
}

static void
gcm_session_profile_gamma_find_device_cb (GObject *object,
                                          GAsyncResult *res,
                                          gpointer user_data)
{
        CdClient *client = CD_CLIENT (object);
        CdDevice *device = NULL;
        GError *error = NULL;
        GsdColorState *state = GSD_COLOR_STATE (user_data);

        device = cd_client_find_device_by_property_finish (client,
                                                           res,
                                                           &error);
        if (device == NULL) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        g_warning ("could not find device: %s", error->message);
                g_error_free (error);
                return;
        }

        /* get properties */
        cd_device_connect (device,
                           state->cancellable,
                           gcm_session_device_assign_connect_cb,
                           state);

        if (device != NULL)
                g_object_unref (device);
}

static void
gcm_session_set_gamma_for_all_devices (GsdColorState *state)
{
        GnomeRROutput **outputs;
        guint i;

        /* setting the temperature before we get the list of devices is fine,
         * as we use the temperature in the calculation */
        if (state->state_screen == NULL)
                return;

        /* get STATE outputs */
        outputs = gnome_rr_screen_list_outputs (state->state_screen);
        if (outputs == NULL) {
                g_warning ("failed to get outputs");
                return;
        }
        for (i = 0; outputs[i] != NULL; i++) {
                /* get CdDevice for this output */
                cd_client_find_device_by_property (state->client,
                                                   CD_DEVICE_METADATA_XRANDR_NAME,
                                                   gnome_rr_output_get_name (outputs[i]),
                                                   state->cancellable,
                                                   gcm_session_profile_gamma_find_device_cb,
                                                   state);
        }
}

/* We have to reset the gamma tables each time as if the primary output
 * has changed then different crtcs are going to be used.
 * See https://bugzilla.gnome.org/show_bug.cgi?id=660164 for an example */
static void
gnome_rr_screen_output_changed_cb (GnomeRRScreen *screen,
                                   GsdColorState *state)
{
        gcm_session_set_gamma_for_all_devices (state);
}

static gboolean
has_changed (char       **strv,
	     const char  *str)
{
        guint i;
        for (i = 0; strv[i] != NULL; i++) {
                if (g_str_equal (str, strv[i]))
                        return TRUE;
        }
        return FALSE;
}

static void
gcm_session_active_changed_cb (GDBusProxy      *session,
                               GVariant        *changed,
                               char           **invalidated,
                               GsdColorState *state)
{
        GVariant *active_v = NULL;
        gboolean is_active;

        if (has_changed (invalidated, "SessionIsActive"))
                return;

        /* not yet connected to the daemon */
        if (!cd_client_get_connected (state->client))
                return;

        active_v = g_dbus_proxy_get_cached_property (session, "SessionIsActive");
        g_return_if_fail (active_v != NULL);
        is_active = g_variant_get_boolean (active_v);
        g_variant_unref (active_v);

        /* When doing the fast-user-switch into a new account, load the
         * new users chosen profiles.
         *
         * If this is the first time the GnomeSettingsSession has been
         * loaded, then we'll get a change from unknown to active
         * and we want to avoid reprobing the devices for that.
         */
        if (is_active && !state->session_is_active) {
                g_debug ("Done switch to new account, reload devices");
                cd_client_get_devices (state->client,
                                       state->cancellable,
                                       gcm_session_get_devices_cb,
                                       state);
        }
        state->session_is_active = is_active;
}

static void
gcm_session_client_connect_cb (GObject *source_object,
                               GAsyncResult *res,
                               gpointer user_data)
{
        gboolean ret;
        GError *error = NULL;
        GnomeRROutput **outputs;
        guint i;
        GsdColorState *state = GSD_COLOR_STATE (user_data);

        /* connected */
        ret = cd_client_connect_finish (state->client, res, &error);
        if (!ret) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        g_warning ("failed to connect to colord: %s", error->message);
                g_error_free (error);
                return;
        }

        /* is there an available colord instance? */
        ret = cd_client_get_has_server (state->client);
        if (!ret) {
                g_warning ("There is no colord server available");
                return;
        }

        /* watch if sessions change */
        g_signal_connect_object (state->session, "g-properties-changed",
                                 G_CALLBACK (gcm_session_active_changed_cb),
                                 state, 0);

        /* add screens */
        gnome_rr_screen_refresh (state->state_screen, &error);
        if (error != NULL) {
                g_warning ("failed to refresh: %s", error->message);
                g_error_free (error);
                return;
        }

        /* get STATE outputs */
        outputs = gnome_rr_screen_list_outputs (state->state_screen);
        if (outputs == NULL) {
                g_warning ("failed to get outputs");
                return;
        }
        for (i = 0; outputs[i] != NULL; i++) {
                gcm_session_add_state_output (state, outputs[i]);
        }

        /* only connect when colord is awake */
        g_signal_connect (state->state_screen, "output-connected",
                          G_CALLBACK (gnome_rr_screen_output_added_cb),
                          state);
        g_signal_connect (state->state_screen, "output-disconnected",
                          G_CALLBACK (gnome_rr_screen_output_removed_cb),
                          state);
        g_signal_connect (state->state_screen, "changed",
                          G_CALLBACK (gnome_rr_screen_output_changed_cb),
                          state);

        g_signal_connect (state->client, "device-added",
                          G_CALLBACK (gcm_session_device_added_assign_cb),
                          state);
        g_signal_connect (state->client, "device-changed",
                          G_CALLBACK (gcm_session_device_changed_assign_cb),
                          state);

        /* set for each device that already exist */
        cd_client_get_devices (state->client,
                               state->cancellable,
                               gcm_session_get_devices_cb,
                               state);
}

static void
on_rr_screen_acquired (GObject      *object,
                       GAsyncResult *result,
                       gpointer      data)
{
        GsdColorState *state = data;
        GnomeRRScreen *screen;
        GError *error = NULL;

        /* gnome_rr_screen_new_async() does not take a GCancellable */
        if (g_cancellable_is_cancelled (state->cancellable))
                goto out;

        screen = gnome_rr_screen_new_finish (result, &error);
        if (screen == NULL) {
                g_warning ("failed to get screens: %s", error->message);
                g_error_free (error);
                goto out;
        }

        state->state_screen = screen;

        cd_client_connect (state->client,
                           state->cancellable,
                           gcm_session_client_connect_cb,
                           state);
out:
        /* manually added */
        g_object_unref (state);
}

void
gsd_color_state_start (GsdColorState *state)
{
        /* use a fresh cancellable for each start->stop operation */
        g_cancellable_cancel (state->cancellable);
        g_clear_object (&state->cancellable);
        state->cancellable = g_cancellable_new ();

        /* coldplug the list of screens */
        gnome_rr_screen_new_async (gdk_screen_get_default (),
                                   on_rr_screen_acquired,
                                   g_object_ref (state));
}

void
gsd_color_state_stop (GsdColorState *state)
{
        g_cancellable_cancel (state->cancellable);
}

static void
gsd_color_state_class_init (GsdColorStateClass *klass)
{
        GObjectClass   *object_class = G_OBJECT_CLASS (klass);

        object_class->finalize = gsd_color_state_finalize;
}

static void
power_proxy_signal_cb (GDBusProxy  *proxy,
                       const gchar *sender_name,
                       const gchar *signal_name,
                       GVariant    *parameters,
                       gpointer     user_data)
{
        GsdColorState *state = user_data;

        if (g_strcmp0 (signal_name, "PropertiesChanged") == 0) {
            gcm_session_set_gamma_for_all_devices (state);
        }
}

static void
on_power_proxy_ready (GObject      *source,
                       GAsyncResult *result,
                       gpointer      data)
{
        GsdColorState *state = data;

        state->power_proxy =
                g_dbus_proxy_new_for_bus_finish (result, NULL);

        g_signal_connect (state->power_proxy, "g-signal",
                          G_CALLBACK (power_proxy_signal_cb),
                          state);
}

static void
power_appeared_cb (GDBusConnection *connection,
                    const gchar     *name,
                    const gchar     *name_owner,
                    gpointer         user_data)
{
        GsdColorState *state = user_data;

        g_dbus_proxy_new_for_bus (G_BUS_TYPE_SESSION,
                                  0, NULL,
                                  GSD_DBUS_NAME_POWER,
                                  GSD_DBUS_PATH_POWER,
                                  "org.freedesktop.DBus.Properties",
                                  state->cancellable,
                                  on_power_proxy_ready, state);
}

static void
gsd_color_state_init (GsdColorState *state)
{
        /* track the active session */
        state->session = gnome_settings_bus_get_session_proxy ();

#ifdef GDK_WINDOWING_X11
        /* set the _ICC_PROFILE atoms on the root screen */
        if (GDK_IS_X11_DISPLAY (gdk_display_get_default ()))
                state->gdk_window = gdk_screen_get_root_window (gdk_screen_get_default ());
#endif

        /* parsing the EDID is expensive */
        state->edid_cache = g_hash_table_new_full (g_str_hash,
                                                  g_str_equal,
                                                  g_free,
                                                  g_object_unref);

        /* we don't want to assign devices multiple times at startup */
        state->device_assign_hash = g_hash_table_new_full (g_str_hash,
                                                          g_str_equal,
                                                          g_free,
                                                          NULL);

        /* default color temperature */
        state->color_temperature = GSD_COLOR_TEMPERATURE_DEFAULT;

        state->client = cd_client_new ();

        state->oled_builtin_display = has_oled_builtin_display();

        state->power_watch_id = g_bus_watch_name (G_BUS_TYPE_SESSION,
                                                 GSD_DBUS_NAME_POWER,
                                                 G_BUS_NAME_WATCHER_FLAGS_NONE,
                                                 power_appeared_cb,
                                                 NULL,
                                                 state, NULL);
}

static void
gsd_color_state_finalize (GObject *object)
{
        GsdColorState *state;

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

        state = GSD_COLOR_STATE (object);

        g_cancellable_cancel (state->cancellable);
        g_clear_object (&state->cancellable);
        g_clear_object (&state->client);
        g_clear_object (&state->session);
        g_clear_pointer (&state->edid_cache, g_hash_table_destroy);
        g_clear_pointer (&state->device_assign_hash, g_hash_table_destroy);
        g_clear_object (&state->state_screen);

        if (state->power_watch_id > 0) {
                g_bus_unwatch_name (state->power_watch_id);
                state->power_watch_id = 0;
        }

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

GsdColorState *
gsd_color_state_new (void)
{
        GsdColorState *state;
        state = g_object_new (GSD_TYPE_COLOR_STATE, NULL);
        return GSD_COLOR_STATE (state);
}
