/*
 * Copyright (c) 2008-2009  Christian Hammond
 * Copyright (c) 2008-2009  David Trowbridge
 * Copyright (c) 2013 Intel Corporation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "config.h"
#include <glib/gi18n-lib.h>

#include <string.h>

#include "object-tree.h"
#include "prop-list.h"

#include "gtkbuildable.h"
#include "gtkbutton.h"
#include "gtkcelllayout.h"
#include "gtkcomboboxprivate.h"
#include "gtkiconview.h"
#include "gtklabel.h"
#include "gtkmenuitem.h"
#include "gtksettings.h"
#include "gtktextview.h"
#include "gtktreeview.h"
#include "gtktreeselection.h"
#include "gtktreestore.h"
#include "gtktreemodelsort.h"
#include "gtktreemodelfilter.h"
#include "gtkwidgetprivate.h"
#include "gtkstylecontext.h"
#include "gtksearchbar.h"
#include "gtksearchentry.h"
#include "treewalk.h"

enum
{
  OBJECT,
  OBJECT_TYPE,
  OBJECT_NAME,
  OBJECT_LABEL,
  OBJECT_CLASSES,
  SENSITIVE
};


enum
{
  OBJECT_SELECTED,
  OBJECT_ACTIVATED,
  LAST_SIGNAL
};


struct _GtkInspectorObjectTreePrivate
{
  GtkTreeView *tree;
  GtkTreeStore *model;
  gulong map_hook;
  gulong unmap_hook;
  GtkTreeViewColumn *object_column;
  GtkWidget *search_bar;
  GtkWidget *search_entry;
  GtkTreeWalk *walk;
  gint search_length;
};

typedef struct _ObjectTreeClassFuncs ObjectTreeClassFuncs;
typedef void (* ObjectTreeForallFunc) (GObject    *object,
                                       const char *name,
                                       gpointer    data);

struct _ObjectTreeClassFuncs {
  GType         (* get_type)            (void);
  GObject *     (* get_parent)          (GObject                *object);
  void          (* forall)              (GObject                *object,
                                         ObjectTreeForallFunc    forall_func,
                                         gpointer                forall_data);
  gboolean      (* get_sensitive)       (GObject                *object);
};

static guint signals[LAST_SIGNAL] = { 0 };

G_DEFINE_TYPE_WITH_PRIVATE (GtkInspectorObjectTree, gtk_inspector_object_tree, GTK_TYPE_BOX)

static GObject *
object_tree_get_parent_default (GObject *object)
{
  return g_object_get_data (object, "inspector-object-tree-parent");
}

static void
object_tree_forall_default (GObject              *object,
                            ObjectTreeForallFunc  forall_func,
                            gpointer              forall_data)
{
}

static gboolean
object_tree_get_sensitive_default (GObject *object)
{
  return TRUE;
}

static GObject *
object_tree_widget_get_parent (GObject *object)
{
  return G_OBJECT (gtk_widget_get_parent (GTK_WIDGET (object)));
}

static gboolean
object_tree_widget_get_sensitive (GObject *object)
{
  return gtk_widget_get_mapped (GTK_WIDGET (object));
}

typedef struct {
  ObjectTreeForallFunc forall_func;
  gpointer             forall_data;
} ForallData;

static void
object_tree_widget_forall (GObject              *object,
                           ObjectTreeForallFunc  forall_func,
                           gpointer              forall_data)
{
  struct {
    GtkPropagationPhase  phase;
    const gchar         *name;
  } phases[] = {
    { GTK_PHASE_CAPTURE, "capture" },
    { GTK_PHASE_TARGET,  "target" },
    { GTK_PHASE_BUBBLE,  "bubble" },
    { GTK_PHASE_NONE,    "" }
  };
  gint i;
  GtkWidget *child;

  for (i = 0; i < G_N_ELEMENTS (phases); i++)
    {
      GList *list, *l;

      list = _gtk_widget_list_controllers (GTK_WIDGET (object), phases[i].phase);
      for (l = list; l; l = l->next)
        {
          GObject *controller = l->data;
          forall_func (controller, phases[i].name, forall_data);
        }
      g_list_free (list);
    }

   if (gtk_widget_is_toplevel (GTK_WIDGET (object)))
     {
       GObject *clock;

       clock = G_OBJECT (gtk_widget_get_frame_clock (GTK_WIDGET (object)));
       if (clock)
         forall_func (clock, "frame-clock", forall_data);
     }

  for (child = gtk_widget_get_first_child (GTK_WIDGET (object));
       child != NULL;
       child = gtk_widget_get_next_sibling (child))
     {
       forall_func (G_OBJECT (child), NULL, forall_data);
     }
}

static void
object_tree_tree_model_sort_forall (GObject              *object,
                                    ObjectTreeForallFunc  forall_func,
                                    gpointer              forall_data)
{
  GObject *child = G_OBJECT (gtk_tree_model_sort_get_model (GTK_TREE_MODEL_SORT (object)));

  if (child)
    forall_func (child, "model", forall_data);
}

static void
object_tree_tree_model_filter_forall (GObject              *object,
                                      ObjectTreeForallFunc  forall_func,
                                      gpointer              forall_data)
{
  GObject *child = G_OBJECT (gtk_tree_model_filter_get_model (GTK_TREE_MODEL_FILTER (object)));

  if (child)
    forall_func (child, "model", forall_data);
}

static void
object_tree_menu_item_forall (GObject              *object,
                              ObjectTreeForallFunc  forall_func,
                              gpointer              forall_data)
{
  GtkWidget *submenu;

  submenu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (object));
  if (submenu)
    forall_func (G_OBJECT (submenu), "submenu", forall_data);
}

static void
object_tree_combo_box_forall (GObject              *object,
                              ObjectTreeForallFunc  forall_func,
                              gpointer              forall_data)
{
  GtkWidget *popup;
  GObject *child;

  popup = gtk_combo_box_get_popup (GTK_COMBO_BOX (object));
  if (popup)
    forall_func (G_OBJECT (popup), "popup", forall_data);

  child = G_OBJECT (gtk_combo_box_get_model (GTK_COMBO_BOX (object)));
  if (child)
    forall_func (child, "model", forall_data);
}

static void
object_tree_tree_view_forall (GObject              *object,
                              ObjectTreeForallFunc  forall_func,
                              gpointer              forall_data)
{
  gint n_columns, i;
  GObject *child;

  child = G_OBJECT (gtk_tree_view_get_model (GTK_TREE_VIEW (object)));
  if (child)
    forall_func (child, "model", forall_data);

  child = G_OBJECT (gtk_tree_view_get_selection (GTK_TREE_VIEW (object)));
  if (child)
    forall_func (child, "selection", forall_data);

  n_columns = gtk_tree_view_get_n_columns (GTK_TREE_VIEW (object));
  for (i = 0; i < n_columns; i++)
    {
      child = G_OBJECT (gtk_tree_view_get_column (GTK_TREE_VIEW (object), i));
      forall_func (child, NULL, forall_data);
    }
}

static void
object_tree_icon_view_forall (GObject              *object,
                              ObjectTreeForallFunc  forall_func,
                              gpointer              forall_data)
{
  GObject *child;

  child = G_OBJECT (gtk_icon_view_get_model (GTK_ICON_VIEW (object)));
  if (child)
    forall_func (child, "model", forall_data);
}

typedef struct {
  ObjectTreeForallFunc forall_func;
  gpointer forall_data;
  GObject *parent;
} ParentForallData;

static gboolean
cell_callback (GtkCellRenderer *renderer,
               gpointer         data)
{
  ParentForallData *d = data;
  gpointer cell_layout;

  cell_layout = g_object_get_data (d->parent, "gtk-inspector-cell-layout");
  g_object_set_data (G_OBJECT (renderer), "gtk-inspector-cell-layout", cell_layout);
  d->forall_func (G_OBJECT (renderer), NULL, d->forall_data);

  return FALSE;
}

static void
object_tree_cell_area_forall (GObject              *object,
                              ObjectTreeForallFunc  forall_func,
                              gpointer              forall_data)
{
  ParentForallData data = {
    forall_func,
    forall_data,
    object
  };

  gtk_cell_area_foreach (GTK_CELL_AREA (object), cell_callback, &data);
}

static void
object_tree_cell_layout_forall (GObject              *object,
                                ObjectTreeForallFunc  forall_func,
                                gpointer              forall_data)
{
  GtkCellArea *area;

  /* cell areas handle their own stuff */
  if (GTK_IS_CELL_AREA (object))
    return;

  area = gtk_cell_layout_get_area (GTK_CELL_LAYOUT (object));
  if (!area)
    return;

  g_object_set_data (G_OBJECT (area), "gtk-inspector-cell-layout", object);
  forall_func (G_OBJECT (area), "cell-area", forall_data);
}

static void
object_tree_text_view_forall (GObject              *object,
                              ObjectTreeForallFunc  forall_func,
                              gpointer              forall_data)
{
  GtkTextBuffer *buffer;

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (object));
  forall_func (G_OBJECT (buffer), "buffer", forall_data);
}

static void
object_tree_text_buffer_forall (GObject              *object,
                                ObjectTreeForallFunc  forall_func,
                                gpointer              forall_data)
{
  GtkTextTagTable *tags;

  tags = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (object));
  forall_func (G_OBJECT (tags), "tag-table", forall_data);
}

static void
tag_callback (GtkTextTag *tag,
              gpointer    data)
{
  ForallData *d = data;
  gchar *name;

  g_object_get (tag, "name", &name, NULL);
  d->forall_func (G_OBJECT (tag), name, d->forall_data);
  g_free (name);
}

static void
object_tree_text_tag_table_forall (GObject              *object,
                                   ObjectTreeForallFunc  forall_func,
                                   gpointer              forall_data)
{
  ForallData data = {
    forall_func,
    forall_data
  };

  gtk_text_tag_table_foreach (GTK_TEXT_TAG_TABLE (object), tag_callback, &data);
}

static void
object_tree_application_forall (GObject              *object,
                                ObjectTreeForallFunc  forall_func,
                                gpointer              forall_data)
{
  GObject *menu;

  menu = (GObject *)gtk_application_get_app_menu (GTK_APPLICATION (object));
  if (menu)
    forall_func (menu, "app-menu", forall_data);

  menu = (GObject *)gtk_application_get_menubar (GTK_APPLICATION (object));
  if (menu)
    forall_func (menu, "menubar", forall_data);
}

/* Note:
 * This tree must be sorted with the most specific types first.
 * We iterate over it top to bottom and return the first match
 * using g_type_is_a ()
 */
static const ObjectTreeClassFuncs object_tree_class_funcs[] = {
  {
    gtk_application_get_type,
    object_tree_get_parent_default,
    object_tree_application_forall,
    object_tree_get_sensitive_default
  },
  {
    gtk_text_tag_table_get_type,
    object_tree_get_parent_default,
    object_tree_text_tag_table_forall,
    object_tree_get_sensitive_default
  },
  {
    gtk_text_buffer_get_type,
    object_tree_get_parent_default,
    object_tree_text_buffer_forall,
    object_tree_get_sensitive_default
  },
  {
    gtk_text_view_get_type,
    object_tree_widget_get_parent,
    object_tree_text_view_forall,
    object_tree_widget_get_sensitive
  },
  {
    gtk_icon_view_get_type,
    object_tree_widget_get_parent,
    object_tree_icon_view_forall,
    object_tree_widget_get_sensitive
  },
  {
    gtk_tree_view_get_type,
    object_tree_widget_get_parent,
    object_tree_tree_view_forall,
    object_tree_widget_get_sensitive
  },
  {
    gtk_combo_box_get_type,
    object_tree_widget_get_parent,
    object_tree_combo_box_forall,
    object_tree_widget_get_sensitive
  },
  {
    gtk_menu_item_get_type,
    object_tree_widget_get_parent,
    object_tree_menu_item_forall,
    object_tree_widget_get_sensitive
  },
  {
    gtk_widget_get_type,
    object_tree_widget_get_parent,
    object_tree_widget_forall,
    object_tree_widget_get_sensitive
  },
  {
    gtk_tree_model_filter_get_type,
    object_tree_get_parent_default,
    object_tree_tree_model_filter_forall,
    object_tree_get_sensitive_default
  },
  {
    gtk_tree_model_sort_get_type,
    object_tree_get_parent_default,
    object_tree_tree_model_sort_forall,
    object_tree_get_sensitive_default
  },
  {
    gtk_cell_area_get_type,
    object_tree_get_parent_default,
    object_tree_cell_area_forall,
    object_tree_get_sensitive_default
  },
  {
    gtk_cell_layout_get_type,
    object_tree_get_parent_default,
    object_tree_cell_layout_forall,
    object_tree_get_sensitive_default
  },
  {
    g_object_get_type,
    object_tree_get_parent_default,
    object_tree_forall_default,
    object_tree_get_sensitive_default
  },
};

static const ObjectTreeClassFuncs *
find_class_funcs (GObject *object)
{
  GType object_type;
  guint i;

  object_type = G_OBJECT_TYPE (object);

  for (i = 0; i < G_N_ELEMENTS (object_tree_class_funcs); i++)
    {
      if (g_type_is_a (object_type, object_tree_class_funcs[i].get_type ()))
        return &object_tree_class_funcs[i];
    }

  g_assert_not_reached ();

  return NULL;
}

static GObject *
object_get_parent (GObject *object)
{
  const ObjectTreeClassFuncs *funcs;

  funcs = find_class_funcs (object);
  
  return funcs->get_parent (object);
}

static void
object_forall (GObject              *object,
               ObjectTreeForallFunc  forall_func,
               gpointer              forall_data)
{
  GType object_type;
  guint i;

  object_type = G_OBJECT_TYPE (object);

  for (i = 0; i < G_N_ELEMENTS (object_tree_class_funcs); i++)
    {
      if (g_type_is_a (object_type, object_tree_class_funcs[i].get_type ()))
        object_tree_class_funcs[i].forall (object, forall_func, forall_data);
    }
}

static gboolean
object_get_sensitive (GObject *object)
{
  const ObjectTreeClassFuncs *funcs;

  funcs = find_class_funcs (object);
  
  return funcs->get_sensitive (object);
}

static void
on_row_activated (GtkTreeView            *tree,
                  GtkTreePath            *path,
                  GtkTreeViewColumn      *col,
                  GtkInspectorObjectTree *wt)
{
  GtkTreeIter iter;
  GObject *object;
  gchar *name;

  gtk_tree_model_get_iter (GTK_TREE_MODEL (wt->priv->model), &iter, path);
  gtk_tree_model_get (GTK_TREE_MODEL (wt->priv->model), &iter,
                      OBJECT, &object,
                      OBJECT_NAME, &name,
                      -1);

  g_signal_emit (wt, signals[OBJECT_ACTIVATED], 0, object, name);

  g_free (name);
}

GObject *
gtk_inspector_object_tree_get_selected (GtkInspectorObjectTree *wt)
{
  GObject *object;
  GtkTreeIter iter;
  GtkTreeSelection *sel;
  GtkTreeModel *model;

  object = NULL;
  sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (wt->priv->tree));
  if (gtk_tree_selection_get_selected (sel, &model, &iter))
    gtk_tree_model_get (model, &iter,
                        OBJECT, &object,
                        -1);

  return object;
}

static void
on_selection_changed (GtkTreeSelection       *selection,
                      GtkInspectorObjectTree *wt)
{
  GObject *object;
  GtkTreeIter iter;

  if (gtk_tree_selection_get_selected (selection, NULL, &iter))
    gtk_tree_walk_reset (wt->priv->walk, &iter);
  else
    gtk_tree_walk_reset (wt->priv->walk, NULL);
  object = gtk_inspector_object_tree_get_selected (wt);
  g_signal_emit (wt, signals[OBJECT_SELECTED], 0, object);
}

typedef struct {
  GObject *dead_object;
  GtkTreeWalk *walk;
  GtkTreePath *walk_pos;
} RemoveData;

static gboolean
remove_cb (GtkTreeModel *model,
           GtkTreePath  *path,
           GtkTreeIter  *iter,
           gpointer      data)
{
  RemoveData *remove_data = data;
  GObject *lookup;

  gtk_tree_model_get (model, iter, OBJECT, &lookup, -1);

  if (lookup == remove_data->dead_object)
    {
      if (remove_data->walk_pos != NULL &&
          gtk_tree_path_compare (path, remove_data->walk_pos) == 0)
        gtk_tree_walk_reset (remove_data->walk, NULL);

      gtk_tree_store_remove (GTK_TREE_STORE (model), iter);

      return TRUE;
    }

  return FALSE;
}

static void
gtk_object_tree_remove_dead_object (gpointer data, GObject *dead_object)
{
  GtkInspectorObjectTree *wt = data;
  GtkTreeIter iter;
  RemoveData remove_data;

  remove_data.dead_object = dead_object;
  remove_data.walk = wt->priv->walk;
  if (gtk_tree_walk_get_position (wt->priv->walk, &iter))
    remove_data.walk_pos = gtk_tree_model_get_path (GTK_TREE_MODEL (wt->priv->model), &iter);
  else
    remove_data.walk_pos = NULL;

  gtk_tree_model_foreach (GTK_TREE_MODEL (wt->priv->model), remove_cb, &remove_data);

  if (remove_data.walk_pos)
    gtk_tree_path_free (remove_data.walk_pos);
}

static gboolean
weak_unref_cb (GtkTreeModel *model,
               GtkTreePath  *path,
               GtkTreeIter  *iter,
               gpointer      data)
{
  GtkInspectorObjectTree *wt = data;
  GObject *object;

  gtk_tree_model_get (model, iter, OBJECT, &object, -1);

  g_object_weak_unref (object, gtk_object_tree_remove_dead_object, wt);

  return FALSE;
}

static void
clear_store (GtkInspectorObjectTree *wt)
{
  if (wt->priv->model)
    {
      gtk_tree_model_foreach (GTK_TREE_MODEL (wt->priv->model), weak_unref_cb, wt);
      gtk_tree_store_clear (wt->priv->model);
      gtk_tree_walk_reset (wt->priv->walk, NULL);
    }
}

static gboolean
map_or_unmap (GSignalInvocationHint *ihint,
              guint                  n_params,
              const GValue          *params,
              gpointer               data)
{
  GtkInspectorObjectTree *wt = data;
  GtkWidget *widget;
  GtkTreeIter iter;

  widget = g_value_get_object (params);
  if (gtk_inspector_object_tree_find_object (wt, G_OBJECT (widget), &iter))
    gtk_tree_store_set (wt->priv->model, &iter,
                        SENSITIVE, gtk_widget_get_mapped (widget),
                        -1);
  return TRUE;
}

static void
move_search_to_row (GtkInspectorObjectTree *wt,
                    GtkTreeIter            *iter)
{
  GtkTreeSelection *selection;
  GtkTreePath *path;

  selection = gtk_tree_view_get_selection (wt->priv->tree);
  path = gtk_tree_model_get_path (GTK_TREE_MODEL (wt->priv->model), iter);
  gtk_tree_view_expand_to_path (wt->priv->tree, path);
  gtk_tree_selection_select_path (selection, path);
  gtk_tree_view_scroll_to_cell (wt->priv->tree, path, NULL, TRUE, 0.5, 0.0);
  gtk_tree_path_free (path);
}

static gboolean
key_press_event (GtkWidget              *window,
                 GdkEvent               *event,
                 GtkInspectorObjectTree *wt)
{
  if (gtk_widget_get_mapped (GTK_WIDGET (wt)))
    {
      GdkModifierType default_accel;
      gboolean search_started;

      search_started = gtk_search_bar_get_search_mode (GTK_SEARCH_BAR (wt->priv->search_bar));
      default_accel = gtk_widget_get_modifier_mask (GTK_WIDGET (wt), GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR);

      if (search_started &&
          (event->key.keyval == GDK_KEY_Return ||
           event->key.keyval == GDK_KEY_ISO_Enter ||
           event->key.keyval == GDK_KEY_KP_Enter))
        {
          GtkTreeSelection *selection;
          GtkTreeModel *model;
          GtkTreeIter iter;
          GtkTreePath *path;

          selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (wt->priv->tree));
          if (gtk_tree_selection_get_selected (selection, &model, &iter))
            {
              path = gtk_tree_model_get_path (model, &iter);
              gtk_tree_view_row_activated (GTK_TREE_VIEW (wt->priv->tree),
                                           path,
                                           wt->priv->object_column);
              gtk_tree_path_free (path);

              return GDK_EVENT_STOP;
            }
          else
            return GDK_EVENT_PROPAGATE;
        }
      else if (search_started &&
               (event->key.keyval == GDK_KEY_Escape))
        {
          gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (wt->priv->search_bar), FALSE);
          return GDK_EVENT_STOP;
        }
      else if (search_started &&
               ((event->key.state & (default_accel | GDK_SHIFT_MASK)) == (default_accel | GDK_SHIFT_MASK)) &&
               (event->key.keyval == GDK_KEY_g || event->key.keyval == GDK_KEY_G))
        {
          GtkTreeIter iter;
          if (gtk_tree_walk_next_match (wt->priv->walk, TRUE, TRUE, &iter))
            move_search_to_row (wt, &iter);
          else
            gtk_widget_error_bell (GTK_WIDGET (wt));

          return GDK_EVENT_STOP;
        }
      else if (search_started &&
               ((event->key.state & (default_accel | GDK_SHIFT_MASK)) == default_accel) &&
               (event->key.keyval == GDK_KEY_g || event->key.keyval == GDK_KEY_G))
        {
          GtkTreeIter iter;

          if (gtk_tree_walk_next_match (wt->priv->walk, TRUE, FALSE, &iter))
            move_search_to_row (wt, &iter);
          else
            gtk_widget_error_bell (GTK_WIDGET (wt));

          return GDK_EVENT_STOP;
        }

      return gtk_search_bar_handle_event (GTK_SEARCH_BAR (wt->priv->search_bar), event);
    }
  else
    return GDK_EVENT_PROPAGATE;
}

static void
on_hierarchy_changed (GtkWidget *widget,
                      GtkWidget *previous_toplevel)
{
  if (previous_toplevel)
    g_signal_handlers_disconnect_by_func (previous_toplevel, key_press_event, widget);
  g_signal_connect (gtk_widget_get_toplevel (widget), "key-press-event",
                    G_CALLBACK (key_press_event), widget);
}

static void
on_search_changed (GtkSearchEntry         *entry,
                   GtkInspectorObjectTree *wt)
{
  GtkTreeIter iter;
  gint length;
  gboolean backwards;

  length = strlen (gtk_entry_get_text (GTK_ENTRY (entry)));
  backwards = length < wt->priv->search_length;
  wt->priv->search_length = length;

  if (length == 0)
    return;

  if (gtk_tree_walk_next_match (wt->priv->walk, backwards, backwards, &iter))
    move_search_to_row (wt, &iter);
  else if (!backwards)
    gtk_widget_error_bell (GTK_WIDGET (wt));
}

static gboolean
match_string (const gchar *string,
              const gchar *text)
{
  gchar *lower;
  gboolean match = FALSE;

  if (string)
    {
      lower = g_ascii_strdown (string, -1);
      match = g_str_has_prefix (lower, text);
      g_free (lower);
    }

  return match;
}

static gboolean
match_row (GtkTreeModel *model,
           GtkTreeIter  *iter,
           gpointer      data)
{
  GtkInspectorObjectTree *wt = data;
  gchar *type, *name, *label;
  const gchar *text;
  gboolean match;

  text = gtk_entry_get_text (GTK_ENTRY (wt->priv->search_entry));
  gtk_tree_model_get (model, iter,
                      OBJECT_TYPE, &type,
                      OBJECT_NAME, &name,
                      OBJECT_LABEL, &label,
                      -1);

  match = (match_string (type, text) ||
           match_string (name, text) ||
           match_string (label, text));

  g_free (type);
  g_free (name);
  g_free (label);

  return match;
}

static void
search_mode_changed (GObject                *search_bar,
                     GParamSpec             *pspec,
                     GtkInspectorObjectTree *wt)
{
  if (!gtk_search_bar_get_search_mode (GTK_SEARCH_BAR (search_bar)))
    {
      gtk_tree_walk_reset (wt->priv->walk, NULL);
      wt->priv->search_length = 0;
    }
}

static void
next_match (GtkButton              *button,
            GtkInspectorObjectTree *wt)
{
  if (gtk_search_bar_get_search_mode (GTK_SEARCH_BAR (wt->priv->search_bar)))
    {
      GtkTreeIter iter;

      if (gtk_tree_walk_next_match (wt->priv->walk, TRUE, FALSE, &iter))
        move_search_to_row (wt, &iter);
      else
        gtk_widget_error_bell (GTK_WIDGET (wt));
    }
}

static void
previous_match (GtkButton              *button,
                GtkInspectorObjectTree *wt)
{
  if (gtk_search_bar_get_search_mode (GTK_SEARCH_BAR (wt->priv->search_bar)))
    {
      GtkTreeIter iter;

      if (gtk_tree_walk_next_match (wt->priv->walk, TRUE, TRUE, &iter))
        move_search_to_row (wt, &iter);
      else
        gtk_widget_error_bell (GTK_WIDGET (wt));
    }
}

static void
stop_search (GtkWidget              *entry,
             GtkInspectorObjectTree *wt)
{
  gtk_entry_set_text (GTK_ENTRY (wt->priv->search_entry), "");
  gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (wt->priv->search_bar), FALSE);
}

static void
gtk_inspector_object_tree_init (GtkInspectorObjectTree *wt)
{
  guint signal_id;

  wt->priv = gtk_inspector_object_tree_get_instance_private (wt);
  gtk_widget_init_template (GTK_WIDGET (wt));

  gtk_search_bar_connect_entry (GTK_SEARCH_BAR (wt->priv->search_bar),
                                GTK_ENTRY (wt->priv->search_entry));

  g_signal_connect (wt->priv->search_bar, "notify::search-mode-enabled",
                    G_CALLBACK (search_mode_changed), wt);
  wt->priv->walk = gtk_tree_walk_new (GTK_TREE_MODEL (wt->priv->model), match_row, wt, NULL);

  signal_id = g_signal_lookup ("map", GTK_TYPE_WIDGET);
  wt->priv->map_hook = g_signal_add_emission_hook (signal_id, 0,
                                                   map_or_unmap, wt, NULL);
  signal_id = g_signal_lookup ("unmap", GTK_TYPE_WIDGET);
  wt->priv->unmap_hook = g_signal_add_emission_hook (signal_id, 0,
                                                   map_or_unmap, wt, NULL);

  gtk_inspector_object_tree_append_object (wt, G_OBJECT (gtk_settings_get_default ()), NULL, NULL);
}

static void
gtk_inspector_object_tree_dispose (GObject *object)
{
  GtkInspectorObjectTree *wt = GTK_INSPECTOR_OBJECT_TREE (object);

  clear_store (wt);

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

static void
gtk_inspector_object_tree_finalize (GObject *object)
{
  GtkInspectorObjectTree *wt = GTK_INSPECTOR_OBJECT_TREE (object);
  guint signal_id;

  signal_id = g_signal_lookup ("map", GTK_TYPE_WIDGET);
  g_signal_remove_emission_hook (signal_id, wt->priv->map_hook);
  signal_id = g_signal_lookup ("unmap", GTK_TYPE_WIDGET);
  g_signal_remove_emission_hook (signal_id, wt->priv->unmap_hook);

  gtk_tree_walk_free (wt->priv->walk);

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

static void
gtk_inspector_object_tree_class_init (GtkInspectorObjectTreeClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->finalize = gtk_inspector_object_tree_finalize;
  object_class->dispose = gtk_inspector_object_tree_dispose;

  signals[OBJECT_ACTIVATED] =
      g_signal_new ("object-activated",
                    G_OBJECT_CLASS_TYPE (klass),
                    G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
                    G_STRUCT_OFFSET (GtkInspectorObjectTreeClass, object_activated),
                    NULL, NULL,
                    NULL,
                    G_TYPE_NONE, 2, G_TYPE_OBJECT, G_TYPE_STRING);

  signals[OBJECT_SELECTED] =
      g_signal_new ("object-selected",
                    G_OBJECT_CLASS_TYPE (klass),
                    G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
                    G_STRUCT_OFFSET (GtkInspectorObjectTreeClass, object_selected),
                    NULL, NULL,
                    NULL,
                    G_TYPE_NONE, 1, G_TYPE_OBJECT);

  gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/inspector/object-tree.ui");
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorObjectTree, model);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorObjectTree, tree);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorObjectTree, object_column);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorObjectTree, search_bar);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorObjectTree, search_entry);
  gtk_widget_class_bind_template_callback (widget_class, on_selection_changed);
  gtk_widget_class_bind_template_callback (widget_class, on_row_activated);
  gtk_widget_class_bind_template_callback (widget_class, on_hierarchy_changed);
  gtk_widget_class_bind_template_callback (widget_class, on_search_changed);
  gtk_widget_class_bind_template_callback (widget_class, next_match);
  gtk_widget_class_bind_template_callback (widget_class, previous_match);
  gtk_widget_class_bind_template_callback (widget_class, stop_search);
}

typedef struct
{
  GtkInspectorObjectTree *wt;
  GtkTreeIter *iter;
  GObject *parent;
} FindAllData;

static void
child_callback (GObject    *object,
                const char *name,
                gpointer    data)
{
  FindAllData *d = data;

  gtk_inspector_object_tree_append_object (d->wt, object, d->iter, NULL);
}

void
gtk_inspector_object_tree_append_object (GtkInspectorObjectTree *wt,
                                         GObject                *object,
                                         GtkTreeIter            *parent_iter,
                                         const gchar            *name)
{
  GtkTreeIter iter;
  const gchar *class_name;
  gchar *classes;
  const gchar *label;
  FindAllData data;

  class_name = G_OBJECT_CLASS_NAME (G_OBJECT_GET_CLASS (object));

  if (GTK_IS_WIDGET (object))
    {
      const gchar *id;
      GtkStyleContext *context;
      GList *list, *l;
      GString *string;

      id = gtk_widget_get_name (GTK_WIDGET (object));
      if (name == NULL && id != NULL && g_strcmp0 (id, class_name) != 0)
        name = id;

      context = gtk_widget_get_style_context (GTK_WIDGET (object));
      string = g_string_new ("");
      list = gtk_style_context_list_classes (context);
      for (l = list; l; l = l->next)
        {
          if (string->len > 0)
            g_string_append_c (string, ' ');
          g_string_append (string, (gchar *)l->data);
        }
      classes = g_string_free (string, FALSE);
      g_list_free (list);
    }
  else
    {
      if (parent_iter)
        {
          GObject *parent;

          gtk_tree_model_get (GTK_TREE_MODEL (wt->priv->model), parent_iter,
                              OBJECT, &parent,
                              -1);
          g_object_set_data (object, "inspector-object-tree-parent", parent);
        }
      classes = g_strdup ("");
    }

  if (GTK_IS_BUILDABLE (object))
    {
      const gchar *id;
      id = gtk_buildable_get_name (GTK_BUILDABLE (object));
      if (name == NULL && id != NULL && !g_str_has_prefix (id, "___object_"))
        name = id;
    }

  if (name == NULL)
    name = "";

  if (GTK_IS_LABEL (object))
    label = gtk_label_get_text (GTK_LABEL (object));
  else if (GTK_IS_BUTTON (object))
    label = gtk_button_get_label (GTK_BUTTON (object));
  else if (GTK_IS_WINDOW (object))
    label = gtk_window_get_title (GTK_WINDOW (object));
  else if (GTK_IS_TREE_VIEW_COLUMN (object))
    label = gtk_tree_view_column_get_title (GTK_TREE_VIEW_COLUMN (object));
  else
    label = "";

  gtk_tree_store_append (wt->priv->model, &iter, parent_iter);
  gtk_tree_store_set (wt->priv->model, &iter,
                      OBJECT, object,
                      OBJECT_TYPE, class_name,
                      OBJECT_NAME, name,
                      OBJECT_LABEL, label,
                      OBJECT_CLASSES, classes,
                      SENSITIVE, object_get_sensitive (object),
                      -1);

  if (name && *name)
    {
      gchar *title;
      title = g_strconcat (class_name, " — ", name, NULL);
      g_object_set_data_full (object, "gtk-inspector-object-title", title, g_free);
    }
  else
    {
      g_object_set_data (object, "gtk-inspector-object-title", (gpointer)class_name);
    }

  g_free (classes);

  g_object_weak_ref (object, gtk_object_tree_remove_dead_object, wt);
  
  data.wt = wt;
  data.iter = &iter;
  data.parent = object;

  object_forall (object, child_callback, &data);
}

static void
block_selection_changed (GtkInspectorObjectTree *wt)
{
  GtkTreeSelection *selection;

  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (wt->priv->tree));
  g_signal_handlers_block_by_func (selection, on_selection_changed, wt);
}

static void
unblock_selection_changed (GtkInspectorObjectTree *wt)
{
  GtkTreeSelection *selection;

  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (wt->priv->tree));
  g_signal_handlers_unblock_by_func (selection, on_selection_changed, wt);
}

static gboolean
select_object_internal (GtkInspectorObjectTree *wt,
                        GObject                *object,
                        gboolean                activate)
{
  GtkTreeIter iter;

  if (gtk_inspector_object_tree_find_object (wt, object, &iter))
    {
      GtkTreePath *path;
      GtkTreeSelection *selection;

      selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (wt->priv->tree));
      path = gtk_tree_model_get_path (GTK_TREE_MODEL (wt->priv->model), &iter);
      gtk_tree_view_expand_to_path (GTK_TREE_VIEW (wt->priv->tree), path);
      if (!activate)
        block_selection_changed (wt);
      gtk_tree_selection_select_iter (selection, &iter);
      if (!activate)
        unblock_selection_changed (wt);

      gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (wt->priv->tree), path, NULL, TRUE, 0.5, 0);
      if (activate)
        gtk_tree_view_row_activated (GTK_TREE_VIEW (wt->priv->tree), path, NULL);
      gtk_tree_path_free (path);

      return TRUE;
    }

  return FALSE;
}

gboolean
gtk_inspector_object_tree_select_object (GtkInspectorObjectTree *wt,
                                         GObject                *object)
{
  return select_object_internal (wt, object, TRUE);
}

void
gtk_inspector_object_tree_scan (GtkInspectorObjectTree *wt,
                                GtkWidget              *window)
{
  GtkWidget *inspector_win;
  GList *toplevels, *l;
  GdkScreen *screen;
  GObject *selected;

  block_selection_changed (wt);

  selected = gtk_inspector_object_tree_get_selected (wt);

  clear_store (wt);
  gtk_inspector_object_tree_append_object (wt, G_OBJECT (gtk_settings_get_default ()), NULL, NULL);
  if (g_application_get_default ())
    gtk_inspector_object_tree_append_object (wt, G_OBJECT (g_application_get_default ()), NULL, NULL);

  if (window)
    gtk_inspector_object_tree_append_object (wt, G_OBJECT (window), NULL, NULL);

  screen = gdk_screen_get_default ();

  inspector_win = gtk_widget_get_toplevel (GTK_WIDGET (wt));
  toplevels = gtk_window_list_toplevels ();
  for (l = toplevels; l; l = l->next)
    {
      if (GTK_IS_WINDOW (l->data) &&
          gtk_window_get_window_type (l->data) == GTK_WINDOW_TOPLEVEL &&
          gtk_widget_get_screen (l->data) == screen &&
          l->data != window &&
          l->data != inspector_win)
        gtk_inspector_object_tree_append_object (wt, G_OBJECT (l->data), NULL, NULL);
    }
  g_list_free (toplevels);

  gtk_tree_view_columns_autosize (GTK_TREE_VIEW (wt->priv->tree));

  if (selected)
    select_object_internal (wt, selected, FALSE);

  unblock_selection_changed (wt);
}

static gboolean
gtk_inspector_object_tree_find_object_at_parent_iter (GtkTreeModel *model,
                                                      GObject      *object,
                                                      GtkTreeIter  *parent,
                                                      GtkTreeIter  *iter)
{
  if (!gtk_tree_model_iter_children (model, iter, parent))
    return FALSE;

  do {
    GObject *lookup;

    gtk_tree_model_get (model, iter, OBJECT, &lookup, -1);

    if (lookup == object)
      return TRUE;

  } while (gtk_tree_model_iter_next (model, iter));

  return FALSE;
}

gboolean
gtk_inspector_object_tree_find_object (GtkInspectorObjectTree *wt,
                                       GObject                *object,
                                       GtkTreeIter            *iter)
{
  GtkTreeIter parent_iter;
  GObject *parent;

  parent = object_get_parent (object);
  if (parent)
    {
      if (!gtk_inspector_object_tree_find_object (wt, parent, &parent_iter))
        return FALSE;

      return gtk_inspector_object_tree_find_object_at_parent_iter (GTK_TREE_MODEL (wt->priv->model),
                                                                   object,
                                                                   &parent_iter,
                                                                   iter);
    }
  else
    {
      return gtk_inspector_object_tree_find_object_at_parent_iter (GTK_TREE_MODEL (wt->priv->model),
                                                                   object,
                                                                   NULL,
                                                                   iter);
    }
}


// vim: set et sw=2 ts=2:
