/* ide-omni-bar.c
 *
 * Copyright 2018-2019 Christian Hergert <chergert@redhat.com>
 *
 * 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 3 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/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#define G_LOG_DOMAIN "ide-omni-bar"

#include "config.h"

#include <libpeas/peas.h>
#include <dazzle.h>

#include "ide-gui-global.h"
#include "ide-gui-private.h"
#include "ide-notification-list-box-row-private.h"
#include "ide-notification-stack-private.h"
#include "ide-omni-bar-addin.h"
#include "ide-omni-bar.h"

struct _IdeOmniBar
{
  GtkBin                parent_instance;

  PeasExtensionSet     *addins;
  GtkGesture           *gesture;
  GtkEventController   *motion;

  GtkEventBox          *entry_event_box;
  GtkStack             *top_stack;
  GtkPopover           *popover;
  DzlEntryBox          *entry_box;
  IdeNotificationStack *notification_stack;
  GtkListBox           *notifications_list_box;
  DzlPriorityBox       *inner_box;
  DzlPriorityBox       *outer_box;
  GtkProgressBar       *progress;
  GtkWidget            *placeholder;
  DzlPriorityBox       *sections_box;

  guint                 in_button : 1;
};

static void ide_omni_bar_move_next     (IdeOmniBar        *self,
                                        GVariant          *param);
static void ide_omni_bar_move_previous (IdeOmniBar        *self,
                                        GVariant          *param);
static void buildable_iface_init       (GtkBuildableIface *iface);

DZL_DEFINE_ACTION_GROUP (IdeOmniBar, ide_omni_bar, {
  { "move-next", ide_omni_bar_move_next },
  { "move-previous", ide_omni_bar_move_previous },
})

G_DEFINE_FINAL_TYPE_WITH_CODE (IdeOmniBar, ide_omni_bar, GTK_TYPE_BIN,
                         G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, ide_omni_bar_init_action_group)
                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init))

static GtkBuildableIface *parent_buildable_iface;

static void
ide_omni_bar_popover_closed_cb (IdeOmniBar *self,
                                GtkPopover *popover)
{
  GtkStyleContext *style_context;
  GtkStateFlags state_flags;

  g_assert (IDE_IS_OMNI_BAR (self));
  g_assert (GTK_IS_POPOVER (popover));

  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
  state_flags = gtk_style_context_get_state (style_context);

  state_flags &= ~GTK_STATE_FLAG_ACTIVE;
  state_flags &= ~GTK_STATE_FLAG_PRELIGHT;

  gtk_style_context_set_state (style_context, state_flags);
}

static void
multipress_pressed_cb (IdeOmniBar           *self,
                       guint                 n_press,
                       gdouble               x,
                       gdouble               y,
                       GtkGestureMultiPress *gesture)
{
  GtkStyleContext *style_context;
  GtkStateFlags state_flags;

  g_assert (IDE_IS_OMNI_BAR (self));
  g_assert (GTK_IS_GESTURE_MULTI_PRESS (gesture));

  if (gtk_widget_get_focus_on_click (GTK_WIDGET (self)) &&
      !gtk_widget_has_focus (GTK_WIDGET (self)))
    gtk_widget_grab_focus (GTK_WIDGET (self));

  self->in_button = TRUE;

  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
  state_flags = gtk_style_context_get_state (style_context);
  gtk_style_context_set_state (style_context, state_flags | GTK_STATE_FLAG_ACTIVE);

  gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
}

static void
multipress_released_cb (IdeOmniBar           *self,
                        guint                 n_press,
                        gdouble               x,
                        gdouble               y,
                        GtkGestureMultiPress *gesture)
{
  GtkStyleContext *style_context;
  GtkStateFlags state_flags;
  gboolean show;

  g_assert (IDE_IS_OMNI_BAR (self));
  g_assert (GTK_IS_GESTURE_MULTI_PRESS (gesture));

  show = self->in_button;
  self->in_button = FALSE;

  if (show)
    {
      gtk_popover_popup (self->popover);
      return;
    }

  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
  state_flags = gtk_style_context_get_state (style_context);
  gtk_style_context_set_state (style_context, state_flags & ~GTK_STATE_FLAG_ACTIVE);
}

static void
multipress_cancel_cb (IdeOmniBar           *self,
                      GdkEventSequence     *sequence,
                      GtkGestureMultiPress *gesture)
{
  GtkStyleContext *style_context;
  GtkStateFlags state_flags;

  g_assert (IDE_IS_OMNI_BAR (self));
  g_assert (GTK_IS_GESTURE_MULTI_PRESS (gesture));

  self->in_button = FALSE;

  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
  state_flags = gtk_style_context_get_state (style_context);
  gtk_style_context_set_state (style_context, state_flags & ~GTK_STATE_FLAG_ACTIVE);
}

static void
ide_omni_bar_notification_stack_changed_cb (IdeOmniBar           *self,
                                            IdeNotificationStack *stack)
{
  IdeNotification *notif;
  gboolean enabled;

  g_assert (IDE_IS_OMNI_BAR (self));
  g_assert (IDE_IS_NOTIFICATION_STACK (stack));

  enabled = ide_notification_stack_get_can_move (stack);

  ide_omni_bar_set_action_enabled (self, "move-previous", enabled);
  ide_omni_bar_set_action_enabled (self, "move-next", enabled);

  _ide_gtk_progress_bar_stop_pulsing (self->progress);
  gtk_widget_hide (GTK_WIDGET (self->progress));

  if ((notif = ide_notification_stack_get_visible (stack)))
    {
      if (ide_notification_get_has_progress (notif))
        {
          if (ide_notification_get_progress_is_imprecise (notif))
            _ide_gtk_progress_bar_start_pulsing (self->progress);
          gtk_widget_show (GTK_WIDGET (self->progress));
        }
    }

  if (ide_notification_stack_is_empty (stack))
    gtk_stack_set_visible_child_name (self->top_stack, "placeholder");
  else
    gtk_stack_set_visible_child_name (self->top_stack, "notifications");
}

static void
ide_omni_bar_extension_added_cb (PeasExtensionSet *set,
                                 PeasPluginInfo   *plugin_info,
                                 PeasExtension    *exten,
                                 gpointer          user_data)
{
  IdeOmniBarAddin *addin = (IdeOmniBarAddin *)exten;
  IdeOmniBar *self = user_data;

  g_assert (PEAS_IS_EXTENSION_SET (set));
  g_assert (plugin_info != NULL);
  g_assert (IDE_IS_OMNI_BAR_ADDIN (addin));
  g_assert (IDE_IS_OMNI_BAR (self));

  ide_omni_bar_addin_load (addin, self);
}

static void
ide_omni_bar_extension_removed_cb (PeasExtensionSet *set,
                                   PeasPluginInfo   *plugin_info,
                                   PeasExtension    *exten,
                                   gpointer          user_data)
{
  IdeOmniBarAddin *addin = (IdeOmniBarAddin *)exten;
  IdeOmniBar *self = user_data;

  g_assert (PEAS_IS_EXTENSION_SET (set));
  g_assert (plugin_info != NULL);
  g_assert (IDE_IS_OMNI_BAR_ADDIN (addin));
  g_assert (IDE_IS_OMNI_BAR (self));

  ide_omni_bar_addin_unload (addin, self);
}

static GtkWidget *
create_notification_row (gpointer item,
                         gpointer user_data)
{
  IdeNotification *notif = item;
  gboolean has_default;

  g_assert (IDE_IS_NOTIFICATION (notif));

  has_default = ide_notification_get_default_action (notif, NULL, NULL);

  return g_object_new (IDE_TYPE_NOTIFICATION_LIST_BOX_ROW,
                       "activatable", has_default,
                       "notification", notif,
                       "visible", TRUE,
                       NULL);
}

static gboolean
filter_for_popover (GObject  *object,
                    gpointer  user_data)
{
  IdeNotification *notif = (IdeNotification *)object;

  g_assert (IDE_IS_NOTIFICATION (notif));
  g_assert (user_data == NULL);

  return !ide_notification_get_has_progress (notif) &&
         ide_notification_get_urgent (notif);
}

static void
ide_omni_bar_context_set_cb (GtkWidget  *widget,
                             IdeContext *context)
{
  IdeOmniBar *self = (IdeOmniBar *)widget;
  g_autoptr(IdeObject) notifications = NULL;
  g_autoptr(DzlListModelFilter) filter = NULL;

  g_assert (IDE_IS_OMNI_BAR (self));
  g_assert (IDE_IS_CONTEXT (context));
  g_assert (self->addins == NULL);

  notifications = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_NOTIFICATIONS);
  ide_notification_stack_bind_model (self->notification_stack, G_LIST_MODEL (notifications));

  filter = dzl_list_model_filter_new (G_LIST_MODEL (notifications));
  dzl_list_model_filter_set_filter_func (filter, filter_for_popover, NULL, NULL);
  gtk_list_box_bind_model (self->notifications_list_box,
                           G_LIST_MODEL (filter),
                           create_notification_row,
                           NULL, NULL);

  self->addins = peas_extension_set_new (peas_engine_get_default (),
                                         IDE_TYPE_OMNI_BAR_ADDIN,
                                         NULL);

  g_signal_connect (self->addins,
                    "extension-added",
                    G_CALLBACK (ide_omni_bar_extension_added_cb),
                    self);
  g_signal_connect (self->addins,
                    "extension-removed",
                    G_CALLBACK (ide_omni_bar_extension_removed_cb),
                    self);

  peas_extension_set_foreach (self->addins,
                              ide_omni_bar_extension_added_cb,
                              self);
}

static void
ide_omni_bar_motion_enter_cb (IdeOmniBar               *self,
                              gdouble                   x,
                              gdouble                   y,
                              GtkEventControllerMotion *motion)
{
  GtkStyleContext *style_context;
  GtkStateFlags state_flags;

  g_assert (IDE_IS_OMNI_BAR (self));
  g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (motion));

  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
  state_flags = gtk_style_context_get_state (style_context);

  if ((state_flags & GTK_STATE_FLAG_PRELIGHT) == 0)
    gtk_style_context_set_state (style_context, state_flags | GTK_STATE_FLAG_PRELIGHT);
}

static void
ide_omni_bar_motion_leave_cb (IdeOmniBar               *self,
                              GtkEventControllerMotion *motion)
{
  GtkStyleContext *style_context;
  GtkStateFlags state_flags;

  g_assert (IDE_IS_OMNI_BAR (self));
  g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (motion));

  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
  state_flags = gtk_style_context_get_state (style_context);

  if (state_flags & GTK_STATE_FLAG_PRELIGHT)
    gtk_style_context_set_state (style_context, state_flags & ~GTK_STATE_FLAG_PRELIGHT);
}

static void
ide_omni_bar_motion_cb (IdeOmniBar               *self,
                        gdouble                   x,
                        gdouble                   y,
                        GtkEventControllerMotion *motion)
{
  g_assert (IDE_IS_OMNI_BAR (self));
  g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (motion));

  /*
   * Because of how crossing-events work with Gtk 3, we don't get reliable
   * crossing events for the motion controller. So every motion (which we do
   * seem to get semi-reliably), just re-run the enter-notify path to ensure
   * we get proper state set.
   */

  ide_omni_bar_motion_enter_cb (self, x, y, motion);
}

static gboolean
ide_omni_bar_query_tooltip (GtkWidget  *widget,
                            gint        x,
                            gint        y,
                            gboolean    keyboard_mode,
                            GtkTooltip *tooltip)
{
  IdeOmniBar *self = (IdeOmniBar *)widget;
  IdeNotification *notif;

  g_assert (IDE_IS_OMNI_BAR (self));
  g_assert (GTK_IS_TOOLTIP (tooltip));

  if ((notif = ide_notification_stack_get_visible (self->notification_stack)))
    {
      g_autofree gchar *body = ide_notification_dup_body (notif);

      if (body != NULL)
        {
          gtk_tooltip_set_text (tooltip, body);
          return TRUE;
        }
    }

  return FALSE;
}

static void
ide_omni_bar_notification_row_activated (IdeOmniBar                *self,
                                         IdeNotificationListBoxRow *row,
                                         GtkListBox                *list_box)
{
  g_autofree gchar *default_action = NULL;
  g_autoptr(GVariant) default_target = NULL;
  IdeNotification *notif;

  g_assert (IDE_IS_OMNI_BAR (self));
  g_assert (IDE_IS_NOTIFICATION_LIST_BOX_ROW (row));
  g_assert (GTK_IS_LIST_BOX (list_box));

  notif = ide_notification_list_box_row_get_notification (row);

  if (ide_notification_get_default_action (notif, &default_action, &default_target))
    {
      gchar *name = strchr (default_action, '.');
      gchar *group = default_action;

      if (name != NULL)
        {
          *name = '\0';
          name++;
        }
      else
        {
          group = NULL;
          name = default_action;
        }

      dzl_gtk_widget_action (GTK_WIDGET (list_box), group, name, default_target);
    }
}

static void
ide_omni_bar_destroy (GtkWidget *widget)
{
  IdeOmniBar *self = (IdeOmniBar *)widget;

  g_assert (IDE_IS_OMNI_BAR (self));

  if (self->progress != NULL)
    _ide_gtk_progress_bar_stop_pulsing (self->progress);

  g_clear_object (&self->addins);
  g_clear_object (&self->gesture);
  g_clear_object (&self->motion);

  GTK_WIDGET_CLASS (ide_omni_bar_parent_class)->destroy (widget);
}

static void
ide_omni_bar_class_init (IdeOmniBarClass *klass)
{
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  widget_class->destroy = ide_omni_bar_destroy;
  widget_class->query_tooltip = ide_omni_bar_query_tooltip;

  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-omni-bar.ui");
  gtk_widget_class_set_css_name (widget_class, "omnibar");
  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, entry_box);
  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, entry_event_box);
  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, inner_box);
  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, notification_stack);
  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, notifications_list_box);
  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, outer_box);
  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, popover);
  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, progress);
  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, sections_box);
  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, top_stack);
  gtk_widget_class_bind_template_callback (widget_class, ide_omni_bar_notification_row_activated);

  g_type_ensure (DZL_TYPE_ENTRY_BOX);
  g_type_ensure (IDE_TYPE_NOTIFICATION_STACK);
}

static void
ide_omni_bar_init (IdeOmniBar *self)
{
  gtk_widget_init_template (GTK_WIDGET (self));

  gtk_widget_set_has_tooltip (GTK_WIDGET (self), TRUE);

  gtk_widget_add_events (GTK_WIDGET (self),
                         (GDK_POINTER_MOTION_MASK |
                          GDK_ENTER_NOTIFY_MASK |
                          GDK_LEAVE_NOTIFY_MASK));

  self->motion = gtk_event_controller_motion_new (GTK_WIDGET (self));
  gtk_event_controller_set_propagation_phase (self->motion, GTK_PHASE_CAPTURE);

  g_signal_connect_swapped (self->motion,
                            "enter",
                            G_CALLBACK (ide_omni_bar_motion_enter_cb),
                            self);

  g_signal_connect_swapped (self->motion,
                            "motion",
                            G_CALLBACK (ide_omni_bar_motion_cb),
                            self);

  g_signal_connect_swapped (self->motion,
                            "leave",
                            G_CALLBACK (ide_omni_bar_motion_leave_cb),
                            self);

  self->gesture = gtk_gesture_multi_press_new (GTK_WIDGET (self->entry_event_box));
  gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (self->gesture), FALSE);
  gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (self->gesture), TRUE);
  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (self->gesture), GDK_BUTTON_PRIMARY);
  g_signal_connect_object (self->gesture,
                           "pressed",
                           G_CALLBACK (multipress_pressed_cb),
                           self,
                           G_CONNECT_SWAPPED);
  g_signal_connect_object (self->gesture,
                           "released",
                           G_CALLBACK (multipress_released_cb),
                           self,
                           G_CONNECT_SWAPPED);
  g_signal_connect_object (self->gesture,
                           "cancel",
                           G_CALLBACK (multipress_cancel_cb),
                           self,
                           G_CONNECT_SWAPPED);

  g_signal_connect_object (self->notification_stack,
                           "changed",
                           G_CALLBACK (ide_omni_bar_notification_stack_changed_cb),
                           self,
                           G_CONNECT_SWAPPED);

  g_signal_connect_object (self->popover,
                           "closed",
                           G_CALLBACK (ide_omni_bar_popover_closed_cb),
                           self,
                           G_CONNECT_SWAPPED);

  gtk_widget_insert_action_group (GTK_WIDGET (self), "omnibar", G_ACTION_GROUP (self));

  ide_omni_bar_set_action_enabled (self, "move-previous", FALSE);
  ide_omni_bar_set_action_enabled (self, "move-next", FALSE);

  ide_widget_set_context_handler (GTK_WIDGET (self), ide_omni_bar_context_set_cb);
}

GtkWidget *
ide_omni_bar_new (void)
{
  return g_object_new (IDE_TYPE_OMNI_BAR, NULL);
}

static void
ide_omni_bar_move_next (IdeOmniBar *self,
                        GVariant   *param)
{
  g_assert (IDE_IS_OMNI_BAR (self));
  g_assert (param == NULL);

  ide_notification_stack_move_next (self->notification_stack);
}

static void
ide_omni_bar_move_previous (IdeOmniBar *self,
                            GVariant   *param)
{
  g_assert (IDE_IS_OMNI_BAR (self));
  g_assert (param == NULL);

  ide_notification_stack_move_previous (self->notification_stack);
}

/**
 * ide_omni_bar_add_status_icon:
 * @self: a #IdeOmniBar
 * @widget: the #GtkWidget to add
 * @priority: the sort priority for @widget
 *
 * Adds a status-icon style widget to the end of the omnibar. Generally,
 * you'll want this to be either a GtkButton, GtkLabel, or something simple.
 *
 * Since: 3.32
 */
void
ide_omni_bar_add_status_icon (IdeOmniBar *self,
                              GtkWidget  *widget,
                              gint        priority)
{
  g_return_if_fail (IDE_IS_OMNI_BAR (self));
  g_return_if_fail (GTK_IS_WIDGET (widget));

  gtk_container_add_with_properties (GTK_CONTAINER (self->inner_box), widget,
                                     "pack-type", GTK_PACK_END,
                                     "priority", priority,
                                     NULL);
}

void
ide_omni_bar_add_button (IdeOmniBar  *self,
                         GtkWidget   *widget,
                         GtkPackType  pack_type,
                         gint         priority)
{
  g_return_if_fail (IDE_IS_OMNI_BAR (self));
  g_return_if_fail (GTK_IS_WIDGET (widget));
  g_return_if_fail (pack_type == GTK_PACK_START ||
                    pack_type == GTK_PACK_END);

  gtk_container_add_with_properties (GTK_CONTAINER (self->outer_box), widget,
                                     "pack-type", pack_type,
                                     "priority", priority,
                                     NULL);
}

void
ide_omni_bar_set_placeholder (IdeOmniBar *self,
                              GtkWidget  *widget)
{
  g_return_if_fail (IDE_IS_OMNI_BAR (self));
  g_return_if_fail (!widget || GTK_IS_WIDGET (widget));

  if (self->placeholder == widget)
    return;

  if (self->placeholder)
    gtk_widget_destroy (self->placeholder);

  self->placeholder = widget;

  if (self->placeholder)
    {
      g_signal_connect (self->placeholder,
                        "destroy",
                        G_CALLBACK (gtk_widget_destroyed),
                        self->placeholder);
      gtk_container_add_with_properties (GTK_CONTAINER (self->top_stack), self->placeholder,
                                         "name", "placeholder",
                                         NULL);
      if (self->notification_stack == NULL ||
          ide_notification_stack_is_empty (self->notification_stack))
        gtk_stack_set_visible_child_name (self->top_stack, "placeholder");
    }
}

static void
ide_omni_bar_add_child (GtkBuildable *buildable,
                          GtkBuilder   *builder,
                        GObject      *child,
                        const gchar  *type)
{
  IdeOmniBar *self = (IdeOmniBar *)buildable;

  g_assert (IDE_IS_OMNI_BAR (self));
  g_assert (GTK_IS_BUILDER (builder));
  g_assert (G_IS_OBJECT (child));

  if (ide_str_equal0 (type, "start") && GTK_IS_WIDGET (child))
    ide_omni_bar_add_button (IDE_OMNI_BAR (self),
                             GTK_WIDGET (child),
                             GTK_PACK_START,
                             0);
  else if (ide_str_equal0 (type, "end") && GTK_IS_WIDGET (child))
    ide_omni_bar_add_button (IDE_OMNI_BAR (self),
                             GTK_WIDGET (child),
                             GTK_PACK_END,
                             0);
  else if (ide_str_equal0 (type, "placeholder") && GTK_IS_WIDGET (child))
    ide_omni_bar_set_placeholder (IDE_OMNI_BAR (self), GTK_WIDGET (child));
  else
    parent_buildable_iface->add_child (buildable, builder, child, type);
}

static void
buildable_iface_init (GtkBuildableIface *iface)
{
  parent_buildable_iface = g_type_interface_peek_parent (iface);
  iface->add_child = ide_omni_bar_add_child;
}

/**
 * ide_omni_bar_add_popover_section:
 * @self: an #IdeOmniBar
 * @widget: a #GtkWidget
 * @priority: sort priority for the section
 *
 * Adds @widget to the omnibar popover, sorted by @priority
 *
 * Since: 3.32
 */
void
ide_omni_bar_add_popover_section (IdeOmniBar *self,
                                  GtkWidget  *widget,
                                  gint        priority)
{
  g_return_if_fail (IDE_IS_OMNI_BAR (self));
  g_return_if_fail (GTK_IS_WIDGET (widget));

  gtk_container_add_with_properties (GTK_CONTAINER (self->sections_box), widget,
                                     "priority", priority,
                                     NULL);
}
