/*
 * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
 * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2020 NVIDIA CORPORATION
 * Copyright (C) 2021 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 "backends/meta-color-store.h"

#include <colord.h>
#include <stdint.h>

#include "backends/meta-color-device.h"
#include "backends/meta-color-profile.h"
#include "backends/meta-monitor.h"

struct _MetaColorStore
{
  GObject parent;

  MetaColorManager *color_manager;

  GFileMonitor *icc_directory_monitor;

  GHashTable *profiles;
  GHashTable *device_profiles;
  GHashTable *pending_device_profiles;
  GHashTable *pending_local_profiles;

  GCancellable *cancellable;
};

typedef struct
{
  MetaColorStore *color_store;
  char *key;
} EnsureDeviceProfileData;

G_DEFINE_TYPE (MetaColorStore, meta_color_store,
               G_TYPE_OBJECT)

static void
meta_color_store_finalize (GObject *object)
{
  MetaColorStore *color_store = META_COLOR_STORE (object);

  g_cancellable_cancel (color_store->cancellable);
  g_clear_object (&color_store->cancellable);

  g_clear_object (&color_store->icc_directory_monitor);
  g_clear_pointer (&color_store->profiles, g_hash_table_unref);
  g_clear_pointer (&color_store->device_profiles, g_hash_table_unref);
  g_clear_pointer (&color_store->pending_device_profiles, g_hash_table_unref);
  g_clear_pointer (&color_store->pending_local_profiles, g_hash_table_unref);

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

static void
meta_color_store_class_init (MetaColorStoreClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = meta_color_store_finalize;
}

static void
meta_color_store_init (MetaColorStore *color_store)
{
  color_store->cancellable = g_cancellable_new ();
}

static void
on_directory_profile_ready (MetaColorProfile *color_profile,
                            gboolean          success,
                            MetaColorStore   *color_store)
{
  if (!success)
    {
      g_hash_table_remove (color_store->pending_local_profiles,
                           meta_color_profile_get_file_path (color_profile));
      return;
    }

  g_object_ref (color_profile);

  if (!g_hash_table_steal (color_store->pending_local_profiles,
                           meta_color_profile_get_file_path (color_profile)))
    g_warn_if_reached ();

  g_hash_table_insert (color_store->profiles,
                       g_strdup (meta_color_profile_get_id (color_profile)),
                       color_profile);

  meta_topic (META_DEBUG_COLOR, "Created colord profile '%s' from '%s'",
              meta_color_profile_get_id (color_profile),
              meta_color_profile_get_file_path (color_profile));

  g_object_unref (color_profile);
}

static void
create_profile_from_contents (MetaColorStore *color_store,
                              const char     *file_path,
                              const char     *contents,
                              size_t          size)
{
  g_autoptr (CdIcc) cd_icc = NULL;
  g_autoptr (GError) error = NULL;
  g_autoptr (GBytes) bytes = NULL;
  g_autofree char *file_md5_checksum = NULL;
  MetaColorCalibration *color_calibration;
  MetaColorProfile *color_profile;

  cd_icc = cd_icc_new ();
  if (!cd_icc_load_data (cd_icc,
                         (uint8_t *) contents,
                         size,
                         CD_ICC_LOAD_FLAGS_METADATA,
                         &error))
    {
      g_warning ("Failed to parse ICC profile '%s': %s", file_path, error->message);
      return;
    }

  bytes = g_bytes_new (contents, size);

  cd_icc_add_metadata (cd_icc, CD_PROFILE_PROPERTY_FILENAME,
                       file_path);

  file_md5_checksum = g_compute_checksum_for_bytes (G_CHECKSUM_MD5,
                                                    bytes);
  cd_icc_add_metadata (cd_icc, CD_PROFILE_METADATA_FILE_CHECKSUM,
                       file_md5_checksum);
  color_calibration = meta_color_calibration_new (cd_icc, NULL);
  color_profile =
    meta_color_profile_new_from_icc (color_store->color_manager,
                                     g_steal_pointer (&cd_icc),
                                     g_steal_pointer (&bytes),
                                     color_calibration);

  g_signal_connect (color_profile, "ready",
                    G_CALLBACK (on_directory_profile_ready),
                    color_store);

  g_hash_table_insert (color_store->pending_local_profiles,
                       g_strdup (file_path),
                       color_profile);
}

static gboolean
should_ignore_store_file (GFile *file)
{
  g_autofree char *file_name = NULL;

  /* Ignore profiles from EDID, as they will always be generated on demand. */
  file_name = g_file_get_basename (file);
  return g_str_has_prefix (file_name, "edid-");
}

static void
process_icc_directory_file (MetaColorStore *color_store,
                            GFile          *file)
{
  g_autoptr (GError) error = NULL;
  g_autofree char *contents = NULL;
  size_t size;

  if (should_ignore_store_file (file))
    return;

  if (!g_file_load_contents (file, NULL, &contents, &size, NULL, &error))
    {
      g_warning ("Failed to read '%s': %s",
                 g_file_peek_path (file), error->message);
      return;
    }

  create_profile_from_contents (color_store, g_file_peek_path (file),
                                contents, size);
}

static gboolean
is_file_info_icc_profile (GFileInfo *info)
{
  const char *content_type;

  content_type =
    g_file_info_get_attribute_string (info,
                                      G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);
  if (g_strcmp0 (content_type, "application/vnd.iccprofile") != 0)
    return FALSE;

  if (g_file_info_get_attribute_boolean (info,
                                         G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN))
    return FALSE;

  if (g_file_info_get_attribute_boolean (info,
                                         G_FILE_ATTRIBUTE_STANDARD_IS_BACKUP))
    return FALSE;

  return TRUE;
}

static gboolean
is_file_icc_profile (GFile *file)
{
  g_autoptr (GFileInfo) info = NULL;
  g_autoptr (GError) error = NULL;

  info = g_file_query_info (file,
                            G_FILE_ATTRIBUTE_STANDARD_NAME ","
                            G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE ","
                            G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
                            G_FILE_ATTRIBUTE_STANDARD_IS_BACKUP ","
                            G_FILE_ATTRIBUTE_STANDARD_TYPE,
                            G_FILE_QUERY_INFO_NONE,
                            NULL,
                            &error);
  if (!info)
    {
      g_warning ("Failed to query file info on '%s': %s",
                 g_file_get_path (file),
                 error->message);
      return FALSE;
    }

  return is_file_info_icc_profile (info);
}

static void
on_store_file_read (GObject      *source_object,
                    GAsyncResult *res,
                    gpointer      user_data)
{
  GFile *file = G_FILE (source_object);
  MetaColorStore *color_store = user_data;
  g_autoptr (GError) error = NULL;
  g_autofree char *contents = NULL;
  size_t size;

  if (!g_file_load_contents_finish (file, res,
                                    &contents,
                                    &size,
                                    NULL,
                                    &error))
    {
      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        {
          g_warning ("Failed to read '%s': %s",
                     g_file_peek_path (file), error->message);
        }
      return;
    }

  create_profile_from_contents (color_store, g_file_peek_path (file),
                                contents, size);
}

static void
query_file_info_cb (GObject      *source_object,
                    GAsyncResult *res,
                    gpointer      user_data)
{
  GFile *file = G_FILE (source_object);
  g_autoptr (GFileInfo) info = NULL;
  g_autoptr (GError) error = NULL;
  MetaColorStore *color_store = user_data;

  info = g_file_query_info_finish (file, res, &error);
  if (!info)
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

      g_warning ("Failed to query file info on '%s': %s",
                 g_file_get_path (file),
                 error->message);
      return;
    }

  if (!is_file_info_icc_profile (info))
    return;

  if (should_ignore_store_file (file))
    return;

  g_file_load_contents_async (file, color_store->cancellable,
                              on_store_file_read, color_store);
}

static void
on_icc_directory_change (GFileMonitor      *monitor,
                         GFile             *file,
                         GFile             *other_file,
                         GFileMonitorEvent  event_type,
                         MetaColorStore    *color_store)
{
  switch (event_type)
    {
    case G_FILE_MONITOR_EVENT_CREATED:
      break;
    default:
      return;
    }

  g_file_query_info_async (file,
                           G_FILE_ATTRIBUTE_STANDARD_NAME ","
                           G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE ","
                           G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
                           G_FILE_ATTRIBUTE_STANDARD_IS_BACKUP ","
                           G_FILE_ATTRIBUTE_STANDARD_TYPE,
                           G_FILE_QUERY_INFO_NONE,
                           G_PRIORITY_DEFAULT,
                           color_store->cancellable,
                           query_file_info_cb,
                           color_store);
}

static gboolean
init_profile_directory (MetaColorStore  *color_store,
                        GError         **error)
{
  g_autofree char *icc_directory_path = NULL;
  g_autoptr (GFile) icc_directory = NULL;
  g_autoptr (GError) local_error = NULL;
  g_autoptr (GFileEnumerator) enumerator = NULL;

  icc_directory_path = g_build_filename (g_get_user_data_dir (),
                                         "icc", NULL);
  icc_directory = g_file_new_for_path (icc_directory_path);

  if (!g_file_query_exists (icc_directory, NULL))
    {
      if (!g_file_make_directory_with_parents (icc_directory, NULL, error))
        return FALSE;
    }

  color_store->icc_directory_monitor =
    g_file_monitor (icc_directory,
                    G_FILE_MONITOR_NONE,
                    NULL, &local_error);
  if (color_store->icc_directory_monitor)
    {
      g_signal_connect (color_store->icc_directory_monitor,
                        "changed", G_CALLBACK (on_icc_directory_change),
                        color_store);
    }
  else
    {
      g_warning ("Failed to monitor ICC profile directory '%s': %s",
                 icc_directory_path,
                 local_error->message);
      g_clear_error (&local_error);
    }

  enumerator = g_file_enumerate_children (icc_directory,
                                          G_FILE_ATTRIBUTE_STANDARD_NAME ","
                                          G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE ","
                                          G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
                                          G_FILE_ATTRIBUTE_STANDARD_IS_BACKUP ","
                                          G_FILE_ATTRIBUTE_STANDARD_TYPE,
                                          G_FILE_QUERY_INFO_NONE,
                                          NULL,
                                          error);
  if (!enumerator)
    return FALSE;

  while (TRUE)
    {
      g_autoptr (GFileInfo) file_info = NULL;

      file_info = g_file_enumerator_next_file (enumerator, NULL, error);
      if (!file_info)
        {
          if (*error)
            return FALSE;
          else
            break;
        }

     switch (g_file_info_get_file_type (file_info))
       {
       case G_FILE_TYPE_REGULAR:
         {
           g_autoptr (GFile) file = NULL;
           g_autofree char *file_path = NULL;

           file_path = g_build_filename (icc_directory_path,
                                         g_file_info_get_name (file_info),
                                         NULL);
           file = g_file_new_for_path (file_path);
           if (is_file_icc_profile (file))
             process_icc_directory_file (color_store, file);

           break;
         }
       case G_FILE_TYPE_SYMBOLIC_LINK:
         {
           const char *target_path;
           g_autoptr (GFile) target = NULL;

           target_path = g_file_info_get_symlink_target (file_info);
           target = g_file_new_for_path (target_path);

           if (is_file_icc_profile (target))
             process_icc_directory_file (color_store, target);
           break;
         }
       default:
         break;
       }
    }

  return TRUE;
}

MetaColorStore *
meta_color_store_new (MetaColorManager *color_manager)
{
  MetaColorStore *color_store;
  g_autoptr (GError) error = NULL;

  color_store = g_object_new (META_TYPE_COLOR_STORE, NULL);
  color_store->color_manager = color_manager;
  color_store->profiles =
    g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
  color_store->device_profiles =
    g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
  color_store->pending_device_profiles =
    g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
  color_store->pending_local_profiles =
    g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);

  if (!init_profile_directory (color_store, &error))
    g_warning ("Failed to monitor ICC directory: %s", error->message);

  return color_store;
}

static void
ensure_device_profile_data_free (EnsureDeviceProfileData *data)
{
  g_free (data->key);
  g_free (data);
}

static void
on_profile_generated (GObject      *source_object,
                      GAsyncResult *res,
                      gpointer      user_data)
{
  MetaColorDevice *color_device = META_COLOR_DEVICE (source_object);
  g_autoptr (GTask) task = G_TASK (user_data);
  g_autoptr (GError) error = NULL;
  MetaColorProfile *color_profile;

  color_profile = meta_color_device_generate_profile_finish (color_device,
                                                             res,
                                                             &error);
  if (!color_profile)
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        {
          g_task_return_error (task, g_steal_pointer (&error));
          return;
        }

      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
                               "Failed to generate and read ICC profile: %s",
                               error->message);
      return;
    }

  g_task_return_pointer (task, color_profile, g_object_unref);
}

gboolean
meta_color_store_ensure_device_profile (MetaColorStore      *color_store,
                                        MetaColorDevice     *color_device,
                                        GCancellable        *cancellable,
                                        GAsyncReadyCallback  callback,
                                        gpointer             user_data)
{
  MetaMonitor *monitor;
  const char *edid_checksum_md5;
  g_autoptr (GTask) task = NULL;
  g_autofree char *file_name = NULL;
  g_autofree char *file_path = NULL;
  EnsureDeviceProfileData *data;
  MetaColorProfile *color_profile;

  monitor = meta_color_device_get_monitor (color_device);
  edid_checksum_md5 = meta_monitor_get_edid_checksum_md5 (monitor);
  if (!edid_checksum_md5)
    return FALSE;

  task = g_task_new (G_OBJECT (color_store), cancellable, callback, user_data);
  g_task_set_source_tag (task, meta_color_store_ensure_device_profile);

  file_name = g_strdup_printf ("edid-%s.icc", edid_checksum_md5);
  file_path = g_build_filename (g_get_user_data_dir (),
                                "icc", file_name, NULL);

  data = g_new0 (EnsureDeviceProfileData, 1);
  data->color_store = color_store;
  data->key = g_strdup (meta_color_device_get_id (color_device));
  g_task_set_task_data (task, data,
                        (GDestroyNotify) ensure_device_profile_data_free);

  color_profile = g_hash_table_lookup (color_store->device_profiles, data->key);
  if (color_profile)
    {
      g_task_return_pointer (task,
                             g_object_ref (color_profile),
                             g_object_unref);
      return TRUE;
    }

  if (g_hash_table_contains (color_store->pending_device_profiles, data->key))
    {
      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
                               "Profile generation already in progress");
      return TRUE;
    }

  g_hash_table_add (color_store->pending_device_profiles, g_strdup (data->key));

  meta_color_device_generate_profile (color_device,
                                      file_path,
                                      cancellable,
                                      on_profile_generated,
                                      g_steal_pointer (&task));
  return TRUE;
}

MetaColorProfile *
meta_color_store_ensure_device_profile_finish (MetaColorStore  *color_store,
                                               GAsyncResult    *res,
                                               GError         **error)
{
  GTask *task = G_TASK (res);
  EnsureDeviceProfileData *data = g_task_get_task_data (task);
  g_autoptr (MetaColorProfile) color_profile = NULL;

  g_assert (g_task_get_source_tag (task) ==
            meta_color_store_ensure_device_profile);

  g_hash_table_remove (color_store->pending_device_profiles, data->key);

  color_profile = g_task_propagate_pointer (task, error);
  if (!color_profile)
    return NULL;

  g_hash_table_insert (color_store->device_profiles,
                       g_steal_pointer (&data->key),
                       g_object_ref (color_profile));
  g_hash_table_insert (color_store->profiles,
                       g_strdup (meta_color_profile_get_id (color_profile)),
                       g_object_ref (color_profile));
  return color_profile;
}

typedef struct
{
  MetaColorStore *color_store;
  CdProfile *cd_profile;

  MetaColorProfile *created_profile;
} EnsureColordProfileData;

static void
ensure_colord_profile_data_free (EnsureColordProfileData *data)
{
  g_clear_object (&data->cd_profile);
  g_free (data);
}

static void
on_cd_profile_contents_loaded (GObject      *source_object,
                               GAsyncResult *res,
                               gpointer      user_data)
{
  GFile *file = G_FILE (source_object);
  g_autoptr (GTask) task = G_TASK (user_data);
  EnsureColordProfileData *data = g_task_get_task_data (task);
  MetaColorStore *color_store = data->color_store;
  MetaColorManager *color_manager = color_store->color_manager;
  CdProfile *cd_profile = data->cd_profile;
  g_autoptr (GError) error = NULL;
  g_autofree char *contents = NULL;
  size_t length;
  g_autoptr (CdIcc) cd_icc = NULL;
  g_autofree char *file_md5_checksum = NULL;
  GBytes *bytes;
  MetaColorCalibration *color_calibration;
  MetaColorProfile *color_profile;

  if (!g_file_load_contents_finish (file, res,
                                    &contents,
                                    &length,
                                    NULL,
                                    &error))
    {
      g_task_return_error (task, g_steal_pointer (&error));
      return;
    }

  cd_icc = cd_icc_new ();
  if (!cd_icc_load_data (cd_icc,
                         (uint8_t *) contents,
                         length,
                         CD_ICC_LOAD_FLAGS_METADATA,
                         &error))
    {
      g_task_return_error (task, g_steal_pointer (&error));
      return;
    }

  cd_icc_add_metadata (cd_icc, CD_PROFILE_PROPERTY_FILENAME,
                       g_file_peek_path (file));

  file_md5_checksum = g_compute_checksum_for_data (G_CHECKSUM_MD5,
                                                   (uint8_t *) contents,
                                                   length);
  cd_icc_add_metadata (cd_icc, CD_PROFILE_METADATA_FILE_CHECKSUM,
                       file_md5_checksum);

  bytes = g_bytes_new_take (g_steal_pointer (&contents), length);
  color_calibration = meta_color_calibration_new (cd_icc, NULL);
  color_profile =
    meta_color_profile_new_from_cd_profile (color_manager,
                                            cd_profile,
                                            g_steal_pointer (&cd_icc),
                                            bytes,
                                            color_calibration);

  g_hash_table_insert (color_store->profiles,
                       g_strdup (meta_color_profile_get_id (color_profile)),
                       color_profile);

  meta_topic (META_DEBUG_COLOR, "Created colord profile '%s' from '%s'",
              cd_profile_get_id (cd_profile),
              cd_profile_get_filename (cd_profile));

  g_task_return_pointer (task, g_object_ref (color_profile), g_object_unref);
}

static void
on_cd_profile_connected (GObject      *source_object,
                         GAsyncResult *res,
                         gpointer      user_data)
{
  CdProfile *cd_profile = CD_PROFILE (source_object);
  g_autoptr (GTask) task = G_TASK (user_data);
  EnsureColordProfileData *data = g_task_get_task_data (task);
  MetaColorStore *color_store = data->color_store;
  g_autoptr (GError) error = NULL;
  const char *file_path;
  g_autoptr (GFile) file = NULL;
  MetaColorProfile *color_profile;
  GCancellable *cancellable;

  if (!cd_profile_connect_finish (cd_profile, res, &error))
    {
      g_task_return_error (task, g_steal_pointer (&error));
      return;
    }

  color_profile = g_hash_table_lookup (color_store->profiles,
                                       cd_profile_get_id (cd_profile));
  if (color_profile)
    {
      meta_topic (META_DEBUG_COLOR, "Found existing colord profile '%s'",
                  cd_profile_get_id (cd_profile));
      g_task_return_pointer (task,
                             g_object_ref (color_profile),
                             g_object_unref);
      return;
    }

  file_path = cd_profile_get_filename (cd_profile);
  if (!file_path)
    {
      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
                               "Tried to assign non-local profile");
      return;
    }

  file = g_file_new_for_path (file_path);
  cancellable = g_task_get_cancellable (task);
  g_file_load_contents_async (file,
                              cancellable,
                              on_cd_profile_contents_loaded,
                              g_steal_pointer (&task));
}

void
meta_color_store_ensure_colord_profile (MetaColorStore      *color_store,
                                        CdProfile           *cd_profile,
                                        GCancellable        *cancellable,
                                        GAsyncReadyCallback  callback,
                                        gpointer             user_data)
{
  GTask *task;
  EnsureColordProfileData *data;

  task = g_task_new (G_OBJECT (color_store), cancellable, callback, user_data);
  g_task_set_source_tag (task, meta_color_store_ensure_colord_profile);

  data = g_new0 (EnsureColordProfileData, 1);
  data->color_store = color_store;
  data->cd_profile = g_object_ref (cd_profile);
  g_task_set_task_data (task, data,
                        (GDestroyNotify) ensure_colord_profile_data_free);

  cd_profile_connect (cd_profile,
                      cancellable,
                      on_cd_profile_connected,
                      task);
}

MetaColorProfile *
meta_color_store_ensure_colord_profile_finish (MetaColorStore  *color_store,
                                               GAsyncResult    *res,
                                               GError         **error)
{
  GTask *task = G_TASK (res);

  g_assert (g_task_get_source_tag (task) ==
            meta_color_store_ensure_colord_profile);

  return g_task_propagate_pointer (G_TASK (res), error);
}

MetaColorProfile *
meta_color_store_get_profile (MetaColorStore *color_store,
                              const char     *profile_id)
{
  return g_hash_table_lookup (color_store->profiles, profile_id);
}

gboolean
meta_color_store_has_pending_profiles (MetaColorStore *color_store)
{
  return (g_hash_table_size (color_store->pending_local_profiles) > 0 ||
          g_hash_table_size (color_store->pending_device_profiles) > 0);
}
