/*
 * ggit-patch.c
 * This file is part of libgit2-glib
 *
 * Copyright (C) 2013 - Sindhu S
 *
 * libgit2-glib 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.
 *
 * libgit2-glib 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 libgit2-glib. If not, see <http://www.gnu.org/licenses/>.
 */

#include "ggit-patch.h"
#include "ggit-diff.h"
#include "ggit-diff-delta.h"
#include "ggit-diff-hunk.h"
#include "ggit-error.h"
#include "ggit-diff-options.h"

struct _GgitPatch
{
	git_patch *patch;
	gint ref_count;
};

G_DEFINE_BOXED_TYPE (GgitPatch, ggit_patch,
                     ggit_patch_ref, ggit_patch_unref)

GgitPatch *
_ggit_patch_wrap (git_patch *patch)
{
	GgitPatch *gpatch;

	gpatch = g_slice_new (GgitPatch);
	gpatch->patch = patch;
	gpatch->ref_count = 1;

	return gpatch;
}

/**
 * ggit_patch_ref:
 * @patch: a #GgitPatch.
 *
 * Atomically increments the reference count of @patch by one.
 * This function is MT-safe and may be called from any thread.
 *
 * Returns: (transfer none) (nullable): a #GgitPatch or %NULL.
 */
GgitPatch *
ggit_patch_ref (GgitPatch *patch)
{
	g_return_val_if_fail (patch != NULL, NULL);

	g_atomic_int_inc (&patch->ref_count);

	return patch;
}

/**
 * ggit_patch_unref:
 * @patch: a #GgitPatch.
 *
 * Atomically decrements the reference count of @patch by one.
 * If the reference count drops to 0, @patch is freed.
 */
void
ggit_patch_unref (GgitPatch *patch)
{
	g_return_if_fail (patch != NULL);

	if (g_atomic_int_dec_and_test (&patch->ref_count))
	{
		git_patch_free (patch->patch);
		g_slice_free (GgitPatch, patch);
	}
}

/**
 * ggit_patch_new_from_diff:
 * @diff: a #GgitDiff.
 * @idx: index into diff list.
 * @error: a #GError for error reporting, or %NULL.
 *
 * The #GgitPatch is a newly created object contains the text diffs
 * for the delta.  You have to call ggit_patch_unref() when you are
 * done with it.  You can use the patch object to loop over all the hunks
 * and lines in the diff of the one delta.
 *
 * Returns: (transfer full) (nullable): a newly created #GgitPatch or %NULL.
 */
GgitPatch *
ggit_patch_new_from_diff (GgitDiff  *diff,
                          gsize      idx,
                          GError   **error)
{
	git_patch *patch;
	gint ret;

	g_return_val_if_fail (GGIT_IS_DIFF (diff), NULL);
	g_return_val_if_fail (error == NULL || *error == NULL, NULL);

	ret = git_patch_from_diff (&patch, _ggit_native_get (diff), idx);

	if (ret != GIT_OK)
	{
		_ggit_error_set (error, ret);
		return NULL;
	}

	return _ggit_patch_wrap (patch);
}

/**
 * ggit_patch_new_from_blobs:
 * @old_blob: (allow-none): a #GgitBlob to diff from.
 * @old_as_path: (allow-none): treat @old_blob as if it had this filename, or %NULL,
 * @new_blob: (allow-none): a #GgitBlob to diff to.
 * @new_as_path: (allow-none): treat @new_blob as if it had this filename, or %NULL,
 * @diff_options: (allow-none): a #GgitDiffOptions, or %NULL.
 * @error: a #GError for error reporting, or %NULL.
 *
 * Directly generate a patch from the difference between two blobs.
 *
 * This is just like ggit_diff_blobs() except it generates a patch object
 * for the difference instead of directly making callbacks.  You can use the
 * standard ggit_patch accessor functions to read the patch data, and
 * you must call ggit_patch_unref on the patch when done.
 *
 * Returns: (transfer full) (nullable): a newly created #GgitPatch or %NULL.
 */
GgitPatch *
ggit_patch_new_from_blobs (GgitBlob         *old_blob,
                           const gchar      *old_as_path,
                           GgitBlob         *new_blob,
                           const gchar      *new_as_path,
                           GgitDiffOptions  *diff_options,
                           GError          **error)
{
	gint ret;
	const git_diff_options *gdiff_options;
	git_patch *patch;

	g_return_val_if_fail (error == NULL || *error == NULL, NULL);

	gdiff_options = _ggit_diff_options_get_diff_options (diff_options);

	ret = git_patch_from_blobs (&patch,
	                            old_blob ? _ggit_native_get (old_blob) : NULL,
	                            old_as_path,
	                            new_blob ? _ggit_native_get (new_blob) : NULL,
	                            new_as_path,
	                            (git_diff_options *) gdiff_options);

	if (ret != GIT_OK)
	{
		_ggit_error_set (error, ret);
		return NULL;
	}

	return _ggit_patch_wrap (patch);
}

/**
 * ggit_patch_to_string:
 * @patch: a #GgitPatch.
 * @error: a #GError for error reporting, or %NULL.
 *
 * Gets the content of a patch as a single diff text.
 *
 * Returns: (transfer full) (nullable): the content of a patch as a single diff text or %NULL.
 */
gchar *
ggit_patch_to_string (GgitPatch  *patch,
                      GError    **error)
{
	git_buf buf = {0,};
	gchar *result = NULL;
	gint ret;

	g_return_val_if_fail (patch != NULL, NULL);
	g_return_val_if_fail (error == NULL || *error == NULL, NULL);

	ret = git_patch_to_buf (&buf, patch->patch);

	if (ret == GIT_OK)
	{
		result = g_strdup (buf.ptr);
#if LIBGIT2_VER_MAJOR > 0 || (LIBGIT2_VER_MAJOR == 0 && LIBGIT2_VER_MINOR >= 28)
		git_buf_dispose (&buf);
#else
		git_buf_free (&buf);
#endif
	}

	return result;
}

typedef struct
{
	GOutputStream  *stream;
	GError        **error;
} PatchToStream;

static int
patch_to_stream (const git_diff_delta *delta,
                 const git_diff_hunk  *hunk,
                 const git_diff_line  *line,
                 void                 *payload)
{
	PatchToStream *info = payload;
	gboolean ret;
	gsize written;

	ret = g_output_stream_write_all (info->stream,
	                                 line->content,
	                                 line->content_len,
	                                 &written,
	                                 NULL,
	                                 info->error);

	if (!ret)
	{
		return -1;
	}
	else
	{
		return written;
	}
}

/**
 * ggit_patch_to_stream:
 * @patch: a #GgitPatch.
 * @stream: a #GOutputStream.
 * @error: a #GError for error reporting, or %NULL.
 *
 * Write the contents of a patch to the provided stream.
 *
 * Returns: %TRUE if the patch was written successfully, %FALSE otherwise.
 *
 **/
gboolean
ggit_patch_to_stream (GgitPatch      *patch,
                      GOutputStream  *stream,
                      GError        **error)
{
	PatchToStream info;
	gint ret;

	g_return_val_if_fail (patch != NULL, FALSE);
	g_return_val_if_fail (G_IS_OUTPUT_STREAM (stream), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	info.stream = stream;
	info.error = error;

	ret = git_patch_print (patch->patch,
	                       patch_to_stream,
	                       &info);

	if (ret != GIT_OK)
	{
		if (error != NULL && *error == NULL)
		{
			_ggit_error_set (error, ret);
		}

		return FALSE;
	}

	return TRUE;
}

/**
 * ggit_patch_get_line_stats:
 * @patch: a #GgitPatch.
 * @total_context: (allow-none) (out): return value for the number of context lines.
 * @total_additions: (allow-none) (out): return value for the number of added lines.
 * @total_deletions: (allow-none) (out): return value for the number of deleted lines.
 * @error: a #GError.
 *
 * Get the line statistics of the patch.
 *
 * Returns: %TRUE if successfull, %FALSE otherwise.
 *
 **/
gboolean
ggit_patch_get_line_stats (GgitPatch      *patch,
                           gsize          *total_context,
                           gsize          *total_additions,
                           gsize          *total_deletions,
                           GError        **error)
{
	size_t tc;
	size_t ta;
	size_t td;
	gint ret;

	g_return_val_if_fail (patch != NULL, FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	ret = git_patch_line_stats (&tc, &ta, &td, patch->patch);

	if (ret != GIT_OK)
	{
		_ggit_error_set (error, ret);
		return FALSE;
	}

	if (total_context)
	{
		*total_context = tc;
	}

	if (total_additions)
	{
		*total_additions = ta;
	}

	if (total_deletions)
	{
		*total_deletions = td;
	}

	return TRUE;
}

/**
 * ggit_patch_get_num_hunks:
 * @patch: a #GgitPatch.
 *
 * Get the number of hunks in the patch.
 *
 * Returns: the number of hunks.
 *
 **/
gsize
ggit_patch_get_num_hunks (GgitPatch *patch)
{
	g_return_val_if_fail (patch != NULL, FALSE);

	return git_patch_num_hunks (patch->patch);
}

/**
 * ggit_patch_get_num_lines_in_hunk:
 * @patch: a #GgitPatch.
 * @hunk: the hunk index.
 *
 * Get the number of lines in @hunk.
 *
 * Returns: the number of lines.
 *
 **/
gint
ggit_patch_get_num_lines_in_hunk (GgitPatch *patch,
                                  gsize      hunk)
{
	g_return_val_if_fail (patch != NULL, FALSE);

	return git_patch_num_lines_in_hunk (patch->patch, hunk);
}

/**
 * ggit_patch_get_delta:
 * @patch: a #GgitPatch.
 *
 * Get the diff delta corresponding to the patch.
 *
 * Returns: (transfer full) (nullable): the #GgitDiffDelta of the patch or %NULL.
 */
GgitDiffDelta *
ggit_patch_get_delta (GgitPatch *patch)
{
	g_return_val_if_fail (patch != NULL, NULL);

	return _ggit_diff_delta_wrap (git_patch_get_delta (patch->patch));
}

/**
 * ggit_patch_get_hunk:
 * @patch: a #GgitPatch
 * @idx: the hunk index.
 * @error: a #GError
 *
 * Get the @idx'th hunk in the patch.
 *
 * Returns: (transfer full) (nullable): a new #GgitDiffHunk or %NULL on error.
 */
GgitDiffHunk *
ggit_patch_get_hunk (GgitPatch  *patch,
                     gsize       idx,
                     GError    **error)
{
	const git_diff_hunk *hunk;
	size_t tlines;
	gint ret;

	g_return_val_if_fail (patch != NULL, NULL);

	ret = git_patch_get_hunk (&hunk, &tlines, patch->patch, idx);

	if (ret != GIT_OK)
	{
		_ggit_error_set (error, ret);
		return NULL;
	}

	return _ggit_diff_hunk_wrap (hunk);
}

/* ex:set ts=8 noet: */
