// Copyright 2020 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 source

import (
	"context"
	"errors"
	"fmt"
	"go/ast"
	"go/token"
	"go/types"
	"path/filepath"

	"golang.org/x/tools/go/ast/astutil"
	"golang.org/x/tools/gopls/internal/lsp/protocol"
	"golang.org/x/tools/gopls/internal/span"
	"golang.org/x/tools/internal/event"
	"golang.org/x/tools/internal/event/tag"
)

// PrepareCallHierarchy returns an array of CallHierarchyItem for a file and the position within the file.
func PrepareCallHierarchy(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyItem, error) {
	ctx, done := event.Start(ctx, "source.PrepareCallHierarchy")
	defer done()

	identifier, err := Identifier(ctx, snapshot, fh, pos)
	if err != nil {
		if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) {
			return nil, nil
		}
		return nil, err
	}

	// The identifier can be nil if it is an import spec.
	if identifier == nil || identifier.Declaration.obj == nil {
		return nil, nil
	}

	if _, ok := identifier.Declaration.obj.Type().Underlying().(*types.Signature); !ok {
		return nil, nil
	}

	if len(identifier.Declaration.MappedRange) == 0 {
		return nil, nil
	}
	declMappedRange := identifier.Declaration.MappedRange[0]
	rng, err := declMappedRange.Range()
	if err != nil {
		return nil, err
	}

	callHierarchyItem := protocol.CallHierarchyItem{
		Name:           identifier.Name,
		Kind:           protocol.Function,
		Tags:           []protocol.SymbolTag{},
		Detail:         fmt.Sprintf("%s • %s", identifier.Declaration.obj.Pkg().Path(), filepath.Base(declMappedRange.URI().Filename())),
		URI:            protocol.DocumentURI(declMappedRange.URI()),
		Range:          rng,
		SelectionRange: rng,
	}
	return []protocol.CallHierarchyItem{callHierarchyItem}, nil
}

// IncomingCalls returns an array of CallHierarchyIncomingCall for a file and the position within the file.
func IncomingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyIncomingCall, error) {
	ctx, done := event.Start(ctx, "source.IncomingCalls")
	defer done()

	refs, err := References(ctx, snapshot, fh, pos, false)
	if err != nil {
		if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) {
			return nil, nil
		}
		return nil, err
	}

	return toProtocolIncomingCalls(ctx, snapshot, refs)
}

// toProtocolIncomingCalls returns an array of protocol.CallHierarchyIncomingCall for ReferenceInfo's.
// References inside same enclosure are assigned to the same enclosing function.
func toProtocolIncomingCalls(ctx context.Context, snapshot Snapshot, refs []*ReferenceInfo) ([]protocol.CallHierarchyIncomingCall, error) {
	// an enclosing node could have multiple calls to a reference, we only show the enclosure
	// once in the result but highlight all calls using FromRanges (ranges at which the calls occur)
	var incomingCalls = map[protocol.Location]*protocol.CallHierarchyIncomingCall{}
	for _, ref := range refs {
		refRange, err := ref.Range()
		if err != nil {
			return nil, err
		}

		callItem, err := enclosingNodeCallItem(snapshot, ref.pkg, ref.URI(), ref.ident.NamePos)
		if err != nil {
			event.Error(ctx, "error getting enclosing node", err, tag.Method.Of(ref.Name))
			continue
		}
		loc := protocol.Location{
			URI:   callItem.URI,
			Range: callItem.Range,
		}

		if incomingCall, ok := incomingCalls[loc]; ok {
			incomingCall.FromRanges = append(incomingCall.FromRanges, refRange)
			continue
		}
		incomingCalls[loc] = &protocol.CallHierarchyIncomingCall{
			From:       callItem,
			FromRanges: []protocol.Range{refRange},
		}
	}

	incomingCallItems := make([]protocol.CallHierarchyIncomingCall, 0, len(incomingCalls))
	for _, callItem := range incomingCalls {
		incomingCallItems = append(incomingCallItems, *callItem)
	}
	return incomingCallItems, nil
}

// enclosingNodeCallItem creates a CallHierarchyItem representing the function call at pos
func enclosingNodeCallItem(snapshot Snapshot, pkg Package, uri span.URI, pos token.Pos) (protocol.CallHierarchyItem, error) {
	pgf, err := pkg.File(uri)
	if err != nil {
		return protocol.CallHierarchyItem{}, err
	}

	var funcDecl *ast.FuncDecl
	var funcLit *ast.FuncLit // innermost function literal
	var litCount int
	// Find the enclosing function, if any, and the number of func literals in between.
	path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos)
outer:
	for _, node := range path {
		switch n := node.(type) {
		case *ast.FuncDecl:
			funcDecl = n
			break outer
		case *ast.FuncLit:
			litCount++
			if litCount > 1 {
				continue
			}
			funcLit = n
		}
	}

	nameIdent := path[len(path)-1].(*ast.File).Name
	kind := protocol.Package
	if funcDecl != nil {
		nameIdent = funcDecl.Name
		kind = protocol.Function
	}

	nameStart, nameEnd := nameIdent.Pos(), nameIdent.End()
	if funcLit != nil {
		nameStart, nameEnd = funcLit.Type.Func, funcLit.Type.Params.Pos()
		kind = protocol.Function
	}
	rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, nameStart, nameEnd).Range()
	if err != nil {
		return protocol.CallHierarchyItem{}, err
	}

	name := nameIdent.Name
	for i := 0; i < litCount; i++ {
		name += ".func()"
	}

	return protocol.CallHierarchyItem{
		Name:           name,
		Kind:           kind,
		Tags:           []protocol.SymbolTag{},
		Detail:         fmt.Sprintf("%s • %s", pkg.PkgPath(), filepath.Base(uri.Filename())),
		URI:            protocol.DocumentURI(uri),
		Range:          rng,
		SelectionRange: rng,
	}, nil
}

// OutgoingCalls returns an array of CallHierarchyOutgoingCall for a file and the position within the file.
func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyOutgoingCall, error) {
	ctx, done := event.Start(ctx, "source.OutgoingCalls")
	defer done()

	identifier, err := Identifier(ctx, snapshot, fh, pos)
	if err != nil {
		if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) {
			return nil, nil
		}
		return nil, err
	}

	if _, ok := identifier.Declaration.obj.Type().Underlying().(*types.Signature); !ok {
		return nil, nil
	}
	node := identifier.Declaration.node
	if node == nil {
		return nil, nil
	}
	if len(identifier.Declaration.MappedRange) == 0 {
		return nil, nil
	}
	declMappedRange := identifier.Declaration.MappedRange[0]
	// TODO(adonovan): avoid Fileset.File call by somehow getting at
	// declMappedRange.spanRange.TokFile, or making Identifier retain the
	// token.File of the identifier and its declaration, since it looks up both anyway.
	tokFile := snapshot.FileSet().File(node.Pos())
	if tokFile == nil {
		return nil, fmt.Errorf("no file for position")
	}
	callExprs, err := collectCallExpressions(tokFile, declMappedRange.m, node)
	if err != nil {
		return nil, err
	}

	return toProtocolOutgoingCalls(ctx, snapshot, fh, callExprs)
}

// collectCallExpressions collects call expression ranges inside a function.
func collectCallExpressions(tokFile *token.File, mapper *protocol.ColumnMapper, node ast.Node) ([]protocol.Range, error) {
	type callPos struct {
		start, end token.Pos
	}
	callPositions := []callPos{}

	ast.Inspect(node, func(n ast.Node) bool {
		if call, ok := n.(*ast.CallExpr); ok {
			var start, end token.Pos
			switch n := call.Fun.(type) {
			case *ast.SelectorExpr:
				start, end = n.Sel.NamePos, call.Lparen
			case *ast.Ident:
				start, end = n.NamePos, call.Lparen
			case *ast.FuncLit:
				// while we don't add the function literal as an 'outgoing' call
				// we still want to traverse into it
				return true
			default:
				// ignore any other kind of call expressions
				// for ex: direct function literal calls since that's not an 'outgoing' call
				return false
			}
			callPositions = append(callPositions, callPos{start: start, end: end})
		}
		return true
	})

	callRanges := []protocol.Range{}
	for _, call := range callPositions {
		callRange, err := NewMappedRange(tokFile, mapper, call.start, call.end).Range()
		if err != nil {
			return nil, err
		}
		callRanges = append(callRanges, callRange)
	}
	return callRanges, nil
}

// toProtocolOutgoingCalls returns an array of protocol.CallHierarchyOutgoingCall for ast call expressions.
// Calls to the same function are assigned to the same declaration.
func toProtocolOutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, callRanges []protocol.Range) ([]protocol.CallHierarchyOutgoingCall, error) {
	// Multiple calls could be made to the same function, defined by "same declaration
	// AST node & same identifier name" to provide a unique identifier key even when
	// the func is declared in a struct or interface.
	type key struct {
		decl ast.Node
		name string
	}
	outgoingCalls := map[key]*protocol.CallHierarchyOutgoingCall{}
	for _, callRange := range callRanges {
		identifier, err := Identifier(ctx, snapshot, fh, callRange.Start)
		if err != nil {
			if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) {
				continue
			}
			return nil, err
		}

		// ignore calls to builtin functions
		if identifier.Declaration.obj.Pkg() == nil {
			continue
		}

		if outgoingCall, ok := outgoingCalls[key{identifier.Declaration.node, identifier.Name}]; ok {
			outgoingCall.FromRanges = append(outgoingCall.FromRanges, callRange)
			continue
		}

		if len(identifier.Declaration.MappedRange) == 0 {
			continue
		}
		declMappedRange := identifier.Declaration.MappedRange[0]
		rng, err := declMappedRange.Range()
		if err != nil {
			return nil, err
		}

		outgoingCalls[key{identifier.Declaration.node, identifier.Name}] = &protocol.CallHierarchyOutgoingCall{
			To: protocol.CallHierarchyItem{
				Name:           identifier.Name,
				Kind:           protocol.Function,
				Tags:           []protocol.SymbolTag{},
				Detail:         fmt.Sprintf("%s • %s", identifier.Declaration.obj.Pkg().Path(), filepath.Base(declMappedRange.URI().Filename())),
				URI:            protocol.DocumentURI(declMappedRange.URI()),
				Range:          rng,
				SelectionRange: rng,
			},
			FromRanges: []protocol.Range{callRange},
		}
	}

	outgoingCallItems := make([]protocol.CallHierarchyOutgoingCall, 0, len(outgoingCalls))
	for _, callItem := range outgoingCalls {
		outgoingCallItems = append(outgoingCallItems, *callItem)
	}
	return outgoingCallItems, nil
}
