package golinters

import (
	"fmt"
	"go/token"
	"strings"
	"sync"

	"github.com/golangci/misspell"
	"golang.org/x/tools/go/analysis"

	"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
	"github.com/golangci/golangci-lint/pkg/lint/linter"
	"github.com/golangci/golangci-lint/pkg/result"
)

func runMisspellOnFile(fileName string, r *misspell.Replacer, lintCtx *linter.Context) ([]result.Issue, error) {
	var res []result.Issue
	fileContent, err := lintCtx.FileCache.GetFileBytes(fileName)
	if err != nil {
		return nil, fmt.Errorf("can't get file %s contents: %s", fileName, err)
	}

	// use r.Replace, not r.ReplaceGo because r.ReplaceGo doesn't find
	// issues inside strings: it searches only inside comments. r.Replace
	// searches all words: it treats input as a plain text. A standalone misspell
	// tool uses r.Replace by default.
	_, diffs := r.Replace(string(fileContent))
	for _, diff := range diffs {
		text := fmt.Sprintf("`%s` is a misspelling of `%s`", diff.Original, diff.Corrected)
		pos := token.Position{
			Filename: fileName,
			Line:     diff.Line,
			Column:   diff.Column + 1,
		}
		replacement := &result.Replacement{
			Inline: &result.InlineFix{
				StartCol:  diff.Column,
				Length:    len(diff.Original),
				NewString: diff.Corrected,
			},
		}

		res = append(res, result.Issue{
			Pos:         pos,
			Text:        text,
			FromLinter:  misspellName,
			Replacement: replacement,
		})
	}

	return res, nil
}

const misspellName = "misspell"

func NewMisspell() *goanalysis.Linter {
	var mu sync.Mutex
	var resIssues []goanalysis.Issue
	var ruleErr error

	analyzer := &analysis.Analyzer{
		Name: misspellName,
		Doc:  goanalysis.TheOnlyanalyzerDoc,
	}
	return goanalysis.NewLinter(
		misspellName,
		"Finds commonly misspelled English words in comments",
		[]*analysis.Analyzer{analyzer},
		nil,
	).WithContextSetter(func(lintCtx *linter.Context) {
		r := misspell.Replacer{
			Replacements: misspell.DictMain,
		}

		// Figure out regional variations
		settings := lintCtx.Settings().Misspell
		locale := settings.Locale
		switch strings.ToUpper(locale) {
		case "":
			// nothing
		case "US":
			r.AddRuleList(misspell.DictAmerican)
		case "UK", "GB":
			r.AddRuleList(misspell.DictBritish)
		case "NZ", "AU", "CA":
			ruleErr = fmt.Errorf("unknown locale: %q", locale)
		}

		if ruleErr == nil {
			if len(settings.IgnoreWords) != 0 {
				r.RemoveRule(settings.IgnoreWords)
			}

			r.Compile()
		}

		analyzer.Run = func(pass *analysis.Pass) (interface{}, error) {
			if ruleErr != nil {
				return nil, ruleErr
			}

			var fileNames []string
			for _, f := range pass.Files {
				pos := pass.Fset.PositionFor(f.Pos(), false)
				fileNames = append(fileNames, pos.Filename)
			}

			var res []goanalysis.Issue
			for _, f := range fileNames {
				issues, err := runMisspellOnFile(f, &r, lintCtx)
				if err != nil {
					return nil, err
				}
				for i := range issues {
					res = append(res, goanalysis.NewIssue(&issues[i], pass))
				}
			}
			if len(res) == 0 {
				return nil, nil
			}

			mu.Lock()
			resIssues = append(resIssues, res...)
			mu.Unlock()

			return nil, nil
		}
	}).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue {
		return resIssues
	}).WithLoadMode(goanalysis.LoadModeSyntax)
}