package astcache

import (
	"go/ast"
	"go/parser"
	"go/token"
	"path/filepath"

	"golang.org/x/tools/go/packages"

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

type File struct {
	F    *ast.File
	Fset *token.FileSet
	Name string
	Err  error
}

type Cache struct {
	m   map[string]*File // map from absolute file path to file data
	s   []*File
	log logutils.Log
}

func NewCache(log logutils.Log) *Cache {
	return &Cache{
		m:   map[string]*File{},
		log: log,
	}
}

func (c Cache) ParsedFilenames() []string {
	var keys []string
	for k := range c.m {
		keys = append(keys, k)
	}
	return keys
}

func (c Cache) normalizeFilename(filename string) string {
	absPath := func() string {
		if filepath.IsAbs(filename) {
			return filepath.Clean(filename)
		}

		absFilename, err := filepath.Abs(filename)
		if err != nil {
			c.log.Warnf("Can't abs-ify filename %s: %s", filename, err)
			return filename
		}

		return absFilename
	}()

	ret, err := fsutils.EvalSymlinks(absPath)
	if err != nil {
		c.log.Warnf("Failed to eval symlinks for %s: %s", absPath, err)
		return absPath
	}

	return ret
}

func (c Cache) Get(filename string) *File {
	return c.m[c.normalizeFilename(filename)]
}

func (c Cache) GetAllValidFiles() []*File {
	return c.s
}

func (c *Cache) prepareValidFiles() {
	files := make([]*File, 0, len(c.m))
	for _, f := range c.m {
		if f.Err != nil || f.F == nil {
			continue
		}
		files = append(files, f)
	}
	c.s = files
}

func LoadFromFilenames(log logutils.Log, filenames ...string) *Cache {
	c := NewCache(log)

	fset := token.NewFileSet()
	for _, filename := range filenames {
		c.parseFile(filename, fset)
	}

	c.prepareValidFiles()
	return c
}

func LoadFromPackages(pkgs []*packages.Package, log logutils.Log) (*Cache, error) {
	c := NewCache(log)

	for _, pkg := range pkgs {
		c.loadFromPackage(pkg)
	}

	c.prepareValidFiles()
	return c, nil
}

func (c *Cache) extractFilenamesForAstFile(fset *token.FileSet, f *ast.File) []string {
	var ret []string

	// false ignores //line comments: name can be incorrect for generated files with //line directives
	// mapping e.g. from .rl to .go files.
	pos := fset.PositionFor(f.Pos(), false)
	if pos.Filename != "" {
		ret = append(ret, pos.Filename)
	}

	return ret
}

func (c *Cache) loadFromPackage(pkg *packages.Package) {
	for _, f := range pkg.Syntax {
		for _, filename := range c.extractFilenamesForAstFile(pkg.Fset, f) {
			filePath := c.normalizeFilename(filename)
			c.m[filePath] = &File{
				F:    f,
				Fset: pkg.Fset,
				Name: filePath,
			}
		}
	}

	// some Go files sometimes aren't present in pkg.Syntax
	fset := token.NewFileSet() // can't use pkg.Fset: it will overwrite offsets by preprocessed files
	for _, filePath := range pkg.GoFiles {
		filePath = c.normalizeFilename(filePath)
		if c.m[filePath] == nil {
			c.parseFile(filePath, fset)
		}
	}
}

func (c *Cache) parseFile(filePath string, fset *token.FileSet) {
	if fset == nil {
		fset = token.NewFileSet()
	}

	filePath = c.normalizeFilename(filePath)

	// comments needed by e.g. golint
	f, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
	c.m[filePath] = &File{
		F:    f,
		Fset: fset,
		Err:  err,
		Name: filePath,
	}
	if err != nil {
		c.log.Warnf("Can't parse AST of %s: %s", filePath, err)
	}
}
