// Copyright 2025 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.

package modernize

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

	"golang.org/x/tools/go/analysis"
	"golang.org/x/tools/go/ast/edge"
	"golang.org/x/tools/go/ast/inspector"
	"golang.org/x/tools/go/types/typeutil"
	"golang.org/x/tools/internal/analysis/analyzerutil"
	typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
	"golang.org/x/tools/internal/astutil"
	"golang.org/x/tools/internal/stdlib"
	"golang.org/x/tools/internal/typesinternal/typeindex"
)

var StdIteratorsAnalyzer = &analysis.Analyzer{
	Name: "stditerators",
	Doc:  analyzerutil.MustExtractDoc(doc, "stditerators"),
	Requires: []*analysis.Analyzer{
		typeindexanalyzer.Analyzer,
	},
	Run: stditerators,
	URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stditerators",
}

// stditeratorsTable records std types that have legacy T.{Len,At}
// iteration methods as well as a newer T.All method that returns an
// iter.Seq.
var stditeratorsTable = [...]struct {
	pkgpath, typename, lenmethod, atmethod, itermethod, elemname string

	seqn int // 1 or 2 => "for x" or "for _, x"
}{
	// Example: in go/types, (*Tuple).Variables returns an
	// iterator that replaces a loop over (*Tuple).{Len,At}.
	// The loop variable is named "v".
	{"go/types", "Interface", "NumEmbeddeds", "EmbeddedType", "EmbeddedTypes", "etyp", 1},
	{"go/types", "Interface", "NumExplicitMethods", "ExplicitMethod", "ExplicitMethods", "method", 1},
	{"go/types", "Interface", "NumMethods", "Method", "Methods", "method", 1},
	{"go/types", "MethodSet", "Len", "At", "Methods", "method", 1},
	{"go/types", "Named", "NumMethods", "Method", "Methods", "method", 1},
	{"go/types", "Scope", "NumChildren", "Child", "Children", "child", 1},
	{"go/types", "Struct", "NumFields", "Field", "Fields", "field", 1},
	{"go/types", "Tuple", "Len", "At", "Variables", "v", 1},
	{"go/types", "TypeList", "Len", "At", "Types", "t", 1},
	{"go/types", "TypeParamList", "Len", "At", "TypeParams", "tparam", 1},
	{"go/types", "Union", "Len", "Term", "Terms", "term", 1},
	{"reflect", "Type", "NumField", "Field", "Fields", "field", 1},
	{"reflect", "Type", "NumMethod", "Method", "Methods", "method", 1},
	{"reflect", "Type", "NumIn", "In", "Ins", "in", 1},
	{"reflect", "Type", "NumOut", "Out", "Outs", "out", 1},
	{"reflect", "Value", "NumField", "Field", "Fields", "field", 2},
	{"reflect", "Value", "NumMethod", "Method", "Methods", "method", 2},
}

// stditerators suggests fixes to replace loops using Len/At-style
// iterator APIs by a range loop over an iterator. The set of
// participating types and methods is defined by [iteratorsTable].
//
// Pattern:
//
//	for i := 0; i < x.Len(); i++ {
//		use(x.At(i))
//	}
//
// =>
//
//	for elem := range x.All() {
//		use(elem)
//	}
//
// Variant:
//
//	for i := range x.Len() { ... }
//
// Note: Iterators have a dynamic cost. How do we know that
// the user hasn't intentionally chosen not to use an
// iterator for that reason? We don't want to go fix to
// undo optimizations. Do we need a suppression mechanism?
//
// TODO(adonovan): recognize the more complex patterns that
// could make full use of both components of an iter.Seq2, e.g.
//
//	for i := 0; i < v.NumField(); i++ {
//		use(v.Field(i), v.Type().Field(i))
//	}
//
// =>
//
//	for structField, field := range v.Fields() {
//		use(structField, field)
//	}
func stditerators(pass *analysis.Pass) (any, error) {
	var (
		index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
		info  = pass.TypesInfo
	)

	for _, row := range stditeratorsTable {
		// Don't offer fixes within the package
		// that defines the iterator in question.
		if within(pass, row.pkgpath) {
			continue
		}

		var (
			lenMethod = index.Selection(row.pkgpath, row.typename, row.lenmethod)
			atMethod  = index.Selection(row.pkgpath, row.typename, row.atmethod)
		)

		// chooseName returns an appropriate fresh name
		// for the index variable of the iterator loop
		// whose body is specified.
		//
		// If the loop body starts with
		//
		//     for ... { e := x.At(i); use(e) }
		//
		// or
		//
		//     for ... { if e := x.At(i); cond { use(e) } }
		//
		// then chooseName prefers the name e and additionally
		// returns the var's symbol. We'll transform this to:
		//
		//     for e := range x.Len() { e := e; use(e) }
		//
		// which leaves a redundant assignment that a
		// subsequent 'forvar' pass will eliminate.
		chooseName := func(curBody inspector.Cursor, x ast.Expr, i *types.Var) (string, *types.Var) {

			// isVarAssign reports whether stmt has the form v := x.At(i)
			// and returns the variable if so.
			isVarAssign := func(stmt ast.Stmt) *types.Var {
				if assign, ok := stmt.(*ast.AssignStmt); ok &&
					assign.Tok == token.DEFINE &&
					len(assign.Lhs) == 1 &&
					len(assign.Rhs) == 1 &&
					is[*ast.Ident](assign.Lhs[0]) {
					// call to x.At(i)?
					if call, ok := assign.Rhs[0].(*ast.CallExpr); ok &&
						typeutil.Callee(info, call) == atMethod &&
						astutil.EqualSyntax(ast.Unparen(call.Fun).(*ast.SelectorExpr).X, x) &&
						is[*ast.Ident](call.Args[0]) &&
						info.Uses[call.Args[0].(*ast.Ident)] == i {
						// Have: elem := x.At(i)
						id := assign.Lhs[0].(*ast.Ident)
						return info.Defs[id].(*types.Var)
					}
				}
				return nil
			}

			body := curBody.Node().(*ast.BlockStmt)
			if len(body.List) > 0 {
				// Is body { elem := x.At(i); ... } ?
				if v := isVarAssign(body.List[0]); v != nil {
					return v.Name(), v
				}

				// Or { if elem := x.At(i); cond { ... } } ?
				if ifstmt, ok := body.List[0].(*ast.IfStmt); ok && ifstmt.Init != nil {
					if v := isVarAssign(ifstmt.Init); v != nil {
						return v.Name(), v
					}
				}
			}

			loop := curBody.Parent().Node()
			// We generate a new name only if the preferred name is already declared here
			// and is used within the loop body.
			name := freshName(info, index, info.Scopes[loop], loop.Pos(), curBody, curBody, token.NoPos, row.elemname)
			return name, nil
		}

		// Process each call of x.Len().
	nextCall:
		for curLenCall := range index.Calls(lenMethod) {
			lenSel, ok := ast.Unparen(curLenCall.Node().(*ast.CallExpr).Fun).(*ast.SelectorExpr)
			if !ok {
				continue
			}
			// lenSel is "x.Len"

			var (
				rng      analysis.Range   // where to report diagnostic
				curBody  inspector.Cursor // loop body
				indexVar *types.Var       // old loop index var
				elemVar  *types.Var       // existing "elem := x.At(i)" var, if present
				elem     string           // name for new loop var
				edits    []analysis.TextEdit
			)

			// Analyze enclosing loop.
			switch curLenCall.ParentEdgeKind() {
			case edge.BinaryExpr_Y:
				// pattern 1: for i := 0; i < x.Len(); i++ { ... }
				var (
					curCmp = curLenCall.Parent()
					cmp    = curCmp.Node().(*ast.BinaryExpr)
				)
				if cmp.Op != token.LSS ||
					curCmp.ParentEdgeKind() != edge.ForStmt_Cond {
					continue
				}
				if id, ok := cmp.X.(*ast.Ident); ok {
					// Have: for _; i < x.Len(); _ { ... }
					var (
						v      = info.Uses[id].(*types.Var)
						curFor = curCmp.Parent()
						loop   = curFor.Node().(*ast.ForStmt)
					)
					if v != isIncrementLoop(info, loop) {
						continue
					}
					// Have: for i := 0; i < x.Len(); i++ { ... }.
					//       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
					rng = astutil.RangeOf(loop.For, loop.Post.End())
					indexVar = v
					curBody = curFor.ChildAt(edge.ForStmt_Body, -1)
					elem, elemVar = chooseName(curBody, lenSel.X, indexVar)
					elemPrefix := cond(row.seqn == 2, "_, ", "")

					//	for i       := 0; i < x.Len(); i++ {
					//          ----       -------  ---  -----
					//	for elem    := range  x.All()      {
					// or   for _, elem := ...
					edits = []analysis.TextEdit{
						{
							Pos:     v.Pos(),
							End:     v.Pos() + token.Pos(len(v.Name())),
							NewText: []byte(elemPrefix + elem),
						},
						{
							Pos:     loop.Init.(*ast.AssignStmt).Rhs[0].Pos(),
							End:     cmp.Y.Pos(),
							NewText: []byte("range "),
						},
						{
							Pos:     lenSel.Sel.Pos(),
							End:     lenSel.Sel.End(),
							NewText: []byte(row.itermethod),
						},
						{
							Pos: curLenCall.Node().End(),
							End: loop.Post.End(),
						},
					}
				}

			case edge.RangeStmt_X:
				// pattern 2: for i := range x.Len() { ... }
				var (
					curRange = curLenCall.Parent()
					loop     = curRange.Node().(*ast.RangeStmt)
				)
				if id, ok := loop.Key.(*ast.Ident); ok &&
					loop.Value == nil &&
					loop.Tok == token.DEFINE {
					// Have: for i := range x.Len() { ... }
					//                ~~~~~~~~~~~~~

					rng = astutil.RangeOf(loop.Range, loop.X.End())
					indexVar = info.Defs[id].(*types.Var)
					curBody = curRange.ChildAt(edge.RangeStmt_Body, -1)
					elem, elemVar = chooseName(curBody, lenSel.X, indexVar)
					elemPrefix := cond(row.seqn == 2, "_, ", "")

					//	for i    := range x.Len() {
					//          ----            ---
					//	for elem := range x.All() {
					edits = []analysis.TextEdit{
						{
							Pos:     loop.Key.Pos(),
							End:     loop.Key.End(),
							NewText: []byte(elemPrefix + elem),
						},
						{
							Pos:     lenSel.Sel.Pos(),
							End:     lenSel.Sel.End(),
							NewText: []byte(row.itermethod),
						},
					}
				}
			}

			if indexVar == nil {
				continue // no loop of the required form
			}

			// TODO(adonovan): what about possible
			// modifications of x within the loop?
			// Aliasing seems to make a conservative
			// treatment impossible.

			// Check that all uses of var i within loop body are x.At(i).
			for curUse := range index.Uses(indexVar) {
				if !curBody.Contains(curUse) {
					continue
				}
				if ek, argidx := curUse.ParentEdge(); ek != edge.CallExpr_Args || argidx != 0 {
					continue nextCall // use is not arg of call
				}
				curAtCall := curUse.Parent()
				atCall := curAtCall.Node().(*ast.CallExpr)
				if typeutil.Callee(info, atCall) != atMethod {
					continue nextCall // use is not arg of call to T.At
				}
				atSel := ast.Unparen(atCall.Fun).(*ast.SelectorExpr)

				// Check receivers of Len, At calls match (syntactically).
				if !astutil.EqualSyntax(lenSel.X, atSel.X) {
					continue nextCall
				}

				// At each point of use, check that
				// the fresh variable is not shadowed
				// by an intervening local declaration
				// (or by the idiomatic elemVar optionally
				// found by chooseName).
				if obj := lookup(info, curAtCall, elem); obj != nil && obj != elemVar && obj.Pos() > indexVar.Pos() {
					// (Ideally, instead of giving up, we would
					// embellish the name and try again.)
					continue nextCall
				}

				// use(x.At(i))
				//     -------
				// use(elem   )
				edits = append(edits, analysis.TextEdit{
					Pos:     atCall.Pos(),
					End:     atCall.End(),
					NewText: []byte(elem),
				})
			}

			// Check file Go version is new enough for the iterator method.
			// (In the long run, version filters are not highly selective,
			// so there's no need to do them first, especially as this check
			// may be somewhat expensive.)
			if v, err := methodGoVersion(row.pkgpath, row.typename, row.itermethod); err != nil {
				panic(err)
			} else if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curLenCall), v.String()) {
				continue nextCall
			}

			pass.Report(analysis.Diagnostic{
				Pos: rng.Pos(),
				End: rng.End(),
				Message: fmt.Sprintf("%s/%s loop can simplified using %s.%s iteration",
					row.lenmethod, row.atmethod, row.typename, row.itermethod),
				SuggestedFixes: []analysis.SuggestedFix{{
					Message: fmt.Sprintf(
						"Replace %s/%s loop with %s.%s iteration",
						row.lenmethod, row.atmethod, row.typename, row.itermethod),
					TextEdits: edits,
				}},
			})
		}
	}
	return nil, nil
}

// -- helpers --

// methodGoVersion reports the version at which the method
// (pkgpath.recvtype).method appeared in the standard library.
func methodGoVersion(pkgpath, recvtype, method string) (stdlib.Version, error) {
	// TODO(adonovan): opt: this might be inefficient for large packages
	// like go/types. If so, memoize using a map (and kill two birds with
	// one stone by also memoizing the 'within' check above).
	for _, sym := range stdlib.PackageSymbols[pkgpath] {
		if sym.Kind == stdlib.Method {
			_, recv, name := sym.SplitMethod()
			if recv == recvtype && name == method {
				return sym.Version, nil
			}
		}
	}
	return 0, fmt.Errorf("methodGoVersion: %s.%s.%s missing from stdlib manifest", pkgpath, recvtype, method)
}
