package filepathfilter

import (
	"strings"

	"github.com/git-lfs/git-lfs/v3/tr"
	"github.com/git-lfs/wildmatch/v2"
	"github.com/rubyist/tracerx"
)

type Pattern interface {
	Match(filename string) bool
	// String returns a string representation (see: regular expressions) of
	// the underlying pattern used to match filenames against this Pattern.
	String() string
}

type Filter struct {
	include      []Pattern
	exclude      []Pattern
	defaultValue bool
}

type PatternType bool

const (
	GitIgnore     = PatternType(false)
	GitAttributes = PatternType(true)
)

func (p PatternType) String() string {
	if p == GitIgnore {
		return "gitignore"
	}
	return "gitattributes"
}

type options struct {
	defaultValue bool
}

type option func(*options)

// DefaultValue is an option representing the default value of a filepathfilter
// if no patterns match.  If this option is not provided, the default is true.
func DefaultValue(val bool) option {
	return func(args *options) {
		args.defaultValue = val
	}
}

func NewFromPatterns(include, exclude []Pattern, setters ...option) *Filter {
	args := &options{defaultValue: true}
	for _, setter := range setters {
		setter(args)
	}
	return &Filter{include: include, exclude: exclude, defaultValue: args.defaultValue}
}

func New(include, exclude []string, ptype PatternType, setters ...option) *Filter {
	return NewFromPatterns(
		convertToWildmatch(include, ptype),
		convertToWildmatch(exclude, ptype), setters...)
}

// Include returns the result of calling String() on each Pattern in the
// include set of this *Filter.
func (f *Filter) Include() []string { return wildmatchToString(f.include...) }

// Exclude returns the result of calling String() on each Pattern in the
// exclude set of this *Filter.
func (f *Filter) Exclude() []string { return wildmatchToString(f.exclude...) }

// wildmatchToString maps the given set of Pattern's to a string slice by
// calling String() on each pattern.
func wildmatchToString(ps ...Pattern) []string {
	s := make([]string, 0, len(ps))
	for _, p := range ps {
		s = append(s, p.String())
	}

	return s
}

func (f *Filter) Allows(filename string) bool {
	if f == nil {
		return true
	}

	var included bool
	for _, inc := range f.include {
		if included = inc.Match(filename); included {
			break
		}
	}

	if !included && len(f.include) > 0 {
		tracerx.Printf("filepathfilter: rejecting %q via %v", filename, f.include)
		return false
	}

	// Beyond this point, the only values we can logically return are false
	// or the default value.  If the default is false, then there's no point
	// traversing the exclude patterns because the return value will always
	// be false; we'd do extra work for no functional benefit.
	if !included && !f.defaultValue {
		tracerx.Printf("filepathfilter: rejecting %q", filename)
		return false
	}

	for _, ex := range f.exclude {
		if ex.Match(filename) {
			tracerx.Printf("filepathfilter: rejecting %q via %q", filename, ex.String())
			return false
		}
	}

	// No patterns matched and our default value is true.
	tracerx.Printf("filepathfilter: accepting %q", filename)
	return true
}

type wm struct {
	w *wildmatch.Wildmatch
	p string
}

func (w *wm) Match(filename string) bool {
	return w.w.Match(filename)
}

func (w *wm) String() string {
	return w.p
}

const (
	sep byte = '/'
)

func NewPattern(p string, ptype PatternType) Pattern {
	tracerx.Printf("filepathfilter: creating pattern %q of type %v", p, ptype)

	switch ptype {
	case GitIgnore:
		return &wm{
			p: p,
			w: wildmatch.NewWildmatch(
				p,
				wildmatch.SystemCase,
				wildmatch.Contents,
			),
		}
	case GitAttributes:
		return &wm{
			p: p,
			w: wildmatch.NewWildmatch(
				p,
				wildmatch.SystemCase,
				wildmatch.Basename,
				wildmatch.GitAttributes,
			),
		}
	default:
		panic(tr.Tr.Get("unreachable"))
	}
}

// join joins path elements together via the separator "sep" and produces valid
// paths without multiple separators (unless multiple separators were included
// in the original paths []string).
func join(paths ...string) string {
	var joined string

	for i, path := range paths {
		joined = joined + path
		if i != len(paths)-1 && !strings.HasSuffix(path, string(sep)) {
			joined = joined + string(sep)
		}
	}

	return joined
}

func convertToWildmatch(rawpatterns []string, ptype PatternType) []Pattern {
	patterns := make([]Pattern, len(rawpatterns))
	for i, raw := range rawpatterns {
		patterns[i] = NewPattern(raw, ptype)
	}
	return patterns
}