/*
 * Copyright (C) 2023 SUSE Software Solutions Germany GmbH
 *
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *
 */

#include "config.h"

#include "grd-settings-system.h"

#include <gio/gio.h>
#include <glib/gstdio.h>

#define GRD_SETTINGS_SYSTEM_GROUP_RDP "RDP"

typedef enum
{
  GRD_SETTINGS_SOURCE_TYPE_INVALID = -1,
  GRD_SETTINGS_SOURCE_TYPE_DEFAULT = 0,
  GRD_SETTINGS_SOURCE_TYPE_CUSTOM = 1,
  GRD_SETTINGS_SOURCE_TYPE_LOCAL_STATE = 2,
  N_GRD_SETTINGS_SOURCES
} GrdSettingsSourceType;

typedef struct
{
  GKeyFile *key_file;
  GFileMonitor *file_monitor;
  GrdSettingsSourceType type;
} GrdSettingsSource;

typedef struct
{
  const char *file_key;
  const char *settings_name;
  void (* read_settings_value) (GrdSettingsSystem *settings_system,
                                GKeyFile          *key_file,
                                const char        *group,
                                const char        *key,
                                const char        *settings_name);
  void (* write_settings_value) (GrdSettingsSystem *settings_system,
                                 GKeyFile          *key_file,
                                 const char        *group,
                                 const char        *key,
                                 const char        *settings_name);
} FileSetting;

struct _GrdSettingsSystem
{
  GrdSettings parent;

  GrdSettingsSource *setting_sources[N_GRD_SETTINGS_SOURCES];

  GKeyFile *key_file;

  gboolean use_local_state;
};

G_DEFINE_TYPE (GrdSettingsSystem, grd_settings_system, GRD_TYPE_SETTINGS)

static void
grd_settings_system_reload_sources (GrdSettingsSystem *settings_system);

static void
read_rdp_file_settings (GrdSettingsSystem *settings_system);

static GrdSettingsSource *
grd_settings_source_new (GrdSettingsSourceType  source_type,
                         const char            *file_path)
{
  g_autofree GrdSettingsSource *source = NULL;
  GKeyFileFlags flags = G_KEY_FILE_NONE;
  g_autoptr (GKeyFile) key_file = NULL;
  g_autoptr (GError) error = NULL;
  g_autoptr (GFile) file = NULL;

  source = g_new0 (GrdSettingsSource, 1);
  key_file = g_key_file_new ();

  if (source_type == GRD_SETTINGS_SOURCE_TYPE_CUSTOM)
    flags |= G_KEY_FILE_KEEP_COMMENTS;

  if  (!g_key_file_load_from_file (key_file,
                                   file_path,
                                   flags,
                                   &error))
    {
      g_debug ("Failed to load key file from '%s': %s",
               file_path, error->message);
      return NULL;
    }

  source->key_file = g_steal_pointer (&key_file);

  file = g_file_new_for_path (file_path);
  source->file_monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE,
                                              NULL, &error);
  if  (!source->file_monitor)
    {
      g_warning ("Failed to monitor file '%s': %s", file_path, error->message);
      return NULL;
    }

  return g_steal_pointer (&source);
}

static void
grd_settings_source_free (GrdSettingsSource *source)
{
  g_clear_pointer (&source->key_file, g_key_file_unref);
  g_clear_object (&source->file_monitor);
  g_clear_pointer (&source, g_free);
}

static char *
get_local_state_conf (void)
{
  return g_build_filename (g_get_user_data_dir (),
                           "gnome-remote-desktop", "grd.conf",
                           NULL);
}

void
grd_settings_system_use_local_state (GrdSettingsSystem *settings_system)
{
  g_assert (GRD_IS_SETTINGS_SYSTEM (settings_system));

  settings_system->use_local_state = TRUE;
}

static void
merge_descendant_keys (GrdSettingsSystem *settings_system,
                       GKeyFile          *key_file,
                       GKeyFile          *descendant_key_file)
{
  g_auto (GStrv) groups = NULL;
  size_t group_count = 0;
  size_t i, j;

  g_assert (key_file);
  g_assert (descendant_key_file);

  groups = g_key_file_get_groups (descendant_key_file, &group_count);
  for (i = 0; i < group_count; i++)
    {
      g_auto (GStrv) keys = NULL;
      size_t key_count = 0;

      keys = g_key_file_get_keys (descendant_key_file,
                                  groups[i], &key_count,
                                  NULL);
      for (j = 0; j < key_count; j++)
        {
          g_autofree char *value = NULL;

          value = g_key_file_get_value (descendant_key_file,
                                        groups[i], keys[j],
                                        NULL);
          if (value)
            g_key_file_set_value (key_file, groups[i], keys[j], value);
        }
    }
}

static gboolean
prune_inherited_keys (GrdSettingsSystem     *settings_system,
                      GrdSettingsSourceType  source_type,
                      GKeyFile              *key_file_to_prune)
{
  g_auto (GStrv) groups = NULL;
  size_t group_count = 0;
  size_t i, j;

  if (source_type == GRD_SETTINGS_SOURCE_TYPE_DEFAULT)
    return TRUE;

  if (source_type != GRD_SETTINGS_SOURCE_TYPE_CUSTOM)
    g_key_file_remove_comment (key_file_to_prune, NULL, NULL, NULL);

  groups = g_key_file_get_groups (key_file_to_prune, &group_count);

  for (i = 0; i < group_count; i++)
    {
      g_auto (GStrv) keys = NULL;
      size_t key_count;

      if (source_type != GRD_SETTINGS_SOURCE_TYPE_CUSTOM)
        g_key_file_remove_comment (key_file_to_prune, groups[i], NULL, NULL);

      keys = g_key_file_get_keys (key_file_to_prune,
                                  groups[i], &key_count,
                                  NULL);

      for (j = 0; j < key_count; j++)
        {
          g_autofree char *value_to_prune = NULL;
          GrdSettingsSourceType ancestor_type;
          gboolean should_prune_key = FALSE;

          value_to_prune = g_key_file_get_value (key_file_to_prune,
                                                 groups[i], keys[j],
                                                 NULL);

          if (source_type != GRD_SETTINGS_SOURCE_TYPE_CUSTOM)
            {
              g_key_file_remove_comment (key_file_to_prune,
                                         groups[i], keys[j],
                                         NULL);
            }

          for (ancestor_type = source_type - 1;
               ancestor_type >= GRD_SETTINGS_SOURCE_TYPE_DEFAULT;
               ancestor_type--)
            {
              GrdSettingsSource *ancestor_source;

              ancestor_source = settings_system->setting_sources[ancestor_type];
              if (ancestor_source == NULL)
                continue;

              if (g_key_file_has_key (ancestor_source->key_file,
                                      groups[i], keys[j],
                                      NULL))
                {
                  g_autofree char *ancestor_value = NULL;

                  ancestor_value = g_key_file_get_value (ancestor_source->key_file,
                                                         groups[i], keys[j],
                                                         NULL);

                  if (g_strcmp0 (value_to_prune, ancestor_value) == 0)
                    should_prune_key = TRUE;

                  break;
                }
            }

          if (should_prune_key)
            {
              g_key_file_remove_key (key_file_to_prune,
                                     groups[i], keys[j],
                                     NULL);
            }
        }
      g_clear_pointer (&keys, g_strfreev);

      keys = g_key_file_get_keys (key_file_to_prune,
                                  groups[i], &key_count,
                                  NULL);
      if (key_count == 0)
        g_key_file_remove_group (key_file_to_prune, groups[i], NULL);
    }
  g_clear_pointer (&groups, g_strfreev);

  groups = g_key_file_get_groups (key_file_to_prune, &group_count);

  if (group_count == 0)
    return FALSE;

  return TRUE;
}

static char **
get_conf_paths (void)
{
  g_autofree char *local_state_conf = NULL;
  char **paths = NULL;

  local_state_conf = get_local_state_conf ();

  paths = g_new (char *, N_GRD_SETTINGS_SOURCES + 1);
  paths[GRD_SETTINGS_SOURCE_TYPE_DEFAULT] = g_strdup (GRD_DEFAULT_CONF);
  paths[GRD_SETTINGS_SOURCE_TYPE_CUSTOM] = g_strdup (GRD_CUSTOM_CONF);
  paths[GRD_SETTINGS_SOURCE_TYPE_LOCAL_STATE] = g_steal_pointer (&local_state_conf);
  paths[N_GRD_SETTINGS_SOURCES] = NULL;

  return paths;
}

static void
on_file_changed (GFileMonitor      *file_monitor,
                 GFile             *file,
                 GFile             *other_file,
                 GFileMonitorEvent  event_type,
                 gpointer           user_data)
{
  GrdSettingsSystem *settings_system = GRD_SETTINGS_SYSTEM (user_data);

  if (event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT)
    {
      grd_settings_system_reload_sources (settings_system);
      read_rdp_file_settings (settings_system);
    }
}

static void
grd_settings_system_reload_sources (GrdSettingsSystem *settings_system)
{
  g_autoptr (GKeyFile) key_file = NULL;
  g_autoptr (GError) error = NULL;
  g_auto (GStrv) paths = NULL;
  size_t source_type;

  paths = get_conf_paths ();
  key_file = g_key_file_new ();

  for (source_type = GRD_SETTINGS_SOURCE_TYPE_DEFAULT;
       source_type < N_GRD_SETTINGS_SOURCES;
       source_type++)
    {
      GrdSettingsSource *source = NULL;

      g_clear_pointer (&settings_system->setting_sources[source_type],
                       grd_settings_source_free);

      source = grd_settings_source_new (source_type, paths[source_type]);
      if (!source)
        continue;

      merge_descendant_keys (settings_system, key_file, source->key_file);

      g_signal_connect (source->file_monitor,
                        "changed", G_CALLBACK (on_file_changed),
                        settings_system);

      settings_system->setting_sources[source_type] = g_steal_pointer (&source);
    }

  g_clear_pointer (&settings_system->key_file, g_key_file_unref);
  settings_system->key_file = g_steal_pointer (&key_file);
}

GrdSettingsSystem *
grd_settings_system_new (void)
{
  return g_object_new (GRD_TYPE_SETTINGS_SYSTEM,
                       "runtime-mode", GRD_RUNTIME_MODE_SYSTEM,
                       "rdp-view-only", FALSE,
                       "rdp-screen-share-mode", GRD_RDP_SCREEN_SHARE_MODE_EXTEND,
                       NULL);
}

static void
write_string (GrdSettingsSystem *settings_system,
              GKeyFile          *key_file,
              const char        *group,
              const char        *key,
              const char        *settings_name)
{
  g_autofree char *value = NULL;

  g_object_get (G_OBJECT (settings_system), settings_name, &value, NULL);

  g_key_file_set_string (key_file,
                         group,
                         key,
                         value);
}

static void
read_filename (GrdSettingsSystem *settings_system,
               GKeyFile          *key_file,
               const char        *group,
               const char        *key,
               const char        *settings_name)
{
  g_autofree char *value = NULL;
  g_autoptr (GError) error = NULL;

  value = g_key_file_get_string (key_file,
                                 group,
                                 key,
                                 &error);
  if (error && error->code != G_KEY_FILE_ERROR_KEY_NOT_FOUND)
    return;

  if (!value)
    return;

  if (!g_file_test (value, G_FILE_TEST_IS_REGULAR))
    return;

  g_object_set (G_OBJECT (settings_system), settings_name, value, NULL);
}

static void
read_int (GrdSettingsSystem *settings_system,
          GKeyFile          *key_file,
          const char        *group,
          const char        *key,
          const char        *settings_name)
{
  g_autoptr (GError) error = NULL;
  int value;

  value = g_key_file_get_integer (key_file,
                                  group,
                                  key,
                                  &error);
  if (error)
    return;

  g_object_set (G_OBJECT (settings_system), settings_name, value, NULL);
}

static void
write_int (GrdSettingsSystem *settings_system,
           GKeyFile          *key_file,
           const char        *group,
           const char        *key,
           const char        *settings_name)
{
  int value = 0;

  g_object_get (G_OBJECT (settings_system), settings_name, &value, NULL);

  g_key_file_set_integer (key_file,
                          group,
                          key,
                          value);
}

static void
read_boolean (GrdSettingsSystem *settings_system,
              GKeyFile          *key_file,
              const char        *group,
              const char        *key,
              const char        *settings_name)
{
  g_autoptr (GError) error = NULL;
  gboolean value = FALSE;

  value = g_key_file_get_boolean (key_file, group, key, &error);
  if (error)
    return;

  g_object_set (G_OBJECT (settings_system), settings_name, value, NULL);
}

static void
write_boolean (GrdSettingsSystem *settings_system,
               GKeyFile          *key_file,
               const char        *group,
               const char        *key,
               const char        *settings_name)
{
  gboolean value = FALSE;

  g_object_get (G_OBJECT (settings_system), settings_name, &value, NULL);

  g_key_file_set_boolean (key_file,
                          group,
                          key,
                          value);
}


static const FileSetting rdp_file_settings[] =
{
  { "enabled", "rdp-enabled", read_boolean, write_boolean },
  { "tls-cert", "rdp-server-cert-path", read_filename, write_string },
  { "tls-key", "rdp-server-key-path", read_filename, write_string },
  { "port", "rdp-port", read_int, write_int },
};

static void
on_rdp_setting_changed (GrdSettingsSystem *settings_system,
                        GParamSpec        *pspec,
                        FileSetting       *file_setting)
{
  GrdSettingsSourceType settings_source_type = GRD_SETTINGS_SOURCE_TYPE_INVALID;
  g_autofree char *local_state_conf = NULL;
  g_autoptr (GKeyFile) key_file = NULL;
  g_autoptr (GError) error = NULL;
  g_autofree char *data = NULL;
  const char *filename;
  size_t length = 0;

  grd_settings_system_reload_sources (settings_system);

  file_setting->write_settings_value (settings_system,
                                      settings_system->key_file,
                                      GRD_SETTINGS_SYSTEM_GROUP_RDP,
                                      file_setting->file_key,
                                      file_setting->settings_name);

  data = g_key_file_to_data (settings_system->key_file, &length, NULL);

  key_file = g_key_file_new ();
  if (!g_key_file_load_from_data (key_file, data, length,
                                  G_KEY_FILE_KEEP_COMMENTS |
                                  G_KEY_FILE_KEEP_TRANSLATIONS,
                                  &error))
    {
      g_warning ("Failed to copy loaded key file: %s", error->message);
      return;
    }

  if (settings_system->use_local_state)
    settings_source_type = GRD_SETTINGS_SOURCE_TYPE_LOCAL_STATE;
  else
    settings_source_type = GRD_SETTINGS_SOURCE_TYPE_CUSTOM;

  local_state_conf = get_local_state_conf ();
  switch (settings_source_type)
    {
    case GRD_SETTINGS_SOURCE_TYPE_LOCAL_STATE:
      filename = local_state_conf;
      break;
    case GRD_SETTINGS_SOURCE_TYPE_CUSTOM:
      g_remove (local_state_conf);
      filename = GRD_CUSTOM_CONF;
      break;
    default:
      g_assert_not_reached ();
    }

  if (prune_inherited_keys (settings_system,
                            settings_source_type,
                            key_file))
    {
      if (!g_key_file_save_to_file (key_file, filename, &error))
        {
          g_warning ("Failed to write %s: %s", file_setting->file_key,
                     error->message);
        }
    }
  else
    {
      g_remove (filename);
    }
}

static void
read_rdp_file_settings (GrdSettingsSystem *settings_system)
{
  g_autoptr (GKeyFile) key_file = NULL;
  g_autoptr (GError) error = NULL;
  int i;

  for (i = 0; i < G_N_ELEMENTS (rdp_file_settings); i++)
    {
      g_signal_handlers_block_by_func (G_OBJECT (settings_system),
                                       G_CALLBACK (on_rdp_setting_changed),
                                       (gpointer) &rdp_file_settings[i]);

      rdp_file_settings[i].read_settings_value (
        settings_system,
        settings_system->key_file,
        GRD_SETTINGS_SYSTEM_GROUP_RDP,
        rdp_file_settings[i].file_key,
        rdp_file_settings[i].settings_name);

      g_signal_handlers_unblock_by_func (G_OBJECT (settings_system),
                                         G_CALLBACK (on_rdp_setting_changed),
                                         (gpointer) &rdp_file_settings[i]);
    }
}

static void
register_write_rdp_file_settings (GrdSettingsSystem *settings_system)
{
  int i;

  for (i = 0; i < G_N_ELEMENTS (rdp_file_settings); i++)
    {
      g_autofree char *signal_name = NULL;

      signal_name = g_strdup_printf ("notify::%s",
                                     rdp_file_settings[i].settings_name);
      g_signal_connect (G_OBJECT (settings_system),
                        signal_name,
                        G_CALLBACK (on_rdp_setting_changed),
                        (gpointer) &rdp_file_settings[i]);
    }
}

static void
grd_settings_system_constructed (GObject *object)
{
  GrdSettingsSystem *settings_system = GRD_SETTINGS_SYSTEM (object);

  grd_settings_system_reload_sources (settings_system);
  read_rdp_file_settings (settings_system);

  register_write_rdp_file_settings (settings_system);

  G_OBJECT_CLASS (grd_settings_system_parent_class)->constructed (object);
}

static void
grd_settings_system_finalize (GObject *object)
{
  GrdSettingsSystem *settings_system = GRD_SETTINGS_SYSTEM (object);
  size_t i;

  g_clear_pointer (&settings_system->key_file, g_key_file_unref);

  for (i = 0; i < N_GRD_SETTINGS_SOURCES; i++)
    {
      g_clear_pointer (&settings_system->setting_sources[i],
                       grd_settings_source_free);
    }

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

static void
grd_settings_system_init (GrdSettingsSystem *settings_system)
{
}

static void
grd_settings_system_class_init (GrdSettingsSystemClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed = grd_settings_system_constructed;
  object_class->finalize = grd_settings_system_finalize;
}
