package git

/*
#include <git2.h>

extern void _go_git_populate_apply_callbacks(git_apply_options *options);
extern int _go_git_diff_foreach(git_diff *diff, int eachFile, int eachHunk, int eachLine, void *payload);
extern void _go_git_setup_diff_notify_callbacks(git_diff_options* opts);
extern 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);
*/
import "C"
import (
	"errors"
	"runtime"
	"unsafe"
)

type DiffFlag uint32

const (
	DiffFlagBinary    DiffFlag = C.GIT_DIFF_FLAG_BINARY
	DiffFlagNotBinary DiffFlag = C.GIT_DIFF_FLAG_NOT_BINARY
	DiffFlagValidOid  DiffFlag = C.GIT_DIFF_FLAG_VALID_ID
	DiffFlagExists    DiffFlag = C.GIT_DIFF_FLAG_EXISTS
)

type Delta int

const (
	DeltaUnmodified Delta = C.GIT_DELTA_UNMODIFIED
	DeltaAdded      Delta = C.GIT_DELTA_ADDED
	DeltaDeleted    Delta = C.GIT_DELTA_DELETED
	DeltaModified   Delta = C.GIT_DELTA_MODIFIED
	DeltaRenamed    Delta = C.GIT_DELTA_RENAMED
	DeltaCopied     Delta = C.GIT_DELTA_COPIED
	DeltaIgnored    Delta = C.GIT_DELTA_IGNORED
	DeltaUntracked  Delta = C.GIT_DELTA_UNTRACKED
	DeltaTypeChange Delta = C.GIT_DELTA_TYPECHANGE
	DeltaUnreadable Delta = C.GIT_DELTA_UNREADABLE
	DeltaConflicted Delta = C.GIT_DELTA_CONFLICTED
)

//go:generate stringer -type Delta -trimprefix Delta -tags static

type DiffLineType int

const (
	DiffLineContext      DiffLineType = C.GIT_DIFF_LINE_CONTEXT
	DiffLineAddition     DiffLineType = C.GIT_DIFF_LINE_ADDITION
	DiffLineDeletion     DiffLineType = C.GIT_DIFF_LINE_DELETION
	DiffLineContextEOFNL DiffLineType = C.GIT_DIFF_LINE_CONTEXT_EOFNL
	DiffLineAddEOFNL     DiffLineType = C.GIT_DIFF_LINE_ADD_EOFNL
	DiffLineDelEOFNL     DiffLineType = C.GIT_DIFF_LINE_DEL_EOFNL

	DiffLineFileHdr DiffLineType = C.GIT_DIFF_LINE_FILE_HDR
	DiffLineHunkHdr DiffLineType = C.GIT_DIFF_LINE_HUNK_HDR
	DiffLineBinary  DiffLineType = C.GIT_DIFF_LINE_BINARY
)

//go:generate stringer -type DiffLineType -trimprefix DiffLine -tags static

type DiffFile struct {
	Path  string
	Oid   *Oid
	Size  int
	Flags DiffFlag
	Mode  uint16
}

func diffFileFromC(file *C.git_diff_file) DiffFile {
	return DiffFile{
		Path:  C.GoString(file.path),
		Oid:   newOidFromC(&file.id),
		Size:  int(file.size),
		Flags: DiffFlag(file.flags),
		Mode:  uint16(file.mode),
	}
}

type DiffDelta struct {
	Status     Delta
	Flags      DiffFlag
	Similarity uint16
	OldFile    DiffFile
	NewFile    DiffFile
}

func diffDeltaFromC(delta *C.git_diff_delta) DiffDelta {
	return DiffDelta{
		Status:     Delta(delta.status),
		Flags:      DiffFlag(delta.flags),
		Similarity: uint16(delta.similarity),
		OldFile:    diffFileFromC(&delta.old_file),
		NewFile:    diffFileFromC(&delta.new_file),
	}
}

type DiffHunk struct {
	OldStart int
	OldLines int
	NewStart int
	NewLines int
	Header   string
}

func diffHunkFromC(hunk *C.git_diff_hunk) DiffHunk {
	return DiffHunk{
		OldStart: int(hunk.old_start),
		OldLines: int(hunk.old_lines),
		NewStart: int(hunk.new_start),
		NewLines: int(hunk.new_lines),
		Header:   C.GoStringN(&hunk.header[0], C.int(hunk.header_len)),
	}
}

type DiffLine struct {
	Origin    DiffLineType
	OldLineno int
	NewLineno int
	NumLines  int
	Content   string
}

func diffLineFromC(line *C.git_diff_line) DiffLine {
	return DiffLine{
		Origin:    DiffLineType(line.origin),
		OldLineno: int(line.old_lineno),
		NewLineno: int(line.new_lineno),
		NumLines:  int(line.num_lines),
		Content:   C.GoStringN(line.content, C.int(line.content_len)),
	}
}

type Diff struct {
	doNotCompare
	ptr          *C.git_diff
	repo         *Repository
	runFinalizer bool
}

func (diff *Diff) NumDeltas() (int, error) {
	if diff.ptr == nil {
		return -1, ErrInvalid
	}
	ret := int(C.git_diff_num_deltas(diff.ptr))
	runtime.KeepAlive(diff)
	return ret, nil
}

func (diff *Diff) Delta(index int) (DiffDelta, error) {
	if diff.ptr == nil {
		return DiffDelta{}, ErrInvalid
	}
	ptr := C.git_diff_get_delta(diff.ptr, C.size_t(index))
	ret := diffDeltaFromC(ptr)
	runtime.KeepAlive(diff)
	return ret, nil
}

// deprecated: You should use `Diff.Delta()` instead.
func (diff *Diff) GetDelta(index int) (DiffDelta, error) {
	return diff.Delta(index)
}

func newDiffFromC(ptr *C.git_diff, repo *Repository) *Diff {
	if ptr == nil {
		return nil
	}

	diff := &Diff{
		ptr:          ptr,
		repo:         repo,
		runFinalizer: true,
	}

	runtime.SetFinalizer(diff, (*Diff).Free)
	return diff
}

func (diff *Diff) Free() error {
	if diff.ptr == nil {
		return ErrInvalid
	}
	if !diff.runFinalizer {
		// This is the case with the Diff objects that are involved in the DiffNotifyCallback.
		diff.ptr = nil
		return nil
	}
	runtime.SetFinalizer(diff, nil)
	C.git_diff_free(diff.ptr)
	diff.ptr = nil
	return nil
}

func (diff *Diff) FindSimilar(opts *DiffFindOptions) error {

	var copts *C.git_diff_find_options
	if opts != nil {
		copts = &C.git_diff_find_options{
			version:                       C.GIT_DIFF_FIND_OPTIONS_VERSION,
			flags:                         C.uint32_t(opts.Flags),
			rename_threshold:              C.uint16_t(opts.RenameThreshold),
			copy_threshold:                C.uint16_t(opts.CopyThreshold),
			rename_from_rewrite_threshold: C.uint16_t(opts.RenameFromRewriteThreshold),
			break_rewrite_threshold:       C.uint16_t(opts.BreakRewriteThreshold),
			rename_limit:                  C.size_t(opts.RenameLimit),
		}
	}

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ecode := C.git_diff_find_similar(diff.ptr, copts)
	runtime.KeepAlive(diff)
	if ecode < 0 {
		return MakeGitError(ecode)
	}

	return nil
}

type DiffStats struct {
	doNotCompare
	ptr *C.git_diff_stats
}

func (stats *DiffStats) Free() error {
	if stats.ptr == nil {
		return ErrInvalid
	}
	runtime.SetFinalizer(stats, nil)
	C.git_diff_stats_free(stats.ptr)
	stats.ptr = nil
	return nil
}

func (stats *DiffStats) Insertions() int {
	ret := int(C.git_diff_stats_insertions(stats.ptr))
	runtime.KeepAlive(stats)
	return ret
}

func (stats *DiffStats) Deletions() int {
	ret := int(C.git_diff_stats_deletions(stats.ptr))
	runtime.KeepAlive(stats)
	return ret
}

func (stats *DiffStats) FilesChanged() int {
	ret := int(C.git_diff_stats_files_changed(stats.ptr))
	runtime.KeepAlive(stats)
	return ret
}

type DiffStatsFormat int

const (
	DiffStatsNone           DiffStatsFormat = C.GIT_DIFF_STATS_NONE
	DiffStatsFull           DiffStatsFormat = C.GIT_DIFF_STATS_FULL
	DiffStatsShort          DiffStatsFormat = C.GIT_DIFF_STATS_SHORT
	DiffStatsNumber         DiffStatsFormat = C.GIT_DIFF_STATS_NUMBER
	DiffStatsIncludeSummary DiffStatsFormat = C.GIT_DIFF_STATS_INCLUDE_SUMMARY
)

func (stats *DiffStats) String(format DiffStatsFormat,
	width uint) (string, error) {
	buf := C.git_buf{}
	defer C.git_buf_dispose(&buf)

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ret := C.git_diff_stats_to_buf(&buf,
		stats.ptr, C.git_diff_stats_format_t(format), C.size_t(width))
	runtime.KeepAlive(stats)
	if ret < 0 {
		return "", MakeGitError(ret)
	}

	return C.GoString(buf.ptr), nil
}

func (diff *Diff) Stats() (*DiffStats, error) {
	stats := new(DiffStats)

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ecode := C.git_diff_get_stats(&stats.ptr, diff.ptr)
	runtime.KeepAlive(diff)
	if ecode < 0 {
		return nil, MakeGitError(ecode)
	}
	runtime.SetFinalizer(stats, (*DiffStats).Free)

	return stats, nil
}

type diffForEachCallbackData struct {
	fileCallback DiffForEachFileCallback
	hunkCallback DiffForEachHunkCallback
	lineCallback DiffForEachLineCallback
	errorTarget  *error
}

type DiffForEachFileCallback func(delta DiffDelta, progress float64) (DiffForEachHunkCallback, error)

type DiffDetail int

const (
	DiffDetailFiles DiffDetail = iota
	DiffDetailHunks
	DiffDetailLines
)

func (diff *Diff) ForEach(cbFile DiffForEachFileCallback, detail DiffDetail) error {
	if diff.ptr == nil {
		return ErrInvalid
	}

	intHunks := C.int(0)
	if detail >= DiffDetailHunks {
		intHunks = C.int(1)
	}

	intLines := C.int(0)
	if detail >= DiffDetailLines {
		intLines = C.int(1)
	}

	var err error
	data := &diffForEachCallbackData{
		fileCallback: cbFile,
		errorTarget:  &err,
	}

	handle := pointerHandles.Track(data)
	defer pointerHandles.Untrack(handle)

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ret := C._go_git_diff_foreach(diff.ptr, 1, intHunks, intLines, handle)
	runtime.KeepAlive(diff)
	if ret == C.int(ErrorCodeUser) && err != nil {
		return err
	}
	if ret < 0 {
		return MakeGitError(ret)
	}

	return nil
}

//export diffForEachFileCallback
func diffForEachFileCallback(delta *C.git_diff_delta, progress C.float, handle unsafe.Pointer) C.int {
	payload := pointerHandles.Get(handle)
	data, ok := payload.(*diffForEachCallbackData)
	if !ok {
		panic("could not retrieve data for handle")
	}

	data.hunkCallback = nil
	if data.fileCallback != nil {
		cb, err := data.fileCallback(diffDeltaFromC(delta), float64(progress))
		if err != nil {
			*data.errorTarget = err
			return C.int(ErrorCodeUser)
		}
		data.hunkCallback = cb
	}

	return C.int(ErrorCodeOK)
}

type DiffForEachHunkCallback func(DiffHunk) (DiffForEachLineCallback, error)

//export diffForEachHunkCallback
func diffForEachHunkCallback(delta *C.git_diff_delta, hunk *C.git_diff_hunk, handle unsafe.Pointer) C.int {
	payload := pointerHandles.Get(handle)
	data, ok := payload.(*diffForEachCallbackData)
	if !ok {
		panic("could not retrieve data for handle")
	}

	data.lineCallback = nil
	if data.hunkCallback != nil {
		cb, err := data.hunkCallback(diffHunkFromC(hunk))
		if err != nil {
			*data.errorTarget = err
			return C.int(ErrorCodeUser)
		}
		data.lineCallback = cb
	}

	return C.int(ErrorCodeOK)
}

type DiffForEachLineCallback func(DiffLine) error

//export diffForEachLineCallback
func diffForEachLineCallback(delta *C.git_diff_delta, hunk *C.git_diff_hunk, line *C.git_diff_line, handle unsafe.Pointer) C.int {
	payload := pointerHandles.Get(handle)
	data, ok := payload.(*diffForEachCallbackData)
	if !ok {
		panic("could not retrieve data for handle")
	}

	err := data.lineCallback(diffLineFromC(line))
	if err != nil {
		*data.errorTarget = err
		return C.int(ErrorCodeUser)
	}

	return C.int(ErrorCodeOK)
}

func (diff *Diff) Patch(deltaIndex int) (*Patch, error) {
	if diff.ptr == nil {
		return nil, ErrInvalid
	}
	var patchPtr *C.git_patch

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ecode := C.git_patch_from_diff(&patchPtr, diff.ptr, C.size_t(deltaIndex))
	runtime.KeepAlive(diff)
	if ecode < 0 {
		return nil, MakeGitError(ecode)
	}

	return newPatchFromC(patchPtr), nil
}

type DiffFormat int

const (
	DiffFormatPatch       DiffFormat = C.GIT_DIFF_FORMAT_PATCH
	DiffFormatPatchHeader DiffFormat = C.GIT_DIFF_FORMAT_PATCH_HEADER
	DiffFormatRaw         DiffFormat = C.GIT_DIFF_FORMAT_RAW
	DiffFormatNameOnly    DiffFormat = C.GIT_DIFF_FORMAT_NAME_ONLY
	DiffFormatNameStatus  DiffFormat = C.GIT_DIFF_FORMAT_NAME_STATUS
)

func (diff *Diff) ToBuf(format DiffFormat) ([]byte, error) {
	if diff.ptr == nil {
		return nil, ErrInvalid
	}

	diffBuf := C.git_buf{}

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ecode := C.git_diff_to_buf(&diffBuf, diff.ptr, C.git_diff_format_t(format))
	runtime.KeepAlive(diff)
	if ecode < 0 {
		return nil, MakeGitError(ecode)
	}
	defer C.git_buf_dispose(&diffBuf)

	return C.GoBytes(unsafe.Pointer(diffBuf.ptr), C.int(diffBuf.size)), nil
}

type DiffOptionsFlag int

const (
	DiffNormal                 DiffOptionsFlag = C.GIT_DIFF_NORMAL
	DiffReverse                DiffOptionsFlag = C.GIT_DIFF_REVERSE
	DiffIncludeIgnored         DiffOptionsFlag = C.GIT_DIFF_INCLUDE_IGNORED
	DiffRecurseIgnoredDirs     DiffOptionsFlag = C.GIT_DIFF_RECURSE_IGNORED_DIRS
	DiffIncludeUntracked       DiffOptionsFlag = C.GIT_DIFF_INCLUDE_UNTRACKED
	DiffRecurseUntracked       DiffOptionsFlag = C.GIT_DIFF_RECURSE_UNTRACKED_DIRS
	DiffIncludeUnmodified      DiffOptionsFlag = C.GIT_DIFF_INCLUDE_UNMODIFIED
	DiffIncludeTypeChange      DiffOptionsFlag = C.GIT_DIFF_INCLUDE_TYPECHANGE
	DiffIncludeTypeChangeTrees DiffOptionsFlag = C.GIT_DIFF_INCLUDE_TYPECHANGE_TREES
	DiffIgnoreFilemode         DiffOptionsFlag = C.GIT_DIFF_IGNORE_FILEMODE
	DiffIgnoreSubmodules       DiffOptionsFlag = C.GIT_DIFF_IGNORE_SUBMODULES
	DiffIgnoreCase             DiffOptionsFlag = C.GIT_DIFF_IGNORE_CASE
	DiffIncludeCaseChange      DiffOptionsFlag = C.GIT_DIFF_INCLUDE_CASECHANGE

	DiffDisablePathspecMatch    DiffOptionsFlag = C.GIT_DIFF_DISABLE_PATHSPEC_MATCH
	DiffSkipBinaryCheck         DiffOptionsFlag = C.GIT_DIFF_SKIP_BINARY_CHECK
	DiffEnableFastUntrackedDirs DiffOptionsFlag = C.GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS

	DiffForceText   DiffOptionsFlag = C.GIT_DIFF_FORCE_TEXT
	DiffForceBinary DiffOptionsFlag = C.GIT_DIFF_FORCE_BINARY

	DiffIgnoreWhitespace       DiffOptionsFlag = C.GIT_DIFF_IGNORE_WHITESPACE
	DiffIgnoreWhitespaceChange DiffOptionsFlag = C.GIT_DIFF_IGNORE_WHITESPACE_CHANGE
	DiffIgnoreWhitespaceEOL    DiffOptionsFlag = C.GIT_DIFF_IGNORE_WHITESPACE_EOL

	DiffShowUntrackedContent DiffOptionsFlag = C.GIT_DIFF_SHOW_UNTRACKED_CONTENT
	DiffShowUnmodified       DiffOptionsFlag = C.GIT_DIFF_SHOW_UNMODIFIED
	DiffPatience             DiffOptionsFlag = C.GIT_DIFF_PATIENCE
	DiffMinimal              DiffOptionsFlag = C.GIT_DIFF_MINIMAL
	DiffShowBinary           DiffOptionsFlag = C.GIT_DIFF_SHOW_BINARY
	DiffIndentHeuristic      DiffOptionsFlag = C.GIT_DIFF_INDENT_HEURISTIC
)

type DiffNotifyCallback func(diffSoFar *Diff, deltaToAdd DiffDelta, matchedPathspec string) error

type DiffOptions struct {
	Flags            DiffOptionsFlag
	IgnoreSubmodules SubmoduleIgnore
	Pathspec         []string
	NotifyCallback   DiffNotifyCallback

	ContextLines   uint32
	InterhunkLines uint32
	IdAbbrev       uint16

	MaxSize int

	OldPrefix string
	NewPrefix string
}

func DefaultDiffOptions() (DiffOptions, error) {
	opts := C.git_diff_options{}

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ecode := C.git_diff_options_init(&opts, C.GIT_DIFF_OPTIONS_VERSION)
	if ecode < 0 {
		return DiffOptions{}, MakeGitError(ecode)
	}

	return DiffOptions{
		Flags:            DiffOptionsFlag(opts.flags),
		IgnoreSubmodules: SubmoduleIgnore(opts.ignore_submodules),
		Pathspec:         makeStringsFromCStrings(opts.pathspec.strings, int(opts.pathspec.count)),
		ContextLines:     uint32(opts.context_lines),
		InterhunkLines:   uint32(opts.interhunk_lines),
		IdAbbrev:         uint16(opts.id_abbrev),
		MaxSize:          int(opts.max_size),
		OldPrefix:        "a",
		NewPrefix:        "b",
	}, nil
}

type DiffFindOptionsFlag int

const (
	DiffFindByConfig                    DiffFindOptionsFlag = C.GIT_DIFF_FIND_BY_CONFIG
	DiffFindRenames                     DiffFindOptionsFlag = C.GIT_DIFF_FIND_RENAMES
	DiffFindRenamesFromRewrites         DiffFindOptionsFlag = C.GIT_DIFF_FIND_RENAMES_FROM_REWRITES
	DiffFindCopies                      DiffFindOptionsFlag = C.GIT_DIFF_FIND_COPIES
	DiffFindCopiesFromUnmodified        DiffFindOptionsFlag = C.GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED
	DiffFindRewrites                    DiffFindOptionsFlag = C.GIT_DIFF_FIND_REWRITES
	DiffFindBreakRewrites               DiffFindOptionsFlag = C.GIT_DIFF_BREAK_REWRITES
	DiffFindAndBreakRewrites            DiffFindOptionsFlag = C.GIT_DIFF_FIND_AND_BREAK_REWRITES
	DiffFindForUntracked                DiffFindOptionsFlag = C.GIT_DIFF_FIND_FOR_UNTRACKED
	DiffFindAll                         DiffFindOptionsFlag = C.GIT_DIFF_FIND_ALL
	DiffFindIgnoreLeadingWhitespace     DiffFindOptionsFlag = C.GIT_DIFF_FIND_IGNORE_LEADING_WHITESPACE
	DiffFindIgnoreWhitespace            DiffFindOptionsFlag = C.GIT_DIFF_FIND_IGNORE_WHITESPACE
	DiffFindDontIgnoreWhitespace        DiffFindOptionsFlag = C.GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE
	DiffFindExactMatchOnly              DiffFindOptionsFlag = C.GIT_DIFF_FIND_EXACT_MATCH_ONLY
	DiffFindBreakRewritesForRenamesOnly DiffFindOptionsFlag = C.GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY
	DiffFindRemoveUnmodified            DiffFindOptionsFlag = C.GIT_DIFF_FIND_REMOVE_UNMODIFIED
)

// TODO implement git_diff_similarity_metric
type DiffFindOptions struct {
	Flags                      DiffFindOptionsFlag
	RenameThreshold            uint16
	CopyThreshold              uint16
	RenameFromRewriteThreshold uint16
	BreakRewriteThreshold      uint16
	RenameLimit                uint
}

func DefaultDiffFindOptions() (DiffFindOptions, error) {
	opts := C.git_diff_find_options{}

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ecode := C.git_diff_find_options_init(&opts, C.GIT_DIFF_FIND_OPTIONS_VERSION)
	if ecode < 0 {
		return DiffFindOptions{}, MakeGitError(ecode)
	}

	return DiffFindOptions{
		Flags:                      DiffFindOptionsFlag(opts.flags),
		RenameThreshold:            uint16(opts.rename_threshold),
		CopyThreshold:              uint16(opts.copy_threshold),
		RenameFromRewriteThreshold: uint16(opts.rename_from_rewrite_threshold),
		BreakRewriteThreshold:      uint16(opts.break_rewrite_threshold),
		RenameLimit:                uint(opts.rename_limit),
	}, nil
}

var (
	ErrDeltaSkip = errors.New("Skip delta")
)

type diffNotifyCallbackData struct {
	callback    DiffNotifyCallback
	repository  *Repository
	errorTarget *error
}

//export diffNotifyCallback
func diffNotifyCallback(_diff_so_far unsafe.Pointer, delta_to_add *C.git_diff_delta, matched_pathspec *C.char, handle unsafe.Pointer) C.int {
	diff_so_far := (*C.git_diff)(_diff_so_far)

	payload := pointerHandles.Get(handle)
	data, ok := payload.(*diffNotifyCallbackData)
	if !ok {
		panic("could not retrieve data for handle")
	}

	if data == nil {
		return C.int(ErrorCodeOK)
	}

	// We are not taking ownership of this diff pointer, so no finalizer is set.
	diff := &Diff{
		ptr:          diff_so_far,
		repo:         data.repository,
		runFinalizer: false,
	}

	err := data.callback(diff, diffDeltaFromC(delta_to_add), C.GoString(matched_pathspec))

	// Since the callback could theoretically keep a reference to the diff
	// (which could be freed by libgit2 if an error occurs later during the
	// diffing process), this converts a use-after-free (terrible!) into a nil
	// dereference ("just" pretty bad).
	diff.ptr = nil

	if err == ErrDeltaSkip {
		return 1
	}
	if err != nil {
		*data.errorTarget = err
		return C.int(ErrorCodeUser)
	}

	return C.int(ErrorCodeOK)
}

func populateDiffOptions(copts *C.git_diff_options, opts *DiffOptions, repo *Repository, errorTarget *error) *C.git_diff_options {
	C.git_diff_options_init(copts, C.GIT_DIFF_OPTIONS_VERSION)
	if opts == nil {
		return nil
	}

	copts.flags = C.uint32_t(opts.Flags)
	copts.ignore_submodules = C.git_submodule_ignore_t(opts.IgnoreSubmodules)
	if len(opts.Pathspec) > 0 {
		copts.pathspec.count = C.size_t(len(opts.Pathspec))
		copts.pathspec.strings = makeCStringsFromStrings(opts.Pathspec)
	}
	copts.context_lines = C.uint32_t(opts.ContextLines)
	copts.interhunk_lines = C.uint32_t(opts.InterhunkLines)
	copts.id_abbrev = C.uint16_t(opts.IdAbbrev)
	copts.max_size = C.git_off_t(opts.MaxSize)
	copts.old_prefix = C.CString(opts.OldPrefix)
	copts.new_prefix = C.CString(opts.NewPrefix)

	if opts.NotifyCallback != nil {
		notifyData := &diffNotifyCallbackData{
			callback:    opts.NotifyCallback,
			repository:  repo,
			errorTarget: errorTarget,
		}
		C._go_git_setup_diff_notify_callbacks(copts)
		copts.payload = pointerHandles.Track(notifyData)
	}
	return copts
}

func freeDiffOptions(copts *C.git_diff_options) {
	if copts == nil {
		return
	}
	freeStrarray(&copts.pathspec)
	C.free(unsafe.Pointer(copts.old_prefix))
	C.free(unsafe.Pointer(copts.new_prefix))
	if copts.payload != nil {
		pointerHandles.Untrack(copts.payload)
	}
}

func (v *Repository) DiffTreeToTree(oldTree, newTree *Tree, opts *DiffOptions) (*Diff, error) {
	var diffPtr *C.git_diff
	var oldPtr, newPtr *C.git_tree

	if oldTree != nil {
		oldPtr = oldTree.cast_ptr
	}

	if newTree != nil {
		newPtr = newTree.cast_ptr
	}

	var err error
	copts := populateDiffOptions(&C.git_diff_options{}, opts, v, &err)
	defer freeDiffOptions(copts)

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ret := C.git_diff_tree_to_tree(&diffPtr, v.ptr, oldPtr, newPtr, copts)
	runtime.KeepAlive(oldTree)
	runtime.KeepAlive(newTree)
	if ret == C.int(ErrorCodeUser) && err != nil {
		return nil, err
	}
	if ret < 0 {
		return nil, MakeGitError(ret)
	}
	return newDiffFromC(diffPtr, v), nil
}

func (v *Repository) DiffTreeToWorkdir(oldTree *Tree, opts *DiffOptions) (*Diff, error) {
	var diffPtr *C.git_diff
	var oldPtr *C.git_tree

	if oldTree != nil {
		oldPtr = oldTree.cast_ptr
	}

	var err error
	copts := populateDiffOptions(&C.git_diff_options{}, opts, v, &err)
	defer freeDiffOptions(copts)

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ret := C.git_diff_tree_to_workdir(&diffPtr, v.ptr, oldPtr, copts)
	runtime.KeepAlive(oldTree)
	if ret == C.int(ErrorCodeUser) && err != nil {
		return nil, err
	}
	if ret < 0 {
		return nil, MakeGitError(ret)
	}

	return newDiffFromC(diffPtr, v), nil
}

func (v *Repository) DiffTreeToIndex(oldTree *Tree, index *Index, opts *DiffOptions) (*Diff, error) {
	var diffPtr *C.git_diff
	var oldPtr *C.git_tree
	var indexPtr *C.git_index

	if oldTree != nil {
		oldPtr = oldTree.cast_ptr
	}

	if index != nil {
		indexPtr = index.ptr
	}

	var err error
	copts := populateDiffOptions(&C.git_diff_options{}, opts, v, &err)
	defer freeDiffOptions(copts)

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ret := C.git_diff_tree_to_index(&diffPtr, v.ptr, oldPtr, indexPtr, copts)
	runtime.KeepAlive(oldTree)
	runtime.KeepAlive(index)
	if ret == C.int(ErrorCodeUser) && err != nil {
		return nil, err
	}
	if ret < 0 {
		return nil, MakeGitError(ret)
	}

	return newDiffFromC(diffPtr, v), nil
}

func (v *Repository) DiffTreeToWorkdirWithIndex(oldTree *Tree, opts *DiffOptions) (*Diff, error) {
	var diffPtr *C.git_diff
	var oldPtr *C.git_tree

	if oldTree != nil {
		oldPtr = oldTree.cast_ptr
	}

	var err error
	copts := populateDiffOptions(&C.git_diff_options{}, opts, v, &err)
	defer freeDiffOptions(copts)

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ret := C.git_diff_tree_to_workdir_with_index(&diffPtr, v.ptr, oldPtr, copts)
	runtime.KeepAlive(oldTree)
	if ret == C.int(ErrorCodeUser) && err != nil {
		return nil, err
	}
	if ret < 0 {
		return nil, MakeGitError(ret)
	}

	return newDiffFromC(diffPtr, v), nil
}

func (v *Repository) DiffIndexToWorkdir(index *Index, opts *DiffOptions) (*Diff, error) {
	var diffPtr *C.git_diff
	var indexPtr *C.git_index

	if index != nil {
		indexPtr = index.ptr
	}

	var err error
	copts := populateDiffOptions(&C.git_diff_options{}, opts, v, &err)
	defer freeDiffOptions(copts)

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ret := C.git_diff_index_to_workdir(&diffPtr, v.ptr, indexPtr, copts)
	runtime.KeepAlive(index)
	if ret == C.int(ErrorCodeUser) && err != nil {
		return nil, err
	}
	if ret < 0 {
		return nil, MakeGitError(ret)
	}

	return newDiffFromC(diffPtr, v), nil
}

// DiffBlobs performs a diff between two arbitrary blobs. You can pass
// whatever file names you'd like for them to appear as in the diff.
func DiffBlobs(oldBlob *Blob, oldAsPath string, newBlob *Blob, newAsPath string, opts *DiffOptions, fileCallback DiffForEachFileCallback, detail DiffDetail) error {
	var err error
	data := &diffForEachCallbackData{
		fileCallback: fileCallback,
		errorTarget:  &err,
	}

	intHunks := C.int(0)
	if detail >= DiffDetailHunks {
		intHunks = C.int(1)
	}

	intLines := C.int(0)
	if detail >= DiffDetailLines {
		intLines = C.int(1)
	}

	handle := pointerHandles.Track(data)
	defer pointerHandles.Untrack(handle)

	var repo *Repository
	var oldBlobPtr, newBlobPtr *C.git_blob
	if oldBlob != nil {
		oldBlobPtr = oldBlob.cast_ptr
		repo = oldBlob.repo
	}
	if newBlob != nil {
		newBlobPtr = newBlob.cast_ptr
		repo = newBlob.repo
	}

	oldBlobPath := C.CString(oldAsPath)
	defer C.free(unsafe.Pointer(oldBlobPath))
	newBlobPath := C.CString(newAsPath)
	defer C.free(unsafe.Pointer(newBlobPath))

	copts := populateDiffOptions(&C.git_diff_options{}, opts, repo, &err)
	defer freeDiffOptions(copts)

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ret := C._go_git_diff_blobs(oldBlobPtr, oldBlobPath, newBlobPtr, newBlobPath, copts, 1, intHunks, intLines, handle)
	runtime.KeepAlive(oldBlob)
	runtime.KeepAlive(newBlob)
	if ret == C.int(ErrorCodeUser) && err != nil {
		return err
	}
	if ret < 0 {
		return MakeGitError(ret)
	}

	return nil
}

// ApplyHunkCallback is a callback that will be made per delta (file) when applying a patch.
type ApplyHunkCallback func(*DiffHunk) (apply bool, err error)

// ApplyDeltaCallback is a callback that will be made per hunk when applying a patch.
type ApplyDeltaCallback func(*DiffDelta) (apply bool, err error)

// ApplyOptions has 2 callbacks that are called for hunks or deltas
// If these functions return an error, abort the apply process immediately.
// If the first return value is true, the delta/hunk will be applied. If it is false, the  delta/hunk will not be applied. In either case, the rest of the apply process will continue.
type ApplyOptions struct {
	ApplyHunkCallback  ApplyHunkCallback
	ApplyDeltaCallback ApplyDeltaCallback
	Flags              uint
}

type applyCallbackData struct {
	options     *ApplyOptions
	errorTarget *error
}

//export hunkApplyCallback
func hunkApplyCallback(_hunk *C.git_diff_hunk, _payload unsafe.Pointer) C.int {
	data, ok := pointerHandles.Get(_payload).(*applyCallbackData)
	if !ok {
		panic("invalid apply options payload")
	}

	if data.options.ApplyHunkCallback == nil {
		return C.int(ErrorCodeOK)
	}

	hunk := diffHunkFromC(_hunk)

	apply, err := data.options.ApplyHunkCallback(&hunk)
	if err != nil {
		*data.errorTarget = err
		return C.int(ErrorCodeUser)
	}

	if !apply {
		return 1
	}
	return C.int(ErrorCodeOK)
}

//export deltaApplyCallback
func deltaApplyCallback(_delta *C.git_diff_delta, _payload unsafe.Pointer) C.int {
	data, ok := pointerHandles.Get(_payload).(*applyCallbackData)
	if !ok {
		panic("invalid apply options payload")
	}

	if data.options.ApplyDeltaCallback == nil {
		return C.int(ErrorCodeOK)
	}

	delta := diffDeltaFromC(_delta)

	apply, err := data.options.ApplyDeltaCallback(&delta)
	if err != nil {
		*data.errorTarget = err
		return C.int(ErrorCodeUser)
	}

	if !apply {
		return 1
	}
	return C.int(ErrorCodeOK)
}

// DefaultApplyOptions returns default options for applying diffs or patches.
func DefaultApplyOptions() (*ApplyOptions, error) {
	opts := C.git_apply_options{}

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ecode := C.git_apply_options_init(&opts, C.GIT_APPLY_OPTIONS_VERSION)
	if int(ecode) != 0 {
		return nil, MakeGitError(ecode)
	}

	return applyOptionsFromC(&opts), nil
}

func populateApplyOptions(copts *C.git_apply_options, opts *ApplyOptions, errorTarget *error) *C.git_apply_options {
	C.git_apply_options_init(copts, C.GIT_APPLY_OPTIONS_VERSION)
	if opts == nil {
		return nil
	}

	copts.flags = C.uint(opts.Flags)

	if opts.ApplyDeltaCallback != nil || opts.ApplyHunkCallback != nil {
		data := &applyCallbackData{
			options:     opts,
			errorTarget: errorTarget,
		}
		C._go_git_populate_apply_callbacks(copts)
		copts.payload = pointerHandles.Track(data)
	}

	return copts
}

func freeApplyOptions(copts *C.git_apply_options) {
	if copts == nil {
		return
	}
	if copts.payload != nil {
		pointerHandles.Untrack(copts.payload)
	}
}

func applyOptionsFromC(copts *C.git_apply_options) *ApplyOptions {
	return &ApplyOptions{
		Flags: uint(copts.flags),
	}
}

// ApplyLocation represents the possible application locations for applying
// diffs.
type ApplyLocation int

const (
	// ApplyLocationWorkdir applies the patch to the workdir, leaving the
	// index untouched. This is the equivalent of `git apply` with no location
	// argument.
	ApplyLocationWorkdir ApplyLocation = C.GIT_APPLY_LOCATION_WORKDIR
	// ApplyLocationIndex applies the patch to the index, leaving the working
	// directory untouched. This is the equivalent of `git apply --cached`.
	ApplyLocationIndex ApplyLocation = C.GIT_APPLY_LOCATION_INDEX
	// ApplyLocationBoth applies the patch to both the working directory and
	// the index. This is the equivalent of `git apply --index`.
	ApplyLocationBoth ApplyLocation = C.GIT_APPLY_LOCATION_BOTH
)

// ApplyDiff appllies a Diff to the given repository, making changes directly
// in the working directory, the index, or both.
func (v *Repository) ApplyDiff(diff *Diff, location ApplyLocation, opts *ApplyOptions) error {
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	var err error
	cOpts := populateApplyOptions(&C.git_apply_options{}, opts, &err)
	defer freeApplyOptions(cOpts)

	ret := C.git_apply(v.ptr, diff.ptr, C.git_apply_location_t(location), cOpts)
	runtime.KeepAlive(v)
	runtime.KeepAlive(diff)
	runtime.KeepAlive(cOpts)
	if ret == C.int(ErrorCodeUser) && err != nil {
		return err
	}
	if ret < 0 {
		return MakeGitError(ret)
	}

	return nil
}

// ApplyToTree applies a Diff to a Tree and returns the resulting image as an Index.
func (v *Repository) ApplyToTree(diff *Diff, tree *Tree, opts *ApplyOptions) (*Index, error) {
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	var err error
	cOpts := populateApplyOptions(&C.git_apply_options{}, opts, &err)
	defer freeApplyOptions(cOpts)

	var indexPtr *C.git_index
	ret := C.git_apply_to_tree(&indexPtr, v.ptr, tree.cast_ptr, diff.ptr, cOpts)
	runtime.KeepAlive(diff)
	runtime.KeepAlive(tree)
	runtime.KeepAlive(cOpts)
	if ret == C.int(ErrorCodeUser) && err != nil {
		return nil, err
	}
	if ret < 0 {
		return nil, MakeGitError(ret)
	}

	return newIndexFromC(indexPtr, v), nil
}

// DiffFromBuffer reads the contents of a git patch file into a Diff object.
//
// The diff object produced is similar to the one that would be produced if you
// actually produced it computationally by comparing two trees, however there
// may be subtle differences. For example, a patch file likely contains
// abbreviated object IDs, so the object IDs in a git_diff_delta produced by
// this function will also be abbreviated.
//
// This function will only read patch files created by a git implementation, it
// will not read unified diffs produced by the diff program, nor any other
// types of patch files.
func DiffFromBuffer(buffer []byte, repo *Repository) (*Diff, error) {
	var diff *C.git_diff

	cBuffer := C.CBytes(buffer)
	defer C.free(unsafe.Pointer(cBuffer))

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ecode := C.git_diff_from_buffer(&diff, (*C.char)(cBuffer), C.size_t(len(buffer)))
	if ecode < 0 {
		return nil, MakeGitError(ecode)
	}
	runtime.KeepAlive(diff)

	return newDiffFromC(diff, repo), nil
}
