// Copyright 2022 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 stubmethods provides the analysis logic for the quick fix
// to "Declare missing methods of TYPE" errors. (The fix logic lives
// in golang.stubMethodsFixer.)
package stubmethods

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

	"golang.org/x/tools/go/ast/edge"
	"golang.org/x/tools/go/ast/inspector"
	"golang.org/x/tools/internal/typesinternal"

	"golang.org/x/tools/gopls/internal/cache/parsego"
	"golang.org/x/tools/gopls/internal/util/typesutil"
)

// TODO(adonovan): eliminate the confusing Fset parameter; only the
// file name and byte offset of Concrete are needed.

// IfaceStubInfo represents a concrete type
// that wants to stub out an interface type
type IfaceStubInfo struct {
	// Interface is the interface that the client wants to implement.
	// When the interface is defined, the underlying object will be a TypeName.
	// Note that we keep track of types.Object instead of types.Type in order
	// to keep a reference to the declaring object's package and the ast file
	// in the case where the concrete type file requires a new import that happens to be renamed
	// in the interface file.
	// TODO(marwan-at-work): implement interface literals.
	Fset      *token.FileSet // the FileSet used to type-check the types below
	Interface *types.TypeName
	Concrete  typesinternal.NamedOrAlias
	Pointer   bool
}

// GetIfaceStubInfo determines whether the "missing method error"
// can be used to deduced what the concrete and interface types are.
//
// TODO(adonovan): this function (and its following 5 helpers) tries
// to deduce a pair of (concrete, interface) types that are related by
// an assignment, either explicitly or through a return statement or
// function call. This is essentially what the refactor/satisfy does,
// more generally. Refactor to share logic, after auditing 'satisfy'
// for safety on ill-typed code.
func GetIfaceStubInfo(fset *token.FileSet, info *types.Info, pgf *parsego.File, pos, end token.Pos) *IfaceStubInfo {
	cur, _ := pgf.Cursor().FindByPos(pos, end)
	for cur := range cur.Enclosing() {
		// TODO: do cur = unparenEnclosing(cur) first, once CL 701035 lands.
		switch cur.ParentEdgeKind() {
		case edge.ValueSpec_Values:
			return fromValueSpec(fset, info, cur)
		case edge.ReturnStmt_Results:
			// An error here may not indicate a real error the user should know about, but it may.
			// Therefore, it would be best to log it out for debugging/reporting purposes instead of ignoring
			// it. However, event.Log takes a context which is not passed via the analysis package.
			// TODO(marwan-at-work): properly log this error.
			si, _ := fromReturnStmt(fset, info, cur)
			return si
		case edge.AssignStmt_Rhs:
			return fromAssignStmt(fset, info, cur)
		case edge.CallExpr_Args:
			// Note that some call expressions don't carry the interface type
			// because they don't point to a function or method declaration elsewhere.
			// For eaxmple, "var Interface = (*Concrete)(nil)". In that case, continue
			// this loop to encounter other possibilities such as *ast.ValueSpec or others.
			si := fromCallExpr(fset, info, cur)
			if si != nil {
				return si
			}
		}
	}
	return nil
}

// Emit writes to out the missing methods of si.Concrete required for it to implement si.Interface
func (si *IfaceStubInfo) Emit(out *bytes.Buffer, qual types.Qualifier) error {
	conc := si.Concrete.Obj()
	// Record all direct methods of the current object
	concreteFuncs := make(map[string]struct{})
	if named, ok := types.Unalias(si.Concrete).(*types.Named); ok {
		for method := range named.Methods() {
			concreteFuncs[method.Name()] = struct{}{}
		}
	}

	var (
		missing []*types.Func
		// Find subset of interface methods that the concrete type lacks.
		ifaceType = si.Interface.Type().Underlying().(*types.Interface)
	)

	for imethod := range ifaceType.Methods() {
		cmethod, index, _ := types.LookupFieldOrMethod(si.Concrete, si.Pointer, imethod.Pkg(), imethod.Name())
		if cmethod == nil {
			missing = append(missing, imethod)
			continue
		}

		if _, ok := cmethod.(*types.Var); ok {
			// len(LookupFieldOrMethod.index) = 1 => conflict, >1 => shadow.
			return fmt.Errorf("adding method %s.%s would conflict with (or shadow) existing field",
				conc.Name(), imethod.Name())
		}

		if _, exist := concreteFuncs[imethod.Name()]; exist {
			if !types.Identical(cmethod.Type(), imethod.Type()) {
				return fmt.Errorf("method %s.%s already exists but has the wrong type: got %s, want %s",
					conc.Name(), imethod.Name(), cmethod.Type(), imethod.Type())
			}
			continue
		}

		if len(index) > 0 {
			// len(index) > 0 implies that the current method is already
			// implemented on some embedded object.
			// Do not generate a method if it would result in shadowing, but
			// continue generating other parts of the stub.
			// For example, if some struct A partially implements an interface I
			// and another struct B embeds A, we should not implement methods on
			// B that are already implemented on A, as these would be shadowed
			// methods.
			continue
		}

		missing = append(missing, imethod)
	}
	if len(missing) == 0 {
		return fmt.Errorf("no missing methods found")
	}

	// Format interface name (used only in a comment).
	iface := si.Interface.Name()
	if ipkg := si.Interface.Pkg(); ipkg != nil && ipkg != conc.Pkg() {
		iface = ipkg.Name() + "." + iface
	}

	// Pointer receiver?
	var star string
	if si.Pointer {
		star = "*"
	}

	// If there are any that have named receiver, choose the first one.
	// Otherwise, use lowercase for the first letter of the object.
	rn := strings.ToLower(si.Concrete.Obj().Name()[0:1])
	if named, ok := types.Unalias(si.Concrete).(*types.Named); ok {
		for method := range named.Methods() {
			if recv := method.Signature().Recv(); recv.Name() != "" {
				rn = recv.Name()
				break
			}
		}
	}

	// Check for receiver name conflicts
	checkRecvName := func(tuple *types.Tuple) bool {
		for v := range tuple.Variables() {
			if rn == v.Name() {
				return true
			}
		}
		return false
	}

	for index := range missing {
		mrn := rn + " "
		sig := missing[index].Signature()
		if checkRecvName(sig.Params()) || checkRecvName(sig.Results()) {
			mrn = ""
		}

		fmt.Fprintf(out, `// %s implements [%s].
func (%s%s%s%s) %s%s {
	panic("unimplemented")
}
`,
			missing[index].Name(),
			iface,
			mrn,
			star,
			si.Concrete.Obj().Name(),
			typesutil.FormatTypeParams(si.Concrete.TypeParams()),
			missing[index].Name(),
			strings.TrimPrefix(types.TypeString(missing[index].Type(), qual), "func"))
	}
	return nil
}

// fromCallExpr tries to find an *ast.CallExpr's function declaration and
// analyzes a function call's signature against the passed in call argument to deduce
// the concrete and interface types.
func fromCallExpr(fset *token.FileSet, info *types.Info, curCallArg inspector.Cursor) *IfaceStubInfo {

	call := curCallArg.Parent().Node().(*ast.CallExpr)
	arg := curCallArg.Node().(ast.Expr)

	concType, pointer := concreteType(arg, info)
	if concType == nil || concType.Obj().Pkg() == nil {
		return nil
	}
	tv, ok := info.Types[call.Fun]
	if !ok {
		return nil
	}
	sig, ok := types.Unalias(tv.Type).(*types.Signature)
	if !ok {
		return nil
	}

	argIdx := curCallArg.ParentEdgeIndex()
	var paramType types.Type
	if sig.Variadic() && argIdx >= sig.Params().Len()-1 {
		v := sig.Params().At(sig.Params().Len() - 1)
		if s, _ := v.Type().(*types.Slice); s != nil {
			paramType = s.Elem()
		}
	} else if argIdx < sig.Params().Len() {
		paramType = sig.Params().At(argIdx).Type()
	}
	if paramType == nil {
		return nil // A type error prevents us from determining the param type.
	}
	iface := ifaceObjFromType(paramType)
	if iface == nil {
		return nil
	}
	return &IfaceStubInfo{
		Fset:      fset,
		Concrete:  concType,
		Pointer:   pointer,
		Interface: iface,
	}
}

// fromReturnStmt analyzes a "return" statement to extract
// a concrete type that is trying to be returned as an interface type.
//
// For example, func() io.Writer { return myType{} }
// would return StubIfaceInfo with the interface being io.Writer and the concrete type being myType{}.
func fromReturnStmt(fset *token.FileSet, info *types.Info, curResult inspector.Cursor) (*IfaceStubInfo, error) {
	concType, pointer := concreteType(curResult.Node().(ast.Expr), info)
	if concType == nil || concType.Obj().Pkg() == nil {
		return nil, nil // result is not a named or *named or alias thereof
	}
	// Inv: the return is not a spread return,
	// such as "return f()" where f() has tuple type.
	conc := concType.Obj()
	if conc.Parent() != conc.Pkg().Scope() {
		return nil, fmt.Errorf("local type %q cannot be stubbed", conc.Name())
	}

	sig := typesutil.EnclosingSignature(curResult, info)
	if sig == nil {
		// Either curResult is not within a function (incontheivable?),
		// or the function's type information is missing (in which case
		// EnclosingSignature will have called bug.Report).
		// Don't report a second bug here.
		// See https://go.dev/issue/70666.
		return nil, fmt.Errorf("internal error: return statement lacks type information or enclosing function (issue 70666)")
	}
	rets := sig.Results()
	// The return operands and function results must match.
	// (Spread returns were rejected earlier.)
	ret := curResult.Parent().Node().(*ast.ReturnStmt)
	if rets.Len() != len(ret.Results) {
		return nil, fmt.Errorf("%d-operand return statement in %d-result function",
			len(ret.Results),
			rets.Len())
	}
	iface := ifaceObjFromType(rets.At(curResult.ParentEdgeIndex()).Type())
	if iface == nil {
		return nil, nil
	}
	return &IfaceStubInfo{
		Fset:      fset,
		Concrete:  concType,
		Pointer:   pointer,
		Interface: iface,
	}, nil
}

// fromValueSpec returns *StubIfaceInfo from a variable declaration such as
// var x io.Writer = &T{}
func fromValueSpec(fset *token.FileSet, info *types.Info, curValue inspector.Cursor) *IfaceStubInfo {

	rhs := curValue.Node().(ast.Expr)
	spec := curValue.Parent().Node().(*ast.ValueSpec)

	// Possible implicit/explicit conversion to interface type?
	ifaceNode := spec.Type // var _ myInterface = ...
	if call, ok := rhs.(*ast.CallExpr); ok && ifaceNode == nil && len(call.Args) == 1 {
		// var _ = myInterface(v)
		ifaceNode = call.Fun
		rhs = call.Args[0]
	}
	concType, pointer := concreteType(rhs, info)
	if concType == nil || concType.Obj().Pkg() == nil {
		return nil
	}
	conc := concType.Obj()
	if conc.Parent() != conc.Pkg().Scope() {
		return nil
	}

	ifaceObj := ifaceType(ifaceNode, info)
	if ifaceObj == nil {
		return nil
	}
	return &IfaceStubInfo{
		Fset:      fset,
		Concrete:  concType,
		Interface: ifaceObj,
		Pointer:   pointer,
	}
}

// fromAssignStmt returns *StubIfaceInfo from a variable assignment such as
// var x io.Writer
// x = &T{}
func fromAssignStmt(fset *token.FileSet, info *types.Info, curRhs inspector.Cursor) *IfaceStubInfo {
	// The interface conversion error in an assignment is against the RHS:
	//
	//      var x io.Writer
	//      x = &T{} // error: missing method
	//          ^^^^

	assign := curRhs.Parent().Node().(*ast.AssignStmt)
	lhs, rhs := assign.Lhs[curRhs.ParentEdgeIndex()], curRhs.Node().(ast.Expr)

	ifaceObj := ifaceType(lhs, info)
	if ifaceObj == nil {
		return nil
	}
	concType, pointer := concreteType(rhs, info)
	if concType == nil || concType.Obj().Pkg() == nil {
		return nil
	}
	conc := concType.Obj()
	if conc.Parent() != conc.Pkg().Scope() {
		return nil
	}
	return &IfaceStubInfo{
		Fset:      fset,
		Concrete:  concType,
		Interface: ifaceObj,
		Pointer:   pointer,
	}
}

// ifaceType returns the named interface type to which e refers, if any.
func ifaceType(e ast.Expr, info *types.Info) *types.TypeName {
	tv, ok := info.Types[e]
	if !ok {
		return nil
	}
	return ifaceObjFromType(tv.Type)
}

func ifaceObjFromType(t types.Type) *types.TypeName {
	named, ok := types.Unalias(t).(*types.Named)
	if !ok {
		return nil
	}
	if !types.IsInterface(named) {
		return nil
	}
	// Interfaces defined in the "builtin" package return nil a Pkg().
	// But they are still real interfaces that we need to make a special case for.
	// Therefore, protect gopls from panicking if a new interface type was added in the future.
	if named.Obj().Pkg() == nil && named.Obj().Name() != "error" {
		return nil
	}
	return named.Obj()
}

// concreteType tries to extract the *types.Named that defines
// the concrete type given the ast.Expr where the "missing method"
// or "conversion" errors happened. If the concrete type is something
// that cannot have methods defined on it (such as basic types), this
// method will return a nil *types.Named. The second return parameter
// is a boolean that indicates whether the concreteType was defined as a
// pointer or value.
func concreteType(e ast.Expr, info *types.Info) (*types.Named, bool) {
	tv, ok := info.Types[e]
	if !ok {
		return nil, false
	}
	typ := tv.Type
	ptr, isPtr := types.Unalias(typ).(*types.Pointer)
	if isPtr {
		typ = ptr.Elem()
	}
	named, ok := types.Unalias(typ).(*types.Named)
	if !ok {
		return nil, false
	}
	return named, isPtr
}
