package sa4006

import (
	"fmt"
	"go/ast"

	"honnef.co/go/tools/analysis/code"
	"honnef.co/go/tools/analysis/facts/generated"
	"honnef.co/go/tools/analysis/lint"
	"honnef.co/go/tools/analysis/report"
	"honnef.co/go/tools/go/ir"
	"honnef.co/go/tools/go/ir/irutil"
	"honnef.co/go/tools/internal/passes/buildir"

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

var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
	Analyzer: &analysis.Analyzer{
		Name:     "SA4006",
		Run:      run,
		Requires: []*analysis.Analyzer{buildir.Analyzer, generated.Analyzer},
	},
	Doc: &lint.RawDocumentation{
		Title:    `A value assigned to a variable is never read before being overwritten. Forgotten error check or dead code?`,
		Since:    "2017.1",
		Severity: lint.SeverityWarning,
		MergeIf:  lint.MergeIfAll,
	},
})

var Analyzer = SCAnalyzer.Analyzer

func run(pass *analysis.Pass) (any, error) {
	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
		if irutil.IsExample(fn) {
			continue
		}
		node := fn.Source()
		if node == nil {
			continue
		}
		if gen, ok := code.Generator(pass, node.Pos()); ok && gen == generated.Goyacc {
			// Don't flag unused values in code generated by goyacc.
			// There may be hundreds of those due to the way the state
			// machine is constructed.
			continue
		}

		switchTags := map[ir.Value]struct{}{}
		ast.Inspect(node, func(node ast.Node) bool {
			s, ok := node.(*ast.SwitchStmt)
			if !ok {
				return true
			}
			v, _ := fn.ValueForExpr(s.Tag)
			switchTags[v] = struct{}{}
			return true
		})

		// OPT(dh): don't use a map, possibly use a bitset
		var hasUse func(v ir.Value, seen map[ir.Value]struct{}) bool
		hasUse = func(v ir.Value, seen map[ir.Value]struct{}) bool {
			if _, ok := seen[v]; ok {
				return false
			}
			if _, ok := switchTags[v]; ok {
				return true
			}
			refs := v.Referrers()
			if refs == nil {
				// TODO investigate why refs can be nil
				return true
			}
			for _, ref := range *refs {
				switch ref := ref.(type) {
				case *ir.DebugRef:
				case *ir.Sigma:
					if seen == nil {
						seen = map[ir.Value]struct{}{}
					}
					seen[v] = struct{}{}
					if hasUse(ref, seen) {
						return true
					}
				case *ir.Phi:
					if seen == nil {
						seen = map[ir.Value]struct{}{}
					}
					seen[v] = struct{}{}
					if hasUse(ref, seen) {
						return true
					}
				default:
					return true
				}
			}
			return false
		}

		ast.Inspect(node, func(node ast.Node) bool {
			assign, ok := node.(*ast.AssignStmt)
			if !ok {
				return true
			}
			if len(assign.Lhs) > 1 && len(assign.Rhs) == 1 {
				// Either a function call with multiple return values,
				// or a comma-ok assignment

				val, _ := fn.ValueForExpr(assign.Rhs[0])
				if val == nil {
					return true
				}
				refs := val.Referrers()
				if refs == nil {
					return true
				}
				for _, ref := range *refs {
					ex, ok := ref.(*ir.Extract)
					if !ok {
						continue
					}
					if !hasUse(ex, nil) {
						lhs := assign.Lhs[ex.Index]
						if ident, ok := lhs.(*ast.Ident); !ok || ok && ident.Name == "_" {
							continue
						}
						report.Report(pass, assign, fmt.Sprintf("this value of %s is never used", lhs))
					}
				}
				return true
			}
			for i, lhs := range assign.Lhs {
				rhs := assign.Rhs[i]
				if ident, ok := lhs.(*ast.Ident); !ok || ok && ident.Name == "_" {
					continue
				}
				val, _ := fn.ValueForExpr(rhs)
				if val == nil {
					continue
				}

				if _, ok := val.(*ir.Const); ok {
					// a zero-valued constant, for example in 'foo := []string(nil)'
					continue
				}
				if !hasUse(val, nil) {
					report.Report(pass, assign, fmt.Sprintf("this value of %s is never used", lhs))
				}
			}
			return true
		})
	}
	return nil, nil
}
