package processors

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

	"github.com/pkg/errors"

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

var autogenDebugf = logutils.Debug("autogen_exclude")

type ageFileSummary struct {
	isGenerated bool
}

type ageFileSummaryCache map[string]*ageFileSummary

type AutogeneratedExclude struct {
	fileSummaryCache ageFileSummaryCache
}

func NewAutogeneratedExclude() *AutogeneratedExclude {
	return &AutogeneratedExclude{
		fileSummaryCache: ageFileSummaryCache{},
	}
}

var _ Processor = &AutogeneratedExclude{}

func (p AutogeneratedExclude) Name() string {
	return "autogenerated_exclude"
}

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

func isSpecialAutogeneratedFile(filePath string) bool {
	fileName := filepath.Base(filePath)
	// fake files to which //line points to for goyacc generated files
	return fileName == "yacctab" || fileName == "yaccpar" || fileName == "NONE"
}

func (p *AutogeneratedExclude) shouldPassIssue(i *result.Issue) (bool, error) {
	if i.FromLinter == "typecheck" {
		// don't hide typechecking errors in generated files: users expect to see why the project isn't compiling
		return true, nil
	}

	if isSpecialAutogeneratedFile(i.FilePath()) {
		return false, nil
	}

	fs, err := p.getOrCreateFileSummary(i)
	if err != nil {
		return false, err
	}

	// don't report issues for autogenerated files
	return !fs.isGenerated, nil
}

// isGenerated reports whether the source file is generated code.
// Using a bit laxer rules than https://golang.org/s/generatedcode to
// match more generated code. See #48 and #72.
func isGeneratedFileByComment(doc string) bool {
	const (
		genCodeGenerated = "code generated"
		genDoNotEdit     = "do not edit"
		genAutoFile      = "autogenerated file" // easyjson
	)

	markers := []string{genCodeGenerated, genDoNotEdit, genAutoFile}
	doc = strings.ToLower(doc)
	for _, marker := range markers {
		if strings.Contains(doc, marker) {
			autogenDebugf("doc contains marker %q: file is generated", marker)
			return true
		}
	}

	autogenDebugf("doc of len %d doesn't contain any of markers: %s", len(doc), markers)
	return false
}

func (p *AutogeneratedExclude) getOrCreateFileSummary(i *result.Issue) (*ageFileSummary, error) {
	fs := p.fileSummaryCache[i.FilePath()]
	if fs != nil {
		return fs, nil
	}

	fs = &ageFileSummary{}
	p.fileSummaryCache[i.FilePath()] = fs

	if i.FilePath() == "" {
		return nil, fmt.Errorf("no file path for issue")
	}

	doc, err := getDoc(i.FilePath())
	if err != nil {
		return nil, errors.Wrapf(err, "failed to get doc of file %s", i.FilePath())
	}

	fs.isGenerated = isGeneratedFileByComment(doc)
	autogenDebugf("file %q is generated: %t", i.FilePath(), fs.isGenerated)
	return fs, nil
}

func getDoc(filePath string) (string, error) {
	file, err := os.Open(filePath)
	if err != nil {
		return "", errors.Wrap(err, "failed to open file")
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)

	// Issue 954: Some lines can be very long, e.g. auto-generated
	// embedded resources. Reported on file of 86.2KB.
	const (
		maxSize     = 10 * 1024 * 1024 // 10MB should be enough
		initialSize = 4096             // same as startBufSize in bufio
	)
	scanner.Buffer(make([]byte, initialSize), maxSize)

	var docLines []string
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, "//") {
			text := strings.TrimSpace(strings.TrimPrefix(line, "//"))
			docLines = append(docLines, text)
		} else if line == "" || strings.HasPrefix(line, "package") {
			// go to next line
		} else {
			break
		}
	}

	if err := scanner.Err(); err != nil {
		return "", errors.Wrap(err, "failed to scan file")
	}

	return strings.Join(docLines, "\n"), nil
}

func (p AutogeneratedExclude) Finish() {}