// 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 goasm

import (
	"context"
	"go/types"

	"golang.org/x/tools/gopls/internal/cache"
	"golang.org/x/tools/gopls/internal/cache/metadata"
	"golang.org/x/tools/gopls/internal/file"
	"golang.org/x/tools/gopls/internal/protocol"
	"golang.org/x/tools/gopls/internal/util/asm"
	"golang.org/x/tools/gopls/internal/util/morestrings"
)

// A resolution is the result of resolving an assembly identifier to its
// definition, shared by Definition and Hover.
type resolution struct {
	// file is the parsed assembly file.
	file *asm.File

	// found is the identifier under the cursor, or nil if the cursor is
	// not on an identifier.
	found *asm.Ident

	// obj is the Go object for a package-qualified symbol (including a
	// current-package symbol such as ·foo), or nil if the symbol has no
	// Go declaration.
	obj types.Object
	// pkg is the type-checked package that declares obj, or nil.
	pkg *cache.Package

	// localDef is the defining identifier in the assembly file for a local
	// symbol — a label, a bare TEXT/GLOBL symbol, or a current-package
	// symbol without a Go declaration. It is nil if none was found. For a
	// label, only the enclosing TEXT function is searched.
	localDef *asm.Ident
}

// resolve resolves the assembly identifier at rng to its definition.
//
// For a package-qualified symbol (including a current-package symbol such
// as ·foo, which is rewritten to pkgpath.foo), resolve type-checks the
// declaring package and returns the Go object in res.obj. For a local
// symbol, resolve returns the defining identifier in the assembly file in
// res.localDef.
//
// res.found is nil if the cursor is not on an identifier.
func resolve(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range) (res resolution, err error) {
	// Package metadata is needed only to resolve package-qualified symbols
	// to Go declarations. An assembly-only file with no Go package has
	// none; tolerate the error and fall back to local assembly definitions.
	mp, err := snapshot.NarrowestMetadataForFile(ctx, fh.URI())
	if err != nil {
		mp = nil
	}

	content, err := fh.Content()
	if err != nil {
		return res, err
	}
	// TODO(adonovan): make this just another
	// attribute of the type-checked cache.Package.
	res.file = asm.Parse(fh.URI(), content)

	start, end, err := res.file.Mapper.RangeOffsets(rng)
	if err != nil {
		return res, err
	}

	// Find the identifier under the cursor.
	// Use the selection range so that haphazard selections that
	// happen to start in an identifier don't produce spurious matches.
	res.found = res.file.IdentAt(start, end)
	if res.found == nil {
		return res, nil
	}

	// Resolve a package-qualified symbol (including a current-package
	// symbol such as ·foo) to its Go declaration.
	if mp != nil {
		sym := res.found.Name
		if sym != "" && sym[0] == '.' {
			sym = string(mp.PkgPath) + sym
		}
		if pkgpath, name, ok := morestrings.CutLast(sym, "."); ok {
			// Find declaring package among dependencies.
			//
			// TODO(adonovan): assembly may legally reference
			// non-dependencies. For example, sync/atomic calls
			// internal/runtime/atomic. Perhaps we should search
			// the entire metadata graph, but that's path-dependent.
			var declaring *metadata.Package
			for pkg := range snapshot.MetadataGraph().ForwardReflexiveTransitiveClosure(mp.ID) {
				if pkg.PkgPath == metadata.PackagePath(pkgpath) {
					declaring = pkg
					break
				}
			}
			if declaring != nil {
				pkgs, err := snapshot.TypeCheck(ctx, declaring.ID)
				if err != nil {
					return res, err
				}
				res.pkg = pkgs[0]
				res.obj = res.pkg.Types().Scope().Lookup(name)
			}
			// If obj is nil (no Go declaration, e.g. an asm-only
			// symbol), fall through to the local-definition search.
		}
	}

	// Find the definition of a local symbol — a label, a bare TEXT/GLOBL
	// symbol, or a package-qualified symbol without a Go declaration — in
	// the assembly file.
	if res.obj == nil {
		// Labels are function-scoped: a label definition matches only
		// within the enclosing TEXT function, so that a jump doesn't land
		// on a same-named label in another function.
		lo, hi := res.file.FunctionRange(res.found.Offset)
		for _, id := range res.file.Idents {
			if id.Name != res.found.Name {
				continue
			}
			switch id.Kind {
			case asm.Text, asm.Global:
				res.localDef = &id
			case asm.Label:
				if lo <= id.Offset && id.Offset < hi {
					res.localDef = &id
				}
			}
			if res.localDef != nil {
				break
			}
		}
	}

	return res, nil
}
