package git

import (
	"bufio"
	"bytes"
	"os"
	"path/filepath"
	"strings"

	"github.com/git-lfs/git-lfs/filepathfilter"
	"github.com/git-lfs/git-lfs/tools"
	"github.com/rubyist/tracerx"
)

const (
	LockableAttrib      = "lockable"
	FilterDisableAttrib = "-filter"
)

// AttributePath is a path entry in a gitattributes file which has the LFS filter
type AttributePath struct {
	// Path entry in the attribute file
	Path string
	// The attribute file which was the source of this entry
	Source *AttributeSource
	// Path also has the 'lockable' attribute
	Lockable bool
	// Path is handled by Git LFS (i.e., filter=lfs)
	Tracked bool
}

type AttributeSource struct {
	Path       string
	LineEnding string
}

func (s *AttributeSource) String() string {
	return s.Path
}

// GetRootAttributePaths beahves as GetRootAttributePaths, and loads information
// only from the global gitattributes file.
func GetRootAttributePaths(cfg Env) []AttributePath {
	af, ok := cfg.Get("core.attributesfile")
	if !ok {
		return nil
	}

	// The working directory for the root gitattributes file is blank.
	return attrPaths(af, "")
}

// GetSystemAttributePaths behaves as GetAttributePaths, and loads information
// only from the system gitattributes file, respecting the $PREFIX environment
// variable.
func GetSystemAttributePaths(env Env) []AttributePath {
	prefix, _ := env.Get("PREFIX")
	if len(prefix) == 0 {
		prefix = string(filepath.Separator)
	}

	path := filepath.Join(prefix, "etc", "gitattributes")

	if _, err := os.Stat(path); os.IsNotExist(err) {
		return nil
	}

	return attrPaths(path, "")
}

// GetAttributePaths returns a list of entries in .gitattributes which are
// configured with the filter=lfs attribute
// workingDir is the root of the working copy
// gitDir is the root of the git repo
func GetAttributePaths(workingDir, gitDir string) []AttributePath {
	paths := make([]AttributePath, 0)

	for _, path := range findAttributeFiles(workingDir, gitDir) {
		paths = append(paths, attrPaths(path, workingDir)...)
	}

	return paths
}

func attrPaths(path, workingDir string) []AttributePath {
	attributes, err := os.Open(path)
	if err != nil {
		return nil
	}

	var paths []AttributePath

	relfile, _ := filepath.Rel(workingDir, path)
	reldir := filepath.Dir(relfile)
	source := &AttributeSource{Path: relfile}

	le := &lineEndingSplitter{}
	scanner := bufio.NewScanner(attributes)
	scanner.Split(le.ScanLines)

	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())

		if strings.HasPrefix(line, "#") {
			continue
		}

		hasFilter := strings.Contains(line, "filter=lfs")

		// Check for filter=lfs (signifying that LFS is tracking
		// this file) or "lockable", which indicates that the
		// file is lockable (and may or may not be tracked by
		// Git LFS).
		if hasFilter ||
			strings.Contains(line, FilterDisableAttrib) ||
			strings.HasSuffix(line, "lockable") {

			fields := strings.Fields(line)
			pattern := fields[0]
			if len(reldir) > 0 {
				pattern = filepath.Join(reldir, pattern)
			}
			// Find lockable flag in any position after pattern to avoid
			// edge case of matching "lockable" to a file pattern
			lockable := false
			tracked := true
			for _, f := range fields[1:] {
				if f == LockableAttrib {
					lockable = true
				}
				if !hasFilter ||
					strings.HasPrefix(f, FilterDisableAttrib) {
					tracked = false
				}
			}
			paths = append(paths, AttributePath{
				Path:     pattern,
				Source:   source,
				Lockable: lockable,
				Tracked:  tracked,
			})
		}
	}

	source.LineEnding = le.LineEnding()

	return paths
}

// GetAttributeFilter returns a list of entries in .gitattributes which are
// configured with the filter=lfs attribute as a file path filter which
// file paths can be matched against
// workingDir is the root of the working copy
// gitDir is the root of the git repo
func GetAttributeFilter(workingDir, gitDir string) *filepathfilter.Filter {
	paths := GetAttributePaths(workingDir, gitDir)
	patterns := make([]filepathfilter.Pattern, 0, len(paths))

	for _, path := range paths {
		// Convert all separators to `/` before creating a pattern to
		// avoid characters being escaped in situations like `subtree\*.md`
		patterns = append(patterns, filepathfilter.NewPattern(filepath.ToSlash(path.Path)))
	}

	return filepathfilter.NewFromPatterns(patterns, nil)
}

// copies bufio.ScanLines(), counting LF vs CRLF in a file
type lineEndingSplitter struct {
	LFCount   int
	CRLFCount int
}

func (s *lineEndingSplitter) LineEnding() string {
	if s.CRLFCount > s.LFCount {
		return "\r\n"
	} else if s.LFCount == 0 {
		return ""
	}
	return "\n"
}

func (s *lineEndingSplitter) ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
	if atEOF && len(data) == 0 {
		return 0, nil, nil
	}
	if i := bytes.IndexByte(data, '\n'); i >= 0 {
		// We have a full newline-terminated line.
		return i + 1, s.dropCR(data[0:i]), nil
	}
	// If we're at EOF, we have a final, non-terminated line. Return it.
	if atEOF {
		return len(data), data, nil
	}
	// Request more data.
	return 0, nil, nil
}

// dropCR drops a terminal \r from the data.
func (s *lineEndingSplitter) dropCR(data []byte) []byte {
	if len(data) > 0 && data[len(data)-1] == '\r' {
		s.CRLFCount++
		return data[0 : len(data)-1]
	}
	s.LFCount++
	return data
}

func findAttributeFiles(workingDir, gitDir string) []string {
	var paths []string

	repoAttributes := filepath.Join(gitDir, "info", "attributes")
	if info, err := os.Stat(repoAttributes); err == nil && !info.IsDir() {
		paths = append(paths, repoAttributes)
	}

	tools.FastWalkGitRepo(workingDir, func(parentDir string, info os.FileInfo, err error) {
		if err != nil {
			tracerx.Printf("Error finding .gitattributes: %v", err)
			return
		}

		if info.IsDir() || info.Name() != ".gitattributes" {
			return
		}
		paths = append(paths, filepath.Join(parentDir, info.Name()))
	})

	// reverse the order of the files so more specific entries are found first
	// when iterating from the front (respects precedence)
	for i, j := 0, len(paths)-1; i < j; i, j = i+1, j-1 {
		paths[i], paths[j] = paths[j], paths[i]
	}

	return paths
}
