// 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"
	"fmt"
	"go/token"

	"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"
	"golang.org/x/tools/internal/event"
)

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

	mp, err := snapshot.NarrowestMetadataForFile(ctx, fh.URI())
	if err != nil {
		return nil, err
	}

	// Read the file.
	content, err := fh.Content()
	if err != nil {
		return nil, err
	}
	mapper := protocol.NewMapper(fh.URI(), content)
	start, end, err := mapper.RangeOffsets(rng)
	if err != nil {
		return nil, err
	}

	// Parse the assembly.
	//
	// TODO(adonovan): make this just another
	// attribute of the type-checked cache.Package.
	file := asm.Parse(fh.URI(), content)

	// Figure out the selected symbol.
	// Use the selection range so that haphazard selections that
	// happen to start in an identifier don't produce spurious matches.
	var found *asm.Ident
	for _, id := range file.Idents {
		if id.Offset <= start && end <= id.End() {
			found = &id
			break
		}
	}
	if found == nil {
		return nil, fmt.Errorf("not an identifier")
	}

	// Resolve a symbol with a "." prefix to the current package.
	sym := found.Name
	if sym != "" && sym[0] == '.' {
		sym = string(mp.PkgPath) + sym
	}

	// package-qualified symbol?
	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 {
			return nil, fmt.Errorf("package %q is not a dependency", pkgpath)
		}

		// Find declared symbol in syntax package.
		pkgs, err := snapshot.TypeCheck(ctx, declaring.ID)
		if err != nil {
			return nil, err
		}
		pkg := pkgs[0]
		def := pkg.Types().Scope().Lookup(name)
		if def == nil {
			return nil, fmt.Errorf("no symbol %q in package %q", name, pkgpath)
		}

		// Map position.
		pos := def.Pos()
		pgf, err := pkg.FileEnclosing(pos)
		if err != nil {
			return nil, err
		}
		loc, err := pgf.PosLocation(pos, pos+token.Pos(len(name)))
		if err != nil {
			return nil, err
		}
		return []protocol.Location{loc}, nil

	} else {
		// local symbols (funcs, vars, labels)
		for _, id := range file.Idents {
			if id.Name == found.Name &&
				(id.Kind == asm.Text || id.Kind == asm.Global || id.Kind == asm.Label) {

				loc, err := mapper.OffsetLocation(id.Offset, id.End())
				if err != nil {
					return nil, err
				}
				return []protocol.Location{loc}, nil
			}
		}
	}

	return nil, nil
}
