/* foundry-git-blame.c
 *
 * Copyright 2025 Christian Hergert <chergert@redhat.com>
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of the
 * License, or (at your option) any later version.
 *
 * This library 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
 * Lesser 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: LGPL-2.1-or-later
 */

#include "config.h"

#include "foundry-git-autocleanups.h"
#include "foundry-git-blame-private.h"
#include "foundry-git-private.h"
#include "foundry-git-signature-private.h"
#include "foundry-vcs-file.h"

/**
 * FoundryGitBlame:
 *
 * Git implementation of blame information for version control files.
 *
 * FoundryGitBlame provides Git-specific functionality for retrieving and
 * managing blame information for files in version control. It handles
 * blame data retrieval, caching, and provides efficient access to
 * authorship and modification information for code analysis and display.
 */

struct _FoundryGitBlame
{
  FoundryVcsBlame  parent_instance;
  GMutex           mutex;
  DexLimiter      *update_limiter;
  git_blame       *base_blame;
  git_blame       *bytes_blame;
};

G_DEFINE_FINAL_TYPE (FoundryGitBlame, foundry_git_blame, FOUNDRY_TYPE_VCS_BLAME)

typedef struct _Update
{
  FoundryGitBlame *self;
  GBytes          *contents;
} Update;

static git_blame *
get_blame_locked (FoundryGitBlame *self)
{
  return self->bytes_blame ? self->bytes_blame : self->base_blame;
}

static void
update_free (Update *state)
{
  g_clear_object (&state->self);
  g_clear_pointer (&state->contents, g_bytes_unref);
  g_free (state);
}

static DexFuture *
foundry_git_blame_update_thread (gpointer user_data)
{
  Update *state = user_data;
  g_autoptr(GMutexLocker) locker = NULL;
  g_autoptr(git_blame) blame = NULL;
  gconstpointer data = NULL;
  gsize size = 0;

  g_assert (state != NULL);
  g_assert (FOUNDRY_IS_GIT_BLAME (state->self));
  g_assert (state->self->base_blame != NULL);

  locker = g_mutex_locker_new (&state->self->mutex);

  if (state->contents == NULL)
    {
      g_clear_pointer (&state->self->bytes_blame, git_blame_free);
      return dex_future_new_true ();
    }

  data = g_bytes_get_data (state->contents, &size);
  g_clear_pointer (&state->self->bytes_blame, git_blame_free);

  if (git_blame_buffer (&blame, state->self->base_blame, data, size) == 0)
    state->self->bytes_blame = g_steal_pointer (&blame);

  return dex_future_new_true ();
}

static DexFuture *
foundry_git_blame_update (FoundryVcsBlame *vcs_blame,
                          GBytes          *contents)
{
  FoundryGitBlame *self = (FoundryGitBlame *)vcs_blame;
  Update *state;

  dex_return_error_if_fail (FOUNDRY_IS_GIT_BLAME (self));
  dex_return_error_if_fail (self->update_limiter != NULL);
  dex_return_error_if_fail (self->base_blame != NULL);

  state = g_new0 (Update, 1);
  state->self = g_object_ref (self);
  state->contents = contents ? g_bytes_ref (contents) : NULL;

  return dex_limiter_run_on_pool (self->update_limiter,
                                  _foundry_git_get_thread_pool (),
                                  foundry_git_blame_update_thread,
                                  state,
                                  (GDestroyNotify) update_free);
}

static FoundryVcsSignature *
foundry_git_blame_query_line (FoundryVcsBlame *blame,
                              guint            line)
{
  FoundryGitBlame *self = (FoundryGitBlame *)blame;
  g_autoptr(GMutexLocker) locker = NULL;
  const git_blame_hunk *hunk;
  git_blame *gblame;

  g_assert (FOUNDRY_IS_GIT_BLAME (self));
  g_assert (self->base_blame != NULL);

  locker = g_mutex_locker_new (&self->mutex);
  gblame = get_blame_locked (self);

  if ((hunk = git_blame_get_hunk_byline (gblame, line + 1)))
    {
      g_autoptr(git_signature) copy = NULL;

      /* TODO: This is often accessed sequentially so it might make
       *       sense to keep the most-recently-used one and then
       *       reuse it instead of copy/construct on each line.
       */

      if (hunk->final_signature == NULL)
        return NULL;

      if (git_signature_dup (&copy, hunk->final_signature) != 0)
        return NULL;

      return _foundry_git_signature_new (g_steal_pointer (&copy));
    }

  return NULL;
}

/**
 * foundry_git_blame_dup_commit_id:
 * @self: a #FoundryGitBlame
 * @line: the line number (0-indexed)
 *
 * Gets the commit ID string for the given line number from the blame
 * information.
 *
 * The commit ID is the hash of the commit that last modified the line.
 *
 * Returns: (transfer full): a newly allocated string containing the commit ID,
 *   or %NULL if the line is not found or not tracked
 *
 * Since: 1.1
 */
char *
foundry_git_blame_dup_commit_id (FoundryGitBlame *self,
                                 guint            line)
{
  g_autoptr(GMutexLocker) locker = NULL;
  const git_blame_hunk *hunk;
  git_blame *gblame;

  g_return_val_if_fail (FOUNDRY_IS_GIT_BLAME (self), NULL);
  g_return_val_if_fail (self->base_blame != NULL, NULL);

  locker = g_mutex_locker_new (&self->mutex);
  gblame = get_blame_locked (self);

  if ((hunk = git_blame_get_hunk_byline (gblame, line + 1)))
    return _foundry_git_oid_dup_string (&hunk->final_commit_id);

  return NULL;
}

static guint
foundry_git_blame_get_n_lines (FoundryVcsBlame *blame)
{
  FoundryGitBlame *self = (FoundryGitBlame *)blame;
  g_autoptr(GMutexLocker) locker = NULL;
  git_blame *gblame;
  gsize hunk_count;
  guint n_lines = 0;

  g_assert (FOUNDRY_IS_GIT_BLAME (self));
  g_assert (self->base_blame != NULL);

  locker = g_mutex_locker_new (&self->mutex);
  gblame = get_blame_locked (self);

  hunk_count = git_blame_get_hunk_count (gblame);

  for (gsize i = 0; i < hunk_count; i++)
    {
      const git_blame_hunk *hunk = git_blame_get_hunk_byindex (gblame, i);

      if (hunk != NULL)
        n_lines += hunk->lines_in_hunk;
    }

  return n_lines;
}

static void
foundry_git_blame_finalize (GObject *object)
{
  FoundryGitBlame *self = (FoundryGitBlame *)object;

  g_clear_pointer (&self->bytes_blame, git_blame_free);
  g_clear_pointer (&self->base_blame, git_blame_free);
  dex_clear (&self->update_limiter);
  g_mutex_clear (&self->mutex);

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

static void
foundry_git_blame_class_init (FoundryGitBlameClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  FoundryVcsBlameClass *vcs_blame_class = FOUNDRY_VCS_BLAME_CLASS (klass);

  object_class->finalize = foundry_git_blame_finalize;

  vcs_blame_class->update = foundry_git_blame_update;
  vcs_blame_class->query_line = foundry_git_blame_query_line;
  vcs_blame_class->get_n_lines = foundry_git_blame_get_n_lines;
}

static void
foundry_git_blame_init (FoundryGitBlame *self)
{
  g_mutex_init (&self->mutex);
  self->update_limiter = dex_limiter_new (1);
}

FoundryGitBlame *
_foundry_git_blame_new (git_blame *base_blame,
                        git_blame *bytes_blame)
{
  FoundryGitBlame *self;

  g_return_val_if_fail (base_blame != NULL, NULL);

  self = g_object_new (FOUNDRY_TYPE_GIT_BLAME, NULL);
  self->base_blame = g_steal_pointer (&base_blame);
  self->bytes_blame = g_steal_pointer (&bytes_blame);

  return self;
}
