/*
 * Copyright (C) 2024-2026 Phosh.mobi e.V.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * Author: Guido Günther <agx@sigxcpu.org>
 */

#define G_LOG_DOMAIN "ms-osk-layout-prefs"

#include "mobile-settings-config.h"

#include "ms-osk-add-layout-dialog.h"
#include "ms-osk-layout-prefs.h"
#include "ms-osk-layout-row-priv.h"
#include "ms-osk-layout-priv.h"

#include <json-glib/json-glib.h>

#define GNOME_DESKTOP_USE_UNSTABLE_API
#include <libgnome-desktop/gnome-xkb-info.h>
#include <libgnome-desktop/gnome-languages.h>

#include <gmobile/gmobile.h>

#include <gtk/gtk.h>

/**
 * MsOskLayoutPrefs:
 *
 * A preferences group managing OSK layouts.
 *
 * Layouts added via this dialog are automatically added to the list
 * of input sources available to the user.
 */

#define INPUT_SOURCES_SETTINGS "org.gnome.desktop.input-sources"
#define SOURCES_KEY            "sources"

struct _MsOskLayoutPrefs {
  AdwPreferencesGroup   parent;

  GtkWidget            *layouts_preferences_group;
  GtkWidget            *layouts_list_box;

  /* Currently configured layouts in input-sources */
  GListStore           *source_layouts;
  GSettings            *input_source_settings;
  GVariant             *sources;
  GnomeXkbInfo         *xkbinfo;

  /* All layouts the OSK supports */
  GListStore           *available_layouts;
  GHashTable           *available_layouts_by_id;

  /* Enabled layouts in the input sources that the OSK supports */
  GtkFilterListModel   *usable_layouts;
  GtkCustomFilter      *usable_filter;

  GCancellable         *cancel;
};
G_DEFINE_TYPE (MsOskLayoutPrefs, ms_osk_layout_prefs, ADW_TYPE_PREFERENCES_GROUP)


static void
update_enabled_move_actions (MsOskLayoutPrefs *self)
{
  GtkWidget *child;

  for (child = gtk_widget_get_first_child (GTK_WIDGET (self->layouts_list_box));
       child != NULL;
       child = gtk_widget_get_next_sibling (child)) {
    gint row_idx;
    GtkWidget *sibling;

    if (!MS_IS_OSK_LAYOUT_ROW (child))
      continue;

    row_idx = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (child));
    gtk_widget_action_set_enabled (GTK_WIDGET (child), "row.move-up", row_idx != 0);


    sibling = gtk_widget_get_next_sibling (GTK_WIDGET (child));
    gtk_widget_action_set_enabled (GTK_WIDGET (child), "row.move-down",
                                   MS_IS_OSK_LAYOUT_ROW (sibling));
  }
}


static void
update_remove_actions (MsOskLayoutPrefs *self)
{
  GtkWidget *child;
  gboolean enable;

  enable = g_list_model_get_n_items (G_LIST_MODEL (self->usable_layouts)) > 1;

  for (child = gtk_widget_get_first_child (GTK_WIDGET (self->layouts_list_box));
       child != NULL;
       child = gtk_widget_get_next_sibling (child)) {

    if (!MS_IS_OSK_LAYOUT_ROW (child))
      continue;

    gtk_widget_action_set_enabled (GTK_WIDGET (child), "row.remove", enable);
  }
}


static MsOskLayout *
get_osk_layout (MsOskLayoutPrefs *self, const char *type, const char *layout_id)
{
  g_autofree char *key = NULL;

  key = g_strdup_printf ("%s:%s", type, layout_id);
  return g_hash_table_lookup (self->available_layouts_by_id, key);
}


static void
on_input_sources_changed (MsOskLayoutPrefs *self, const char *unused, GSettings *settings)
{
  const char *type, *layout_id;
  GVariantIter iter;

  g_debug ("Setting changed, reloading input settings");

  g_clear_pointer (&self->sources, g_variant_unref);
  self->sources = g_settings_get_value (settings, SOURCES_KEY);

  g_list_store_remove_all (self->source_layouts);

  g_variant_iter_init (&iter, self->sources);
  while (g_variant_iter_next (&iter, "(&s&s)", &type, &layout_id)) {
    g_autoptr (MsOskLayout) layout = NULL;
    g_autofree char *name = NULL;

    layout = get_osk_layout (self, type, layout_id);
    if (layout) {
      g_list_store_append (self->source_layouts, g_steal_pointer (&layout));
    } else {
      g_debug ("Failed to get layout for %s %s", type, layout_id);
      /* Even without a name we don't drop a layout as the user
       * might want to use those in docked mode */
      layout = ms_osk_layout_new (NULL, type, layout_id, NULL, NULL);
      g_list_store_append (self->source_layouts, layout);
    }
  }

  gtk_filter_changed (GTK_FILTER (self->usable_filter), GTK_FILTER_CHANGE_DIFFERENT);
  update_enabled_move_actions (self);
  update_remove_actions (self);
}


static void
update_input_sources (MsOskLayoutPrefs *self)
{
  guint n_layouts;
  GVariantBuilder builder;

  g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ss)"));

  n_layouts = g_list_model_get_n_items (G_LIST_MODEL (self->source_layouts));
  for (guint i = 0; i < n_layouts; i++) {
    MsOskLayout *layout = g_list_model_get_item (G_LIST_MODEL (self->source_layouts), i);

    g_variant_builder_add (&builder, "(ss)",
                           ms_osk_layout_get_type_ (layout),
                           ms_osk_layout_get_id (layout));
  }

  g_settings_set_value (self->input_source_settings, SOURCES_KEY, g_variant_builder_end (&builder));
}


static void
ms_osk_layout_prefs_append_layout (MsOskLayoutPrefs *self, MsOskLayout *layout)
{
  g_signal_handlers_block_by_func (self->input_source_settings,
                                   on_input_sources_changed,
                                   self);

  g_list_store_append (self->source_layouts, layout);
  update_input_sources (self);

  g_signal_handlers_unblock_by_func (self->input_source_settings,
                                     on_input_sources_changed,
                                     self);

  on_input_sources_changed (self, NULL, self->input_source_settings);
}


static void
on_layout_selected (MsOskLayoutPrefs *self, MsOskLayout *layout)
{
  g_assert (MS_IS_OSK_LAYOUT_PREFS (self));
  g_assert (MS_IS_OSK_LAYOUT (layout));

  ms_osk_layout_prefs_append_layout (self, layout);
}


static gboolean
is_source_layout (MsOskLayoutPrefs *self, MsOskLayout *layout)
{
  const char *type, *layout_id;
  GVariantIter iter;

  g_variant_iter_init (&iter, self->sources);
  while (g_variant_iter_next (&iter, "(&s&s)", &type, &layout_id)) {
    if (g_str_equal (type, ms_osk_layout_get_type_ (layout)) &&
        g_str_equal (layout_id, ms_osk_layout_get_id (layout))) {
      return TRUE;
    }
  }

  return FALSE;
}


static void
on_add_layout_activated (GtkWidget *widget, const char *action_name, GVariant *parameter)
{
  MsOskLayoutPrefs *self = MS_OSK_LAYOUT_PREFS (widget);
  GtkWidget *dialog;
  g_autoptr (GListStore) layouts = g_list_store_new (MS_TYPE_OSK_LAYOUT);
  guint n_items;

  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->available_layouts));
  for (guint i = 0; i < n_items; i++) {
    g_autoptr (MsOskLayout) layout = NULL;

    layout = g_list_model_get_item (G_LIST_MODEL (self->available_layouts), i);
    if (is_source_layout (self, layout))
      continue;

    g_list_store_append (layouts, layout);
  }

  dialog = ms_osk_add_layout_dialog_new (G_LIST_MODEL (layouts));
  g_signal_connect_object (dialog,
                           "layout-selected",
                           G_CALLBACK (on_layout_selected),
                           self,
                           G_CONNECT_SWAPPED);

  adw_dialog_present (ADW_DIALOG (dialog), widget);
}


static gboolean
filter_usable (MsOskLayout *layout, MsOskLayoutPrefs *self)
{
  g_autofree char *key = NULL;

  /* Assume all are usable if we couldn't get layout info */
  if (!self->available_layouts)
    return TRUE;

  key = g_strdup_printf ("%s:%s",
                         ms_osk_layout_get_type_ (layout),
                         ms_osk_layout_get_id (layout));
  return g_hash_table_contains (self->available_layouts_by_id, key);
}



static void
on_row_moved (MsOskLayoutPrefs *self, MsOskLayoutRow *dest_row, MsOskLayoutRow *row)
{
  guint idx, dest_idx;
  MsOskLayout *layout, *dest_layout;
  gboolean success;

  layout = ms_osk_layout_row_get_layout (row);
  g_assert (layout);
  success = g_list_store_find (self->source_layouts, layout, &idx);
  g_assert (success);

  dest_layout = ms_osk_layout_row_get_layout (dest_row);
  g_assert (dest_layout);
  success = g_list_store_find (self->source_layouts, dest_layout, &dest_idx);
  g_assert (success);

  g_signal_handlers_block_by_func (self->input_source_settings,
                                   on_input_sources_changed,
                                   self);
  /* The row holds a ref on layout so it won't get destroyed on
   * removal */
  g_list_store_remove (self->source_layouts, idx);
  g_list_store_insert (self->source_layouts, dest_idx, layout);

  update_input_sources (self);

  g_signal_handlers_unblock_by_func (self->input_source_settings,
                                     on_input_sources_changed,
                                     self);

  on_input_sources_changed (self, NULL, self->input_source_settings);
}


static void
on_row_removed (MsOskLayoutPrefs *self, MsOskLayoutRow *row)
{
  guint idx;
  MsOskLayout *layout;
  gboolean success;

  layout = ms_osk_layout_row_get_layout (row);
  g_assert (layout);
  success = g_list_store_find (self->source_layouts, layout, &idx);
  g_assert (success);

  g_signal_handlers_block_by_func (self->input_source_settings,
                                   on_input_sources_changed,
                                   self);
  /* The row holds a ref on layout so it won't get destroyed on
   * removal */
  g_list_store_remove (self->source_layouts, idx);

  update_input_sources (self);

  g_signal_handlers_unblock_by_func (self->input_source_settings,
                                     on_input_sources_changed,
                                     self);

  on_input_sources_changed (self, NULL, self->input_source_settings);
}


static GtkWidget *
create_layout_row (gpointer item, gpointer user_data)
{
  MsOskLayoutPrefs *self = MS_OSK_LAYOUT_PREFS (user_data);
  MsOskLayout *layout = MS_OSK_LAYOUT (item);
  GtkWidget *row;

  row = ms_osk_layout_row_new (layout);
  g_signal_connect_object (row, "move-row", G_CALLBACK (on_row_moved), self, G_CONNECT_SWAPPED);
  g_signal_connect_object (row, "remove-row",
                           G_CALLBACK (on_row_removed),
                           self,
                           G_CONNECT_SWAPPED);

  return row;
}


static gboolean
ms_osk_layout_prefs_add_for_lang (MsOskLayoutPrefs *self,
                                  const char       *lang,
                                  const char       *flavor)
{
  guint n_items;

  /* Is layout already present and usable ? */
  for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (self->usable_layouts)); i++) {
    g_autoptr (MsOskLayout) usable_layout = NULL;
    const char *usable_lang;

    usable_layout = g_list_model_get_item (G_LIST_MODEL (self->usable_layouts), i);
    usable_lang = ms_osk_layout_get_lang (usable_layout);

    if (g_ascii_strcasecmp (lang, usable_lang) == 0)
      return TRUE;
  }

  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->available_layouts));
  for (guint i = 0; i < n_items; i++) {
    g_autoptr (MsOskLayout) layout = NULL;

    layout = g_list_model_get_item (G_LIST_MODEL (self->available_layouts), i);

    if (g_ascii_strcasecmp (lang, ms_osk_layout_get_lang (layout)))
      continue;

    /* Want default layout but osk layout is a flavor */
    if (!flavor && ms_osk_layout_get_flavor (layout))
      continue;

    /* If flavor is given, it must match */
    if (flavor && g_ascii_strcasecmp (lang, ms_osk_layout_get_flavor (layout)))
      continue;

    g_debug ("Found layout for '%s', flavor '%s'", lang, flavor);
    ms_osk_layout_prefs_append_layout (self, layout);
    return TRUE;
  }

  return FALSE;
}


static void
on_load_osk_layouts_from_stream_ready (JsonParser *parser, GAsyncResult *res, gpointer user_data)
{
  gboolean success;
  g_autoptr (GTask) task = G_TASK (user_data);
  g_autoptr (GError) err = NULL;
  MsOskLayoutPrefs *self = NULL;
  JsonNode *node;
  JsonObject *obj;
  JsonArray *layouts;

  success = json_parser_load_from_stream_finish  (parser, res, &err);
  if (!success) {
    g_task_return_error (task, g_steal_pointer (&err));
    return;
  }

  self = MS_OSK_LAYOUT_PREFS (g_task_get_source_object (task));
  g_assert (MS_IS_OSK_LAYOUT_PREFS (self));

  node = json_parser_get_root (parser);

  obj = json_node_get_object (node);
  if (!obj) {
    GError *local_err = g_error_new_literal (JSON_PARSER_ERROR,
                                             JSON_PARSER_ERROR_PARSE,
                                             "Failed to get root object");
    g_task_return_error (task, local_err);
    return;
  }

  layouts = json_object_get_array_member (obj, "layouts");
  if (!obj) {
    GError *local_err = g_error_new_literal (JSON_PARSER_ERROR,
                                             JSON_PARSER_ERROR_PARSE,
                                             "Failed to get array of layouts");
    g_task_return_error (task, local_err);
    return;
  }

  for (guint i = 0; i < json_array_get_length (layouts); i++) {
    g_autoptr (MsOskLayout) layout = NULL;
    JsonObject *layout_obj;
    const char *name, *type, *layout_id, *display_name, *lang, *flavor;
    g_autofree char *key = NULL, *layout_name = NULL;

    layout_obj = json_array_get_object_element (layouts, i);
    g_assert (layout_obj);

    name = json_object_get_string_member (layout_obj, "name");
    type = json_object_get_string_member (layout_obj, "type");
    layout_id = json_object_get_string_member (layout_obj, "layout-id");
    lang = json_object_get_string_member_with_default (layout_obj, "language", NULL);
    flavor = json_object_get_string_member_with_default (layout_obj, "flavor", NULL);

    if (!name || !type || !layout_id) {
      g_warning ("Skipping layout %d", i);
      continue;
    }

    /* Get a nicer (translated) display name for xkb layouts */
    if (g_str_equal (type, "xkb")) {
      if (gnome_xkb_info_get_layout_info (self->xkbinfo, layout_id, &display_name,
                                          NULL, NULL, NULL)) {
        layout_name = g_strdup (display_name);
      }
    }

    if (!layout_name)
      layout_name = g_strdup (name);

    g_debug ("Adding layout %s", layout_name);
    layout = ms_osk_layout_new (layout_name, type, layout_id, lang, flavor);
    g_list_store_append (self->available_layouts, layout);

    key = g_strdup_printf ("%s:%s", type, layout_id);
    g_hash_table_insert (self->available_layouts_by_id,
                         g_steal_pointer (&key),
                         g_steal_pointer (&layout));
  }

  /* Reload the layouts as we got more layout information */
  on_input_sources_changed (self, SOURCES_KEY, self->input_source_settings);

  g_task_return_boolean (task, TRUE);
}


static void
ms_osk_layout_prefs_dispose (GObject *object)
{
  MsOskLayoutPrefs *self = MS_OSK_LAYOUT_PREFS (object);

  g_cancellable_cancel (self->cancel);
  g_clear_object (&self->cancel);

  g_clear_object (&self->usable_filter);
  g_clear_object (&self->usable_layouts);

  g_clear_pointer (&self->available_layouts_by_id, g_hash_table_destroy);
  g_clear_object (&self->available_layouts);

  g_clear_object (&self->xkbinfo);
  g_clear_pointer (&self->sources, g_variant_unref);
  g_clear_object (&self->input_source_settings);
  g_clear_object (&self->source_layouts);

  G_OBJECT_CLASS (ms_osk_layout_prefs_parent_class)->dispose (object);
}


static void
ms_osk_layout_prefs_class_init (MsOskLayoutPrefsClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->dispose = ms_osk_layout_prefs_dispose;


  gtk_widget_class_set_template_from_resource (widget_class,
                                               "/mobi/phosh/LibMobileSettings/ui/"
                                               "ms-osk-layout-prefs.ui");

  gtk_widget_class_install_action (widget_class, "osk-prefs.add-layout", NULL,
                                   on_add_layout_activated);

  gtk_widget_class_bind_template_child (widget_class, MsOskLayoutPrefs, layouts_list_box);
}


static void
ms_osk_layout_prefs_init (MsOskLayoutPrefs *self)
{
  gtk_widget_init_template (GTK_WIDGET (self));

  self->cancel = g_cancellable_new ();
  self->xkbinfo = gnome_xkb_info_new ();

  self->available_layouts = g_list_store_new (MS_TYPE_OSK_LAYOUT);
  self->available_layouts_by_id = g_hash_table_new_full (g_str_hash,
                                                         g_str_equal,
                                                         g_free,
                                                         g_object_unref);

  self->source_layouts = g_list_store_new (MS_TYPE_OSK_LAYOUT);
  self->usable_filter = gtk_custom_filter_new ((GtkCustomFilterFunc)filter_usable, self, NULL);
  g_object_ref (self->source_layouts);
  g_object_ref (self->usable_filter);
  self->usable_layouts = gtk_filter_list_model_new (G_LIST_MODEL (self->source_layouts),
                                                    GTK_FILTER (self->usable_filter));
  gtk_list_box_bind_model (GTK_LIST_BOX (self->layouts_list_box),
                           G_LIST_MODEL (self->usable_layouts),
                           create_layout_row,
                           self,
                           NULL);

  self->input_source_settings = g_settings_new (INPUT_SOURCES_SETTINGS);
  g_signal_connect_swapped (self->input_source_settings, "changed::" SOURCES_KEY,
                            G_CALLBACK (on_input_sources_changed),
                            self);
  on_input_sources_changed (self, SOURCES_KEY, self->input_source_settings);
}


MsOskLayoutPrefs *
ms_osk_layout_prefs_new (void)
{
  return g_object_new (MS_TYPE_OSK_LAYOUT_PREFS, NULL);
}


typedef struct
{
  GAsyncResult *res;
  GMainLoop    *loop;
} LoadOskLayoutsData;


static void
on_load_osk_layouts_sync_ready (GObject      *object,
                                GAsyncResult *res,
                                gpointer      user_data)
{
  LoadOskLayoutsData *data = user_data;

  data->res = g_object_ref (res);
  g_main_loop_quit (data->loop);
}

/**
 * ms_osk_layout_prefs_load_osk_layouts:
 * @self: The OSK layout prefs
 *
 * Load the layouts the currently running OSK can handle. This is a
 * blocking sync call. See [MsOskLayouts.load_osk_layouts_async] for
 * an async version.
 */
void
ms_osk_layout_prefs_load_osk_layouts (MsOskLayoutPrefs *self)
{
  g_autoptr (GMainContext) context = g_main_context_new ();
  g_autoptr (GMainLoop) loop = NULL;
  g_autoptr (GError) err = NULL;
  LoadOskLayoutsData data;
  gboolean success;

  loop = g_main_loop_new (context, FALSE);
  g_main_context_push_thread_default (context);

  data = (LoadOskLayoutsData) {
    .loop = loop,
    .res = NULL,
  };

  ms_osk_layout_prefs_load_osk_layouts_async (self,
                                              self->cancel,
                                              on_load_osk_layouts_sync_ready,
                                              &data);
  g_main_loop_run (loop);

  success = ms_osk_layout_prefs_load_osk_layouts_finish (self, data.res, &err);
  g_main_context_pop_thread_default (context);

  if (!success) {
    if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED))
      return;
    g_warning ("Failed to load layouts: %s", err->message);
    return;
  }

  if (data.res)
    g_object_unref (data.res);
}


gboolean
ms_osk_layout_prefs_load_osk_layouts_finish (MsOskLayoutPrefs *self,
                                             GAsyncResult     *res,
                                             GError          **error)
{
  g_return_val_if_fail (MS_IS_OSK_LAYOUT_PREFS (self), FALSE);
  g_return_val_if_fail (G_IS_TASK (res), FALSE);
  g_return_val_if_fail (!error || !*error, FALSE);

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


/**
 * ms_osk_layout_prefs_load_osk_layouts_async:
 * @self: The OSK layout prefs
 *
 * Load the layouts the currently running OSK can handle. This is an async call.
 * Use `ms_osk_layout_prefs_load_osk_layouts_async` to retrieve the result.
 */
void
ms_osk_layout_prefs_load_osk_layouts_async (MsOskLayoutPrefs   *self,
                                            GCancellable       *cancellable,
                                            GAsyncReadyCallback callback,
                                            gpointer            user_data)
{
  GTask *task = g_task_new (self, cancellable, callback, user_data);
  const char *layouts_path;
  g_autoptr (GFile) layouts_file = NULL;
  g_autoptr (JsonParser) parser = json_parser_new_immutable ();
  g_autoptr (GFileInputStream) stream = NULL;
  g_autoptr (GError) err = NULL;

  layouts_path = getenv ("MS_OSK_LAYOUTS") ?: MOBILE_SETTINGS_OSK_LAYOUTS;
  layouts_file = g_file_new_for_path (layouts_path);

  stream = g_file_read (layouts_file, cancellable, &err);
  if (!stream) {
    g_task_return_error (task, g_steal_pointer (&err));
    return;
  }

  json_parser_load_from_stream_async (parser,
                                      G_INPUT_STREAM (stream),
                                      self->cancel,
                                      (GAsyncReadyCallback)on_load_osk_layouts_from_stream_ready,
                                      task);
}

/**
 * ms_osk_layout_prefs_add_for_locale:
 * @self: The OSK layout prefs
 * @locale: The locale to add a layout for
 * @flavor:(nullable): A flavor if s.th. like dvorak or colemak is preferred
 *
 * Add a layout based on the given information to the currently active
 * layouts.
 *
 * Returns: `TRUE` if a layout was added or the layout is already present.
 */
gboolean
ms_osk_layout_prefs_add_for_locale (MsOskLayoutPrefs *self,
                                    const char       *locale,
                                    const char       *flavor)
{
  g_autofree char *lcp = NULL, *ccp = NULL;

  g_return_val_if_fail (MS_IS_OSK_LAYOUT_PREFS (self), FALSE);
  g_return_val_if_fail (locale, FALSE);

  if (!gnome_parse_locale (locale, &lcp, &ccp, NULL, NULL))
    return FALSE;

  if (!gm_str_is_null_or_empty (lcp) && !gm_str_is_null_or_empty (ccp)) {
    g_autofree char *lang = g_strdup_printf ("%s-%s", lcp, ccp);

    if (ms_osk_layout_prefs_add_for_lang (self, lang, flavor))
      return TRUE;
  }

  return ms_osk_layout_prefs_add_for_lang (self, lcp, flavor);

}
