package golinters

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

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

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

const misspellName = "misspell"

func NewMisspell(settings *config.MisspellSettings) *goanalysis.Linter {
	var mu sync.Mutex
	var resIssues []goanalysis.Issue

	analyzer := &analysis.Analyzer{
		Name: misspellName,
		Doc:  goanalysis.TheOnlyanalyzerDoc,
		Run:  goanalysis.DummyRun,
	}

	return goanalysis.NewLinter(
		misspellName,
		"Finds commonly misspelled English words in comments",
		[]*analysis.Analyzer{analyzer},
		nil,
	).WithContextSetter(func(lintCtx *linter.Context) {
		replacer, ruleErr := createMisspellReplacer(settings)

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

			issues, err := runMisspell(lintCtx, pass, replacer)
			if err != nil {
				return nil, err
			}

			if len(issues) == 0 {
				return nil, nil
			}

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

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

func runMisspell(lintCtx *linter.Context, pass *analysis.Pass, replacer *misspell.Replacer) ([]goanalysis.Issue, error) {
	var fileNames []string

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

	var issues []goanalysis.Issue
	for _, filename := range fileNames {
		lintIssues, err := runMisspellOnFile(lintCtx, filename, replacer)
		if err != nil {
			return nil, err
		}
		for i := range lintIssues {
			issues = append(issues, goanalysis.NewIssue(&lintIssues[i], pass))
		}
	}

	return issues, nil
}

func createMisspellReplacer(settings *config.MisspellSettings) (*misspell.Replacer, error) {
	replacer := &misspell.Replacer{
		Replacements: misspell.DictMain,
	}

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

	if len(settings.IgnoreWords) != 0 {
		replacer.RemoveRule(settings.IgnoreWords)
	}

	// It can panic.
	replacer.Compile()

	return replacer, nil
}

func runMisspellOnFile(lintCtx *linter.Context, filename string, replacer *misspell.Replacer) ([]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 := replacer.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
}
