package sa4031

import (
	"fmt"
	"go/ast"
	"go/token"
	"go/types"
	"sort"

	"honnef.co/go/tools/analysis/code"
	"honnef.co/go/tools/analysis/lint"
	"honnef.co/go/tools/analysis/report"
	"honnef.co/go/tools/go/ir"
	"honnef.co/go/tools/internal/passes/buildir"
	"honnef.co/go/tools/pattern"
	"honnef.co/go/tools/staticcheck/sa4022"

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

var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
	Analyzer: &analysis.Analyzer{
		Name:     "SA4031",
		Run:      run,
		Requires: []*analysis.Analyzer{buildir.Analyzer, inspect.Analyzer},
	},
	Doc: &lint.RawDocumentation{
		Title:    `Checking never-nil value against nil`,
		Since:    "2022.1",
		Severity: lint.SeverityWarning,
		MergeIf:  lint.MergeIfAny,
	},
})

var Analyzer = SCAnalyzer.Analyzer

var allocationNilCheckQ = pattern.MustParse(`(IfStmt _ cond@(BinaryExpr lhs op@(Or "==" "!=") (Builtin "nil")) _ _)`)

func run(pass *analysis.Pass) (any, error) {
	irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg

	var path []ast.Node
	fn := func(node ast.Node, stack []ast.Node) {
		m, ok := code.Match(pass, allocationNilCheckQ, node)
		if !ok {
			return
		}
		cond := m.State["cond"].(ast.Node)
		if _, ok := code.Match(pass, sa4022.CheckAddressIsNilQ, cond); ok {
			// Don't duplicate diagnostics reported by SA4022
			return
		}
		lhs := m.State["lhs"].(ast.Expr)
		path = path[:0]
		for i := len(stack) - 1; i >= 0; i-- {
			path = append(path, stack[i])
		}
		irfn := ir.EnclosingFunction(irpkg, path)
		if irfn == nil {
			// For example for functions named "_", because we don't generate IR for them.
			return
		}
		v, isAddr := irfn.ValueForExpr(lhs)
		if isAddr {
			return
		}

		seen := map[ir.Value]struct{}{}
		var values []ir.Value
		var neverNil func(v ir.Value, track bool) bool
		neverNil = func(v ir.Value, track bool) bool {
			if _, ok := seen[v]; ok {
				return true
			}
			seen[v] = struct{}{}
			switch v := v.(type) {
			case *ir.MakeClosure, *ir.Function:
				if track {
					values = append(values, v)
				}
				return true
			case *ir.MakeChan, *ir.MakeMap, *ir.MakeSlice, *ir.Alloc:
				if track {
					values = append(values, v)
				}
				return true
			case *ir.Slice:
				if track {
					values = append(values, v)
				}
				return neverNil(v.X, false)
			case *ir.FieldAddr:
				if track {
					values = append(values, v)
				}
				return neverNil(v.X, false)
			case *ir.Phi:
				for _, e := range v.Edges {
					if !neverNil(e, true) {
						return false
					}
				}
				return true
			default:
				return false
			}
		}

		if !neverNil(v, true) {
			return
		}

		var qualifier string
		if op := m.State["op"].(token.Token); op == token.EQL {
			qualifier = "never"
		} else {
			qualifier = "always"
		}
		fallback := fmt.Sprintf("this nil check is %s true", qualifier)

		sort.Slice(values, func(i, j int) bool { return values[i].Pos() < values[j].Pos() })

		if ident, ok := m.State["lhs"].(*ast.Ident); ok {
			if _, ok := pass.TypesInfo.ObjectOf(ident).(*types.Var); ok {
				var opts []report.Option
				if v.Parent() == irfn {
					if len(values) == 1 {
						opts = append(opts, report.Related(values[0], fmt.Sprintf("this is the value of %s", ident.Name)))
					} else {
						for _, vv := range values {
							opts = append(opts, report.Related(vv, fmt.Sprintf("this is one of the value of %s", ident.Name)))
						}
					}
				}

				switch v.(type) {
				case *ir.MakeClosure, *ir.Function:
					report.Report(pass, cond, "the checked variable contains a function and is never nil; did you mean to call it?", opts...)
				default:
					report.Report(pass, cond, fallback, opts...)
				}
			} else {
				if _, ok := v.(*ir.Function); ok {
					report.Report(pass, cond, "functions are never nil; did you mean to call it?")
				} else {
					report.Report(pass, cond, fallback)
				}
			}
		} else {
			if _, ok := v.(*ir.Function); ok {
				report.Report(pass, cond, "functions are never nil; did you mean to call it?")
			} else {
				report.Report(pass, cond, fallback)
			}
		}
	}
	code.PreorderStack(pass, fn, (*ast.IfStmt)(nil))
	return nil, nil
}
