// Copyright 2016 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// The importers package uses go/ast to analyze Go packages or Go files
// and collect references to types whose package has a package prefix.
// It is used by the language specific importers to determine the set of
// wrapper types to be generated.
//
// # For example, in the Go file
//
// package javaprogram
//
// import "Java/java/lang"
//
// func F() {
//
//	o := lang.Object.New()
//	...
//
// }
//
// the java importer uses this package to determine that the "java/lang"
// package and the wrapper interface, lang.Object, needs to be generated.
// After calling AnalyzeFile or AnalyzePackages, the References result
// contains the reference to lang.Object and the names set will contain
// "New".
package importers

import (
	"errors"
	"go/ast"
	"go/token"
	"path"
	"sort"
	"strconv"
	"strings"

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

// References is the result of analyzing a Go file or set of Go packages.
//
// # For example, the Go file
//
// package pkg
//
// import "Prefix/some/Package"
//
// var A = Package.Identifier
//
// Will result in a single PkgRef with the "some/Package" package and
// the Identifier name. The Names set will contain the single name,
// "Identifier".
type References struct {
	// The list of references to identifiers in packages that are
	// identified by a package prefix.
	Refs []PkgRef
	// The list of names used in at least one selector expression.
	// Useful as a conservative upper bound on the set of identifiers
	// referenced from a set of packages.
	Names map[string]struct{}
	// Embedders is a list of struct types with prefixed types
	// embedded.
	Embedders []Struct
}

// Struct is a representation of a struct type with embedded
// types.
type Struct struct {
	Name    string
	Pkg     string
	PkgPath string
	Refs    []PkgRef
}

// PkgRef is a reference to an identifier in a package.
type PkgRef struct {
	Name string
	Pkg  string
}

type refsSaver struct {
	pkgPrefix string
	*References
	refMap       map[PkgRef]struct{}
	insideStruct bool
}

// AnalyzeFile scans the provided file for references to packages with the given
// package prefix. The list of unique (package, identifier) pairs is returned
func AnalyzeFile(file *ast.File, pkgPrefix string) (*References, error) {
	visitor := newRefsSaver(pkgPrefix)
	fset := token.NewFileSet()
	files := map[string]*ast.File{file.Name.Name: file}
	// Ignore errors (from unknown packages)
	pkg, _ := ast.NewPackage(fset, files, visitor.importer(), nil)
	ast.Walk(visitor, pkg)
	visitor.findEmbeddingStructs("", pkg)
	return visitor.References, nil
}

// AnalyzePackages scans the provided packages for references to packages with the given
// package prefix. The list of unique (package, identifier) pairs is returned
func AnalyzePackages(pkgs []*packages.Package, pkgPrefix string) (*References, error) {
	visitor := newRefsSaver(pkgPrefix)
	imp := visitor.importer()
	fset := token.NewFileSet()
	for _, pkg := range pkgs {
		files := make(map[string]*ast.File)
		for i, name := range pkg.GoFiles {
			files[name] = pkg.Syntax[i]
		}
		// Ignore errors (from unknown packages)
		astpkg, _ := ast.NewPackage(fset, files, imp, nil)
		ast.Walk(visitor, astpkg)
		visitor.findEmbeddingStructs(pkg.PkgPath, astpkg)
	}
	return visitor.References, nil
}

// findEmbeddingStructs finds all top level declarations embedding a prefixed type.
//
// For example:
//
// import "Prefix/some/Package"
//
// type T struct {
//
//	Package.Class
//
// }
func (v *refsSaver) findEmbeddingStructs(pkgpath string, pkg *ast.Package) {
	var names []string
	for _, obj := range pkg.Scope.Objects {
		if obj.Kind != ast.Typ || !ast.IsExported(obj.Name) {
			continue
		}
		names = append(names, obj.Name)
	}
	sort.Strings(names)
	for _, name := range names {
		obj := pkg.Scope.Objects[name]

		t, ok := obj.Decl.(*ast.TypeSpec).Type.(*ast.StructType)
		if !ok {
			continue
		}
		var refs []PkgRef
		for _, f := range t.Fields.List {
			sel, ok := f.Type.(*ast.SelectorExpr)
			if !ok {
				continue
			}
			ref, ok := v.addRef(sel)
			if !ok {
				continue
			}
			if len(f.Names) > 0 && !f.Names[0].IsExported() {
				continue
			}
			refs = append(refs, ref)
		}
		if len(refs) > 0 {
			v.Embedders = append(v.Embedders, Struct{
				Name:    obj.Name,
				Pkg:     pkg.Name,
				PkgPath: pkgpath,

				Refs: refs,
			})
		}
	}
}

func newRefsSaver(pkgPrefix string) *refsSaver {
	s := &refsSaver{
		pkgPrefix:  pkgPrefix,
		refMap:     make(map[PkgRef]struct{}),
		References: &References{},
	}
	s.Names = make(map[string]struct{})
	return s
}

func (v *refsSaver) importer() ast.Importer {
	return func(imports map[string]*ast.Object, pkgPath string) (*ast.Object, error) {
		if pkg, exists := imports[pkgPath]; exists {
			return pkg, nil
		}
		if !strings.HasPrefix(pkgPath, v.pkgPrefix) {
			return nil, errors.New("ignored")
		}
		pkg := ast.NewObj(ast.Pkg, path.Base(pkgPath))
		imports[pkgPath] = pkg
		return pkg, nil
	}
}

func (v *refsSaver) addRef(sel *ast.SelectorExpr) (PkgRef, bool) {
	x, ok := sel.X.(*ast.Ident)
	if !ok || x.Obj == nil {
		return PkgRef{}, false
	}
	imp, ok := x.Obj.Decl.(*ast.ImportSpec)
	if !ok {
		return PkgRef{}, false
	}
	pkgPath, err := strconv.Unquote(imp.Path.Value)
	if err != nil {
		return PkgRef{}, false
	}
	if !strings.HasPrefix(pkgPath, v.pkgPrefix) {
		return PkgRef{}, false
	}
	pkgPath = pkgPath[len(v.pkgPrefix):]
	ref := PkgRef{Pkg: pkgPath, Name: sel.Sel.Name}
	if _, exists := v.refMap[ref]; !exists {
		v.refMap[ref] = struct{}{}
		v.Refs = append(v.Refs, ref)
	}
	return ref, true
}

func (v *refsSaver) Visit(n ast.Node) ast.Visitor {
	switch n := n.(type) {
	case *ast.StructType:
		// Use a copy of refsSaver that only accepts exported fields. It refers
		// to the original refsSaver for collecting references.
		v2 := *v
		v2.insideStruct = true
		return &v2
	case *ast.Field:
		if v.insideStruct && len(n.Names) == 1 && !n.Names[0].IsExported() {
			return nil
		}
	case *ast.SelectorExpr:
		v.Names[n.Sel.Name] = struct{}{}
		if _, ok := v.addRef(n); ok {
			return nil
		}
	case *ast.FuncDecl:
		if n.Recv != nil { // Methods
			v.Names[n.Name.Name] = struct{}{}
		}
	}
	return v
}
