package processors

import (
	"fmt"
	"path/filepath"
	"regexp"

	"github.com/golangci/golangci-lint/pkg/fsutils"
	"github.com/golangci/golangci-lint/pkg/logutils"
	"github.com/golangci/golangci-lint/pkg/result"
)

var _ Processor = (*SkipDirs)(nil)

var StdExcludeDirRegexps = []string{
	normalizePathRegex("vendor"),
	normalizePathRegex("third_party"),
	normalizePathRegex("testdata"),
	normalizePathRegex("examples"),
	normalizePathRegex("Godeps"),
	normalizePathRegex("builtin"),
}

type skipStat struct {
	pattern string
	count   int
}

type SkipDirs struct {
	patterns         []*regexp.Regexp
	log              logutils.Log
	skippedDirs      map[string]*skipStat
	absArgsDirs      []string
	skippedDirsCache map[string]bool
	pathPrefix       string
}

func NewSkipDirs(log logutils.Log, patterns, args []string, pathPrefix string) (*SkipDirs, error) {
	var patternsRe []*regexp.Regexp
	for _, p := range patterns {
		p = fsutils.NormalizePathInRegex(p)
		patternRe, err := regexp.Compile(p)
		if err != nil {
			return nil, fmt.Errorf("can't compile regexp %q: %w", p, err)
		}
		patternsRe = append(patternsRe, patternRe)
	}

	absArgsDirs, err := absDirs(args)
	if err != nil {
		return nil, err
	}

	return &SkipDirs{
		patterns:         patternsRe,
		log:              log,
		skippedDirs:      map[string]*skipStat{},
		absArgsDirs:      absArgsDirs,
		skippedDirsCache: map[string]bool{},
		pathPrefix:       pathPrefix,
	}, nil
}

func (*SkipDirs) Name() string {
	return "skip_dirs"
}

func (p *SkipDirs) Process(issues []result.Issue) ([]result.Issue, error) {
	if len(p.patterns) == 0 {
		return issues, nil
	}

	return filterIssues(issues, p.shouldPassIssue), nil
}

func (p *SkipDirs) Finish() {
	for dir, stat := range p.skippedDirs {
		p.log.Infof("Skipped %d issues from dir %s by pattern %s", stat.count, dir, stat.pattern)
	}
}

func (p *SkipDirs) shouldPassIssue(issue *result.Issue) bool {
	if filepath.IsAbs(issue.FilePath()) {
		if isGoFile(issue.FilePath()) {
			p.log.Warnf("Got abs path %s in skip dirs processor, it should be relative", issue.FilePath())
		}
		return true
	}

	issueRelDir := filepath.Dir(issue.FilePath())

	if toPass, ok := p.skippedDirsCache[issueRelDir]; ok {
		if !toPass {
			p.skippedDirs[issueRelDir].count++
		}
		return toPass
	}

	issueAbsDir, err := filepath.Abs(issueRelDir)
	if err != nil {
		p.log.Warnf("Can't abs-ify path %q: %s", issueRelDir, err)
		return true
	}

	toPass := p.shouldPassIssueDirs(issueRelDir, issueAbsDir)
	p.skippedDirsCache[issueRelDir] = toPass
	return toPass
}

func (p *SkipDirs) shouldPassIssueDirs(issueRelDir, issueAbsDir string) bool {
	for _, absArgDir := range p.absArgsDirs {
		if absArgDir == issueAbsDir {
			// we must not skip issues if they are from explicitly set dirs
			// even if they match skip patterns
			return true
		}
	}

	// We use issueRelDir for matching: it's the relative to the current
	// work dir path of directory of source file with the issue. It can lead
	// to unexpected behavior if we're analyzing files out of current work dir.
	// The alternative solution is to find relative to args path, but it has
	// disadvantages (https://github.com/golangci/golangci-lint/pull/313).

	path := fsutils.WithPathPrefix(p.pathPrefix, issueRelDir)
	for _, pattern := range p.patterns {
		if pattern.MatchString(path) {
			ps := pattern.String()
			if p.skippedDirs[issueRelDir] == nil {
				p.skippedDirs[issueRelDir] = &skipStat{
					pattern: ps,
				}
			}
			p.skippedDirs[issueRelDir].count++
			return false
		}
	}

	return true
}

func absDirs(args []string) ([]string, error) {
	if len(args) == 0 {
		args = append(args, "./...")
	}

	var absArgsDirs []string
	for _, arg := range args {
		base := filepath.Base(arg)
		if base == "..." || isGoFile(base) {
			arg = filepath.Dir(arg)
		}

		absArg, err := filepath.Abs(arg)
		if err != nil {
			return nil, fmt.Errorf("failed to abs-ify arg %q: %w", arg, err)
		}

		absArgsDirs = append(absArgsDirs, absArg)
	}

	return absArgsDirs, nil
}

func normalizePathRegex(e string) string {
	return createPathRegex(e, filepath.Separator)
}

func createPathRegex(e string, sep rune) string {
	escapedSep := regexp.QuoteMeta(string(sep)) // needed for windows sep '\\'
	return fmt.Sprintf(`(^|%[1]s)%[2]s($|%[1]s)`, escapedSep, e)
}