// Copyright 2018 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 golang

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

	"golang.org/x/tools/go/ast/astutil"
	"golang.org/x/tools/gopls/internal/cache"
	"golang.org/x/tools/gopls/internal/file"
	"golang.org/x/tools/gopls/internal/protocol"
	"golang.org/x/tools/gopls/internal/settings"
	"golang.org/x/tools/internal/event"
	"golang.org/x/tools/internal/typesinternal"
)

// SignatureHelp returns information about the signature of the innermost
// function call enclosing the position, or nil if there is none.
func SignatureHelp(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range, context *protocol.SignatureHelpContext) (*protocol.SignatureInformation, error) {
	ctx, done := event.Start(ctx, "golang.SignatureHelp")
	defer done()

	// We need full type-checking here, as we must type-check function bodies in
	// order to provide signature help at the requested position.
	pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
	if err != nil {
		return nil, fmt.Errorf("getting file for SignatureHelp: %w", err)
	}
	start, end, err := pgf.RangePos(rng)
	if err != nil {
		return nil, err
	}
	// Find a call expression surrounding the query position.
	var callExpr *ast.CallExpr
	path, _ := astutil.PathEnclosingInterval(pgf.File, start, end)
	if path == nil {
		return nil, fmt.Errorf("cannot find node enclosing position")
	}
	info := pkg.TypesInfo()
	var fnval ast.Expr
loop:
	for i, node := range path {
		switch node := node.(type) {
		case *ast.Ident:
			// If the selected text is a function/method Ident or SelectorExpr,
			// even one not in function call position,
			// show help for its signature. Example:
			//    once.Do(initialize⁁)
			// should show help for initialize, not once.Do.
			if t := info.TypeOf(node); t != nil &&
				info.Defs[node] == nil &&
				is[*types.Signature](t.Underlying()) {
				if sel, ok := path[i+1].(*ast.SelectorExpr); ok && sel.Sel == node {
					fnval = sel // e.g. fmt.Println⁁
				} else {
					fnval = node
				}
				break loop
			}
		case *ast.CallExpr:
			// Beware: the ')' may be missing.
			if node.Lparen <= start && end <= node.Rparen {
				callExpr = node
				fnval = callExpr.Fun
				break loop
			}
		case *ast.FuncLit, *ast.FuncType, *ast.CompositeLit:
			// The user is within an anonymous function or
			// a composite literal, which may be the argument
			// to the *ast.CallExpr.
			// Don't show signature help in this case.
			return nil, nil
		case *ast.BasicLit:
			// golang/go#43397: don't offer signature help when the user is typing
			// in a string literal unless it was manually invoked or help is already active.
			if node.Kind == token.STRING &&
				(context == nil || (context.TriggerKind != protocol.SigInvoked && !context.IsRetrigger)) {
				return nil, nil
			}
		}
	}

	if fnval == nil {
		return nil, nil
	}

	// Get the type information for the function being called.
	var sig *types.Signature
	if tv, ok := info.Types[fnval]; !ok {
		return nil, fmt.Errorf("cannot get type for Fun %[1]T (%[1]v)", fnval)
	} else if tv.IsType() {
		return nil, nil // a conversion, not a call
	} else if sig, ok = tv.Type.Underlying().(*types.Signature); !ok {
		return nil, fmt.Errorf("call operand is not a func or type: %[1]T (%[1]v)", fnval)
	}
	// Inv: sig != nil

	// Get the object representing the function, if available.
	// There is no object in certain cases such as calling a function returned by
	// a function (e.g. "foo()()").
	var obj types.Object
	switch t := fnval.(type) {
	case *ast.Ident:
		obj = info.ObjectOf(t)
	case *ast.SelectorExpr:
		obj = info.ObjectOf(t.Sel)
	}

	if obj != nil && isBuiltin(obj) {
		// Special handling for error.Error, which is the only builtin method.
		if obj.Name() == "Error" {
			return &protocol.SignatureInformation{
				Label: "Error() string",
				// TODO(skewb1k): move the docstring for error.Error to builtin.go and reuse it across all relevant LSP methods.
				Documentation:   stringToSigInfoDocumentation("Error returns the error message.", snapshot.Options()),
				Parameters:      nil,
				ActiveParameter: nil,
			}, nil
		}
		s, err := NewBuiltinSignature(ctx, snapshot, obj.Name())
		if err != nil {
			return nil, err
		}
		return signatureInformation(s, snapshot.Options(), start, end, callExpr)
	}

	mq := MetadataQualifierForFile(snapshot, pgf.File, pkg.Metadata())
	qual := typesinternal.FileQualifier(pgf.File, pkg.Types())
	var (
		comment *ast.CommentGroup
		name    string
	)

	if obj != nil {
		comment, err = HoverDocForObject(ctx, snapshot, pkg.FileSet(), obj)
		if err != nil {
			return nil, err
		}
		name = obj.Name()
	} else {
		name = "func"
	}

	s, err := NewSignature(ctx, snapshot, pkg, sig, comment, qual, mq)
	if err != nil {
		return nil, err
	}
	s.name = name
	return signatureInformation(s, snapshot.Options(), start, end, callExpr)
}

func signatureInformation(sig *signature, options *settings.Options, start, end token.Pos, call *ast.CallExpr) (*protocol.SignatureInformation, error) {
	paramInfo := make([]protocol.ParameterInformation, 0, len(sig.params))
	for _, p := range sig.params {
		paramInfo = append(paramInfo, protocol.ParameterInformation{Label: p})
	}
	return &protocol.SignatureInformation{
		Label:           sig.name + sig.Format(),
		Documentation:   stringToSigInfoDocumentation(sig.doc, options),
		Parameters:      paramInfo,
		ActiveParameter: activeParameter(sig, start, end, call),
	}, nil
}

// activeParameter returns a pointer to a variable containing
// the index of the active parameter (if known), or nil otherwise.
func activeParameter(sig *signature, start, end token.Pos, call *ast.CallExpr) *uint32 {
	if call == nil {
		return nil
	}
	numParams := uint32(len(sig.params))
	if numParams == 0 {
		return nil
	}
	// Check if the position is even in the range of the arguments.
	if !(call.Lparen < start && end <= call.Rparen) {
		return nil
	}

	var activeParam uint32
	for _, arg := range call.Args {
		if end <= arg.End() {
			break
		}
		// Don't advance the active parameter for the last parameter of a variadic function.
		if !sig.variadic || activeParam < numParams-1 {
			activeParam++
		}
	}
	return &activeParam
}

func stringToSigInfoDocumentation(s string, options *settings.Options) *protocol.Or_SignatureInformation_documentation {
	v := s
	k := protocol.PlainText
	if options.PreferredContentFormat == protocol.Markdown {
		v = DocCommentToMarkdown(s, options)
		// whether or not content is newline terminated may not matter for LSP clients,
		// but our tests expect trailing newlines to be stripped.
		v = strings.TrimSuffix(v, "\n") // TODO(pjw): change the golden files
		k = protocol.Markdown
	}
	return &protocol.Or_SignatureInformation_documentation{
		Value: protocol.MarkupContent{
			Kind:  k,
			Value: v,
		},
	}
}
