package processors

import (
	"bufio"
	"bytes"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"io/ioutil"
	"strings"

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

type comment struct {
	linters []string
	line    int
}
type fileComments []comment
type fileData struct {
	comments    fileComments
	isGenerated bool
}
type filesCache map[string]*fileData

type Nolint struct {
	fset  *token.FileSet
	cache filesCache
}

func NewNolint(fset *token.FileSet) *Nolint {
	return &Nolint{
		fset:  fset,
		cache: filesCache{},
	}
}

var _ Processor = &Nolint{}

func (p Nolint) Name() string {
	return "nolint"
}

func (p *Nolint) Process(issues []result.Issue) ([]result.Issue, error) {
	return filterIssuesErr(issues, p.shouldPassIssue)
}

var (
	genHdr = []byte("// Code generated ")
	genFtr = []byte(" DO NOT EDIT.")
)

// isGenerated reports whether the source file is generated code
// according the rules from https://golang.org/s/generatedcode.
func isGenerated(src []byte) bool {
	sc := bufio.NewScanner(bytes.NewReader(src))
	for sc.Scan() {
		b := sc.Bytes()
		if bytes.HasPrefix(b, genHdr) && bytes.HasSuffix(b, genFtr) && len(b) >= len(genHdr)+len(genFtr) {
			return true
		}
	}
	return false
}

func (p *Nolint) getOrCreateFileData(i *result.Issue) (*fileData, error) {
	fd := p.cache[i.FilePath()]
	if fd != nil {
		return fd, nil
	}

	fd = &fileData{}
	p.cache[i.FilePath()] = fd

	src, err := ioutil.ReadFile(i.FilePath())
	if err != nil {
		return nil, fmt.Errorf("can't read file %s: %s", i.FilePath(), err)
	}

	fd.isGenerated = isGenerated(src)
	if fd.isGenerated { // don't report issues for autogenerated files
		return fd, nil
	}

	file, err := parser.ParseFile(p.fset, i.FilePath(), src, parser.ParseComments)
	if err != nil {
		return nil, fmt.Errorf("can't parse file %s", i.FilePath())
	}

	fd.comments = extractFileComments(p.fset, file.Comments...)
	return fd, nil
}

func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) {
	fd, err := p.getOrCreateFileData(i)
	if err != nil {
		return false, err
	}

	if fd.isGenerated { // don't report issues for autogenerated files
		return false, nil
	}

	for _, comment := range fd.comments {
		if comment.line != i.Line() {
			continue
		}

		if len(comment.linters) == 0 {
			return false, nil // skip all linters
		}

		for _, linter := range comment.linters {
			if i.FromLinter == linter {
				return false, nil
			}
			// TODO: check linter name
		}
	}

	return true, nil
}

func extractFileComments(fset *token.FileSet, comments ...*ast.CommentGroup) fileComments {
	ret := fileComments{}
	for _, g := range comments {
		for _, c := range g.List {
			text := strings.TrimLeft(c.Text, "/ ")
			if strings.HasPrefix(text, "nolint") {
				var linters []string
				if strings.HasPrefix(text, "nolint:") {
					text = strings.Split(text, " ")[0] // allow arbitrary text after this comment
					for _, linter := range strings.Split(strings.TrimPrefix(text, "nolint:"), ",") {
						linters = append(linters, strings.TrimSpace(linter))
					}
				}
				pos := fset.Position(g.Pos())
				ret = append(ret, comment{
					linters: linters,
					line:    pos.Line,
				})
			}
		}
	}

	return ret
}

func (p Nolint) Finish() {}