/* symbol-tree.c
 *
 * Copyright (C) 2015 Christian Hergert <christian@hergert.me>
 *
 * 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/>.
 */

#include <glib/gi18n.h>
#include <ide.h>
#include <libpeas/peas.h>

#include "egg-task-cache.h"

#include "gb-editor-view.h"
#include "gb-tree.h"
#include "gb-workspace.h"
#include "gb-widget.h"

#include "symbol-tree.h"
#include "symbol-tree-builder.h"
#include "symbol-tree-resources.h"

#define REFRESH_TREE_INTERVAL_MSEC (15 * 1000)

struct _SymbolTree
{
  GtkBox          parent_instance;

  GCancellable   *cancellable;
  EggTaskCache   *symbols_cache;
  GbTree         *tree;
  GtkSearchEntry *search_entry;

  GbDocument     *last_document;
  gsize           last_change_count;

  guint           refresh_tree_timeout;
};

static void workbench_addin_init (GbWorkbenchAddinInterface *iface);

G_DEFINE_DYNAMIC_TYPE_EXTENDED (SymbolTree, symbol_tree, GTK_TYPE_BOX, 0,
                                G_IMPLEMENT_INTERFACE_DYNAMIC (GB_TYPE_WORKBENCH_ADDIN,
                                                               workbench_addin_init))

static void refresh_tree (SymbolTree *symbol_tree);

static gboolean
refresh_tree_timeout (gpointer user_data)
{
  SymbolTree *self = user_data;
  refresh_tree (self);
  return G_SOURCE_CONTINUE;
}

static void
get_cached_symbol_tree_cb (GObject      *object,
                           GAsyncResult *result,
                           gpointer      user_data)
{
  EggTaskCache *cache = (EggTaskCache *)object;
  g_autoptr(SymbolTree) self = user_data;
  g_autoptr(IdeSymbolTree) symbol_tree = NULL;
  g_autoptr(GError) error = NULL;
  GbTreeNode *root;
  GtkTreeIter iter;
  GtkTreeModel *model;

  g_assert (EGG_IS_TASK_CACHE (cache));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (SYMBOL_IS_TREE (self));

  if (!(symbol_tree = egg_task_cache_get_finish (cache, result, &error)))
    {
      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
        g_warning ("%s", error->message);
      return;
    }

  self->refresh_tree_timeout = g_timeout_add (REFRESH_TREE_INTERVAL_MSEC,
                                              refresh_tree_timeout,
                                              self);

  root = g_object_new (GB_TYPE_TREE_NODE,
                       "item", symbol_tree,
                       NULL);
  gb_tree_set_root (self->tree, root);

  model = gtk_tree_view_get_model (GTK_TREE_VIEW (self->tree));

  if (gtk_tree_model_get_iter_first (model, &iter))
    {
      do
        {
          g_autoptr(GbTreeNode) node = NULL;

          gtk_tree_model_get (model, &iter, 0, &node, -1);
          if (node != NULL)
            gb_tree_node_expand (node, FALSE);
        }
      while (gtk_tree_model_iter_next (model, &iter));
    }
}

static void
refresh_tree (SymbolTree *self)
{
  GtkWidget *active_view;
  GbWorkbench *workbench;
  GbDocument *document = NULL;
  gsize change_count = 0;

  g_assert (SYMBOL_IS_TREE (self));

  workbench = gb_widget_get_workbench (GTK_WIDGET (self));

  if ((active_view = gb_workbench_get_active_view (workbench)) && GB_IS_EDITOR_VIEW (active_view))
    {
      document = gb_view_get_document (GB_VIEW (active_view));
      if (IDE_IS_BUFFER (document))
        change_count = ide_buffer_get_change_count (IDE_BUFFER (document));
    }

  if ((document != self->last_document) || (self->last_change_count < change_count))
    {
      ide_clear_source (&self->refresh_tree_timeout);

      self->last_document = document;
      self->last_change_count = change_count;

      /*
       * Clear the old tree items.
       *
       * TODO: Get cross compile names for nodes so that we can
       *       recompute the open state.
       */
      gb_tree_set_root (self->tree, gb_tree_node_new ());;

      /*
       * Fetch the symbols via the transparent cache.
       */
      if (document != NULL)
        {
          if (self->cancellable != NULL)
            {
              g_cancellable_cancel (self->cancellable);
              g_clear_object (&self->cancellable);
            }

          self->cancellable = g_cancellable_new ();

          egg_task_cache_get_async (self->symbols_cache,
                                    document,
                                    FALSE,
                                    self->cancellable,
                                    get_cached_symbol_tree_cb,
                                    g_object_ref (self));
        }
    }
}

static void
notify_active_view_cb (SymbolTree  *self,
                       GParamFlags *pspec,
                       GbWorkbench *workbench)
{
  g_assert (SYMBOL_IS_TREE (self));
  g_assert (pspec != NULL);
  g_assert (GB_IS_WORKBENCH (workbench));

  refresh_tree (self);
  gtk_entry_set_text (GTK_ENTRY (self->search_entry), "");
}

static void
get_symbol_tree_cb (GObject      *object,
                    GAsyncResult *result,
                    gpointer      user_data)
{
  IdeSymbolResolver *resolver = (IdeSymbolResolver *)object;
  g_autoptr(GTask) task = user_data;
  g_autoptr(IdeSymbolTree) symbol_tree = NULL;
  GError *error = NULL;

  g_assert (IDE_IS_SYMBOL_RESOLVER (resolver));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (G_IS_TASK (task));

  symbol_tree = ide_symbol_resolver_get_symbol_tree_finish (resolver, result, &error);

  if (!symbol_tree)
    g_task_return_error (task, error);
  else
    g_task_return_pointer (task, g_object_ref (symbol_tree), g_object_unref);
}

static void
populate_cache_cb (EggTaskCache  *cache,
                   gconstpointer  key,
                   GTask         *task,
                   gpointer       user_data)
{
  GbEditorDocument *document = (GbEditorDocument *)key;
  IdeSymbolResolver *resolver;
  IdeFile *file;

  g_assert (EGG_IS_TASK_CACHE (cache));
  g_assert (GB_IS_EDITOR_DOCUMENT (document));
  g_assert (G_IS_TASK (task));

  if ((resolver = ide_buffer_get_symbol_resolver (IDE_BUFFER (document))) &&
      (file = ide_buffer_get_file (IDE_BUFFER (document))))
    {
      ide_symbol_resolver_get_symbol_tree_async (resolver,
                                                 ide_file_get_file (file),
                                                 g_task_get_cancellable (task),
                                                 get_symbol_tree_cb,
                                                 g_object_ref (task));
    }
  else
    {
      g_task_return_new_error (task,
                               G_IO_ERROR,
                               G_IO_ERROR_NOT_SUPPORTED,
                               _("Current language does not support symbol resolvers"));
    }
}

static void
symbol_tree_load (GbWorkbenchAddin *addin,
                  GbWorkbench      *workbench)
{
  SymbolTree *self = (SymbolTree *)addin;
  GbWorkspace *workspace;
  GtkWidget *right_pane;

  g_assert (SYMBOL_IS_TREE (self));
  g_assert (GB_IS_WORKBENCH (workbench));

  g_signal_connect_object (workbench,
                           "notify::active-view",
                           G_CALLBACK (notify_active_view_cb),
                           self,
                           G_CONNECT_SWAPPED);

  workspace = GB_WORKSPACE (gb_workbench_get_workspace (workbench));
  right_pane = gb_workspace_get_right_pane (workspace);
  gb_workspace_pane_add_page (GB_WORKSPACE_PANE (right_pane),
                              GTK_WIDGET (self),
                              _("Symbol Tree"),
                              "lang-function-symbolic");

  gtk_container_child_set (GTK_CONTAINER (gtk_widget_get_parent (GTK_WIDGET (self))),
                           GTK_WIDGET (self),
                           "position", 1,
                           NULL);
}

static void
symbol_tree_unload (GbWorkbenchAddin *addin,
                    GbWorkbench      *workbench)
{
  SymbolTree *self = (SymbolTree *)addin;

  g_assert (SYMBOL_IS_TREE (self));
  g_assert (GB_IS_WORKBENCH (workbench));

  /*
   * TODO: We don't want this to be the addin and the widget added to the pane.
   *       It makes object lifecycle difficult to manage. We basically rely
   *       on being destroyed right now until we rewrite this (which will happen soon).
   */

#if 0
  workspace = GB_WORKSPACE (gb_workbench_get_workspace (workbench));
  right_pane = gb_workspace_get_right_pane (workspace);
  gb_workspace_pane_remove_page (GB_WORKSPACE_PANE (right_pane), GTK_WIDGET (self));
#endif
}

static gboolean
filter_symbols_cb (GbTree     *tree,
                   GbTreeNode *node,
                   gpointer    user_data)
{
  IdePatternSpec *spec = user_data;
  const gchar *text;

  g_assert (GB_IS_TREE (tree));
  g_assert (GB_IS_TREE_NODE (node));
  g_assert (spec != NULL);

  if ((text = gb_tree_node_get_text (node)) != NULL)
    return ide_pattern_spec_match (spec, text);

  return NULL;
}

static void
symbol_tree__search_entry_changed (SymbolTree     *self,
                                   GtkSearchEntry *search_entry)
{
  const gchar *text;

  g_return_if_fail (SYMBOL_IS_TREE (self));
  g_return_if_fail (GTK_IS_SEARCH_ENTRY (search_entry));

  text = gtk_entry_get_text (GTK_ENTRY (search_entry));

  if (ide_str_empty0 (text))
    {
      gb_tree_set_filter (self->tree, NULL, NULL, NULL);
    }
  else
    {
      IdePatternSpec *spec;

      spec = ide_pattern_spec_new (text);
      gb_tree_set_filter (self->tree,
                          filter_symbols_cb,
                          spec,
                          (GDestroyNotify)ide_pattern_spec_unref);
      gtk_tree_view_expand_all (GTK_TREE_VIEW (self->tree));
    }
}

static void
symbol_tree_finalize (GObject *object)
{
  SymbolTree *self = (SymbolTree *)object;

  ide_clear_source (&self->refresh_tree_timeout);
  g_clear_object (&self->cancellable);

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

static void
workbench_addin_init (GbWorkbenchAddinInterface *iface)
{
  iface->load = symbol_tree_load;
  iface->unload = symbol_tree_unload;
}

static void
symbol_tree_class_init (SymbolTreeClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->finalize = symbol_tree_finalize;

  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/plugins/symbol-tree/symbol-tree.ui");
  gtk_widget_class_bind_template_child (widget_class, SymbolTree, tree);
  gtk_widget_class_bind_template_child (widget_class, SymbolTree, search_entry);

  g_type_ensure (GB_TYPE_TREE);
}

static void
symbol_tree_class_finalize (SymbolTreeClass *klass)
{
}

static void
symbol_tree_init (SymbolTree *self)
{
  GbTreeNode *root;
  GbTreeBuilder *builder;

  self->symbols_cache = egg_task_cache_new (g_direct_hash,
                                            g_direct_equal,
                                            g_object_ref,
                                            g_object_unref,
                                            g_object_ref,
                                            g_object_unref,
                                            20 * 1000L,
                                            populate_cache_cb,
                                            self,
                                            NULL);

  gtk_widget_init_template (GTK_WIDGET (self));

  root = gb_tree_node_new ();
  gb_tree_set_root (self->tree, root);

  builder = g_object_new (SYMBOL_TYPE_TREE_BUILDER, NULL);
  gb_tree_add_builder (self->tree, builder);

  g_signal_connect_object (self->search_entry,
                           "changed",
                           G_CALLBACK (symbol_tree__search_entry_changed),
                           self,
                           G_CONNECT_SWAPPED);
}

void
peas_register_types (PeasObjectModule *module)
{
  symbol_tree_register_type (G_TYPE_MODULE (module));

  peas_object_module_register_extension_type (module,
                                              GB_TYPE_WORKBENCH_ADDIN,
                                              SYMBOL_TYPE_TREE);
}
