/*
 * Copyright (C) 2011 Colin Walters <walters@verbum.org>
 *
 * SPDX-License-Identifier: LGPL-2.0+
 *
 * 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 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 Lesser General Public
 * License along with this library. If not, see <https://www.gnu.org/licenses/>.
 *
 * Author: Colin Walters <walters@verbum.org>
 */

#include "config.h"

#include "ostree.h"
#include "ot-builtins.h"
#include "otutil.h"

static gboolean opt_stats;
static gboolean opt_fs_diff;
static gboolean opt_no_xattrs;
static gint opt_owner_uid = -1;
static gint opt_owner_gid = -1;

/* ATTENTION:
 * Please remember to update the bash-completion script (bash/ostree) and
 * man page (man/ostree-diff.xml) when changing the option list.
 */

static GOptionEntry options[]
    = { { "stats", 0, 0, G_OPTION_ARG_NONE, &opt_stats, "Print various statistics", NULL },
        { "fs-diff", 0, 0, G_OPTION_ARG_NONE, &opt_fs_diff, "Print filesystem diff", NULL },
        { "no-xattrs", 0, 0, G_OPTION_ARG_NONE, &opt_no_xattrs,
          "Skip output of extended attributes", NULL },
        { "owner-uid", 0, 0, G_OPTION_ARG_INT, &opt_owner_uid,
          "Use file ownership user id for local files", "UID" },
        { "owner-gid", 0, 0, G_OPTION_ARG_INT, &opt_owner_gid,
          "Use file ownership group id for local files", "GID" },
        { NULL } };

static gboolean
parse_file_or_commit (OstreeRepo *repo, const char *arg, GFile **out_file,
                      GCancellable *cancellable, GError **error)
{
  g_autoptr (GFile) ret_file = NULL;

  if (g_str_has_prefix (arg, "/") || g_str_has_prefix (arg, "./"))
    {
      ret_file = g_file_new_for_path (arg);
    }
  else
    {
      if (!ostree_repo_read_commit (repo, arg, &ret_file, NULL, cancellable, error))
        return FALSE;
    }

  ot_transfer_out_value (out_file, &ret_file);
  return TRUE;
}

static GHashTable *
reachable_set_intersect (GHashTable *a, GHashTable *b)
{
  GHashTable *ret = ostree_repo_traverse_new_reachable ();
  GHashTableIter hashiter;
  gpointer key, value;

  g_hash_table_iter_init (&hashiter, a);
  while (g_hash_table_iter_next (&hashiter, &key, &value))
    {
      GVariant *v = key;
      if (g_hash_table_contains (b, v))
        g_hash_table_insert (ret, g_variant_ref (v), v);
    }

  return ret;
}

static gboolean
object_set_total_size (OstreeRepo *repo, GHashTable *reachable, guint64 *out_total,
                       GCancellable *cancellable, GError **error)
{
  GHashTableIter hashiter;
  gpointer key, value;

  *out_total = 0;

  g_hash_table_iter_init (&hashiter, reachable);
  while (g_hash_table_iter_next (&hashiter, &key, &value))
    {
      GVariant *v = key;
      const char *csum;
      OstreeObjectType objtype;
      guint64 size;

      ostree_object_name_deserialize (v, &csum, &objtype);
      if (!ostree_repo_query_object_storage_size (repo, objtype, csum, &size, cancellable, error))
        return FALSE;
      *out_total += size;
    }

  return TRUE;
}

gboolean
ostree_builtin_diff (int argc, char **argv, OstreeCommandInvocation *invocation,
                     GCancellable *cancellable, GError **error)
{
  g_autoptr (GOptionContext) context = g_option_context_new ("REV_OR_DIR REV_OR_DIR");

  g_autoptr (OstreeRepo) repo = NULL;
  if (!ostree_option_context_parse (context, options, &argc, &argv, invocation, &repo, cancellable,
                                    error))
    return FALSE;

  if (argc < 2)
    {
      gchar *help = g_option_context_get_help (context, TRUE, NULL);
      g_printerr ("%s\n", help);
      g_free (help);
      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, "REV must be specified");
      return FALSE;
    }

  g_autofree char *src_prev = NULL;
  const char *src;
  const char *target;
  if (argc == 2)
    {
      src_prev = g_strconcat (argv[1], "^", NULL);
      src = src_prev;
      target = argv[1];
    }
  else
    {
      src = argv[1];
      target = argv[2];
    }

  if (!opt_stats && !opt_fs_diff)
    opt_fs_diff = TRUE;

  g_autoptr (GFile) srcf = NULL;
  g_autoptr (GFile) targetf = NULL;
  g_autoptr (GPtrArray) modified = NULL;
  g_autoptr (GPtrArray) removed = NULL;
  g_autoptr (GPtrArray) added = NULL;

  if (opt_fs_diff)
    {
      OstreeDiffFlags diff_flags = OSTREE_DIFF_FLAGS_NONE;

      if (opt_no_xattrs)
        diff_flags |= OSTREE_DIFF_FLAGS_IGNORE_XATTRS;

      if (!parse_file_or_commit (repo, src, &srcf, cancellable, error))
        return FALSE;
      if (!parse_file_or_commit (repo, target, &targetf, cancellable, error))
        return FALSE;

      modified = g_ptr_array_new_with_free_func ((GDestroyNotify)ostree_diff_item_unref);
      removed = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);
      added = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);

      OstreeDiffDirsOptions diff_opts = { opt_owner_uid, opt_owner_gid };
      if (!ostree_diff_dirs_with_options (diff_flags, srcf, targetf, modified, removed, added,
                                          &diff_opts, cancellable, error))
        return FALSE;

      ostree_diff_print (srcf, targetf, modified, removed, added);
    }

  if (opt_stats)
    {
      g_autoptr (GHashTable) reachable_a = NULL;
      g_autoptr (GHashTable) reachable_b = NULL;
      g_autoptr (GHashTable) reachable_intersection = NULL;
      g_autofree char *rev_a = NULL;
      g_autofree char *rev_b = NULL;
      g_autofree char *size = NULL;
      guint a_size;
      guint b_size;
      guint64 total_common;

      if (!ostree_repo_resolve_rev (repo, src, FALSE, &rev_a, error))
        return FALSE;
      if (!ostree_repo_resolve_rev (repo, target, FALSE, &rev_b, error))
        return FALSE;

      if (!ostree_repo_traverse_commit (repo, rev_a, 0, &reachable_a, cancellable, error))
        return FALSE;
      if (!ostree_repo_traverse_commit (repo, rev_b, 0, &reachable_b, cancellable, error))
        return FALSE;

      a_size = g_hash_table_size (reachable_a);
      b_size = g_hash_table_size (reachable_b);
      g_print ("[A] Object Count: %u\n", a_size);
      g_print ("[B] Object Count: %u\n", b_size);

      reachable_intersection = reachable_set_intersect (reachable_a, reachable_b);

      g_print ("Common Object Count: %u\n", g_hash_table_size (reachable_intersection));

      if (!object_set_total_size (repo, reachable_intersection, &total_common, cancellable, error))
        return FALSE;
      size = g_format_size_full (total_common, 0);
      g_print ("Common Object Size: %s\n", size);
    }

  return TRUE;
}
