// Copyright 2023 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"
	"errors"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"go/types"
	"regexp"
	"strings"

	"golang.org/x/tools/gopls/internal/cache"
	"golang.org/x/tools/gopls/internal/cache/metadata"
	"golang.org/x/tools/gopls/internal/cache/parsego"
	"golang.org/x/tools/gopls/internal/file"
	"golang.org/x/tools/gopls/internal/protocol"
	"golang.org/x/tools/gopls/internal/util/bug"
	"golang.org/x/tools/internal/astutil"
	"golang.org/x/tools/internal/event"
)

// Definition handles the textDocument/definition request for Go files.
func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Location, error) {
	ctx, done := event.Start(ctx, "golang.Definition")
	defer done()

	pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
	if err != nil {
		return nil, err
	}
	pos, err := pgf.PositionPos(position)
	if err != nil {
		return nil, err
	}
	cur, _ := pgf.Cursor.FindByPos(pos, pos) // can't fail

	// Handle the case where the cursor is in an import.
	importLocations, err := importDefinition(ctx, snapshot, pkg, pgf, pos)
	if err != nil {
		return nil, err
	}
	if len(importLocations) > 0 {
		return importLocations, nil
	}

	// Handle the case where the cursor is in the package name.
	// We use "<= End" to accept a query immediately after the package name.
	if pgf.File != nil && astutil.NodeContainsPos(pgf.File.Name, pos) {
		// If there's no package documentation, just use current file.
		declFile := pgf
		for _, pgf := range pkg.CompiledGoFiles() {
			if pgf.File.Name != nil && pgf.File.Doc != nil {
				declFile = pgf
				break
			}
		}
		loc, err := declFile.NodeLocation(declFile.File.Name)
		if err != nil {
			return nil, err
		}
		return []protocol.Location{loc}, nil
	}

	// Handle the case where the cursor is in a linkname directive.
	locations, err := linknameDefinition(ctx, snapshot, pgf.Mapper, position)
	if !errors.Is(err, ErrNoLinkname) {
		return locations, err // may be success or failure
	}

	// Handle the case where the cursor is in an embed directive.
	locations, err = embedDefinition(pgf.Mapper, position)
	if !errors.Is(err, ErrNoEmbed) {
		return locations, err // may be success or failure
	}

	// Handle the case where the cursor is in a doc link.
	locations, err = docLinkDefinition(ctx, snapshot, pkg, pgf, pos)
	if !errors.Is(err, errNoCommentReference) {
		return locations, err // may be success or failure
	}

	// Handle definition requests for various special kinds of syntax node.
	switch node := cur.Node().(type) {
	// Handle the case where the cursor is on a return statement by jumping to the result variables.
	case *ast.ReturnStmt:
		var funcType *ast.FuncType
		for c := range cur.Enclosing() {
			switch n := c.Node().(type) {
			case *ast.FuncLit:
				funcType = n.Type
			case *ast.FuncDecl:
				funcType = n.Type
			}
			if funcType != nil {
				break
			}
		}
		// Inv: funcType != nil, as a return stmt cannot appear outside a function.
		if funcType.Results == nil {
			return nil, nil // no result variables
		}
		loc, err := pgf.NodeLocation(funcType.Results)
		if err != nil {
			return nil, err
		}
		return []protocol.Location{loc}, nil

	case *ast.BranchStmt:
		// Handle the case where the cursor is on a goto, break or continue statement by returning the
		// location of the label, the closing brace of the relevant block statement, or the
		// start of the relevant loop, respectively.
		label, isLabeled := pkg.TypesInfo().Uses[node.Label].(*types.Label)
		switch node.Tok {
		case token.GOTO:
			if !isLabeled {
				return nil, fmt.Errorf("undefined label")
			}
			loc, err := pgf.PosLocation(label.Pos(), label.Pos()+token.Pos(len(label.Name())))
			if err != nil {
				return nil, err
			}
			return []protocol.Location{loc}, nil

		case token.BREAK, token.CONTINUE:
			// Find innermost relevant ancestor for break/continue.
			for c := range cur.Enclosing() {
				if isLabeled {
					l, ok := c.Parent().Node().(*ast.LabeledStmt)
					if !(ok && l.Label.Name == label.Name()) {
						continue
					}
				}
				switch n := c.Node().(type) {
				case *ast.ForStmt, *ast.RangeStmt:
					var start, end token.Pos
					if node.Tok == token.BREAK {
						start, end = n.End()-token.Pos(len("}")), n.End()
					} else { // CONTINUE
						start, end = n.Pos(), n.Pos()+token.Pos(len("for"))
					}
					loc, err := pgf.PosLocation(start, end)
					if err != nil {
						return nil, err
					}
					return []protocol.Location{loc}, nil
				case *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt:
					if node.Tok == token.BREAK {
						loc, err := pgf.PosLocation(n.End()-1, n.End())
						if err != nil {
							return nil, err
						}
						return []protocol.Location{loc}, nil
					}
				case *ast.FuncDecl, *ast.FuncLit:
					// bad syntax; avoid jumping outside the current function
					return nil, nil
				}
			}
		}
	}

	// The general case: the cursor is on (or near) an identifier.
	objects, err := objectsAt(pkg.TypesInfo(), cur)
	if err != nil {
		return nil, nil
	}
	obj := objects[0].obj
	cur = objects[0].cur // nearby
	id, ok := cur.Node().(*ast.Ident)
	if !ok {
		// objectsAt can return a non-Ident (e.g. ImportSpec),
		// but we dealt with them earlier. Can't happen?
		return nil, nil
	}

	// If the query position was an embedded field, we want to jump
	// to the field's type definition, not the field's definition (#42254).
	if v, ok := obj.(*types.Var); ok && v.Embedded() {
		// types.Info.Uses contains the embedded field's *types.TypeName.
		if typeName := pkg.TypesInfo().Uses[id]; typeName != nil {
			obj = typeName
		}
	}

	// Non-go (e.g. assembly) symbols
	//
	// When already at the definition of a Go function without
	// a body, we jump to its non-Go (C or assembly) definition.
	for _, decl := range pgf.File.Decls {
		if decl, ok := decl.(*ast.FuncDecl); ok &&
			decl.Body == nil &&
			astutil.NodeContainsPos(decl.Name, pos) {
			return nonGoDefinition(ctx, snapshot, pkg, decl.Name.Name)
		}
	}

	// Finally, map the object position.
	loc, err := ObjectLocation(ctx, pkg.FileSet(), snapshot, obj)
	if err != nil {
		return nil, err
	}
	return []protocol.Location{loc}, nil
}

// builtinDecl returns the parsed Go file and node corresponding to a builtin
// object, which may be a universe object or part of types.Unsafe, as well as
// its declaring identifier.
func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) (*parsego.File, *ast.Ident, error) {
	// declaringIdent returns the file-level declaration node (as reported by
	// ast.Object) and declaring identifier of name using legacy (go/ast) object
	// resolution.
	declaringIdent := func(file *ast.File, name string) (ast.Node, *ast.Ident, error) {
		astObj := file.Scope.Lookup(name)
		if astObj == nil {
			// Every built-in should have documentation syntax.
			// However, it is possible to reach this statement by
			// commenting out declarations in {builtin,unsafe}.go.
			return nil, nil, fmt.Errorf("internal error: no object for %s", name)
		}
		decl, ok := astObj.Decl.(ast.Node)
		if !ok {
			return nil, nil, bug.Errorf("internal error: no declaration for %s", obj.Name())
		}
		var ident *ast.Ident
		switch node := decl.(type) {
		case *ast.Field:
			for _, id := range node.Names {
				if id.Name == name {
					ident = id
				}
			}
		case *ast.ValueSpec:
			for _, id := range node.Names {
				if id.Name == name {
					ident = id
				}
			}
		case *ast.TypeSpec:
			ident = node.Name
		case *ast.Ident:
			ident = node
		case *ast.FuncDecl:
			ident = node.Name
		case *ast.ImportSpec, *ast.LabeledStmt, *ast.AssignStmt:
			// Not reachable for imported objects.
		default:
			return nil, nil, bug.Errorf("internal error: unexpected decl type %T", decl)
		}
		if ident == nil {
			return nil, nil, bug.Errorf("internal error: no declaring identifier for %s", obj.Name())
		}
		return decl, ident, nil
	}

	var (
		pgf   *parsego.File
		ident *ast.Ident
		err   error
	)
	if obj.Pkg() == types.Unsafe {
		// package "unsafe":
		// parse $GOROOT/src/unsafe/unsafe.go
		//
		// (Strictly, we shouldn't assume that the ID of a std
		// package is its PkgPath, but no Bazel+gopackagesdriver
		// users have complained about this yet.)
		unsafe := snapshot.Metadata("unsafe")
		if unsafe == nil {
			// If the type checker somehow resolved 'unsafe', we must have metadata
			// for it.
			return nil, nil, bug.Errorf("no metadata for package 'unsafe'")
		}
		uri := unsafe.GoFiles[0]
		fh, err := snapshot.ReadFile(ctx, uri)
		if err != nil {
			return nil, nil, err
		}
		// TODO(rfindley): treat unsafe symmetrically with the builtin file. Either
		// pre-parse them both, or look up metadata for both.
		pgf, err = snapshot.ParseGo(ctx, fh, parsego.Full&^parser.SkipObjectResolution)
		if err != nil {
			return nil, nil, err
		}
		_, ident, err = declaringIdent(pgf.File, obj.Name())
		if err != nil {
			return nil, nil, err
		}
	} else {
		// pseudo-package "builtin":
		// use parsed $GOROOT/src/builtin/builtin.go
		pgf, err = snapshot.BuiltinFile(ctx)
		if err != nil {
			return nil, nil, err
		}

		if obj.Parent() == types.Universe {
			// built-in function or type
			_, ident, err = declaringIdent(pgf.File, obj.Name())
			if err != nil {
				return nil, nil, err
			}
		} else if obj.Name() == "Error" {
			// error.Error method
			decl, _, err := declaringIdent(pgf.File, "error")
			if err != nil {
				return nil, nil, err
			}
			field := decl.(*ast.TypeSpec).Type.(*ast.InterfaceType).Methods.List[0]
			ident = field.Names[0]
		} else {
			return nil, nil, bug.Errorf("unknown built-in %v", obj)
		}
	}
	_ = ident.Name // ident != nil

	return pgf, ident, nil
}

// importDefinition returns locations defining a package referenced by the
// import spec containing pos.
//
// If pos is not inside an import spec, it returns nil, nil.
func importDefinition(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, pos token.Pos) ([]protocol.Location, error) {
	var imp *ast.ImportSpec
	for _, spec := range pgf.File.Imports {
		// We use "<= End" to accept a query immediately after an ImportSpec.
		if astutil.NodeContainsPos(spec.Path, pos) {
			imp = spec
		}
	}
	if imp == nil {
		return nil, nil
	}

	importPath := metadata.UnquoteImportPath(imp)
	impID := pkg.Metadata().DepsByImpPath[importPath]
	if impID == "" {
		return nil, fmt.Errorf("failed to resolve import %q", importPath)
	}
	impMetadata := s.Metadata(impID)
	if impMetadata == nil {
		return nil, fmt.Errorf("missing information for package %q", impID)
	}

	var locs []protocol.Location
	for _, f := range impMetadata.CompiledGoFiles {
		fh, err := s.ReadFile(ctx, f)
		if err != nil {
			if ctx.Err() != nil {
				return nil, ctx.Err()
			}
			continue
		}
		pgf, err := s.ParseGo(ctx, fh, parsego.Header)
		if err != nil {
			if ctx.Err() != nil {
				return nil, ctx.Err()
			}
			continue
		}
		loc, err := pgf.NodeLocation(pgf.File)
		if err != nil {
			return nil, err
		}
		locs = append(locs, loc)
	}

	if len(locs) == 0 {
		return nil, fmt.Errorf("package %q has no readable files", impID) // incl. unsafe
	}

	return locs, nil
}

// ObjectLocation returns the location of the declaring identifier of obj.
// If valid, obj.Pos() must be mapped by fset.
// It may need to read the declaring file content, hence (ctx, s).
// It supports the builtin and unsafe pseudo-packages.
func ObjectLocation(ctx context.Context, fset *token.FileSet, snapshot *cache.Snapshot, obj types.Object) (protocol.Location, error) {
	if isBuiltin(obj) {
		// Returns fake source declaration in {builtin,unsafe}.go.
		pgf, ident, err := builtinDecl(ctx, snapshot, obj)
		if err != nil {
			return protocol.Location{}, err
		}
		return pgf.NodeLocation(ident)
	}

	// An imported Go package has a package-local, unqualified name.
	// When the name matches the imported package name, there is usually
	// no identifier in the import spec with the local package name.
	//
	// For example:
	// 	import "go/‸ast" 	// name "ast" matches package name
	// 	import ‸a "go/ast"  	// name "a" does not match package name
	//
	// When the identifier does not appear in the source, have the range
	// of the object be the import path, including quotes.
	//
	// But this is just a heuristic, and it's wrong in this case:
	// 	import ‸ast "go/ast"  	// name matches (spurious result is `ast "go/`)
	nameLen := len(obj.Name())
	if pkgName, ok := obj.(*types.PkgName); ok && pkgName.Imported().Name() == pkgName.Name() {
		nameLen = len(pkgName.Imported().Path()) + len(`""`)
	}

	var (
		start = obj.Pos()
		end   = start + token.Pos(nameLen)
	)
	file := fset.File(start)
	if file == nil {
		return protocol.Location{}, bug.Errorf("FileSet does not map Pos %d", start)
	}
	uri := protocol.URIFromPath(file.Name())
	fh, err := snapshot.ReadFile(ctx, uri)
	if err != nil {
		return protocol.Location{}, err
	}
	content, err := fh.Content()
	if err != nil {
		return protocol.Location{}, err
	}
	// TODO(rfindley): avoid the duplicate column mapping here, by associating a
	// column mapper with each file handle.
	m := protocol.NewMapper(fh.URI(), content)
	return m.PosLocation(file, start, end)
}

// nonGoDefinition returns the location of the definition of a non-Go symbol.
// Only assembly is supported for now.
func nonGoDefinition(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, symbol string) ([]protocol.Location, error) {
	// Examples:
	//   TEXT runtime·foo(SB)
	//   TEXT ·foo<ABIInternal>(SB)
	// TODO(adonovan): why does ^TEXT cause it not to match?
	pattern := regexp.MustCompile("TEXT\\b.*·(" + regexp.QuoteMeta(symbol) + ")[\\(<]")

	for _, uri := range pkg.Metadata().OtherFiles {
		if strings.HasSuffix(uri.Path(), ".s") {
			fh, err := snapshot.ReadFile(ctx, uri)
			if err != nil {
				return nil, err // context cancelled
			}
			content, err := fh.Content()
			if err != nil {
				continue // can't read file
			}
			if match := pattern.FindSubmatchIndex(content); match != nil {
				mapper := protocol.NewMapper(uri, content)
				loc, err := mapper.OffsetLocation(match[2], match[3])
				if err != nil {
					return nil, err
				}
				return []protocol.Location{loc}, nil
			}
		}
	}

	// TODO(adonovan): try C files

	// This may be reached for functions that aren't implemented
	// in assembly (e.g. compiler intrinsics like getg).
	return nil, fmt.Errorf("can't find non-Go definition of %s", symbol)
}
