// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

// Package check implements the unparam linter. Note that its API is not
// stable.
package check // import "mvdan.cc/unparam/check"

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/constant"
	"go/parser"
	"go/printer"
	"go/token"
	"go/types"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"

	"golang.org/x/tools/go/callgraph"
	"golang.org/x/tools/go/callgraph/cha"
	"golang.org/x/tools/go/callgraph/rta"
	"golang.org/x/tools/go/loader"
	"golang.org/x/tools/go/ssa"
	"golang.org/x/tools/go/ssa/ssautil"

	"github.com/kisielk/gotool"
	"mvdan.cc/lint"
)

func UnusedParams(tests bool, algo string, exported, debug bool, args ...string) ([]string, error) {
	wd, err := os.Getwd()
	if err != nil {
		return nil, err
	}
	c := &Checker{
		wd:       wd,
		tests:    tests,
		algo:     algo,
		exported: exported,
	}
	if debug {
		c.debugLog = os.Stderr
	}
	return c.lines(args...)
}

type Checker struct {
	lprog *loader.Program
	prog  *ssa.Program

	wd string

	tests    bool
	algo     string
	exported bool
	debugLog io.Writer

	cachedDeclCounts map[string]map[string]int

	callByPos map[token.Pos]*ast.CallExpr
}

var (
	_ lint.Checker = (*Checker)(nil)
	_ lint.WithSSA = (*Checker)(nil)

	errorType = types.Universe.Lookup("error").Type()
)

func (c *Checker) lines(args ...string) ([]string, error) {
	paths := gotool.ImportPaths(args)
	conf := loader.Config{
		ParserMode: parser.ParseComments,
	}
	if _, err := conf.FromArgs(paths, c.tests); err != nil {
		return nil, err
	}
	lprog, err := conf.Load()
	if err != nil {
		return nil, err
	}
	prog := ssautil.CreateProgram(lprog, 0)
	prog.Build()
	c.Program(lprog)
	c.ProgramSSA(prog)
	issues, err := c.Check()
	if err != nil {
		return nil, err
	}
	lines := make([]string, len(issues))
	for i, issue := range issues {
		fpos := prog.Fset.Position(issue.Pos()).String()
		if strings.HasPrefix(fpos, c.wd) {
			fpos = fpos[len(c.wd)+1:]
		}
		lines[i] = fmt.Sprintf("%s: %s", fpos, issue.Message())
	}
	return lines, nil
}

type Issue struct {
	pos   token.Pos
	fname string
	msg   string
}

func (i Issue) Pos() token.Pos  { return i.pos }
func (i Issue) Message() string { return i.fname + " - " + i.msg }

func (c *Checker) Program(lprog *loader.Program) {
	c.lprog = lprog
}

func (c *Checker) ProgramSSA(prog *ssa.Program) {
	c.prog = prog
}

func (c *Checker) debug(format string, a ...interface{}) {
	if c.debugLog != nil {
		fmt.Fprintf(c.debugLog, format, a...)
	}
}

func generatedDoc(text string) bool {
	return strings.Contains(text, "Code generated") ||
		strings.Contains(text, "DO NOT EDIT")
}

func eqlConsts(v1, v2 constant.Value) bool {
	if v1 == nil || v2 == nil {
		return v1 == v2
	}
	return constant.Compare(v1, token.EQL, v2)
}

var stdSizes = types.SizesFor("gc", "amd64")

func (c *Checker) Check() ([]lint.Issue, error) {
	c.cachedDeclCounts = make(map[string]map[string]int)
	c.callByPos = make(map[token.Pos]*ast.CallExpr)
	wantPkg := make(map[*types.Package]*loader.PackageInfo)
	genFiles := make(map[string]bool)
	for _, info := range c.lprog.InitialPackages() {
		wantPkg[info.Pkg] = info
		for _, f := range info.Files {
			if len(f.Comments) > 0 && generatedDoc(f.Comments[0].Text()) {
				fname := c.prog.Fset.Position(f.Pos()).Filename
				genFiles[fname] = true
			}
			ast.Inspect(f, func(node ast.Node) bool {
				if ce, ok := node.(*ast.CallExpr); ok {
					c.callByPos[ce.Lparen] = ce
				}
				return true
			})
		}
	}
	var cg *callgraph.Graph
	switch c.algo {
	case "cha":
		cg = cha.CallGraph(c.prog)
	case "rta":
		mains, err := mainPackages(c.prog, wantPkg)
		if err != nil {
			return nil, err
		}
		var roots []*ssa.Function
		for _, main := range mains {
			roots = append(roots, main.Func("init"), main.Func("main"))
		}
		result := rta.Analyze(roots, true)
		cg = result.CallGraph
	default:
		return nil, fmt.Errorf("unknown call graph construction algorithm: %q", c.algo)
	}
	cg.DeleteSyntheticNodes()

	var issues []lint.Issue
	for fn := range ssautil.AllFunctions(c.prog) {
		switch {
		case fn.Pkg == nil: // builtin?
			continue
		case fn.Name() == "init":
			continue
		case len(fn.Blocks) == 0: // stub
			continue
		}
		info := wantPkg[fn.Pkg.Pkg]
		if info == nil { // not part of given pkgs
			continue
		}
		if c.exported || fn.Pkg.Pkg.Name() == "main" {
			// we want exported funcs, or this is a main
			// package so nothing is exported
		} else if strings.Contains(fn.Name(), "$") {
			// anonymous function
		} else if ast.IsExported(fn.Name()) {
			continue // user doesn't want to change signatures here
		}
		fname := c.prog.Fset.Position(fn.Pos()).Filename
		if genFiles[fname] {
			continue // generated file
		}
		c.debug("func %s\n", fn.RelString(fn.Package().Pkg))
		if dummyImpl(fn.Blocks[0]) { // panic implementation
			c.debug("  skip - dummy implementation\n")
			continue
		}
		var calls []*callgraph.Edge
		if node := cg.Nodes[fn]; node != nil {
			calls = node.In
		}
		if requiredViaCall(fn, calls) {
			c.debug("  skip - type is required via call\n")
			continue
		}
		if c.multipleImpls(info, fn) {
			c.debug("  skip - multiple implementations via build tags\n")
			continue
		}

		results := fn.Signature.Results()
		seenConsts := make([]*constant.Value, results.Len())
		seenParams := make([]*ssa.Parameter, results.Len())
		numRets := 0
		allRetsExtracting := true
		for _, block := range fn.Blocks {
			last := block.Instrs[len(block.Instrs)-1]
			ret, ok := last.(*ssa.Return)
			if !ok {
				continue
			}
			for i, val := range ret.Results {
				switch x := val.(type) {
				case *ssa.Const:
					allRetsExtracting = false
					seenParams[i] = nil
					switch {
					case numRets == 0:
						seenConsts[i] = &x.Value
					case seenConsts[i] == nil:
					case !eqlConsts(*seenConsts[i], x.Value):
						seenConsts[i] = nil
					}
				case *ssa.Parameter:
					allRetsExtracting = false
					seenConsts[i] = nil
					switch {
					case numRets == 0:
						seenParams[i] = x
					case seenParams[i] == nil:
					case seenParams[i] != x:
						seenParams[i] = nil
					}
				case *ssa.Extract:
					seenConsts[i] = nil
					seenParams[i] = nil
				default:
					allRetsExtracting = false
					seenConsts[i] = nil
					seenParams[i] = nil
				}
			}
			numRets++
		}
		for i, val := range seenConsts {
			if val == nil {
				// no consistent returned constant
				continue
			}
			if *val != nil && numRets == 1 {
				// just one non-nil return (too many
				// false positives)
				continue
			}
			valStr := "nil" // always returned untyped nil
			if *val != nil {
				valStr = (*val).String()
			}
			if calledInReturn(calls) {
				continue
			}
			res := results.At(i)
			name := paramDesc(i, res)
			issues = append(issues, Issue{
				pos:   res.Pos(),
				fname: fn.RelString(fn.Package().Pkg),
				msg:   fmt.Sprintf("result %s is always %s", name, valStr),
			})
		}

	resLoop:
		for i := 0; i < results.Len(); i++ {
			if allRetsExtracting {
				continue
			}
			res := results.At(i)
			if res.Type() == errorType {
				// "error is never unused" is less
				// useful, and it's up to tools like
				// errcheck anyway.
				continue
			}
			count := 0
			for _, edge := range calls {
				val := edge.Site.Value()
				if val == nil { // e.g. go statement
					count++
					continue
				}
				for _, instr := range *val.Referrers() {
					extract, ok := instr.(*ssa.Extract)
					if !ok {
						continue resLoop // direct, real use
					}
					if extract.Index != i {
						continue // not the same result param
					}
					if len(*extract.Referrers()) > 0 {
						continue resLoop // real use after extraction
					}
				}
				count++
			}
			if count < 2 {
				continue // require ignoring at least twice
			}
			name := paramDesc(i, res)
			issues = append(issues, Issue{
				pos:   res.Pos(),
				fname: fn.RelString(fn.Package().Pkg),
				msg:   fmt.Sprintf("result %s is never used", name),
			})
		}

		for i, par := range fn.Params {
			if i == 0 && fn.Signature.Recv() != nil { // receiver
				continue
			}
			c.debug("%s\n", par.String())
			switch par.Object().Name() {
			case "", "_": // unnamed
				c.debug("  skip - unnamed\n")
				continue
			}
			if stdSizes.Sizeof(par.Type()) == 0 {
				c.debug("  skip - zero size\n")
				continue
			}
			reason := "is unused"
			if valStr := c.receivesSameValues(calls, par, i); valStr != "" {
				reason = fmt.Sprintf("always receives %s", valStr)
			} else if anyRealUse(par, i) {
				c.debug("  skip - used somewhere in the func body\n")
				continue
			}
			issues = append(issues, Issue{
				pos:   par.Pos(),
				fname: fn.RelString(fn.Package().Pkg),
				msg:   fmt.Sprintf("%s %s", par.Name(), reason),
			})
		}

	}
	sort.Slice(issues, func(i, j int) bool {
		p1 := c.prog.Fset.Position(issues[i].Pos())
		p2 := c.prog.Fset.Position(issues[j].Pos())
		if p1.Filename == p2.Filename {
			return p1.Offset < p2.Offset
		}
		return p1.Filename < p2.Filename
	})
	return issues, nil
}

func mainPackages(prog *ssa.Program, wantPkg map[*types.Package]*loader.PackageInfo) ([]*ssa.Package, error) {
	mains := make([]*ssa.Package, 0, len(wantPkg))
	for tpkg := range wantPkg {
		pkg := prog.Package(tpkg)
		if tpkg.Name() == "main" && pkg.Func("main") != nil {
			mains = append(mains, pkg)
		}
	}
	if len(mains) == 0 {
		return nil, fmt.Errorf("no main packages")
	}
	return mains, nil
}

func calledInReturn(calls []*callgraph.Edge) bool {
	for _, edge := range calls {
		val := edge.Site.Value()
		if val == nil { // e.g. go statement
			continue
		}
		refs := *val.Referrers()
		if len(refs) == 0 { // no use of return values
			continue
		}
		allReturnExtracts := true
		for _, instr := range refs {
			switch x := instr.(type) {
			case *ssa.Return:
				return true
			case *ssa.Extract:
				refs := *x.Referrers()
				if len(refs) != 1 {
					allReturnExtracts = false
					break
				}
				if _, ok := refs[0].(*ssa.Return); !ok {
					allReturnExtracts = false
				}
			}
		}
		if allReturnExtracts {
			return true
		}
	}
	return false
}

func nodeStr(node ast.Node) string {
	var buf bytes.Buffer
	fset := token.NewFileSet()
	if err := printer.Fprint(&buf, fset, node); err != nil {
		panic(err)
	}
	return buf.String()
}

func (c *Checker) receivesSameValues(in []*callgraph.Edge, par *ssa.Parameter, pos int) string {
	if ast.IsExported(par.Parent().Name()) {
		// we might not have all call sites for an exported func
		return ""
	}
	var seen constant.Value
	origPos := pos
	if par.Parent().Signature.Recv() != nil {
		// go/ast's CallExpr.Args does not include the receiver,
		// but go/ssa's equivalent does.
		origPos--
	}
	seenOrig := ""
	count := 0
	for _, edge := range in {
		call := edge.Site.Common()
		cnst, ok := call.Args[pos].(*ssa.Const)
		if !ok {
			return "" // not a constant
		}
		origArg := ""
		origCall := c.callByPos[call.Pos()]
		if origPos >= len(origCall.Args) {
			// variadic parameter that wasn't given
		} else {
			origArg = nodeStr(origCall.Args[origPos])
		}
		if seen == nil {
			seen = cnst.Value // first constant
			seenOrig = origArg
			count = 1
		} else if !eqlConsts(seen, cnst.Value) {
			return "" // different constants
		} else {
			count++
			if origArg != seenOrig {
				seenOrig = ""
			}
		}
	}
	if count < 4 {
		return "" // not enough times, likely false positive
	}
	if seenOrig != "" && seenOrig != seen.String() {
		return fmt.Sprintf("%s (%v)", seenOrig, seen)
	}
	return seen.String()
}

func anyRealUse(par *ssa.Parameter, pos int) bool {
refLoop:
	for _, ref := range *par.Referrers() {
		switch x := ref.(type) {
		case *ssa.Call:
			if x.Call.Value != par.Parent() {
				return true // not a recursive call
			}
			for i, arg := range x.Call.Args {
				if arg != par {
					continue
				}
				if i == pos {
					// reused directly in a recursive call
					continue refLoop
				}
			}
			return true
		case *ssa.Store:
			if insertedStore(x) {
				continue // inserted by go/ssa, not from the code
			}
			return true
		default:
			return true
		}
	}
	return false
}

func insertedStore(instr ssa.Instruction) bool {
	if instr.Pos() != token.NoPos {
		return false
	}
	store, ok := instr.(*ssa.Store)
	if !ok {
		return false
	}
	alloc, ok := store.Addr.(*ssa.Alloc)
	// we want exactly one use of this alloc value for it to be
	// inserted by ssa and dummy - the alloc instruction itself.
	return ok && len(*alloc.Referrers()) == 1
}

var rxHarmlessCall = regexp.MustCompile(`(?i)\b(log(ger)?|errors)\b|\bf?print`)

// dummyImpl reports whether a block is a dummy implementation. This is
// true if the block will almost immediately panic, throw or return
// constants only.
func dummyImpl(blk *ssa.BasicBlock) bool {
	var ops [8]*ssa.Value
	for _, instr := range blk.Instrs {
		if insertedStore(instr) {
			continue // inserted by go/ssa, not from the code
		}
		for _, val := range instr.Operands(ops[:0]) {
			switch x := (*val).(type) {
			case nil, *ssa.Const, *ssa.ChangeType, *ssa.Alloc,
				*ssa.MakeInterface, *ssa.MakeMap,
				*ssa.Function, *ssa.Global,
				*ssa.IndexAddr, *ssa.Slice,
				*ssa.UnOp, *ssa.Parameter:
			case *ssa.Call:
				if rxHarmlessCall.MatchString(x.Call.Value.String()) {
					continue
				}
			default:
				return false
			}
		}
		switch x := instr.(type) {
		case *ssa.Alloc, *ssa.Store, *ssa.UnOp, *ssa.BinOp,
			*ssa.MakeInterface, *ssa.MakeMap, *ssa.Extract,
			*ssa.IndexAddr, *ssa.FieldAddr, *ssa.Slice,
			*ssa.Lookup, *ssa.ChangeType, *ssa.TypeAssert,
			*ssa.Convert, *ssa.ChangeInterface:
			// non-trivial expressions in panic/log/print
			// calls
		case *ssa.Return, *ssa.Panic:
			return true
		case *ssa.Call:
			if rxHarmlessCall.MatchString(x.Call.Value.String()) {
				continue
			}
			return x.Call.Value.Name() == "throw" // runtime's panic
		default:
			return false
		}
	}
	return false
}

func (c *Checker) declCounts(pkgDir string, pkgName string) map[string]int {
	if m := c.cachedDeclCounts[pkgDir]; m != nil {
		return m
	}
	fset := token.NewFileSet()
	pkgs, err := parser.ParseDir(fset, pkgDir, nil, 0)
	if err != nil {
		println(err.Error())
		c.cachedDeclCounts[pkgDir] = map[string]int{}
		return map[string]int{}
	}
	pkg := pkgs[pkgName]
	count := make(map[string]int)
	for _, file := range pkg.Files {
		for _, decl := range file.Decls {
			fd, _ := decl.(*ast.FuncDecl)
			if fd == nil {
				continue
			}
			name := astPrefix(fd.Recv) + fd.Name.Name
			count[name]++
		}
	}
	c.cachedDeclCounts[pkgDir] = count
	return count
}

func astPrefix(recv *ast.FieldList) string {
	if recv == nil {
		return ""
	}
	expr := recv.List[0].Type
	for {
		star, _ := expr.(*ast.StarExpr)
		if star == nil {
			break
		}
		expr = star.X
	}
	id := expr.(*ast.Ident)
	return id.Name + "."
}

func (c *Checker) multipleImpls(info *loader.PackageInfo, fn *ssa.Function) bool {
	if fn.Parent() != nil { // nested func
		return false
	}
	path := c.prog.Fset.Position(fn.Pos()).Filename
	count := c.declCounts(filepath.Dir(path), info.Pkg.Name())
	name := fn.Name()
	if fn.Signature.Recv() != nil {
		tp := fn.Params[0].Type()
		for {
			point, _ := tp.(*types.Pointer)
			if point == nil {
				break
			}
			tp = point.Elem()
		}
		named := tp.(*types.Named)
		name = named.Obj().Name() + "." + name
	}
	return count[name] > 1
}

func receivesExtractedArgs(sign *types.Signature, call *ssa.Call) bool {
	if call == nil {
		return false
	}
	if sign.Params().Len() < 2 {
		return false // extracting into one param is ok
	}
	args := call.Operands(nil)
	for i, arg := range args {
		if i == 0 {
			continue // *ssa.Function, func itself
		}
		if i == 1 && sign.Recv() != nil {
			continue // method receiver
		}
		if _, ok := (*arg).(*ssa.Extract); !ok {
			return false
		}
	}
	return true
}

func paramDesc(i int, v *types.Var) string {
	name := v.Name()
	if name != "" {
		return name
	}
	return fmt.Sprintf("%d (%s)", i, v.Type().String())
}

func requiredViaCall(fn *ssa.Function, calls []*callgraph.Edge) bool {
	for _, edge := range calls {
		call := edge.Site.Value()
		if receivesExtractedArgs(fn.Signature, call) {
			// called via function(results())
			return true
		}
		_, ok := edge.Site.Common().Value.(*ssa.Function)
		if !ok {
			// called via a parameter or field, type
			// is set in stone.
			return true
		}
	}
	return false
}
