#include "_cgo_export.h"

#include <git2.h>
#include <git2/sys/odb_backend.h>
#include <git2/sys/refdb_backend.h>
#include <git2/sys/cred.h>

// There are two ways in which to declare a callback:
//
// * If there is a guarantee that the callback will always be called within the
//   same stack (e.g. by passing the callback directly into a function / into a
//   struct that goes into a function), the following pattern is preferred,
//   which preserves the error object as-is:
//
//   // myfile.go
//   type FooCallback func(...) (..., error)
//   type fooCallbackData struct {
//     callback    FooCallback
//     errorTarget *error
//   }
//
//   //export fooCallback
//   func fooCallback(..., handle unsafe.Pointer) C.int {
//     payload := pointerHandles.Get(handle)
//     data := payload.(*fooCallbackData)
//     ...
//     err := data.callback(...)
//     if err != nil {
//       *data.errorTarget = err
//       return C.int(ErrorCodeUser)
//     }
//     return C.int(ErrorCodeOK)
//   }
//
//   func MyFunction(... callback FooCallback) error {
//    var err error
//    data := fooCallbackData{
//      callback:    callback,
//      errorTarget: &err,
//    }
//    handle := pointerHandles.Track(&data)
//    defer pointerHandles.Untrack(handle)
//
//    runtime.LockOSThread()
//    defer runtime.UnlockOSThread()
//
//    ret := C._go_git_my_function(..., handle)
//    if ret == C.int(ErrorCodeUser) && err != nil {
//      return err
//    }
//    if ret < 0 {
//      return MakeGitError(ret)
//    }
//    return nil
//   }
//
//   // wrapper.c
//   int _go_git_my_function(..., void *payload)
//   {
//     return git_my_function(..., (git_foo_cb)&fooCallback, payload);
//   }
//
// * Additionally, if the same callback can be invoked from multiple functions or
//   from different stacks (e.g. when passing the callback to an object), the
//   following pattern should be used in tandem, which has the downside of
//   losing the original error object and converting it to a GitError if the
//   callback happens from a different stack:
//
//   // myfile.go
//   type FooCallback func(...) (..., error)
//   type fooCallbackData struct {
//     callback    FooCallback
//     errorTarget *error
//   }
//
//   //export fooCallback
//   func fooCallback(errorMessage **C.char, ..., handle unsafe.Pointer) C.int {
//     data := pointerHandles.Get(data).(*fooCallbackData)
//     ...
//     err := data.callback(...)
//     if err != nil {
//       if data.errorTarget != nil {
//         *data.errorTarget = err
//       }
//       return setCallbackError(errorMessage, err)
//     }
//     return C.int(ErrorCodeOK)
//   }
//
//   // wrapper.c
//   static int foo_callback(...)
//   {
//     char *error_message = NULL;
//     const int ret = fooCallback(&error_message, ...);
//     return set_callback_error(error_message, ret);
//   }

/**
 * Sets the thread-local error to the provided string. This needs to happen in
 * C because Go might change Goroutines _just_ before returning, which would
 * lose the contents of the error message.
 */
static int set_callback_error(char *error_message, int ret)
{
	if (error_message != NULL) {
		if (ret < 0)
			git_error_set_str(GIT_ERROR_CALLBACK, error_message);
		free(error_message);
	}
	return ret;
}

void _go_git_populate_apply_callbacks(git_apply_options *options)
{
	options->delta_cb = (git_apply_delta_cb)&deltaApplyCallback;
	options->hunk_cb = (git_apply_hunk_cb)&hunkApplyCallback;
}

static int commit_create_callback(
		git_oid *out,
		const git_signature *author,
		const git_signature *committer,
		const char *message_encoding,
		const char *message,
		const git_tree *tree,
		size_t parent_count,
		const git_commit *parents[],
		void *payload)
{
	char *error_message = NULL;
	const int ret = commitCreateCallback(
			&error_message,
			out,
			(git_signature *)author,
			(git_signature *)committer,
			(char *)message_encoding,
			(char *)message,
			(git_tree *)tree,
			parent_count,
			(git_commit **)parents,
			payload
	);
	return set_callback_error(error_message, ret);
}

void _go_git_populate_rebase_callbacks(git_rebase_options *opts)
{
	opts->commit_create_cb = commit_create_callback;
}

void _go_git_populate_clone_callbacks(git_clone_options *opts)
{
	opts->remote_cb = (git_remote_create_cb)&remoteCreateCallback;
}

void _go_git_populate_checkout_callbacks(git_checkout_options *opts)
{
	opts->notify_cb = (git_checkout_notify_cb)&checkoutNotifyCallback;
	opts->progress_cb = (git_checkout_progress_cb)&checkoutProgressCallback;
}

int _go_git_visit_submodule(git_repository *repo, void *fct)
{
	return git_submodule_foreach(repo, (git_submodule_cb)&submoduleCallback, fct);
}

int _go_git_treewalk(git_tree *tree, git_treewalk_mode mode, void *ptr)
{
	return git_tree_walk(tree, mode, (git_treewalk_cb)&treeWalkCallback, ptr);
}

int _go_git_packbuilder_foreach(git_packbuilder *pb, void *payload)
{
	return git_packbuilder_foreach(pb, (git_packbuilder_foreach_cb)&packbuilderForEachCallback, payload);
}

int _go_git_odb_foreach(git_odb *db, void *payload)
{
	return git_odb_foreach(db, (git_odb_foreach_cb)&odbForEachCallback, payload);
}

void _go_git_odb_backend_free(git_odb_backend *backend)
{
	if (!backend->free)
		return;
	backend->free(backend);
}

void _go_git_refdb_backend_free(git_refdb_backend *backend)
{
	if (!backend->free)
		return;
	backend->free(backend);
}

int _go_git_diff_foreach(git_diff *diff, int eachFile, int eachHunk, int eachLine, void *payload)
{
	git_diff_file_cb fcb = NULL;
	git_diff_hunk_cb hcb = NULL;
	git_diff_line_cb lcb = NULL;

	if (eachFile)
		fcb = (git_diff_file_cb)&diffForEachFileCallback;
	if (eachHunk)
		hcb = (git_diff_hunk_cb)&diffForEachHunkCallback;
	if (eachLine)
		lcb = (git_diff_line_cb)&diffForEachLineCallback;

	return git_diff_foreach(diff, fcb, NULL, hcb, lcb, payload);
}

int _go_git_diff_blobs(
		git_blob *old,
		const char *old_path,
		git_blob *new,
		const char *new_path,
		git_diff_options *opts,
		int eachFile,
		int eachHunk,
		int eachLine,
		void *payload)
{
	git_diff_file_cb fcb = NULL;
	git_diff_hunk_cb hcb = NULL;
	git_diff_line_cb lcb = NULL;

	if (eachFile)
		fcb = (git_diff_file_cb)&diffForEachFileCallback;
	if (eachHunk)
		hcb = (git_diff_hunk_cb)&diffForEachHunkCallback;
	if (eachLine)
		lcb = (git_diff_line_cb)&diffForEachLineCallback;

	return git_diff_blobs(old, old_path, new, new_path, opts, fcb, NULL, hcb, lcb, payload);
}

void _go_git_setup_diff_notify_callbacks(git_diff_options *opts)
{
	opts->notify_cb = (git_diff_notify_cb)&diffNotifyCallback;
}

static int sideband_progress_callback(const char *str, int len, void *payload)
{
	char *error_message = NULL;
	const int ret = sidebandProgressCallback(&error_message, (char *)str, len, payload);
	return set_callback_error(error_message, ret);
}

static int completion_callback(git_remote_completion_type completion_type, void *data)
{
	char *error_message = NULL;
	const int ret = completionCallback(&error_message, completion_type, data);
	return set_callback_error(error_message, ret);
}

static int credentials_callback(
		git_credential **cred,
		const char *url,
		const char *username_from_url,
		unsigned int allowed_types,
		void *data)
{
	char *error_message = NULL;
	const int ret = credentialsCallback(
			&error_message,
			cred,
			(char *)url,
			(char *)username_from_url,
			allowed_types,
			data
	);
	return set_callback_error(error_message, ret);
}

static int transfer_progress_callback(const git_transfer_progress *stats, void *data)
{
	char *error_message = NULL;
	const int ret = transferProgressCallback(
			&error_message,
			(git_transfer_progress *)stats,
			data
	);
	return set_callback_error(error_message, ret);
}

static int update_tips_callback(const char *refname, const git_oid *a, const git_oid *b, void *data)
{
	char *error_message = NULL;
	const int ret = updateTipsCallback(
			&error_message,
			(char *)refname,
			(git_oid *)a,
			(git_oid *)b,
			data
	);
	return set_callback_error(error_message, ret);
}

static int certificate_check_callback(git_cert *cert, int valid, const char *host, void *data)
{
	char *error_message = NULL;
	const int ret = certificateCheckCallback(
			&error_message,
			cert,
			valid,
			(char *)host,
			data
	);
	return set_callback_error(error_message, ret);
}

static int pack_progress_callback(int stage, unsigned int current, unsigned int total, void *data)
{
	char *error_message = NULL;
	const int ret = packProgressCallback(
			&error_message,
			stage,
			current,
			total,
			data
	);
	return set_callback_error(error_message, ret);
}

static int push_transfer_progress_callback(
		unsigned int current,
		unsigned int total,
		size_t bytes,
		void *data)
{
	char *error_message = NULL;
	const int ret = pushTransferProgressCallback(
			&error_message,
			current,
			total,
			bytes,
			data
	);
	return set_callback_error(error_message, ret);
}

static int push_update_reference_callback(const char *refname, const char *status, void *data)
{
	char *error_message = NULL;
	const int ret = pushUpdateReferenceCallback(
			&error_message,
			(char *)refname,
			(char *)status,
			data
	);
	return set_callback_error(error_message, ret);
}

void _go_git_populate_remote_callbacks(git_remote_callbacks *callbacks)
{
	callbacks->sideband_progress = sideband_progress_callback;
	callbacks->completion = completion_callback;
	callbacks->credentials = credentials_callback;
	callbacks->transfer_progress = transfer_progress_callback;
	callbacks->update_tips = update_tips_callback;
	callbacks->certificate_check = certificate_check_callback;
	callbacks->pack_progress = pack_progress_callback;
	callbacks->push_transfer_progress = push_transfer_progress_callback;
	callbacks->push_update_reference = push_update_reference_callback;
}

int _go_git_index_add_all(git_index *index, const git_strarray *pathspec, unsigned int flags, void *callback)
{
	git_index_matched_path_cb cb = callback ? (git_index_matched_path_cb)&indexMatchedPathCallback : NULL;
	return git_index_add_all(index, pathspec, flags, cb, callback);
}

int _go_git_index_update_all(git_index *index, const git_strarray *pathspec, void *callback)
{
	git_index_matched_path_cb cb = callback ? (git_index_matched_path_cb)&indexMatchedPathCallback : NULL;
	return git_index_update_all(index, pathspec, cb, callback);
}

int _go_git_index_remove_all(git_index *index, const git_strarray *pathspec, void *callback)
{
	git_index_matched_path_cb cb = callback ? (git_index_matched_path_cb)&indexMatchedPathCallback : NULL;
	return git_index_remove_all(index, pathspec, cb, callback);
}

int _go_git_tag_foreach(git_repository *repo, void *payload)
{
	return git_tag_foreach(repo, (git_tag_foreach_cb)&tagForeachCallback, payload);
}

int _go_git_merge_file(
		git_merge_file_result* out,
		char* ancestorContents,
		size_t ancestorLen,
		char* ancestorPath,
		unsigned int ancestorMode,
		char* oursContents,
		size_t oursLen,
		char* oursPath,
		unsigned int oursMode,
		char* theirsContents,
		size_t theirsLen,
		char* theirsPath,
		unsigned int theirsMode,
		git_merge_file_options* copts)
{
	git_merge_file_input ancestor = GIT_MERGE_FILE_INPUT_INIT;
	git_merge_file_input ours = GIT_MERGE_FILE_INPUT_INIT;
	git_merge_file_input theirs = GIT_MERGE_FILE_INPUT_INIT;

	ancestor.ptr = ancestorContents;
	ancestor.size = ancestorLen;
	ancestor.path = ancestorPath;
	ancestor.mode = ancestorMode;

	ours.ptr = oursContents;
	ours.size = oursLen;
	ours.path = oursPath;
	ours.mode = oursMode;

	theirs.ptr = theirsContents;
	theirs.size = theirsLen;
	theirs.path = theirsPath;
	theirs.mode = theirsMode;

	return git_merge_file(out, &ancestor, &ours, &theirs, copts);
}

void _go_git_populate_stash_apply_callbacks(git_stash_apply_options *opts)
{
	opts->progress_cb = (git_stash_apply_progress_cb)&stashApplyProgressCallback;
}

int _go_git_stash_foreach(git_repository *repo, void *payload)
{
	return git_stash_foreach(repo, (git_stash_cb)&stashForeachCallback, payload);
}

int _go_git_writestream_write(git_writestream *stream, const char *buffer, size_t len)
{
	return stream->write(stream, buffer, len);
}

int _go_git_writestream_close(git_writestream *stream)
{
	return stream->close(stream);
}

void _go_git_writestream_free(git_writestream *stream)
{
	stream->free(stream);
}

git_credential_t _go_git_credential_credtype(git_credential *cred)
{
	return cred->credtype;
}

static int credential_ssh_sign_callback(
		LIBSSH2_SESSION *session,
		unsigned char **sig, size_t *sig_len,
		const unsigned char *data, size_t data_len,
		void **abstract)
{
	char *error_message = NULL;
	const int ret = credentialSSHSignCallback(
			&error_message,
			sig,
			sig_len,
			(unsigned char *)data,
			data_len,
			(void *)*(uintptr_t *)abstract);
	return set_callback_error(error_message, ret);
}

void _go_git_populate_credential_ssh_custom(git_credential_ssh_custom *cred)
{
	cred->parent.free = (void (*)(git_credential *))credentialSSHCustomFree;
	cred->sign_callback = credential_ssh_sign_callback;
}

int _go_git_odb_write_pack(git_odb_writepack **out, git_odb *db, void *progress_payload)
{
	return git_odb_write_pack(out, db, transfer_progress_callback, progress_payload);
}

int _go_git_odb_writepack_append(
		git_odb_writepack *writepack,
		const void *data,
		size_t size,
		git_transfer_progress *stats)
{
	return writepack->append(writepack, data, size, stats);
}

int _go_git_odb_writepack_commit(git_odb_writepack *writepack, git_transfer_progress *stats)
{
	return writepack->commit(writepack, stats);
}

void _go_git_odb_writepack_free(git_odb_writepack *writepack)
{
	writepack->free(writepack);
}

int _go_git_indexer_new(
		git_indexer **out,
		const char *path,
		unsigned int mode,
		git_odb *odb,
		void *progress_cb_payload)
{
	git_indexer_options indexer_options = GIT_INDEXER_OPTIONS_INIT;
	indexer_options.progress_cb = transfer_progress_callback;
	indexer_options.progress_cb_payload = progress_cb_payload;
	return git_indexer_new(out, path, mode, odb, &indexer_options);
}

static int smart_transport_callback(
		git_transport **out,
		git_remote *owner,
		void *param)
{
	char *error_message = NULL;
	const int ret = smartTransportCallback(
			&error_message,
			out,
			owner,
			param);
	return set_callback_error(error_message, ret);
}

int _go_git_transport_register(const char *prefix, void *param)
{
	return git_transport_register(prefix, smart_transport_callback, param);
}

static int smart_subtransport_action_callback(
		git_smart_subtransport_stream **out,
		git_smart_subtransport *transport,
		const char *url,
		git_smart_service_t action)
{
	char *error_message = NULL;
	const int ret = smartSubtransportActionCallback(
			&error_message,
			out,
			transport,
			(char *)url,
			action);
	return set_callback_error(error_message, ret);
}

static int smart_subtransport_close_callback(git_smart_subtransport *transport)
{
	char *error_message = NULL;
	const int ret = smartSubtransportCloseCallback(
			&error_message,
			transport);
	return set_callback_error(error_message, ret);
}

static int smart_subtransport_callback(
		git_smart_subtransport **out,
		git_transport *owner,
		void *param)
{
	_go_managed_smart_subtransport *subtransport = (_go_managed_smart_subtransport *)param;
	subtransport->parent.action = smart_subtransport_action_callback;
	subtransport->parent.close = smart_subtransport_close_callback;
	subtransport->parent.free = smartSubtransportFreeCallback;

	*out = &subtransport->parent;
	char *error_message = NULL;
	const int ret = smartTransportSubtransportCallback(&error_message, subtransport, owner);
	return set_callback_error(error_message, ret);
}

int _go_git_transport_smart(
		git_transport **out,
		git_remote *owner,
		int stateless,
		_go_managed_smart_subtransport *subtransport_payload)
{
	git_smart_subtransport_definition definition = {
		smart_subtransport_callback,
		stateless,
		subtransport_payload,
	};

	return git_transport_smart(out, owner, &definition);
}

static int smart_subtransport_stream_read_callback(
		git_smart_subtransport_stream *stream,
		char *buffer,
		size_t buf_size,
		size_t *bytes_read)
{
	char *error_message = NULL;
	const int ret = smartSubtransportStreamReadCallback(
			&error_message,
			stream,
			buffer,
			buf_size,
			bytes_read);
	return set_callback_error(error_message, ret);
}

static int smart_subtransport_stream_write_callback(
		git_smart_subtransport_stream *stream,
		const char *buffer,
		size_t len)
{
	char *error_message = NULL;
	const int ret = smartSubtransportStreamWriteCallback(
			&error_message,
			stream,
			(char *)buffer,
			len);
	return set_callback_error(error_message, ret);
}

void _go_git_setup_smart_subtransport_stream(_go_managed_smart_subtransport_stream *stream)
{
	_go_managed_smart_subtransport_stream *managed_stream = (_go_managed_smart_subtransport_stream *)stream;
	managed_stream->parent.read = smart_subtransport_stream_read_callback;
	managed_stream->parent.write = smart_subtransport_stream_write_callback;
	managed_stream->parent.free = smartSubtransportStreamFreeCallback;
}