/* cc-keyboard-shortcut-dialog.c
 *
 * Copyright (C) 2010 Intel, Inc
 * Copyright (C) 2016 Endless, Inc
 * Copyright (C) 2020 System76, Inc.
 * Copyright (C) 2022 Purism SPC
 *
 * 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/>.
 *
 * Author: Thomas Wood <thomas.wood@intel.com>
 *         Georges Basile Stavracas Neto <gbsneto@gnome.org>
 *         Ian Douglas Scott <idscott@system76.com>
 *         Mohammed Sadiq <sadiq@sadiqpk.org>
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include <config.h>
#include <glib/gi18n.h>
#include <adwaita.h>

#include "cc-keyboard-shortcut-dialog.h"
#include "cc-keyboard-item.h"
#include "cc-keyboard-manager.h"
#include "cc-keyboard-shortcut-editor.h"
#include "cc-keyboard-shortcut-group.h"
#include "cc-keyboard-shortcut-row.h"
#include "cc-list-row.h"
#include "cc-util.h"
#include "keyboard-shortcuts.h"

struct _CcKeyboardShortcutDialog
{
  AdwWindow             parent_instance;

  AdwLeaflet           *leaflet;
  GtkBox               *main_box;
  GtkButton            *reset_all_button;
  GtkSearchEntry       *search_entry;
  GtkStack             *section_stack;
  AdwPreferencesPage   *section_list_page;
  GtkListBox           *section_list_box;
  AdwPreferencesPage   *search_result_page;
  AdwStatusPage        *empty_results_page;

  GtkBox               *subview_box;
  AdwWindowTitle       *subview_title;
  GtkButton            *back_button;
  GtkStack             *subview_stack;
  GtkStack             *shortcut_list_stack;
  AdwStatusPage        *empty_custom_shortcut_page;
  GtkSizeGroup         *accelerator_size_group;

  /* A GListStore of sections containing a GListStore of CcKeyboardItem */
  GListStore           *sections;
  GListStore           *visible_section;
  GtkFlattenListModel  *filtered_shortcuts;

  CcKeyboardManager    *manager;
  GtkWidget            *shortcut_editor;
  GStrv                 search_terms;
 };

G_DEFINE_TYPE (CcKeyboardShortcutDialog, cc_keyboard_shortcut_dialog, ADW_TYPE_WINDOW)


static GListStore *
keyboard_shortcut_get_section_store (CcKeyboardShortcutDialog *self,
                                     const char               *section_id,
                                     const char               *section_title)
{
  g_autoptr(GListStore) section = NULL;
  GtkWidget *group;
  guint n_items;

  g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));
  g_assert (section_id && *section_id);

  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->sections));

  for (guint i = 0; i < n_items; i++)
    {
      g_autoptr(GObject) item = NULL;
      const char *item_section_id;

      item = g_list_model_get_item (G_LIST_MODEL (self->sections), i);
      item_section_id = g_object_get_data (item, "id");

      if (g_str_equal (item_section_id, section_id))
        return G_LIST_STORE (item);
    }

  /* Found no matching section, so create one */
  section = g_list_store_new (CC_TYPE_KEYBOARD_ITEM);
  g_object_set_data_full (G_OBJECT (section), "id", g_strdup (section_id), g_free);
  g_object_set_data_full (G_OBJECT (section), "title", g_strdup (section_title), g_free);

  /* This group shall be shown in the search results page */
  group = cc_keyboard_shortcut_group_new (G_LIST_MODEL (section),
                                          section_id, section_title,
                                          self->manager,
                                          CC_KEYBOARD_SHORTCUT_EDITOR (self->shortcut_editor),
                                          self->accelerator_size_group);
  g_object_set_data (G_OBJECT (section), "search-group", group);

  /* This group shall be shown when a section title row is activated */
  group = cc_keyboard_shortcut_group_new (G_LIST_MODEL (section),
                                          section_id, NULL,
                                          self->manager,
                                          CC_KEYBOARD_SHORTCUT_EDITOR (self->shortcut_editor),
                                          self->accelerator_size_group);
  g_object_set_data (G_OBJECT (section), "group", group);

  g_list_store_append (self->sections, section);

  return section;
}

static void
shortcut_added_cb (CcKeyboardShortcutDialog *self,
                   CcKeyboardItem           *item,
                   const char               *section_id,
                   const char               *section_title)
{
  GListStore *section;

  section = keyboard_shortcut_get_section_store (self, section_id, section_title);
  g_object_set_data (G_OBJECT (item), "section", section);
  g_list_store_append (section, item);
}

static void
shortcut_removed_cb (CcKeyboardShortcutDialog *self,
                     CcKeyboardItem           *item)
{
  GListStore *section;
  guint position;

  section = g_object_get_data (G_OBJECT (item), "section");
  g_return_if_fail (section);

  if (g_list_store_find (section, item, &position))
    g_list_store_remove (section, position);
}

static void
shortuct_custom_items_changed (CcKeyboardShortcutDialog *self)
{
  GListStore *section;

  g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));

  section = keyboard_shortcut_get_section_store (self, "custom", "Custom Shortcuts");

  if (self->visible_section == section)
    {
      GtkWidget *page;
      guint n_items;

      n_items = g_list_model_get_n_items (G_LIST_MODEL (section));

      if (n_items)
        page = GTK_WIDGET (self->shortcut_list_stack);
      else
        page = GTK_WIDGET (self->empty_custom_shortcut_page);

      gtk_stack_set_visible_child (self->subview_stack, page);
    }
}

static int
compare_sections_title (gconstpointer a,
                        gconstpointer b,
                        gpointer     user_data)
{
  GObject *obj_a, *obj_b;

  const char *title_a, *title_b, *id_a, *id_b;

  obj_a = G_OBJECT (a);
  obj_b = G_OBJECT (b);

  id_a = g_object_get_data (obj_a, "id");
  id_b = g_object_get_data (obj_b, "id");

  /* Always place custom row as the last item */
  if (g_str_equal (id_a, "custom"))
    return 1;

  if (g_str_equal (id_b, "custom"))
    return -1;

  title_a = _(g_object_get_data (obj_a, "title"));
  title_b = _(g_object_get_data (obj_b, "title"));

  return g_strcmp0 (title_a, title_b);
}

static void
shortcut_search_result_changed_cb (CcKeyboardShortcutDialog *self)
{
  GListModel *model;
  GtkWidget *page;
  guint n_items;

  g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));

  /* If a section is already shown, it is handled in search change callback */
  if (self->visible_section)
    return;

  model = G_LIST_MODEL (self->filtered_shortcuts);
  n_items = g_list_model_get_n_items (model);

  if (n_items == 0)
    page = GTK_WIDGET (self->empty_results_page);
  else if (self->search_terms)
    page = GTK_WIDGET (self->search_result_page);
  else
    page = GTK_WIDGET (self->section_list_page);

  gtk_stack_set_visible_child (self->section_stack, page);
}

/* All items have loaded, now sort the groups and add them to the page */
static void
shortcuts_loaded_cb (CcKeyboardShortcutDialog *self)
{
  g_autoptr(GPtrArray) filtered_items = NULL;
  g_autoptr(GPtrArray) widgets = NULL;
  GListStore *filtered_lists;
  GListStore *custom_store;
  guint n_items;

  /* Ensure that custom shorcuts section exists */
  custom_store = keyboard_shortcut_get_section_store (self, "custom", "Custom Shortcuts");
  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->sections));
  widgets = g_ptr_array_new ();
  filtered_items = g_ptr_array_new ();
  filtered_lists = g_list_store_new (G_TYPE_LIST_MODEL);

  g_signal_connect_object (custom_store, "items-changed",
                           G_CALLBACK (shortuct_custom_items_changed),
                           self, G_CONNECT_SWAPPED);

  g_list_store_sort (self->sections, compare_sections_title, NULL);

  for (guint i = 0; i < n_items; i++)
    {
      g_autoptr(GObject) item = NULL;
      CcKeyboardShortcutGroup *group;
      GListModel *model;
      GtkWidget *page;

      item = g_list_model_get_item (G_LIST_MODEL (self->sections), i);
      group = g_object_get_data (item, "search-group");
      g_ptr_array_add (widgets, group);

      model = cc_keyboard_shortcut_group_get_model (group);
      g_ptr_array_add (filtered_items, model);

      /* Populate shortcut section page */
      group = g_object_get_data (item, "group");
      page = adw_preferences_page_new ();
      g_object_set_data (item, "page", page);
      adw_preferences_page_add (ADW_PREFERENCES_PAGE (page), ADW_PREFERENCES_GROUP (group));
      gtk_stack_add_child (self->shortcut_list_stack, page);
    }

  /* Populate search results page */
  for (guint i = 0; i < widgets->len; i++)
    adw_preferences_page_add (self->search_result_page, widgets->pdata[i]);

  /* Keep track of search results so as to update empty state */
  g_list_store_splice (filtered_lists, 0, 0, filtered_items->pdata, filtered_items->len);
  self->filtered_shortcuts = gtk_flatten_list_model_new (G_LIST_MODEL (filtered_lists));

  g_signal_connect_object (self->filtered_shortcuts, "items-changed",
                           G_CALLBACK (shortcut_search_result_changed_cb),
                           self, G_CONNECT_SWAPPED);
}

static GtkWidget *
shortcut_dialog_row_new (gpointer item,
                         gpointer user_data)
{
  GtkWidget *row, *group;
  const char *title;

  group = g_object_get_data (item, "search-group");
  title = g_object_get_data (item, "title");
  row = g_object_new (CC_TYPE_LIST_ROW, NULL);

  g_object_set_data (G_OBJECT (row), "section", item);
  cc_list_row_set_show_arrow (CC_LIST_ROW (row), TRUE);
  gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), TRUE);
  adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row), _(title));

  g_object_bind_property (group, "modified-text",
                          row, "secondary-label",
                          G_BINDING_SYNC_CREATE);

  return row;
}

static void
add_custom_shortcut_clicked_cb (CcKeyboardShortcutDialog *self)
{
  CcKeyboardShortcutEditor *editor;

  editor = CC_KEYBOARD_SHORTCUT_EDITOR (self->shortcut_editor);

  cc_keyboard_shortcut_editor_set_mode (editor, CC_SHORTCUT_EDITOR_CREATE);
  cc_keyboard_shortcut_editor_set_item (editor, NULL);

  gtk_widget_show (self->shortcut_editor);
}

static void
back_button_clicked_cb (CcKeyboardShortcutDialog *self)
{
  adw_leaflet_navigate (self->leaflet, ADW_NAVIGATION_DIRECTION_BACK);
}

static void
on_reset_all_dialog_response_cb (GtkDialog                *dialog,
                                 gint                      response,
                                 CcKeyboardShortcutDialog *self)
{
  guint n_items, j_items;

  gtk_window_destroy (GTK_WINDOW (dialog));
  if (response != GTK_RESPONSE_ACCEPT)
    return;

  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->sections));

  for (guint i = 0; i < n_items; i++)
    {
      g_autoptr(GListModel) section = NULL;

      section = g_list_model_get_item (G_LIST_MODEL (self->sections), i);
      j_items = g_list_model_get_n_items (section);

      for (guint j = 0; j < j_items; j++)
        {
          g_autoptr(CcKeyboardItem) item = NULL;

          item = g_list_model_get_item (section, j);

          /* Don't reset custom shortcuts */
          if (cc_keyboard_item_get_item_type (item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH)
            return;

          /* cc_keyboard_manager_reset_shortcut() already resets conflicting shortcuts,
           * so no other check is needed here. */
          cc_keyboard_manager_reset_shortcut (self->manager, item);
        }
    }
}

static void
reset_all_clicked_cb (CcKeyboardShortcutDialog *self)
{
  GtkWidget *dialog, *button;

  dialog = gtk_message_dialog_new (GTK_WINDOW (self),
                                   GTK_DIALOG_MODAL | GTK_DIALOG_USE_HEADER_BAR | GTK_DIALOG_DESTROY_WITH_PARENT,
                                   GTK_MESSAGE_WARNING,
                                   GTK_BUTTONS_NONE,
                                   _("Reset All Shortcuts?"));

  gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
                                            _("Resetting the shortcuts may affect your custom shortcuts. "
                                              "This cannot be undone."));

  gtk_dialog_add_buttons (GTK_DIALOG (dialog),
                          _("Cancel"), GTK_RESPONSE_CANCEL,
                          _("Reset All"), GTK_RESPONSE_ACCEPT,
                          NULL);

  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);

  /* Make the "Reset All" button destructive */
  button = gtk_dialog_get_widget_for_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
  gtk_widget_add_css_class (button, "destructive-action");

  g_signal_connect (dialog, "response", G_CALLBACK (on_reset_all_dialog_response_cb), self);

  gtk_window_present (GTK_WINDOW (dialog));
}

static void
shortcut_dialog_visible_child_changed_cb (CcKeyboardShortcutDialog *self)
{
  gpointer visible_child;
  gboolean is_main_view;

  visible_child = adw_leaflet_get_visible_child (self->leaflet);
  is_main_view = visible_child == self->main_box;

  if (is_main_view)
    {
      gtk_editable_set_text (GTK_EDITABLE (self->search_entry), "");
      gtk_widget_grab_focus (GTK_WIDGET (self->search_entry));

      self->visible_section = NULL;
    }
  else if (self->visible_section)
    {
      const char *title;

      title = g_object_get_data (G_OBJECT (self->visible_section), "title");
      adw_window_title_set_title (self->subview_title, _(title) ?: "");
    }
}

static void
shortcut_search_entry_changed_cb (CcKeyboardShortcutDialog *self)
{
  g_autofree char *search = NULL;
  const char *search_text;
  guint n_items;

  g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));

  /* Don't update search if we are in a subview */
  if (self->visible_section)
    return;

  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->sections));
  search_text = gtk_editable_get_text (GTK_EDITABLE (self->search_entry));
  search = cc_util_normalize_casefold_and_unaccent (search_text);

  g_clear_pointer (&self->search_terms, g_strfreev);
  if (search && *search && *search != ' ')
    self->search_terms = g_strsplit (search, " ", -1);

  /* "Reset all..." button should be sensitive only if the search is not active */
  gtk_widget_set_sensitive (GTK_WIDGET (self->reset_all_button), !self->search_terms);

  for (guint i = 0; i < n_items; i++)
    {
      g_autoptr(GObject) item = NULL;
      CcKeyboardShortcutGroup *group;

      item = g_list_model_get_item (G_LIST_MODEL (self->sections), i);
      group = g_object_get_data (item, "search-group");

      cc_keyboard_shortcut_group_set_filter (group, self->search_terms);
    }

  shortcut_search_result_changed_cb (self);
}

static void
shortcut_section_row_activated_cb (CcKeyboardShortcutDialog *self,
                                   GtkListBoxRow            *row)
{
  GListStore *section;
  GtkWidget *page;

  g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));
  g_assert (GTK_IS_LIST_BOX_ROW (row));

  section = g_object_get_data (G_OBJECT (row), "section");
  self->visible_section = section;

  page = g_object_get_data (G_OBJECT (section), "page");
  gtk_stack_set_visible_child (self->shortcut_list_stack, page);
  adw_leaflet_set_visible_child (self->leaflet, GTK_WIDGET (self->subview_box));
  shortuct_custom_items_changed (self);
}

static void
cc_keyboard_shortcut_dialog_constructed (GObject *object)
{
  CcKeyboardShortcutDialog *self = CC_KEYBOARD_SHORTCUT_DIALOG (object);

  G_OBJECT_CLASS (cc_keyboard_shortcut_dialog_parent_class)->constructed (object);

  /* Setup the dialog's transient parent */
  gtk_window_set_transient_for (GTK_WINDOW (self->shortcut_editor), GTK_WINDOW (self));
}

static void
cc_keyboard_shortcut_dialog_finalize (GObject *object)
{
  CcKeyboardShortcutDialog *self = CC_KEYBOARD_SHORTCUT_DIALOG (object);

  g_clear_object (&self->manager);
  g_clear_object (&self->sections);
  g_clear_pointer (&self->search_terms, g_strfreev);
  g_clear_object (&self->sections);
  g_clear_object (&self->filtered_shortcuts);
  g_clear_pointer ((GtkWindow**)&self->shortcut_editor, gtk_window_destroy);

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

static void
cc_keyboard_shortcut_dialog_class_init (CcKeyboardShortcutDialogClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->constructed = cc_keyboard_shortcut_dialog_constructed;
  object_class->finalize = cc_keyboard_shortcut_dialog_finalize;

  gtk_widget_class_set_template_from_resource (widget_class,
                                               "/org/gnome/control-center/"
                                               "keyboard/cc-keyboard-shortcut-dialog.ui");

  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, leaflet);
  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, main_box);
  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, reset_all_button);
  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, search_entry);
  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, section_stack);
  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, section_list_page);
  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, section_list_box);
  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, search_result_page);
  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, empty_results_page);

  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, subview_box);
  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, subview_title);
  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, back_button);
  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, subview_stack);
  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, shortcut_list_stack);
  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, empty_custom_shortcut_page);
  gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, accelerator_size_group);

  gtk_widget_class_bind_template_callback (widget_class, add_custom_shortcut_clicked_cb);
  gtk_widget_class_bind_template_callback (widget_class, back_button_clicked_cb);
  gtk_widget_class_bind_template_callback (widget_class, reset_all_clicked_cb);
  gtk_widget_class_bind_template_callback (widget_class, shortcut_dialog_visible_child_changed_cb);
  gtk_widget_class_bind_template_callback (widget_class, shortcut_search_entry_changed_cb);
  gtk_widget_class_bind_template_callback (widget_class, shortcut_section_row_activated_cb);
}

static void
cc_keyboard_shortcut_dialog_init (CcKeyboardShortcutDialog *self)
{
  gtk_widget_init_template (GTK_WIDGET (self));
  gtk_search_entry_set_key_capture_widget (self->search_entry, GTK_WIDGET (self));
  shortcut_dialog_visible_child_changed_cb (self);

  self->manager = cc_keyboard_manager_new ();

  self->shortcut_editor = cc_keyboard_shortcut_editor_new (self->manager);
  shortcut_dialog_visible_child_changed_cb (self);

  self->sections = g_list_store_new (G_TYPE_LIST_STORE);

  g_signal_connect_object (self->manager,
                           "shortcut-added",
                           G_CALLBACK (shortcut_added_cb),
                           self, G_CONNECT_SWAPPED);
  g_signal_connect_object (self->manager,
                           "shortcut-removed",
                           G_CALLBACK (shortcut_removed_cb),
                           self, G_CONNECT_SWAPPED);
  g_signal_connect_object (self->manager,
                           "shortcuts-loaded",
                           G_CALLBACK (shortcuts_loaded_cb),
                           self, G_CONNECT_SWAPPED);

  cc_keyboard_manager_load_shortcuts (self->manager);

  gtk_list_box_bind_model (self->section_list_box,
                           G_LIST_MODEL (self->sections),
                           shortcut_dialog_row_new,
                           self, NULL);
}

GtkWidget*
cc_keyboard_shortcut_dialog_new (void)
{
  return g_object_new (CC_TYPE_KEYBOARD_SHORTCUT_DIALOG, NULL);
}
