/* ide-diagnostics-manager.c
 *
 * Copyright 2016-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-diagnostics-manager"

#include "config.h"

#include <gtksourceview/gtksource.h>
#include <libide-plugins.h>

#include "ide-buffer.h"
#include "ide-buffer-manager.h"
#include "ide-buffer-private.h"
#include "ide-diagnostic.h"
#include "ide-diagnostic-provider.h"
#include "ide-diagnostics.h"
#include "ide-diagnostics-manager.h"
#include "ide-diagnostics-manager-private.h"

#define DEFAULT_DIAGNOSE_DELAY 333
#define DIAG_GROUP_MAGIC       0xF1282727
#define IS_DIAGNOSTICS_GROUP(g) ((g) && (g)->magic == DIAG_GROUP_MAGIC)

typedef struct
{
  /*
   * Used to give ourself a modicum of assurance our structure hasn't
   * been miss-used.
   */
  guint magic;

  /*
   * This is our identifier for the diagnostics. We use this as the key in
   * the hash table so that we can quickly find the target buffer. If the
   * IdeBuffer:file property changes, we will have to fallback to the
   * buffer to clear old entries.
   */
  GFile *file;

  /*
   * This hash table uses the given provider as the key and the last
   * reported IdeDiagnostics as the value.
   */
  GHashTable *diagnostics_by_provider;

  /*
   * This extension set adapter is used to update the providers that are
   * available based on the buffers current language. They may change
   * at runtime due to the buffers language changing. When that happens
   * we purge items from @diagnostics_by_provider and queue a diagnose
   * request of the new provider.
   */
  IdeExtensionSetAdapter *adapter;

  /* The most recent bytes we received for a future diagnosis. */
  GBytes *contents;

  /* The last language id we were notified about */
  const gchar *lang_id;

  /*
   * This is our sequence number for diagnostics. It is monotonically
   * increasing with every diagnostic discovered.
   */
  guint sequence;

  /*
   * If we are currently diagnosing, then this will be set to a
   * number greater than zero.
   */
  guint in_diagnose;

  /*
   * If we need a diagnose this bit will be set. If we complete a
   * diagnosis and this bit is set, then we will automatically queue
   * another diagnose upon completion.
   */
  guint needs_diagnose : 1;

  /*
   * This bit is set if we know the file or buffer has diagnostics. This
   * is useful when we've cleaned up our extensions and no longer have
   * the diagnostics loaded in memory, but we know that it previously
   * had diagnostics which have not been rectified.
   */
  guint has_diagnostics : 1;

  /*
   * This bit is set when the group has been removed from the
   * IdeDiagnosticsManager. That allows the providers to cleanup
   * as necessary when their async operations complete.
   */
  guint was_removed : 1;

} IdeDiagnosticsGroup;

struct _IdeDiagnosticsManager
{
  IdeObject parent_instance;

  /*
   * This hashtable contains a mapping of GFile to the IdeDiagnosticsGroup
   * for the file. When a buffer is renamed (the IdeBuffer:file property
   * is changed) we need to update this entry so it reflects the new
   * location.
   */
  GHashTable *groups_by_file;

  /*
   * If any group has a queued diagnose in process, this will be set so
   * we can coalesce the dispatch of everything at the same time.
   */
  guint queued_diagnose_source;
};

enum {
  PROP_0,
  PROP_BUSY,
  N_PROPS
};

enum {
  CHANGED,
  N_SIGNALS
};


static gboolean ide_diagnostics_manager_clear_by_provider (IdeDiagnosticsManager *self,
                                                           IdeDiagnosticProvider *provider);
static void     ide_diagnostics_manager_add_diagnostic    (IdeDiagnosticsManager *self,
                                                           IdeDiagnosticProvider *provider,
                                                           IdeDiagnostic         *diagnostic);
static void     ide_diagnostics_group_queue_diagnose      (IdeDiagnosticsGroup   *group,
                                                           IdeDiagnosticsManager *self);


static GParamSpec *properties [N_PROPS];
static guint signals [N_SIGNALS];

G_DEFINE_FINAL_TYPE (IdeDiagnosticsManager, ide_diagnostics_manager, IDE_TYPE_OBJECT)

static void
free_diagnostics (gpointer data)
{
  IdeDiagnostics *diagnostics = data;

  g_clear_object (&diagnostics);
}

static inline guint
diagnostics_get_size (IdeDiagnostics *diags)
{
  return diags ? g_list_model_get_n_items (G_LIST_MODEL (diags)) : 0;
}

static void
ide_diagnostics_group_finalize (IdeDiagnosticsGroup *group)
{
  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (group != NULL);
  g_assert (IS_DIAGNOSTICS_GROUP (group));

  group->magic = 0;

  g_clear_pointer (&group->diagnostics_by_provider, g_hash_table_unref);
  g_clear_pointer (&group->contents, g_bytes_unref);
  ide_clear_and_destroy_object (&group->adapter);
  g_clear_object (&group->file);
}

static IdeDiagnosticsGroup *
ide_diagnostics_group_new (GFile *file)
{
  IdeDiagnosticsGroup *group;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (G_IS_FILE (file));

  group = g_rc_box_new0 (IdeDiagnosticsGroup);
  group->magic = DIAG_GROUP_MAGIC;
  group->file = g_object_ref (file);

  return group;
}

static IdeDiagnosticsGroup *
ide_diagnostics_group_ref (IdeDiagnosticsGroup *group)
{
  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (group != NULL);
  g_assert (IS_DIAGNOSTICS_GROUP (group));

  return g_rc_box_acquire (group);
}

static void
ide_diagnostics_group_unref (IdeDiagnosticsGroup *group)
{
  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (group != NULL);
  g_assert (IS_DIAGNOSTICS_GROUP (group));

  g_rc_box_release_full (group, (GDestroyNotify)ide_diagnostics_group_finalize);
}

static guint
ide_diagnostics_group_has_diagnostics (IdeDiagnosticsGroup *group)
{
  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (group != NULL);
  g_assert (IS_DIAGNOSTICS_GROUP (group));

  if (group->diagnostics_by_provider != NULL)
    {
      GHashTableIter iter;
      gpointer value;

      g_hash_table_iter_init (&iter, group->diagnostics_by_provider);

      while (g_hash_table_iter_next (&iter, NULL, &value))
        {
          IdeDiagnostics *diagnostics = value;

          if (diagnostics_get_size (diagnostics) > 0)
            return TRUE;
        }
    }

  return FALSE;
}

static gboolean
ide_diagnostics_group_can_dispose (IdeDiagnosticsGroup *group)
{
  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (group != NULL);
  g_assert (IS_DIAGNOSTICS_GROUP (group));

  /*
   * We can cleanup this group if we don't have a buffer loaded and
   * the adapters have been unloaded and there are no diagnostics
   * registered for the group.
   */

  return group->adapter == NULL &&
         group->has_diagnostics == FALSE;
}

static void
ide_diagnostics_group_add (IdeDiagnosticsGroup   *group,
                           IdeDiagnosticProvider *provider,
                           IdeDiagnostic         *diagnostic)
{
  IdeDiagnostics *diagnostics;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (group != NULL);
  g_assert (IS_DIAGNOSTICS_GROUP (group));
  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
  g_assert (diagnostic != NULL);

  if (group->diagnostics_by_provider == NULL)
    group->diagnostics_by_provider = g_hash_table_new_full (NULL, NULL, NULL, free_diagnostics);

  diagnostics = g_hash_table_lookup (group->diagnostics_by_provider, provider);

  if (diagnostics == NULL)
    {
      diagnostics = ide_diagnostics_new ();
      g_hash_table_insert (group->diagnostics_by_provider, provider, diagnostics);
    }

  ide_diagnostics_add (diagnostics, diagnostic);

  group->has_diagnostics = TRUE;
  group->sequence++;
}

static void
ide_diagnostics_group_diagnose_cb (GObject      *object,
                                   GAsyncResult *result,
                                   gpointer      user_data)
{
  IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)object;
  g_autoptr(IdeDiagnosticsManager) self = user_data;
  g_autoptr(IdeDiagnostics) diagnostics = NULL;
  g_autoptr(GError) error = NULL;
  IdeDiagnosticsGroup *group;
  gboolean changed;

  IDE_ENTRY;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));

  IDE_TRACE_MSG ("%s diagnosis completed", G_OBJECT_TYPE_NAME (provider));

  diagnostics = ide_diagnostic_provider_diagnose_finish (provider, result, &error);

  if (error != NULL &&
      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
    g_debug ("%s", error->message);

  /*
   * This fetches the group our provider belongs to. Since the group is
   * reference counted (and we only release it when our provider is
   * finalized), we should be guaranteed we have a valid group.
   */
  group = g_object_get_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP");

  if (group == NULL)
    {
      /* Warning and bail if we failed to get the diagnostic group.
       * This shouldn't be happening, but I have definitely seen it
       * so it is probably related to disposal.
       */
      g_warning ("Failed to locate group, possibly disposed.");
      IDE_EXIT;
    }

  g_assert (IS_DIAGNOSTICS_GROUP (group));

  /*
   * Clear all of our old diagnostics no matter where they ended up.
   */
  changed = ide_diagnostics_manager_clear_by_provider (self, provider);

  /*
   * The following adds diagnostics to the appropriate group, but tries the
   * group we belong to first as our fast path. That will almost always be
   * the case, except when a diagnostic came up for a header or something
   * while parsing a given file.
   */
  if (diagnostics != NULL)
    {
      guint length = diagnostics_get_size (diagnostics);

      for (guint i = 0; i < length; i++)
        {
          g_autoptr(IdeDiagnostic) diagnostic = g_list_model_get_item (G_LIST_MODEL (diagnostics), i);
          GFile *file = ide_diagnostic_get_file (diagnostic);

          if (file != NULL)
            {
              if (g_file_equal (file, group->file))
                ide_diagnostics_group_add (group, provider, diagnostic);
              else
                ide_diagnostics_manager_add_diagnostic (self, provider, diagnostic);
            }
        }

      if (length > 0)
        changed = TRUE;
    }

  group->in_diagnose--;

  /*
   * Ensure we increment our sequence number even when no diagnostics were
   * reported. This ensures that the gutter gets cleared and line-flags
   * cache updated.
   */
  group->sequence++;

  /*
   * Since the individual groups have sequence numbers associated with changes,
   * it's okay to emit this for every provider completion. That allows the UIs
   * to update faster as each provider completes at the expensive of a little
   * more CPU activity.
   */
  if (changed)
    g_signal_emit (self, signals [CHANGED], 0);

  /*
   * If there are no more diagnostics providers active and the group needs
   * another diagnosis, then we can start the next one now.
   *
   * If we are completing this diagnosis and the buffer was already released
   * (and other diagnose providers have unloaded), we might be able to clean
   * up the group and be done with things.
   */
  if (group->was_removed == FALSE && group->in_diagnose == 0 && group->needs_diagnose)
    {
      ide_diagnostics_group_queue_diagnose (group, self);
    }
  else if (ide_diagnostics_group_can_dispose (group))
    {
      group->was_removed = TRUE;
      g_hash_table_remove (self->groups_by_file, group->file);
      IDE_EXIT;
    }

  IDE_EXIT;
}

static void
ide_diagnostics_group_diagnose_foreach (IdeExtensionSetAdapter *adapter,
                                        PeasPluginInfo         *plugin_info,
                                        PeasExtension          *exten,
                                        gpointer                user_data)
{
  IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)exten;
  IdeDiagnosticsManager *self = user_data;
  IdeDiagnosticsGroup *group;

  IDE_ENTRY;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
  g_assert (plugin_info != NULL);
  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));

  group = g_object_get_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP");

  g_assert (group != NULL);
  g_assert (IS_DIAGNOSTICS_GROUP (group));

  group->in_diagnose++;

#ifdef IDE_ENABLE_TRACE
  {
    g_autofree gchar *uri = g_file_get_uri (group->file);
    IDE_TRACE_MSG ("Beginning diagnose on %s with provider %s",
                   uri, G_OBJECT_TYPE_NAME (provider));
  }
#endif

  ide_diagnostic_provider_diagnose_async (provider,
                                          group->file,
                                          group->contents,
                                          group->lang_id,
                                          NULL,
                                          ide_diagnostics_group_diagnose_cb,
                                          g_object_ref (self));

  IDE_EXIT;
}

static void
ide_diagnostics_group_diagnose (IdeDiagnosticsGroup   *group,
                                IdeDiagnosticsManager *self)
{
  IDE_ENTRY;

  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
  g_assert (group != NULL);
  g_assert (group->in_diagnose == FALSE);
  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (group->adapter));

  group->needs_diagnose = FALSE;
  group->has_diagnostics = FALSE;

  if (group->contents == NULL)
    group->contents = g_bytes_new ("", 0);

  ide_extension_set_adapter_foreach (group->adapter,
                                     ide_diagnostics_group_diagnose_foreach,
                                     self);

  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);

  IDE_EXIT;
}

static gboolean
ide_diagnostics_manager_begin_diagnose (gpointer data)
{
  IdeDiagnosticsManager *self = data;
  GHashTableIter iter;
  gpointer value;

  IDE_ENTRY;

  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));

  self->queued_diagnose_source = 0;

  g_hash_table_iter_init (&iter, self->groups_by_file);

  while (g_hash_table_iter_next (&iter, NULL, &value))
    {
      IdeDiagnosticsGroup *group = value;

      g_assert (group != NULL);
      g_assert (IS_DIAGNOSTICS_GROUP (group));

      if (group->needs_diagnose && group->adapter != NULL && group->in_diagnose == 0)
        ide_diagnostics_group_diagnose (group, self);
    }

  IDE_RETURN (G_SOURCE_REMOVE);
}

static void
ide_diagnostics_group_queue_diagnose (IdeDiagnosticsGroup   *group,
                                      IdeDiagnosticsManager *self)
{
  g_assert (group != NULL);
  g_assert (IS_DIAGNOSTICS_GROUP (group));
  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));

  /*
   * This checks to see if we are diagnosing and if not queues a diagnose.
   * If a diagnosis is already running, we don't need to do anything now
   * because the completion of the diagnose will tick off the next diagnose
   * upon seening group->needs_diagnose==TRUE.
   */

  group->needs_diagnose = TRUE;

  if (group->in_diagnose == 0 && self->queued_diagnose_source == 0)
    self->queued_diagnose_source = g_timeout_add_full (G_PRIORITY_LOW,
                                                       DEFAULT_DIAGNOSE_DELAY,
                                                       ide_diagnostics_manager_begin_diagnose,
                                                       self, NULL);
}

static void
ide_diagnostics_manager_finalize (GObject *object)
{
  IdeDiagnosticsManager *self = (IdeDiagnosticsManager *)object;

  g_clear_handle_id (&self->queued_diagnose_source, g_source_remove);
  g_clear_pointer (&self->groups_by_file, g_hash_table_unref);

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

static void
ide_diagnostics_manager_get_property (GObject    *object,
                                      guint       prop_id,
                                      GValue     *value,
                                      GParamSpec *pspec)
{
  IdeDiagnosticsManager *self = (IdeDiagnosticsManager *)object;

  switch (prop_id)
    {
    case PROP_BUSY:
      g_value_set_boolean (value, ide_diagnostics_manager_get_busy (self));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
ide_diagnostics_manager_class_init (IdeDiagnosticsManagerClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = ide_diagnostics_manager_finalize;
  object_class->get_property = ide_diagnostics_manager_get_property;

  properties [PROP_BUSY] =
    g_param_spec_boolean ("busy",
                          "Busy",
                          "If the diagnostics manager is busy",
                          FALSE,
                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (object_class, N_PROPS, properties);

  /**
   * IdeDiagnosticsManager::changed:
   * @self: an #IdeDiagnosticsManager
   *
   * This signal is emitted when the diagnostics have changed for any
   * file managed by the IdeDiagnosticsManager.
   *
   * Since: 3.32
   */
  signals [CHANGED] =
    g_signal_new ("changed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
}

static void
ide_diagnostics_manager_init (IdeDiagnosticsManager *self)
{
  self->groups_by_file = g_hash_table_new_full (g_file_hash,
                                                (GEqualFunc)g_file_equal,
                                                NULL,
                                                (GDestroyNotify)ide_diagnostics_group_unref);
}

static void
ide_diagnostics_manager_add_diagnostic (IdeDiagnosticsManager *self,
                                        IdeDiagnosticProvider *provider,
                                        IdeDiagnostic         *diagnostic)
{
  IdeDiagnosticsGroup *group;
  GFile *file;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
  g_assert (diagnostic != NULL);

  /*
   * This is our slow path for adding a diagnostic to the system. We have
   * to locate the proper group for the diagnostic and then insert it
   * into that group.
   */

  if (NULL == (file = ide_diagnostic_get_file (diagnostic)))
    return;

  group = g_hash_table_lookup (self->groups_by_file, file);

  if (group == NULL)
    {
      group = ide_diagnostics_group_new (file);
      g_hash_table_insert (self->groups_by_file, group->file, group);
    }

  g_assert (group != NULL);
  g_assert (IS_DIAGNOSTICS_GROUP (group));

  ide_diagnostics_group_add (group, provider, diagnostic);
}

static IdeDiagnosticsGroup *
ide_diagnostics_manager_find_group (IdeDiagnosticsManager *self,
                                    GFile                 *file)
{
  IdeDiagnosticsGroup *group;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
  g_assert (G_IS_FILE (file));

  if (!(group = g_hash_table_lookup (self->groups_by_file, file)))
    {
      group = ide_diagnostics_group_new (file);
      g_hash_table_insert (self->groups_by_file, group->file, group);
    }

  g_assert (group != NULL);
  g_assert (IS_DIAGNOSTICS_GROUP (group));

  return group;
}

static IdeDiagnosticsGroup *
ide_diagnostics_manager_find_group_from_adapter (IdeDiagnosticsManager  *self,
                                                 IdeExtensionSetAdapter *adapter)
{
  GHashTableIter iter;
  gpointer value;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));

  g_hash_table_iter_init (&iter, self->groups_by_file);

  while (g_hash_table_iter_next (&iter, NULL, &value))
    {
      IdeDiagnosticsGroup *group = value;

      g_assert (group != NULL);
      g_assert (IS_DIAGNOSTICS_GROUP (group));

      if (group->adapter == adapter)
        return group;
    }

  g_assert_not_reached ();

  return NULL;
}

static void
ide_diagnostics_manager_provider_invalidated (IdeDiagnosticsManager *self,
                                              IdeDiagnosticProvider *provider)
{
  IdeDiagnosticsGroup *group;

  IDE_ENTRY;

  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));

  group = g_object_get_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP");

  ide_diagnostics_group_queue_diagnose (group, self);

  IDE_EXIT;
}

static void
ide_diagnostics_manager_extension_added (IdeExtensionSetAdapter *adapter,
                                         PeasPluginInfo         *plugin_info,
                                         PeasExtension          *exten,
                                         gpointer                user_data)
{
  IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)exten;
  IdeDiagnosticsManager *self = user_data;
  IdeDiagnosticsGroup *group;

  IDE_ENTRY;

  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
  g_assert (plugin_info != NULL);
  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));

  group = ide_diagnostics_manager_find_group_from_adapter (self, adapter);

  /*
   * We will need access to the group upon completion of the diagnostics,
   * so we add a reference to the group and allow it to be automatically
   * cleaned up when the provider finalizes.
   */
  g_object_set_data_full (G_OBJECT (provider),
                          "IDE_DIAGNOSTICS_GROUP",
                          ide_diagnostics_group_ref (group),
                          (GDestroyNotify)ide_diagnostics_group_unref);

  /*
   * We insert a dummy entry into the hashtable upon creation so
   * that when an async diagnosis completes we can use the presence
   * of this key to know if we've been unloaded.
   */
  g_hash_table_insert (group->diagnostics_by_provider, provider, NULL);

  /*
   * We need to keep track of when the provider has been invalidated so
   * that we can queue another request to fetch the diagnostics.
   */
  g_signal_connect_object (provider,
                           "invalidated",
                           G_CALLBACK (ide_diagnostics_manager_provider_invalidated),
                           self,
                           G_CONNECT_SWAPPED);

  ide_diagnostic_provider_load (provider);

  ide_diagnostics_group_queue_diagnose (group, self);

  IDE_EXIT;
}

static gboolean
ide_diagnostics_manager_clear_by_provider (IdeDiagnosticsManager *self,
                                           IdeDiagnosticProvider *provider)
{
  GHashTableIter iter;
  gpointer value;
  gboolean changed = FALSE;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));

  g_hash_table_iter_init (&iter, self->groups_by_file);

  while (g_hash_table_iter_next (&iter, NULL, &value))
    {
      IdeDiagnosticsGroup *group = value;

      g_assert (group != NULL);
      g_assert (IS_DIAGNOSTICS_GROUP (group));

      if (group->diagnostics_by_provider != NULL)
        {
          g_hash_table_remove (group->diagnostics_by_provider, provider);

          /*
           * If we caused this hashtable to become empty, we can release the
           * hashtable. The hashtable is guaranteed to not be empty if there
           * are other providers loaded for this group.
           */
          if (g_hash_table_size (group->diagnostics_by_provider) == 0)
            g_clear_pointer (&group->diagnostics_by_provider, g_hash_table_unref);

          /*
           * TODO: If this provider is not part of this group, we can possibly
           *       dispose of the group if there are no diagnostics.
           */

          changed = TRUE;
        }
    }

  return changed;
}

static void
ide_diagnostics_manager_extension_removed (IdeExtensionSetAdapter *adapter,
                                           PeasPluginInfo         *plugin_info,
                                           PeasExtension          *exten,
                                           gpointer                user_data)
{
  IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)exten;
  IdeDiagnosticsManager *self = user_data;

  IDE_ENTRY;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
  g_assert (plugin_info != NULL);
  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));

  g_signal_handlers_disconnect_by_func (provider,
                                        G_CALLBACK (ide_diagnostics_manager_provider_invalidated),
                                        self);

  /*
   * The goal of the following is to reomve our diagnostics from any file
   * that has been loaded. It is possible for diagnostic providers to effect
   * files outside the buffer they are loaded for and this ensures that we
   * clean those up.
   */
  ide_diagnostics_manager_clear_by_provider (self, provider);

  /* Clear the diagnostics group */
  g_object_set_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP", NULL);

  IDE_EXIT;
}

/**
 * ide_diagnostics_manager_get_busy:
 *
 * Gets if the diagnostics manager is currently executing a diagnosis.
 *
 * Returns: %TRUE if the #IdeDiagnosticsManager is busy diagnosing.
 *
 * Since: 3.32
 */
gboolean
ide_diagnostics_manager_get_busy (IdeDiagnosticsManager *self)
{
  GHashTableIter iter;
  gpointer value;

  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
  g_return_val_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self), FALSE);

  g_hash_table_iter_init (&iter, self->groups_by_file);

  while (g_hash_table_iter_next (&iter, NULL, &value))
    {
      IdeDiagnosticsGroup *group = value;

      g_assert (group != NULL);
      g_assert (IS_DIAGNOSTICS_GROUP (group));

      if (group->in_diagnose > 0)
        return TRUE;
    }

  return FALSE;
}

/**
 * ide_diagnostics_manager_get_diagnostics_for_file:
 * @self: An #IdeDiagnosticsManager
 * @file: a #GFile to retrieve diagnostics for
 *
 * This function collects all of the diagnostics that have been collected
 * for @file and returns them as a new #IdeDiagnostics to the caller.
 *
 * The #IdeDiagnostics structure will contain zero items if there are
 * no diagnostics discovered. Therefore, this function will never return
 * a %NULL value.
 *
 * Returns: (transfer full): A new #IdeDiagnostics.
 *
 * Since: 3.32
 */
IdeDiagnostics *
ide_diagnostics_manager_get_diagnostics_for_file (IdeDiagnosticsManager *self,
                                                  GFile                 *file)
{
  g_autoptr(IdeDiagnostics) ret = NULL;
  IdeDiagnosticsGroup *group;

  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
  g_return_val_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self), NULL);
  g_return_val_if_fail (G_IS_FILE (file), NULL);

  ret = ide_diagnostics_new ();

  group = g_hash_table_lookup (self->groups_by_file, file);

  if (group != NULL && group->diagnostics_by_provider != NULL)
    {
      GHashTableIter iter;
      gpointer value;

      g_hash_table_iter_init (&iter, group->diagnostics_by_provider);

      while (g_hash_table_iter_next (&iter, NULL, &value))
        {
          IdeDiagnostics *diagnostics = value;
          guint length;

          if (diagnostics == NULL)
            continue;

          length = g_list_model_get_n_items (G_LIST_MODEL (diagnostics));

          for (guint i = 0; i < length; i++)
            {
              g_autoptr(IdeDiagnostic) diagnostic = NULL;

              diagnostic = g_list_model_get_item (G_LIST_MODEL (diagnostics), i);
              ide_diagnostics_add (ret, diagnostic);
            }
        }
    }

  return g_steal_pointer (&ret);
}

guint
ide_diagnostics_manager_get_sequence_for_file (IdeDiagnosticsManager *self,
                                               GFile                 *file)
{
  IdeDiagnosticsGroup *group;

  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), 0);
  g_return_val_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self), 0);
  g_return_val_if_fail (G_IS_FILE (file), 0);

  group = g_hash_table_lookup (self->groups_by_file, file);

  if (group != NULL)
    {
      g_assert (IS_DIAGNOSTICS_GROUP (group));
      g_assert (G_IS_FILE (group->file));
      g_assert (g_file_equal (group->file, file));

      return group->sequence;
    }

  return 0;
}

/**
 * ide_diagnostics_manager_rediagnose:
 * @self: an #IdeDiagnosticsManager
 * @buffer: an #IdeBuffer
 *
 * Requests that the diagnostics be reloaded for @buffer.
 *
 * You may want to call this if you changed something that a buffer depends on,
 * and want to seamlessly update its diagnostics with that updated information.
 *
 * Since: 3.32
 */
void
ide_diagnostics_manager_rediagnose (IdeDiagnosticsManager *self,
                                    IdeBuffer             *buffer)
{
  IdeDiagnosticsGroup *group;
  GFile *file;

  g_return_if_fail (IDE_IS_MAIN_THREAD ());
  g_return_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self));
  g_return_if_fail (IDE_IS_BUFFER (buffer));

  file = ide_buffer_get_file (buffer);
  group = ide_diagnostics_manager_find_group (self, file);

  ide_diagnostics_group_queue_diagnose (group, self);
}

/**
 * ide_diagnostics_manager_from_context:
 * @context: an #IdeContext
 *
 * Gets the diagnostics manager for the context.
 *
 * Returns: (transfer none): an #IdeDiagnosticsManager
 *
 * Since: 3.32
 */
IdeDiagnosticsManager *
ide_diagnostics_manager_from_context (IdeContext *context)
{
  IdeDiagnosticsManager *self;

  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);

  ide_object_lock (IDE_OBJECT (context));
  if (!(self = ide_context_peek_child_typed (context, IDE_TYPE_DIAGNOSTICS_MANAGER)))
    {
      g_autoptr(IdeDiagnosticsManager) created = NULL;
      created = ide_object_ensure_child_typed (IDE_OBJECT (context),
                                               IDE_TYPE_DIAGNOSTICS_MANAGER);
      self = ide_context_peek_child_typed (context, IDE_TYPE_DIAGNOSTICS_MANAGER);
    }
  ide_object_unlock (IDE_OBJECT (context));

  return self;
}

void
_ide_diagnostics_manager_file_closed (IdeDiagnosticsManager *self,
                                      GFile                 *file)
{
  IdeDiagnosticsGroup *group;
  gboolean has_diagnostics;

  IDE_ENTRY;

  g_return_if_fail (IDE_IS_MAIN_THREAD ());
  g_return_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self));
  g_return_if_fail (G_IS_FILE (file));

  /*
   * The goal here is to cleanup everything we can about this group that
   * is part of a loaded buffer. We might want to keep the group around
   * in case it is useful from other providers.
   */

  group = ide_diagnostics_manager_find_group (self, file);

  g_assert (group != NULL);
  g_assert (IS_DIAGNOSTICS_GROUP (group));

  /* Clear some state we've been tracking */
  g_clear_pointer (&group->contents, g_bytes_unref);
  group->lang_id = NULL;
  group->needs_diagnose = FALSE;

  /*
   * We track if we have diagnostics now so that after we unload the
   * the providers, we can save that bit for later.
   */
  has_diagnostics = ide_diagnostics_group_has_diagnostics (group);

  /*
   * Force our diagnostic providers to unload. This will cause them
   * extension-removed signal to be called for each provider which
   * in turn will perform per-provider cleanup including the removal
   * of its diagnostics from all groups. (A provider can in practice
   * affect another group since a .c file could create a diagnostic
   * for a .h).
   */
  ide_clear_and_destroy_object (&group->adapter);

  /*
   * Even after unloading the diagnostic providers, we might still have
   * diagnostics that were created from other files (this could happen when
   * one diagnostic is created for a header from a source file). So we don't
   * want to wipe out the hashtable unless everything was unloaded. The other
   * provider will cleanup during its own destruction.
   */
  if (group->diagnostics_by_provider != NULL &&
      g_hash_table_size (group->diagnostics_by_provider) == 0)
    g_clear_pointer (&group->diagnostics_by_provider, g_hash_table_unref);

  group->has_diagnostics = has_diagnostics;

  IDE_EXIT;
}

void
_ide_diagnostics_manager_file_changed (IdeDiagnosticsManager *self,
                                       GFile                 *file,
                                       GBytes                *contents,
                                       const gchar           *lang_id)
{
  IdeDiagnosticsGroup *group;

  g_return_if_fail (IDE_IS_MAIN_THREAD ());
  g_return_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self));
  g_return_if_fail (G_IS_FILE (file));

  group = ide_diagnostics_manager_find_group (self, file);

  g_clear_pointer (&group->contents, g_bytes_unref);

  group->lang_id = g_intern_string (lang_id);
  group->contents = contents ? g_bytes_ref (contents) : NULL;

  ide_diagnostics_group_queue_diagnose (group, self);
}

void
_ide_diagnostics_manager_language_changed (IdeDiagnosticsManager *self,
                                           GFile                 *file,
                                           const gchar           *lang_id)
{
  IdeDiagnosticsGroup *group;

  g_return_if_fail (IDE_IS_MAIN_THREAD ());
  g_return_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self));

  group = ide_diagnostics_manager_find_group (self, file);
  group->lang_id = g_intern_string (lang_id);

  if (group->adapter != NULL)
    ide_extension_set_adapter_set_value (group->adapter, lang_id);

  ide_diagnostics_group_queue_diagnose (group, self);
}

void
_ide_diagnostics_manager_file_opened (IdeDiagnosticsManager *self,
                                      GFile                 *file,
                                      const gchar           *lang_id)
{
  IdeDiagnosticsGroup *group;

  IDE_ENTRY;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
  g_assert (G_IS_FILE (file));

  group = ide_diagnostics_manager_find_group (self, file);

  if (group->diagnostics_by_provider == NULL)
    group->diagnostics_by_provider =
      g_hash_table_new_full (NULL, NULL, NULL, free_diagnostics);

  group->lang_id = g_intern_string (lang_id);

  if (group->adapter == NULL)
    {
      group->adapter = ide_extension_set_adapter_new (IDE_OBJECT (self),
                                                      peas_engine_get_default (),
                                                      IDE_TYPE_DIAGNOSTIC_PROVIDER,
                                                      "Diagnostic-Provider-Languages",
                                                      lang_id);

      g_signal_connect_object (group->adapter,
                               "extension-added",
                               G_CALLBACK (ide_diagnostics_manager_extension_added),
                               self,
                               0);

      g_signal_connect_object (group->adapter,
                               "extension-removed",
                               G_CALLBACK (ide_diagnostics_manager_extension_removed),
                               self,
                               0);

      ide_extension_set_adapter_foreach (group->adapter,
                                         ide_diagnostics_manager_extension_added,
                                         self);
    }

  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (group->adapter));
  g_assert (g_hash_table_lookup (self->groups_by_file, file) == group);

  ide_diagnostics_group_queue_diagnose (group, self);

  IDE_EXIT;
}

