package git

/*
#include <git2.h>
*/
import "C"
import (
	"runtime"
	"unsafe"
)

// DescribeOptions represents the describe operation configuration.
//
// You can use DefaultDescribeOptions() to get default options.
type DescribeOptions struct {
	// How many tags as candidates to consider to describe the input commit-ish.
	// Increasing it above 10 will take slightly longer but may produce a more
	// accurate result. 0 will cause only exact matches to be output.
	MaxCandidatesTags uint // default: 10

	// By default describe only shows annotated tags. Change this in order
	// to show all refs from refs/tags or refs/.
	Strategy DescribeOptionsStrategy // default: DescribeDefault

	// Only consider tags matching the given glob(7) pattern, excluding
	// the "refs/tags/" prefix. Can be used to avoid leaking private
	// tags from the repo.
	Pattern string

	// When calculating the distance from the matching tag or
	// reference, only walk down the first-parent ancestry.
	OnlyFollowFirstParent bool

	// If no matching tag or reference is found, the describe
	// operation would normally fail. If this option is set, it
	// will instead fall back to showing the full id of the commit.
	ShowCommitOidAsFallback bool
}

// DefaultDescribeOptions returns default options for the describe operation.
func DefaultDescribeOptions() (DescribeOptions, error) {
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	opts := C.git_describe_options{}
	ecode := C.git_describe_options_init(&opts, C.GIT_DESCRIBE_OPTIONS_VERSION)
	if ecode < 0 {
		return DescribeOptions{}, MakeGitError(ecode)
	}

	return DescribeOptions{
		MaxCandidatesTags: uint(opts.max_candidates_tags),
		Strategy:          DescribeOptionsStrategy(opts.describe_strategy),
	}, nil
}

// DescribeFormatOptions can be used for formatting the describe string.
//
// You can use DefaultDescribeFormatOptions() to get default options.
type DescribeFormatOptions struct {
	// Size of the abbreviated commit id to use. This value is the
	// lower bound for the length of the abbreviated string.
	AbbreviatedSize uint // default: 7

	// Set to use the long format even when a shorter name could be used.
	AlwaysUseLongFormat bool

	// If the workdir is dirty and this is set, this string will be
	// appended to the description string.
	DirtySuffix string
}

// DefaultDescribeFormatOptions returns default options for formatting
// the output.
func DefaultDescribeFormatOptions() (DescribeFormatOptions, error) {
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	opts := C.git_describe_format_options{}
	ecode := C.git_describe_format_options_init(&opts, C.GIT_DESCRIBE_FORMAT_OPTIONS_VERSION)
	if ecode < 0 {
		return DescribeFormatOptions{}, MakeGitError(ecode)
	}

	return DescribeFormatOptions{
		AbbreviatedSize:     uint(opts.abbreviated_size),
		AlwaysUseLongFormat: opts.always_use_long_format == 1,
	}, nil
}

// DescribeOptionsStrategy behaves like the --tags and --all options
// to git-describe, namely they say to look for any reference in
// either refs/tags/ or refs/ respectively.
//
// By default it only shows annotated tags.
type DescribeOptionsStrategy uint

// Describe strategy options.
const (
	DescribeDefault DescribeOptionsStrategy = C.GIT_DESCRIBE_DEFAULT
	DescribeTags    DescribeOptionsStrategy = C.GIT_DESCRIBE_TAGS
	DescribeAll     DescribeOptionsStrategy = C.GIT_DESCRIBE_ALL
)

// Describe performs the describe operation on the commit.
func (c *Commit) Describe(opts *DescribeOptions) (*DescribeResult, error) {
	var resultPtr *C.git_describe_result

	var cDescribeOpts *C.git_describe_options
	if opts != nil {
		var cpattern *C.char
		if len(opts.Pattern) > 0 {
			cpattern = C.CString(opts.Pattern)
			defer C.free(unsafe.Pointer(cpattern))
		}

		cDescribeOpts = &C.git_describe_options{
			version:                     C.GIT_DESCRIBE_OPTIONS_VERSION,
			max_candidates_tags:         C.uint(opts.MaxCandidatesTags),
			describe_strategy:           C.uint(opts.Strategy),
			pattern:                     cpattern,
			only_follow_first_parent:    cbool(opts.OnlyFollowFirstParent),
			show_commit_oid_as_fallback: cbool(opts.ShowCommitOidAsFallback),
		}
	}

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ecode := C.git_describe_commit(&resultPtr, c.ptr, cDescribeOpts)
	runtime.KeepAlive(c)
	if ecode < 0 {
		return nil, MakeGitError(ecode)
	}

	return newDescribeResultFromC(resultPtr), nil
}

// DescribeWorkdir describes the working tree. It means describe HEAD
// and appends <mark> (-dirty by default) if the working tree is dirty.
func (repo *Repository) DescribeWorkdir(opts *DescribeOptions) (*DescribeResult, error) {
	var resultPtr *C.git_describe_result

	var cDescribeOpts *C.git_describe_options
	if opts != nil {
		var cpattern *C.char
		if len(opts.Pattern) > 0 {
			cpattern = C.CString(opts.Pattern)
			defer C.free(unsafe.Pointer(cpattern))
		}

		cDescribeOpts = &C.git_describe_options{
			version:                     C.GIT_DESCRIBE_OPTIONS_VERSION,
			max_candidates_tags:         C.uint(opts.MaxCandidatesTags),
			describe_strategy:           C.uint(opts.Strategy),
			pattern:                     cpattern,
			only_follow_first_parent:    cbool(opts.OnlyFollowFirstParent),
			show_commit_oid_as_fallback: cbool(opts.ShowCommitOidAsFallback),
		}
	}

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ecode := C.git_describe_workdir(&resultPtr, repo.ptr, cDescribeOpts)
	runtime.KeepAlive(repo)
	if ecode < 0 {
		return nil, MakeGitError(ecode)
	}

	return newDescribeResultFromC(resultPtr), nil
}

// DescribeResult represents the output from the 'git_describe_commit'
// and 'git_describe_workdir' functions in libgit2.
//
// Use Format() to get a string out of it.
type DescribeResult struct {
	doNotCompare
	ptr *C.git_describe_result
}

func newDescribeResultFromC(ptr *C.git_describe_result) *DescribeResult {
	result := &DescribeResult{
		ptr: ptr,
	}
	runtime.SetFinalizer(result, (*DescribeResult).Free)
	return result
}

// Format prints the DescribeResult as a string.
func (result *DescribeResult) Format(opts *DescribeFormatOptions) (string, error) {
	resultBuf := C.git_buf{}

	var cFormatOpts *C.git_describe_format_options
	if opts != nil {
		cDirtySuffix := C.CString(opts.DirtySuffix)
		defer C.free(unsafe.Pointer(cDirtySuffix))

		cFormatOpts = &C.git_describe_format_options{
			version:                C.GIT_DESCRIBE_FORMAT_OPTIONS_VERSION,
			abbreviated_size:       C.uint(opts.AbbreviatedSize),
			always_use_long_format: cbool(opts.AlwaysUseLongFormat),
			dirty_suffix:           cDirtySuffix,
		}
	}

	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	ecode := C.git_describe_format(&resultBuf, result.ptr, cFormatOpts)
	runtime.KeepAlive(result)
	if ecode < 0 {
		return "", MakeGitError(ecode)
	}
	defer C.git_buf_dispose(&resultBuf)

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

// Free cleans up the C reference.
func (result *DescribeResult) Free() {
	runtime.SetFinalizer(result, nil)
	C.git_describe_result_free(result.ptr)
	result.ptr = nil
}
