/* Copyright 2018 The Bazel Authors. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package golang

import (
	"fmt"
	"go/build"
	"log"
	"path"
	"path/filepath"
	"sort"
	"strings"
	"sync"

	"github.com/bazelbuild/bazel-gazelle/config"
	"github.com/bazelbuild/bazel-gazelle/language"
	"github.com/bazelbuild/bazel-gazelle/language/proto"
	"github.com/bazelbuild/bazel-gazelle/pathtools"
	"github.com/bazelbuild/bazel-gazelle/rule"
)

func (gl *goLang) GenerateRules(args language.GenerateArgs) language.GenerateResult {
	// Extract information about proto files. We need this to exclude .pb.go
	// files and generate go_proto_library rules.
	c := args.Config
	gc := getGoConfig(c)
	pcMode := getProtoMode(c)

	// This is a collection of proto_library rule names that have a corresponding
	// go_proto_library rule already generated.
	goProtoRules := make(map[string]struct{})

	var protoRuleNames []string
	protoPackages := make(map[string]proto.Package)
	protoFileInfo := make(map[string]proto.FileInfo)
	for _, r := range args.OtherGen {
		if r.Kind() == "go_proto_library" {
			if proto := r.AttrString("proto"); proto != "" {
				goProtoRules[proto] = struct{}{}
			}
			if protos := r.AttrStrings("protos"); protos != nil {
				for _, proto := range protos {
					goProtoRules[proto] = struct{}{}
				}
			}

		}
		if r.Kind() != "proto_library" {
			continue
		}
		pkg := r.PrivateAttr(proto.PackageKey).(proto.Package)
		protoPackages[r.Name()] = pkg
		for name, info := range pkg.Files {
			protoFileInfo[name] = info
		}
		protoRuleNames = append(protoRuleNames, r.Name())
	}
	sort.Strings(protoRuleNames)
	var emptyProtoRuleNames []string
	for _, r := range args.OtherEmpty {
		if r.Kind() == "proto_library" {
			emptyProtoRuleNames = append(emptyProtoRuleNames, r.Name())
		}
	}

	// If proto rule generation is enabled, exclude .pb.go files that correspond
	// to any .proto files present.
	regularFiles := append([]string{}, args.RegularFiles...)
	genFiles := append([]string{}, args.GenFiles...)
	if !pcMode.ShouldIncludePregeneratedFiles() {
		keep := func(f string) bool {
			for _, suffix := range []string{".pb.go", "_grpc.pb.go"} {
				if strings.HasSuffix(f, suffix) {
					if _, ok := protoFileInfo[strings.TrimSuffix(f, suffix)+".proto"]; ok {
						return false
					}
				}
			}
			return true
		}
		filterFiles(&regularFiles, keep)
		filterFiles(&genFiles, keep)
	}

	// Split regular files into files which can determine the package name and
	// import path and other files.
	var goFiles, otherFiles []string
	for _, f := range regularFiles {
		if strings.HasSuffix(f, ".go") {
			goFiles = append(goFiles, f)
		} else {
			otherFiles = append(otherFiles, f)
		}
	}

	// Look for a subdirectory named testdata. Only treat it as data if it does
	// not contain a buildable package.
	var hasTestdata bool
	for _, sub := range args.Subdirs {
		if sub == "testdata" {
			_, ok := gl.goPkgRels[path.Join(args.Rel, "testdata")]
			hasTestdata = !ok
			break
		}
	}

	// Build a set of packages from files in this directory.
	srcdir := args.Rel
	if gc.goRepositoryMode {
		// cgo opts such as '-L${SRCDIR}/libs' should become
		// '-Lexternal/my_repo~/libs' in an external repo.
		// We obtain the path from the repo root to support both cases of
		// --experimental_sibling_repository_layout.
		slashPath := filepath.ToSlash(c.RepoRoot)
		segments := strings.Split(slashPath, "/")
		repoName := segments[len(segments)-1]
		previousSegment := segments[len(segments)-2]
		if previousSegment == "external" {
			srcdir = path.Join("external", repoName, srcdir)
		} else {
			srcdir = path.Join("..", repoName, srcdir)
		}
	}
	goFileInfos := make([]fileInfo, len(goFiles))
	var er *embedResolver
	for i, name := range goFiles {
		path := filepath.Join(args.Dir, name)
		goFileInfos[i] = goFileInfo(path, srcdir)
		if len(goFileInfos[i].embeds) > 0 && er == nil {
			er = newEmbedResolver(args.Dir, args.Rel, c.ValidBuildFileNames, gl.goPkgRels, args.Subdirs, args.RegularFiles, args.GenFiles)
		}
	}
	goPackageMap, goFilesWithUnknownPackage := buildPackages(c, args.Dir, args.Rel, hasTestdata, er, goFileInfos)

	// Select a package to generate rules for. If there is no package, create
	// an empty package so we can generate empty rules.
	var protoName string
	pkg, err := selectPackage(c, args.Dir, goPackageMap)
	if err != nil {
		if _, ok := err.(*build.NoGoError); ok {
			if len(protoPackages) == 1 {
				for name, ppkg := range protoPackages {
					if _, ok := goProtoRules[":"+name]; ok {
						// if a go_proto_library rule already exists for this
						// proto package, treat it as if the proto package
						// doesn't exist.
						pkg = emptyPackage(c, args.Dir, args.Rel, args.File)
						break
					}
					pkg = &goPackage{
						name:       goProtoPackageName(ppkg),
						importPath: goProtoImportPath(c, ppkg, args.Rel),
						proto:      protoTargetFromProtoPackage(name, ppkg),
					}
					protoName = name
					break
				}
			} else {
				pkg = emptyPackage(c, args.Dir, args.Rel, args.File)
			}
		} else {
			log.Print(err)
		}
	}

	// Try to link the selected package with a proto package.
	if pkg != nil {
		if pkg.importPath == "" {
			if err := pkg.inferImportPath(c); err != nil && pkg.firstGoFile() != "" {
				inferImportPathErrorOnce.Do(func() { log.Print(err) })
			}
		}
		for _, name := range protoRuleNames {
			ppkg := protoPackages[name]
			if pkg.importPath == goProtoImportPath(c, ppkg, args.Rel) {
				protoName = name
				pkg.proto = protoTargetFromProtoPackage(name, ppkg)
				break
			}
		}
	}

	// Generate rules for proto packages. These should come before the other
	// Go rules.
	g := newGenerator(c, gc, args)
	var res language.GenerateResult
	var rules []*rule.Rule
	var protoEmbeds []string
	var protoEmbed string
	if pcMode == proto.FileMode {
		// get the list of protoRuleNames that are not already in goProtoRules
		var newProtoRuleNames []string
		for _, name := range protoRuleNames {
			if _, ok := goProtoRules[":"+name]; !ok {
				newProtoRuleNames = append(newProtoRuleNames, name)
			}
		}

		// get the protoTargets for the new protoRuleNames
		var importPathToProtoTargets = make(map[string][]protoTarget)
		for _, name := range newProtoRuleNames {
			ppkg := protoPackages[name]
			importPath := goProtoImportPath(c, ppkg, args.Rel)
			importPathToProtoTargets[importPath] = append(importPathToProtoTargets[importPath], protoTargetFromProtoPackage(name, ppkg))
		}

		// Deterministically sort by order of importpath.
		importPaths := []string{}
		for importPath, _ := range importPathToProtoTargets {
			importPaths = append(importPaths, importPath)
		}
		sort.Strings(importPaths)

		for _, importPath := range importPaths {
			var rs []*rule.Rule
			protoTargets := importPathToProtoTargets[importPath]
			protoEmbed, rs = g.generateProto(pcMode, protoTargets, importPath)
			if protoEmbed != "" {
				// check if rs is non-empty and that the first rule is a go_proto_library with the same importPath
				if len(rs) > 0 && rs[0].Kind() == "go_proto_library" && rs[0].AttrString("importpath") == pkg.importPath {
					protoEmbeds = append(protoEmbeds, protoEmbed)
				}
			}
			rules = append(rules, rs...)
		}
	} else {
		for _, name := range protoRuleNames {
			// if a go_proto_library rule exists for this proto_library rule
			// already, skip creating another go_proto_library for it, assuming
			// that a different gazelle extension is responsible for
			// go_proto_library rule generation.
			if _, ok := goProtoRules[":"+name]; ok {
				continue
			}
			ppkg := protoPackages[name]
			var rs []*rule.Rule
			if name == protoName {
				var protoTargets []protoTarget
				if pkg != nil {
					protoTargets = append(protoTargets, pkg.proto)
				}
				protoEmbed, rs = g.generateProto(pcMode, []protoTarget{pkg.proto}, pkg.importPath)
				if protoEmbed != "" {
					// check if rs is non-empty and that the first rule is a go_proto_library with the same importPath
					if len(rs) > 0 && rs[0].Kind() == "go_proto_library" && rs[0].AttrString("importpath") == pkg.importPath {
						protoEmbeds = append(protoEmbeds, protoEmbed)
					}
				}
			} else {
				target := protoTargetFromProtoPackage(name, ppkg)
				importPath := goProtoImportPath(c, ppkg, args.Rel)
				_, rs = g.generateProto(pcMode, []protoTarget{target}, importPath)
			}
			rules = append(rules, rs...)
		}
	}

	for _, name := range emptyProtoRuleNames {
		goProtoName := strings.TrimSuffix(name, "_proto") + goProtoSuffix
		res.Empty = append(res.Empty, rule.NewRule("go_proto_library", goProtoName))
	}
	if pkg != nil && (pcMode == proto.PackageMode || pcMode == proto.FileMode) && pkg.firstGoFile() == "" {
		// In proto package mode, don't generate a go_library embedding a
		// go_proto_library unless there are actually go files.
		protoEmbeds = nil
	}

	// Complete the Go package and generate rules for that.
	if pkg != nil {
		// Add files with unknown packages. This happens when there are parse
		// or I/O errors. We should keep the file in the srcs list and let the
		// compiler deal with the error.
		cgo := pkg.haveCgo()
		for _, info := range goFilesWithUnknownPackage {
			if err := pkg.addFile(c, er, info, cgo); err != nil {
				log.Print(err)
			}
		}

		// Process the other static files.
		for _, file := range otherFiles {
			info := otherFileInfo(filepath.Join(args.Dir, file))
			if err := pkg.addFile(c, er, info, cgo); err != nil {
				log.Print(err)
			}
		}

		// Process generated files. Note that generated files may have the same names
		// as static files. Bazel will use the generated files, but we will look at
		// the content of static files, assuming they will be the same.
		regularFileSet := make(map[string]bool)
		for _, f := range regularFiles {
			regularFileSet[f] = true
		}
		// Some of the generated files may have been consumed by other rules
		consumedFileSet := make(map[string]bool)
		for _, r := range args.OtherGen {
			for _, f := range r.AttrStrings("srcs") {
				consumedFileSet[f] = true
			}
			if f := r.AttrString("src"); f != "" {
				consumedFileSet[f] = true
			}
		}
		for _, f := range genFiles {
			if regularFileSet[f] || consumedFileSet[f] {
				continue
			}
			info := fileNameInfo(filepath.Join(args.Dir, f))
			if err := pkg.addFile(c, er, info, cgo); err != nil {
				log.Print(err)
			}
		}

		var genGoProtoRules []string
		for _, r := range rules {
			if r.Kind() == "go_proto_library" {
				genGoProtoRules = append(genGoProtoRules, r.Name())
			}
		}

		// Generate Go rules.
		if protoName == "" {
			// Empty proto rules for deletion.
			_, rs := g.generateProto(pcMode, []protoTarget{pkg.proto}, pkg.importPath)

			// Do not generate empty rule if already created above
			found := false
			for _, r := range rs {
				if r.Kind() == "go_proto_library" {
					for _, pr := range genGoProtoRules {
						if pr == r.Name() {
							found = true
							break
						}
					}
					if found {
						break
					}
				}
			}
			if !found {
				rules = append(rules, rs...)
			}
		}
		lib := g.generateLib(pkg, protoEmbeds)
		var libName string
		if !lib.IsEmpty(goKinds[lib.Kind()]) {
			libName = lib.Name()
		}
		rules = append(rules, lib)
		g.maybePublishToolLib(lib, pkg)
		if r := g.maybeGenerateExtraLib(lib, pkg); r != nil {
			rules = append(rules, r)
		}
		if r := g.maybeGenerateAlias(pkg, libName); r != nil {
			g.maybePublishToolLib(r, pkg)
			rules = append(rules, r)
		}
		rules = append(rules, g.generateBin(pkg, libName))
		rules = append(rules, g.generateTests(pkg, libName)...)
	}

	for _, r := range rules {
		if r.IsEmpty(goKinds[r.Kind()]) {
			res.Empty = append(res.Empty, r)
		} else {
			res.Gen = append(res.Gen, r)
			rawImports := r.PrivateAttr(config.GazelleImportsKey)
			res.Imports = append(res.Imports, rawImports)
			if imports, ok := rawImports.(rule.PlatformStrings); ok && g.shouldIndex {
				g.addRelsToIndex(imports)
			}
		}
	}

	for r := range g.relsToIndexSeen {
		res.RelsToIndex = append(res.RelsToIndex, r)
	}
	sort.Strings(res.RelsToIndex) // for deterministic output

	if args.File != nil || len(res.Gen) > 0 {
		gl.goPkgRels[args.Rel] = true
	} else {
		for _, sub := range args.Subdirs {
			if _, ok := gl.goPkgRels[path.Join(args.Rel, sub)]; ok {
				gl.goPkgRels[args.Rel] = false
				break
			}
		}
	}

	return res
}

func filterFiles(files *[]string, pred func(string) bool) {
	w := 0
	for r := 0; r < len(*files); r++ {
		f := (*files)[r]
		if pred(f) {
			(*files)[w] = f
			w++
		}
	}
	*files = (*files)[:w]
}

func buildPackages(c *config.Config, dir, rel string, hasTestdata bool, er *embedResolver, goFiles []fileInfo) (packageMap map[string]*goPackage, goFilesWithUnknownPackage []fileInfo) {
	// Process .go and .proto files first, since these determine the package name.
	packageMap = make(map[string]*goPackage)
	for _, f := range goFiles {
		if f.packageName == "" {
			goFilesWithUnknownPackage = append(goFilesWithUnknownPackage, f)
			continue
		}
		if f.packageName == "documentation" {
			// go/build ignores this package
			continue
		}

		if _, ok := packageMap[f.packageName]; !ok {
			packageMap[f.packageName] = &goPackage{
				name:        f.packageName,
				dir:         dir,
				rel:         rel,
				hasTestdata: hasTestdata,
			}
		}
		if err := packageMap[f.packageName].addFile(c, er, f, false); err != nil {
			log.Print(err)
		}
	}
	return packageMap, goFilesWithUnknownPackage
}

var inferImportPathErrorOnce sync.Once

// selectPackages selects one Go packages out of the buildable packages found
// in a directory. If multiple packages are found, it returns the package
// whose name matches the directory if such a package exists.
func selectPackage(c *config.Config, dir string, packageMap map[string]*goPackage) (*goPackage, error) {
	buildablePackages := make(map[string]*goPackage)
	for name, pkg := range packageMap {
		if pkg.isBuildable(c) {
			buildablePackages[name] = pkg
		}
	}

	if len(buildablePackages) == 0 {
		return nil, &build.NoGoError{Dir: dir}
	}

	if len(buildablePackages) == 1 {
		for _, pkg := range buildablePackages {
			return pkg, nil
		}
	}

	if pkg, ok := buildablePackages[defaultPackageName(c, dir)]; ok {
		return pkg, nil
	}

	err := &build.MultiplePackageError{Dir: dir}
	for name, pkg := range buildablePackages {
		// Add the first file for each package for the error message.
		// Error() method expects these lists to be the same length. File
		// lists must be non-empty. These lists are only created by
		// buildPackage for packages with .go files present.
		err.Packages = append(err.Packages, name)
		err.Files = append(err.Files, pkg.firstGoFile())
	}
	return nil, err
}

func emptyPackage(c *config.Config, dir, rel string, f *rule.File) *goPackage {
	var pkgName string
	if fileContainsGoBinary(c, f) {
		// If the file contained a go_binary, its library may have a "_lib" suffix.
		// Set the package name to "main" so that we generate an empty library rule
		// with that name.
		pkgName = "main"
	} else {
		pkgName = defaultPackageName(c, dir)
	}
	pkg := &goPackage{
		name: pkgName,
		dir:  dir,
		rel:  rel,
	}

	return pkg
}

func defaultPackageName(c *config.Config, rel string) string {
	gc := getGoConfig(c)
	return pathtools.RelBaseName(rel, gc.prefix, "")
}

type generator struct {
	c                   *config.Config
	gc                  *goConfig
	rel                 string
	shouldSetVisibility bool

	shouldIndex     bool
	relsToIndexSeen map[string]struct{}
}

func newGenerator(c *config.Config, gc *goConfig, args language.GenerateArgs) *generator {
	g := &generator{
		c:                   c,
		gc:                  gc,
		rel:                 args.Rel,
		shouldSetVisibility: shouldSetVisibility(args),
		shouldIndex:         c.IndexLibraries && len(gc.goSearch) > 0,
	}
	if g.shouldIndex {
		g.relsToIndexSeen = make(map[string]struct{})
	}
	return g
}

func (g *generator) generateProto(mode proto.Mode, targets []protoTarget, importPath string) (string, []*rule.Rule) {
	if !mode.ShouldGenerateRules() && mode != proto.LegacyMode {
		// Don't create or delete proto rules in this mode. When proto mode is disabled,
		// there may be hand-written rules or pre-generated Go files
		return "", nil
	}

	gc := getGoConfig(g.c)
	filegroupName := legacyProtoFilegroupName
	var protoName string
	if len(targets) == 1 {
		protoName = targets[0].name
	}
	if protoName == "" {
		if importPath == "" {
			importPath = InferImportPath(g.c, g.rel)
		}
		protoName = proto.RuleName(importPath)
	}
	goProtoName := strings.TrimSuffix(protoName, "_proto") + goProtoSuffix
	visibility := g.commonVisibility(importPath)

	if mode == proto.LegacyMode {
		filegroup := rule.NewRule("filegroup", filegroupName)
		if targets[0].sources.isEmpty() {
			return "", []*rule.Rule{filegroup}
		}
		filegroup.SetAttr("srcs", targets[0].sources.build())
		if g.shouldSetVisibility {
			filegroup.SetAttr("visibility", visibility)
		}
		return "", []*rule.Rule{filegroup}
	}

	var atLeastOneTargetHasSources bool
	for _, target := range targets {
		if !target.sources.isEmpty() {
			atLeastOneTargetHasSources = true
			break
		}
	}

	if !atLeastOneTargetHasSources {
		return "", []*rule.Rule{
			rule.NewRule("filegroup", filegroupName),
			rule.NewRule("go_proto_library", goProtoName),
		}
	}

	goProtoLibrary := rule.NewRule("go_proto_library", goProtoName)
	if len(targets) == 1 {
		goProtoLibrary.SetAttr("proto", ":"+protoName)
	} else {
		// generate protoNames with prefix ":"
		protoNames := make([]string, len(targets))
		for i := range targets {
			protoNames[i] = ":" + targets[i].name
		}
		goProtoLibrary.SetAttr("protos", protoNames)
	}

	g.setImportAttrs(goProtoLibrary, importPath)
	var atLeastOneTargetHasServices bool
	for _, target := range targets {
		if target.hasServices {
			atLeastOneTargetHasServices = true
			break
		}
	}
	if atLeastOneTargetHasServices {
		goProtoLibrary.SetAttr("compilers", gc.goGrpcCompilers)
	} else if gc.goProtoCompilersSet {
		goProtoLibrary.SetAttr("compilers", gc.goProtoCompilers)
	}
	if g.shouldSetVisibility {
		goProtoLibrary.SetAttr("visibility", visibility)
	}
	if len(targets) == 1 {
		goProtoLibrary.SetPrivateAttr(config.GazelleImportsKey, targets[0].imports.build())
	} else {
		protoSources := make(map[string]struct{})
		for _, target := range targets {
			for _, src := range target.sources.build().Generic {
				// if src starts with the repo root plus a slash, trim it
				// so that src is relative to the repo root, which is
				// needed for the dep resolution to work correctly
				if strings.HasPrefix(src, g.c.RepoRoot+"/") {
					src = src[len(g.c.RepoRoot)+1:]
				}
				protoSources[src] = struct{}{}
			}
		}
		var combinedImports platformStringsBuilder
		for _, target := range targets {
			builtImports := target.imports.build()
			// handle Generics
			for _, genericString := range builtImports.Generic {
				// if the generic string is not in the set of generic src strings, add it
				if _, ok := protoSources[genericString]; !ok {
					combinedImports.addGenericString(genericString)
				}
			}
		}

		goProtoLibrary.SetPrivateAttr(config.GazelleImportsKey, combinedImports.build())
	}
	return goProtoName, []*rule.Rule{goProtoLibrary}
}

func (g *generator) generateLib(pkg *goPackage, embeds []string) *rule.Rule {
	gc := getGoConfig(g.c)
	name := libNameByConvention(gc.goNamingConvention, pkg.importPath, pkg.name)
	goLibrary := rule.NewRule("go_library", name)
	if !pkg.library.sources.hasGo() && len(embeds) == 0 {
		return goLibrary // empty
	}
	var visibility []string
	if pkg.isCommand() {
		// By default, libraries made for a go_binary should not be exposed to the public.
		visibility = []string{"//visibility:private"}
		if len(getGoConfig(g.c).goVisibility) > 0 {
			visibility = getGoConfig(g.c).goVisibility
		}
	} else {
		visibility = g.commonVisibility(pkg.importPath)
	}
	g.setCommonAttrs(goLibrary, pkg.rel, visibility, pkg.library, embeds)
	g.setImportAttrs(goLibrary, pkg.importPath)
	return goLibrary
}

func (g *generator) maybeGenerateAlias(pkg *goPackage, libName string) *rule.Rule {
	if pkg.isCommand() || libName == "" {
		return nil
	}
	gc := getGoConfig(g.c)
	if gc.goNamingConvention == goDefaultLibraryNamingConvention {
		return nil
	}
	alias := rule.NewRule("alias", defaultLibName)
	alias.SetAttr("visibility", g.commonVisibility(pkg.importPath))
	if gc.goNamingConvention == importAliasNamingConvention {
		alias.SetAttr("actual", ":"+libName)
	}
	return alias
}

func (g *generator) generateBin(pkg *goPackage, library string) *rule.Rule {
	gc := getGoConfig(g.c)
	name := binName(pkg.rel, gc.prefix, g.c.RepoRoot)
	goBinary := rule.NewRule("go_binary", name)
	if !pkg.isCommand() || pkg.binary.sources.isEmpty() && library == "" {
		return goBinary // empty
	}
	visibility := g.commonVisibility(pkg.importPath)
	g.setCommonAttrs(goBinary, pkg.rel, visibility, pkg.binary, []string{library})
	return goBinary
}

func (g *generator) generateTests(pkg *goPackage, library string) []*rule.Rule {
	gc := getGoConfig(g.c)
	tests := pkg.tests
	if len(tests) == 0 && gc.testMode == defaultTestMode {
		tests = []goTarget{goTarget{}}
	}
	var name func(goTarget) string
	switch gc.testMode {
	case defaultTestMode:
		name = func(goTarget) string {
			return testNameByConvention(gc.goNamingConvention, pkg.importPath)
		}
	case fileTestMode:
		name = func(test goTarget) string {
			if test.sources.hasGo() {
				if srcs := test.sources.buildFlat(); len(srcs) == 1 {
					return testNameFromSingleSource(srcs[0])
				}
			}
			return testNameByConvention(gc.goNamingConvention, pkg.importPath)
		}
	}
	var res []*rule.Rule
	for i, test := range tests {
		goTest := rule.NewRule("go_test", name(test))
		hasGo := test.sources.hasGo()
		if hasGo || i == 0 {
			res = append(res, goTest)
			if !hasGo {
				continue
			}
		}
		var embeds []string
		if test.hasInternalTest {
			if library != "" {
				embeds = append(embeds, library)
			}
		}
		g.setCommonAttrs(goTest, pkg.rel, nil, test, embeds)
		if pkg.hasTestdata {
			goTest.SetAttr("data", rule.GlobValue{Patterns: []string{"testdata/**"}})
		}
	}
	return res
}

// maybePublishToolLib makes the given go_library rule public if needed for nogo.
// Updating it here automatically makes it easier to upgrade org_golang_x_tools.
func (g *generator) maybePublishToolLib(lib *rule.Rule, pkg *goPackage) {
	if pkg.importPath == "golang.org/x/tools/go/analysis/internal/facts" || pkg.importPath == "golang.org/x/tools/internal/facts" {
		// Imported by nogo main. We add a visibility exception.
		lib.SetAttr("visibility", []string{"//visibility:public"})
	}
}

// maybeGenerateExtraLib generates extra equivalent library targets for
// certain protobuf libraries. Historically, these "_gen" targets depend on Well Known Types
// built with go_proto_library and are used together with go_proto_library.
// However, these are no longer needed and are kept as aliases to be backward-compatible
func (g *generator) maybeGenerateExtraLib(lib *rule.Rule, pkg *goPackage) *rule.Rule {
	gc := getGoConfig(g.c)
	if gc.prefix != "github.com/golang/protobuf" || gc.prefixRel != "" {
		return nil
	}

	var r *rule.Rule
	switch pkg.importPath {
	case "github.com/golang/protobuf/descriptor":
		r = rule.NewRule("alias", "go_default_library_gen")
		r.SetAttr("actual", ":go_default_library")
		r.SetAttr("visibility", []string{"//visibility:public"})

	case "github.com/golang/protobuf/jsonpb":
		r = rule.NewRule("alias", "go_default_library_gen")
		r.SetAttr("actual", ":go_default_library")
		r.SetAttr("visibility", []string{"//visibility:public"})

	case "github.com/golang/protobuf/protoc-gen-go/generator":
		r = rule.NewRule("alias", "go_default_library_gen")
		r.SetAttr("actual", ":go_default_library")
		r.SetAttr("visibility", []string{"//visibility:public"})

	case "github.com/golang/protobuf/ptypes":
		r = rule.NewRule("alias", "go_default_library_gen")
		r.SetAttr("actual", ":go_default_library")
		r.SetAttr("visibility", []string{"//visibility:public"})
	}

	return r
}

func (g *generator) setCommonAttrs(r *rule.Rule, pkgRel string, visibility []string, target goTarget, embeds []string) {
	if !target.sources.isEmpty() {
		r.SetAttr("srcs", target.sources.buildFlat())
	}
	if !target.embedSrcs.isEmpty() {
		r.SetAttr("embedsrcs", target.embedSrcs.build())
	}
	if target.cgo {
		r.SetAttr("cgo", true)
	}
	if !target.clinkopts.isEmpty() {
		r.SetAttr("clinkopts", g.options(target.clinkopts.build(), pkgRel))
	}
	if !target.cppopts.isEmpty() {
		r.SetAttr("cppopts", g.options(target.cppopts.build(), pkgRel))
	}
	if !target.copts.isEmpty() {
		r.SetAttr("copts", g.options(target.copts.build(), pkgRel))
	}
	if !target.cxxopts.isEmpty() {
		r.SetAttr("cxxopts", g.options(target.cxxopts.build(), pkgRel))
	}
	if g.shouldSetVisibility && len(visibility) > 0 {
		r.SetAttr("visibility", visibility)
	}
	if len(embeds) > 0 {
		colonEmbeds := make([]string, 0, len(embeds))
		for _, embed := range embeds {
			colonEmbeds = append(colonEmbeds, ":"+embed)
		}
		r.SetAttr("embed", colonEmbeds)
	}
	r.SetPrivateAttr(config.GazelleImportsKey, target.imports.build())
}

func (g *generator) setImportAttrs(r *rule.Rule, importPath string) {
	gc := getGoConfig(g.c)
	r.SetAttr("importpath", importPath)

	// Set importpath_aliases if we need minimal module compatibility.
	// If a package is part of a module with a v2+ semantic import version
	// suffix, packages that are not part of modules may import it without
	// the suffix.
	if gc.goRepositoryMode && gc.moduleMode && pathtools.HasPrefix(importPath, gc.prefix) && gc.prefixRel == "" {
		if mmcImportPath := pathWithoutSemver(importPath); mmcImportPath != "" {
			r.SetAttr("importpath_aliases", []string{mmcImportPath})
		}
	}

	if gc.importMapPrefix != "" {
		fromPrefixRel := pathtools.TrimPrefix(g.rel, gc.importMapPrefixRel)
		importMap := path.Join(gc.importMapPrefix, fromPrefixRel)
		if importMap != importPath {
			r.SetAttr("importmap", importMap)
		}
	}
}

func (g *generator) commonVisibility(importPath string) []string {
	// If the Bazel package name (rel) contains "internal", add visibility for
	// subpackages of the parent.
	// If the import path contains "internal" but rel does not, this is
	// probably an internal submodule. Add visibility for all subpackages.
	relIndex := pathtools.LastIndex(g.rel, "internal")
	importIndex := pathtools.LastIndex(importPath, "internal")
	visibility := getGoConfig(g.c).goVisibility
	if relIndex >= 0 {
		parent := strings.TrimSuffix(g.rel[:relIndex], "/")
		visibility = append(visibility, fmt.Sprintf("//%s:__subpackages__", parent))
	} else if importIndex >= 0 {
		// This entire module is within an internal directory.
		// Identify other repos which should have access too.
		visibility = append(visibility, "//:__subpackages__")
		for _, repo := range g.c.Repos {
			if pathtools.HasPrefix(repo.AttrString("importpath"), importPath[:importIndex]) {
				visibility = append(visibility, "@"+repo.Name()+"//:__subpackages__")
			}
		}
	} else {
		return []string{"//visibility:public"}
	}

	// Add visibility for any submodules that have the internal parent as
	// a prefix of their module path.
	if importIndex >= 0 {
		gc := getGoConfig(g.c)
		internalRoot := strings.TrimSuffix(importPath[:importIndex], "/")
		for _, m := range gc.submodules {
			if strings.HasPrefix(m.modulePath, internalRoot) {
				visibility = append(visibility, fmt.Sprintf("@%s//:__subpackages__", m.repoName))
			}
		}
	}

	return visibility
}

var (
	// shortOptPrefixes are strings that come at the beginning of an option
	// argument that includes a path, e.g., -Ifoo/bar.
	shortOptPrefixes = []string{"-I", "-L", "-F"}

	// longOptPrefixes are separate arguments that come before a path argument,
	// e.g., -iquote foo/bar.
	longOptPrefixes = []string{"-I", "-L", "-F", "-iquote", "-isystem"}
)

// options transforms package-relative paths in cgo options into repository-
// root-relative paths that Bazel can understand. For example, if a cgo file
// in //foo declares an include flag in its copts: "-Ibar", this method
// will transform that flag into "-Ifoo/bar".
func (g *generator) options(opts rule.PlatformStrings, pkgRel string) rule.PlatformStrings {
	fixPath := func(opt string) string {
		if strings.HasPrefix(opt, "/") {
			return opt
		}
		return path.Clean(path.Join(pkgRel, opt))
	}

	fixGroups := func(groups []string) ([]string, error) {
		fixedGroups := make([]string, len(groups))
		for i, group := range groups {
			opts := strings.Split(group, optSeparator)
			fixedOpts := make([]string, len(opts))
			isPath := false
			for j, opt := range opts {
				if isPath {
					opt = fixPath(opt)
					isPath = false
					goto next
				}

				for _, short := range shortOptPrefixes {
					if strings.HasPrefix(opt, short) && len(opt) > len(short) {
						opt = short + fixPath(opt[len(short):])
						goto next
					}
				}

				for _, long := range longOptPrefixes {
					if opt == long {
						isPath = true
						goto next
					}
				}

			next:
				fixedOpts[j] = escapeOption(opt)
			}
			fixedGroups[i] = strings.Join(fixedOpts, " ")
		}

		return fixedGroups, nil
	}

	opts, errs := opts.MapSlice(fixGroups)
	if errs != nil {
		log.Panicf("unexpected error when transforming options with pkg %q: %v", pkgRel, errs)
	}
	return opts
}

func escapeOption(opt string) string {
	return strings.NewReplacer(
		`\`, `\\`,
		`'`, `\'`,
		`"`, `\"`,
		` `, `\ `,
		"\t", "\\\t",
		"\n", "\\\n",
		"\r", "\\\r",
		"$(", "$(",
		"$", "$$",
	).Replace(opt)
}

func shouldSetVisibility(args language.GenerateArgs) bool {
	if args.File != nil && args.File.HasDefaultVisibility() {
		return false
	}

	for _, r := range args.OtherGen {
		// This is kind of the same test as *File.HasDefaultVisibility(),
		// but for previously defined rules.
		if r.Kind() == "package" && r.Attr("default_visibility") != nil {
			return false
		}
	}
	return true
}

func (g *generator) addRelsToIndex(ps rule.PlatformStrings) {
	// TODO: refactor to for-iterator loop after Go 1.23 is the minimum version.
	ps.Each()(func(imp string) bool {
		for _, goSearch := range g.gc.goSearch {
			if trimmed := pathtools.TrimPrefix(imp, goSearch.prefix); goSearch.prefix == "" || trimmed != imp {
				rel := path.Join(goSearch.rel, trimmed)
				if _, ok := g.relsToIndexSeen[rel]; !ok {
					g.relsToIndexSeen[rel] = struct{}{}
				}
			}
		}
		return true
	})
}
