// Copyright 2020-2025 Buf Technologies, Inc.
//
// 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 linker

import (
	"errors"
	"fmt"
	"strings"

	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/reflect/protoregistry"
	"google.golang.org/protobuf/types/descriptorpb"

	"github.com/bufbuild/protocompile/ast"
	"github.com/bufbuild/protocompile/internal"
	"github.com/bufbuild/protocompile/reporter"
	"github.com/bufbuild/protocompile/walk"
)

func (r *result) ResolveMessageLiteralExtensionName(node ast.IdentValueNode) string {
	return r.optionQualifiedNames[node]
}

func (r *result) resolveElement(name protoreflect.FullName, checkedCache []string) protoreflect.Descriptor {
	if len(name) > 0 && name[0] == '.' {
		name = name[1:]
	}
	res, _ := resolveInFile(r, false, checkedCache[:0], func(f File) (protoreflect.Descriptor, error) {
		d := resolveElementInFile(name, f)
		if d != nil {
			return d, nil
		}
		return nil, protoregistry.NotFound
	})
	return res
}

func resolveInFile[T any](f File, publicImportsOnly bool, checked []string, fn func(File) (T, error)) (T, error) {
	var zero T
	path := f.Path()
	for _, str := range checked {
		if str == path {
			// already checked
			return zero, protoregistry.NotFound
		}
	}
	checked = append(checked, path)

	res, err := fn(f)
	if err == nil {
		// found it
		return res, nil
	}
	if !errors.Is(err, protoregistry.NotFound) {
		return zero, err
	}

	imports := f.Imports()
	for i, l := 0, imports.Len(); i < l; i++ {
		imp := imports.Get(i)
		if publicImportsOnly && !imp.IsPublic {
			continue
		}
		res, err := resolveInFile(f.FindImportByPath(imp.Path()), true, checked, fn)
		if errors.Is(err, protoregistry.NotFound) {
			continue
		}
		if err != nil {
			return zero, err
		}
		if !imp.IsPublic {
			if r, ok := f.(*result); ok {
				r.markUsed(imp.Path())
			}
		}
		return res, nil
	}
	return zero, err
}

func (r *result) markUsed(importPath string) {
	r.usedImports[importPath] = struct{}{}
}

func (r *result) CheckForUnusedImports(handler *reporter.Handler) {
	fd := r.FileDescriptorProto()
	file, _ := r.FileNode().(*ast.FileNode)
	for i, dep := range fd.Dependency {
		if _, ok := r.usedImports[dep]; !ok {
			isPublic := false
			// it's fine if it's a public import
			for _, j := range fd.PublicDependency {
				if i == int(j) {
					isPublic = true
					break
				}
			}
			if isPublic {
				continue
			}
			span := ast.UnknownSpan(fd.GetName())
			if file != nil {
				for _, decl := range file.Decls {
					imp, ok := decl.(*ast.ImportNode)
					if ok && imp.Name.AsString() == dep {
						span = file.NodeInfo(imp)
					}
				}
			}
			handler.HandleWarningWithPos(span, errUnusedImport(dep))
		}
	}
}

func descriptorTypeWithArticle(d protoreflect.Descriptor) string {
	switch d := d.(type) {
	case protoreflect.MessageDescriptor:
		return "a message"
	case protoreflect.FieldDescriptor:
		if d.IsExtension() {
			return "an extension"
		}
		return "a field"
	case protoreflect.OneofDescriptor:
		return "a oneof"
	case protoreflect.EnumDescriptor:
		return "an enum"
	case protoreflect.EnumValueDescriptor:
		return "an enum value"
	case protoreflect.ServiceDescriptor:
		return "a service"
	case protoreflect.MethodDescriptor:
		return "a method"
	case protoreflect.FileDescriptor:
		return "a file"
	default:
		// shouldn't be possible
		return fmt.Sprintf("a %T", d)
	}
}

func (r *result) createDescendants() {
	fd := r.FileDescriptorProto()
	pool := newAllocPool(fd)
	prefix := ""
	if fd.GetPackage() != "" {
		prefix = fd.GetPackage() + "."
	}
	r.imports = r.createImports()
	r.optionImports = fileImports{files: r.createBasicImports(fd.OptionDependency)}
	r.messages = r.createMessages(prefix, r, fd.MessageType, pool)
	r.enums = r.createEnums(prefix, r, fd.EnumType, pool)
	r.extensions = r.createExtensions(prefix, r, fd.Extension, pool)
	r.services = r.createServices(prefix, fd.Service, pool)
}

func (r *result) resolveReferences(handler *reporter.Handler, s *Symbols) error {
	fd := r.FileDescriptorProto()
	checkedCache := make([]string, 0, 16)
	scopes := []scope{fileScope(r, checkedCache)}
	if fd.Options != nil {
		if err := r.resolveOptions(handler, "file", protoreflect.FullName(fd.GetName()), fd.Options.UninterpretedOption, scopes, checkedCache); err != nil {
			return err
		}
	}

	// This is to de-dupe extendee-releated error messages when the same
	// extendee is referenced from multiple extension field definitions.
	// We leave it nil if there's no AST.
	var extendeeNodes map[ast.Node]struct{}

	return walk.DescriptorsEnterAndExit(r,
		func(d protoreflect.Descriptor) error {
			fqn := d.FullName()
			switch d := d.(type) {
			case *msgDescriptor:
				// Strangely, when protoc resolves extension names, it uses the *enclosing* scope
				// instead of the message's scope. So if the message contains an extension named "i",
				// an option cannot refer to it as simply "i" but must qualify it (at a minimum "Msg.i").
				// So we don't add this messages scope to our scopes slice until *after* we do options.
				if d.proto.Options != nil {
					if err := r.resolveOptions(handler, "message", fqn, d.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
						return err
					}
				}
				scopes = append(scopes, messageScope(r, fqn)) // push new scope on entry
				// walk only visits descriptors, so we need to loop over extension ranges ourselves
				for _, er := range d.proto.ExtensionRange {
					if er.Options != nil {
						erName := protoreflect.FullName(fmt.Sprintf("%s:%d-%d", fqn, er.GetStart(), er.GetEnd()-1))
						if err := r.resolveOptions(handler, "extension range", erName, er.Options.UninterpretedOption, scopes, checkedCache); err != nil {
							return err
						}
					}
				}
			case *extTypeDescriptor:
				if d.field.proto.Options != nil {
					if err := r.resolveOptions(handler, "extension", fqn, d.field.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
						return err
					}
				}
				if extendeeNodes == nil && r.AST() != nil {
					extendeeNodes = map[ast.Node]struct{}{}
				}
				if err := resolveFieldTypes(&d.field, handler, extendeeNodes, s, scopes, checkedCache); err != nil {
					return err
				}
				if r.Syntax() == protoreflect.Proto3 && !allowedProto3Extendee(d.field.proto.GetExtendee()) {
					file := r.FileNode()
					node := r.FieldNode(d.field.proto).FieldExtendee()
					if err := handler.HandleErrorf(file.NodeInfo(node), "extend blocks in proto3 can only be used to define custom options"); err != nil {
						return err
					}
				}
			case *fldDescriptor:
				if d.proto.Options != nil {
					if err := r.resolveOptions(handler, "field", fqn, d.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
						return err
					}
				}
				if err := resolveFieldTypes(d, handler, nil, s, scopes, checkedCache); err != nil {
					return err
				}
			case *oneofDescriptor:
				if d.proto.Options != nil {
					if err := r.resolveOptions(handler, "oneof", fqn, d.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
						return err
					}
				}
			case *enumDescriptor:
				if d.proto.Options != nil {
					if err := r.resolveOptions(handler, "enum", fqn, d.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
						return err
					}
				}
			case *enValDescriptor:
				if d.proto.Options != nil {
					if err := r.resolveOptions(handler, "enum value", fqn, d.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
						return err
					}
				}
			case *svcDescriptor:
				if d.proto.Options != nil {
					if err := r.resolveOptions(handler, "service", fqn, d.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
						return err
					}
				}
				// not a message, but same scoping rules for nested elements as if it were
				scopes = append(scopes, messageScope(r, fqn)) // push new scope on entry
			case *mtdDescriptor:
				if d.proto.Options != nil {
					if err := r.resolveOptions(handler, "method", fqn, d.proto.Options.UninterpretedOption, scopes, checkedCache); err != nil {
						return err
					}
				}
				if err := resolveMethodTypes(d, handler, scopes, checkedCache); err != nil {
					return err
				}
			}
			return nil
		},
		func(d protoreflect.Descriptor) error {
			switch d.(type) {
			case protoreflect.MessageDescriptor, protoreflect.ServiceDescriptor:
				// pop message scope on exit
				scopes = scopes[:len(scopes)-1]
			}
			return nil
		})
}

var allowedProto3Extendees = map[string]struct{}{
	".google.protobuf.FileOptions":           {},
	".google.protobuf.MessageOptions":        {},
	".google.protobuf.FieldOptions":          {},
	".google.protobuf.OneofOptions":          {},
	".google.protobuf.ExtensionRangeOptions": {},
	".google.protobuf.EnumOptions":           {},
	".google.protobuf.EnumValueOptions":      {},
	".google.protobuf.ServiceOptions":        {},
	".google.protobuf.MethodOptions":         {},
}

func allowedProto3Extendee(n string) bool {
	if n == "" {
		// not an extension, allowed
		return true
	}
	_, ok := allowedProto3Extendees[n]
	return ok
}

func resolveFieldTypes(f *fldDescriptor, handler *reporter.Handler, extendees map[ast.Node]struct{}, s *Symbols, scopes []scope, checkedCache []string) error {
	r := f.file
	fld := f.proto
	file := r.FileNode()
	node := r.FieldNode(fld)
	kind := "field"
	if fld.GetExtendee() != "" {
		kind = "extension"
		var alreadyReported bool
		if extendees != nil {
			_, alreadyReported = extendees[node.FieldExtendee()]
			if !alreadyReported {
				extendees[node.FieldExtendee()] = struct{}{}
			}
		}
		dsc := r.resolve(fld.GetExtendee(), false, scopes, checkedCache)
		if dsc == nil {
			if alreadyReported {
				return nil
			}
			var extendeePrefix string
			if extendees == nil {
				extendeePrefix = kind + " " + f.fqn + ": "
			}
			return handler.HandleErrorf(file.NodeInfo(node.FieldExtendee()), "%sunknown extendee type %s", extendeePrefix, fld.GetExtendee())
		}
		if isSentinelDescriptor(dsc) {
			if alreadyReported {
				return nil
			}
			var extendeePrefix string
			if extendees == nil {
				extendeePrefix = kind + " " + f.fqn + ": "
			}
			return handler.HandleErrorf(file.NodeInfo(node.FieldExtendee()), "%sunknown extendee type %s; resolved to %s which is not defined; consider using a leading dot", extendeePrefix, fld.GetExtendee(), dsc.FullName())
		}
		extd, ok := dsc.(protoreflect.MessageDescriptor)
		if !ok {
			if alreadyReported {
				return nil
			}
			var extendeePrefix string
			if extendees == nil {
				extendeePrefix = kind + " " + f.fqn + ": "
			}
			return handler.HandleErrorf(file.NodeInfo(node.FieldExtendee()), "%sextendee is invalid: %s is %s, not a message", extendeePrefix, dsc.FullName(), descriptorTypeWithArticle(dsc))
		}

		f.extendee = extd
		extendeeName := "." + string(dsc.FullName())
		if fld.GetExtendee() != extendeeName {
			fld.Extendee = proto.String(extendeeName)
		}
		// make sure the tag number is in range
		found := false
		tag := protoreflect.FieldNumber(fld.GetNumber())
		for i := range extd.ExtensionRanges().Len() {
			rng := extd.ExtensionRanges().Get(i)
			if tag >= rng[0] && tag < rng[1] {
				found = true
				break
			}
		}
		if !found {
			if err := handler.HandleErrorf(file.NodeInfo(node.FieldTag()), "%s %s: tag %d is not in valid range for extended type %s", kind, f.fqn, tag, dsc.FullName()); err != nil {
				return err
			}
		} else {
			// make sure tag is not a duplicate
			if err := s.AddExtension(packageFor(dsc), dsc.FullName(), tag, file.NodeInfo(node.FieldTag()), handler); err != nil {
				return err
			}
		}
	} else if f.proto.OneofIndex != nil {
		parent := f.parent.(protoreflect.MessageDescriptor) //nolint:errcheck
		index := int(f.proto.GetOneofIndex())
		f.oneof = parent.Oneofs().Get(index)
	}

	if fld.GetTypeName() == "" {
		// scalar type; no further resolution required
		return nil
	}

	dsc := r.resolve(fld.GetTypeName(), true, scopes, checkedCache)
	if dsc == nil {
		return handler.HandleErrorf(file.NodeInfo(node.FieldType()), "%s %s: unknown type %s", kind, f.fqn, fld.GetTypeName())
	}
	if isSentinelDescriptor(dsc) {
		return handler.HandleErrorf(file.NodeInfo(node.FieldType()), "%s %s: unknown type %s; resolved to %s which is not defined; consider using a leading dot", kind, f.fqn, fld.GetTypeName(), dsc.FullName())
	}
	switch dsc := dsc.(type) {
	case protoreflect.MessageDescriptor:
		if dsc.IsMapEntry() {
			isValid := false
			switch node.(type) {
			case *ast.MapFieldNode:
				// We have an AST for this file and can see this field is from a map declaration
				isValid = true
			case *ast.NoSourceNode:
				// We don't have an AST for the file (it came from a provided descriptor). So we
				// need to validate that it's not an illegal reference. To be valid, the field
				// must be repeated and the entry type must be nested in the same enclosing
				// message as the field.
				isValid = isValidMap(f, dsc)
				if isValid && f.index > 0 {
					// also make sure there are no earlier fields that are valid for this map entry
					flds := f.Parent().(protoreflect.MessageDescriptor).Fields() //nolint:errcheck
					for i := range f.index {
						if isValidMap(flds.Get(i), dsc) {
							isValid = false
							break
						}
					}
				}
			}
			if !isValid {
				return handler.HandleErrorf(file.NodeInfo(node.FieldType()), "%s %s: %s is a synthetic map entry and may not be referenced explicitly", kind, f.fqn, dsc.FullName())
			}
		}
		typeName := "." + string(dsc.FullName())
		if fld.GetTypeName() != typeName {
			fld.TypeName = proto.String(typeName)
		}
		if fld.Type == nil {
			// if type was tentatively unset, we now know it's actually a message
			fld.Type = descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
		} else if fld.GetType() != descriptorpb.FieldDescriptorProto_TYPE_MESSAGE && fld.GetType() != descriptorpb.FieldDescriptorProto_TYPE_GROUP {
			return handler.HandleErrorf(file.NodeInfo(node.FieldType()), "%s %s: descriptor proto indicates type %v but should be %v", kind, f.fqn, fld.GetType(), descriptorpb.FieldDescriptorProto_TYPE_MESSAGE)
		}
		f.msgType = dsc
	case protoreflect.EnumDescriptor:
		typeName := "." + string(dsc.FullName())
		if fld.GetTypeName() != typeName {
			fld.TypeName = proto.String(typeName)
		}
		if fld.Type == nil {
			// the type was tentatively unset, but now we know it's actually an enum
			fld.Type = descriptorpb.FieldDescriptorProto_TYPE_ENUM.Enum()
		} else if fld.GetType() != descriptorpb.FieldDescriptorProto_TYPE_ENUM {
			return handler.HandleErrorf(file.NodeInfo(node.FieldType()), "%s %s: descriptor proto indicates type %v but should be %v", kind, f.fqn, fld.GetType(), descriptorpb.FieldDescriptorProto_TYPE_ENUM)
		}
		f.enumType = dsc
	default:
		return handler.HandleErrorf(file.NodeInfo(node.FieldType()), "%s %s: invalid type: %s is %s, not a message or enum", kind, f.fqn, dsc.FullName(), descriptorTypeWithArticle(dsc))
	}
	return nil
}

func packageFor(dsc protoreflect.Descriptor) protoreflect.FullName {
	if dsc.ParentFile() != nil {
		return dsc.ParentFile().Package()
	}
	// Can't access package? Make a best effort guess.
	return dsc.FullName().Parent()
}

func isValidMap(mapField protoreflect.FieldDescriptor, mapEntry protoreflect.MessageDescriptor) bool {
	return !mapField.IsExtension() &&
		mapEntry.Parent() == mapField.ContainingMessage() &&
		mapField.Cardinality() == protoreflect.Repeated &&
		string(mapEntry.Name()) == internal.MapEntry(string(mapField.Name()))
}

func resolveMethodTypes(m *mtdDescriptor, handler *reporter.Handler, scopes []scope, checkedCache []string) error {
	scope := "method " + m.fqn
	r := m.file
	mtd := m.proto
	file := r.FileNode()
	node := r.MethodNode(mtd)
	dsc := r.resolve(mtd.GetInputType(), false, scopes, checkedCache)
	if dsc == nil {
		if err := handler.HandleErrorf(file.NodeInfo(node.GetInputType()), "%s: unknown request type %s", scope, mtd.GetInputType()); err != nil {
			return err
		}
	} else if isSentinelDescriptor(dsc) {
		if err := handler.HandleErrorf(file.NodeInfo(node.GetInputType()), "%s: unknown request type %s; resolved to %s which is not defined; consider using a leading dot", scope, mtd.GetInputType(), dsc.FullName()); err != nil {
			return err
		}
	} else if msg, ok := dsc.(protoreflect.MessageDescriptor); !ok {
		if err := handler.HandleErrorf(file.NodeInfo(node.GetInputType()), "%s: invalid request type: %s is %s, not a message", scope, dsc.FullName(), descriptorTypeWithArticle(dsc)); err != nil {
			return err
		}
	} else {
		typeName := "." + string(dsc.FullName())
		if mtd.GetInputType() != typeName {
			mtd.InputType = proto.String(typeName)
		}
		m.inputType = msg
	}

	// TODO: make input and output type resolution more DRY
	dsc = r.resolve(mtd.GetOutputType(), false, scopes, checkedCache)
	if dsc == nil {
		if err := handler.HandleErrorf(file.NodeInfo(node.GetOutputType()), "%s: unknown response type %s", scope, mtd.GetOutputType()); err != nil {
			return err
		}
	} else if isSentinelDescriptor(dsc) {
		if err := handler.HandleErrorf(file.NodeInfo(node.GetOutputType()), "%s: unknown response type %s; resolved to %s which is not defined; consider using a leading dot", scope, mtd.GetOutputType(), dsc.FullName()); err != nil {
			return err
		}
	} else if msg, ok := dsc.(protoreflect.MessageDescriptor); !ok {
		if err := handler.HandleErrorf(file.NodeInfo(node.GetOutputType()), "%s: invalid response type: %s is %s, not a message", scope, dsc.FullName(), descriptorTypeWithArticle(dsc)); err != nil {
			return err
		}
	} else {
		typeName := "." + string(dsc.FullName())
		if mtd.GetOutputType() != typeName {
			mtd.OutputType = proto.String(typeName)
		}
		m.outputType = msg
	}

	return nil
}

func (r *result) resolveOptions(handler *reporter.Handler, elemType string, elemName protoreflect.FullName, opts []*descriptorpb.UninterpretedOption, scopes []scope, checkedCache []string) error {
	mc := &internal.MessageContext{
		File:        r,
		ElementName: string(elemName),
		ElementType: elemType,
	}
	file := r.FileNode()
opts:
	for _, opt := range opts {
		// resolve any extension names found in option names
		for _, nm := range opt.Name {
			if nm.GetIsExtension() {
				node := r.OptionNamePartNode(nm)
				fqn, err := r.resolveExtensionName(nm.GetNamePart(), scopes, checkedCache)
				if err != nil {
					if err := handler.HandleErrorf(file.NodeInfo(node), "%v%v", mc, err); err != nil {
						return err
					}
					continue opts
				}
				nm.NamePart = proto.String(fqn)
			}
		}
		// also resolve any extension names found inside message literals in option values
		mc.Option = opt
		optVal := r.OptionNode(opt).GetValue()
		if err := r.resolveOptionValue(handler, mc, optVal, scopes, checkedCache); err != nil {
			return err
		}
		mc.Option = nil
	}
	return nil
}

func (r *result) resolveOptionValue(handler *reporter.Handler, mc *internal.MessageContext, val ast.ValueNode, scopes []scope, checkedCache []string) error {
	optVal := val.Value()
	switch optVal := optVal.(type) {
	case []ast.ValueNode:
		origPath := mc.OptAggPath
		defer func() {
			mc.OptAggPath = origPath
		}()
		for i, v := range optVal {
			mc.OptAggPath = fmt.Sprintf("%s[%d]", origPath, i)
			if err := r.resolveOptionValue(handler, mc, v, scopes, checkedCache); err != nil {
				return err
			}
		}
	case []*ast.MessageFieldNode:
		origPath := mc.OptAggPath
		defer func() {
			mc.OptAggPath = origPath
		}()
		for _, fld := range optVal {
			// check for extension name
			if fld.Name.IsExtension() {
				// Confusingly, an extension reference inside a message literal cannot refer to
				// elements in the same enclosing message without a qualifier. Basically, we
				// treat this as if there were no message scopes, so only the package name is
				// used for resolving relative references. (Inconsistent protoc behavior, but
				// likely due to how it re-uses C++ text format implementation, and normal text
				// format doesn't expect that kind of relative reference.)
				scopes := scopes[:1] // first scope is file, the rest are enclosing messages
				fqn, err := r.resolveExtensionName(string(fld.Name.Name.AsIdentifier()), scopes, checkedCache)
				if err != nil {
					if err := handler.HandleErrorf(r.FileNode().NodeInfo(fld.Name.Name), "%v%v", mc, err); err != nil {
						return err
					}
				} else {
					r.optionQualifiedNames[fld.Name.Name] = fqn
				}
			}

			// recurse into value
			mc.OptAggPath = origPath
			if origPath != "" {
				mc.OptAggPath += "."
			}
			if fld.Name.IsExtension() {
				mc.OptAggPath = fmt.Sprintf("%s[%s]", mc.OptAggPath, string(fld.Name.Name.AsIdentifier()))
			} else {
				mc.OptAggPath = fmt.Sprintf("%s%s", mc.OptAggPath, string(fld.Name.Name.AsIdentifier()))
			}

			if err := r.resolveOptionValue(handler, mc, fld.Val, scopes, checkedCache); err != nil {
				return err
			}
		}
	}
	return nil
}

func (r *result) resolveExtensionName(name string, scopes []scope, checkedCache []string) (string, error) {
	dsc := r.resolve(name, false, scopes, checkedCache)
	if dsc == nil {
		return "", fmt.Errorf("unknown extension %s", name)
	}
	if isSentinelDescriptor(dsc) {
		return "", fmt.Errorf("unknown extension %s; resolved to %s which is not defined; consider using a leading dot", name, dsc.FullName())
	}
	if ext, ok := dsc.(protoreflect.FieldDescriptor); !ok {
		return "", fmt.Errorf("invalid extension: %s is %s, not an extension", name, descriptorTypeWithArticle(dsc))
	} else if !ext.IsExtension() {
		return "", fmt.Errorf("invalid extension: %s is a field but not an extension", name)
	}
	return string("." + dsc.FullName()), nil
}

func (r *result) resolve(name string, onlyTypes bool, scopes []scope, checkedCache []string) protoreflect.Descriptor {
	if strings.HasPrefix(name, ".") {
		// already fully-qualified
		return r.resolveElement(protoreflect.FullName(name[1:]), checkedCache)
	}
	// unqualified, so we look in the enclosing (last) scope first and move
	// towards outermost (first) scope, trying to resolve the symbol
	pos := strings.IndexByte(name, '.')
	firstName := name
	if pos > 0 {
		firstName = name[:pos]
	}
	var bestGuess protoreflect.Descriptor
	for i := len(scopes) - 1; i >= 0; i-- {
		d := scopes[i](firstName, name)
		if d != nil {
			// In `protoc`, it will skip a match of the wrong type and move on
			// to the next scope, but only if the reference is unqualified. So
			// we mirror that behavior here. When we skip and move on, we go
			// ahead and save the match of the wrong type so we can at least use
			// it to construct a better error in the event that we don't find
			// any match of the right type.
			if !onlyTypes || isType(d) || firstName != name {
				return d
			}
			if bestGuess == nil {
				bestGuess = d
			}
		}
	}
	// we return best guess, even though it was not an allowed kind of
	// descriptor, so caller can print a better error message (e.g.
	// indicating that the name was found but that it's the wrong type)
	return bestGuess
}

func isType(d protoreflect.Descriptor) bool {
	switch d.(type) {
	case protoreflect.MessageDescriptor, protoreflect.EnumDescriptor:
		return true
	}
	return false
}

// scope represents a lexical scope in a proto file in which messages and enums
// can be declared.
type scope func(firstName, fullName string) protoreflect.Descriptor

func fileScope(r *result, checkedCache []string) scope {
	// we search symbols in this file, but also symbols in other files that have
	// the same package as this file or a "parent" package (in protobuf,
	// packages are a hierarchy like C++ namespaces)
	prefixes := internal.CreatePrefixList(r.FileDescriptorProto().GetPackage())
	querySymbol := func(n string) protoreflect.Descriptor {
		return r.resolveElement(protoreflect.FullName(n), checkedCache)
	}
	return func(firstName, fullName string) protoreflect.Descriptor {
		for _, prefix := range prefixes {
			var n1, n string
			if prefix == "" {
				// exhausted all prefixes, so it must be in this one
				n1, n = fullName, fullName
			} else {
				n = prefix + "." + fullName
				n1 = prefix + "." + firstName
			}
			d := resolveElementRelative(n1, n, querySymbol)
			if d != nil {
				return d
			}
		}
		return nil
	}
}

func messageScope(r *result, messageName protoreflect.FullName) scope {
	querySymbol := func(n string) protoreflect.Descriptor {
		return resolveElementInFile(protoreflect.FullName(n), r)
	}
	return func(firstName, fullName string) protoreflect.Descriptor {
		n1 := string(messageName) + "." + firstName
		n := string(messageName) + "." + fullName
		return resolveElementRelative(n1, n, querySymbol)
	}
}

func resolveElementRelative(firstName, fullName string, query func(name string) protoreflect.Descriptor) protoreflect.Descriptor {
	d := query(firstName)
	if d == nil {
		return nil
	}
	if firstName == fullName {
		return d
	}
	if !isAggregateDescriptor(d) {
		// can't possibly find the rest of full name if
		// the first name indicated a leaf descriptor
		return nil
	}
	d = query(fullName)
	if d == nil {
		return newSentinelDescriptor(fullName)
	}
	return d
}

func resolveElementInFile(name protoreflect.FullName, f File) protoreflect.Descriptor {
	d := f.FindDescriptorByName(name)
	if d != nil {
		return d
	}

	if matchesPkgNamespace(name, f.Package()) {
		// this sentinel means the name is a valid namespace but
		// does not refer to a descriptor
		return newSentinelDescriptor(string(name))
	}
	return nil
}

func matchesPkgNamespace(fqn, pkg protoreflect.FullName) bool {
	if pkg == "" {
		return false
	}
	if fqn == pkg {
		return true
	}
	if len(pkg) > len(fqn) && strings.HasPrefix(string(pkg), string(fqn)) {
		// if char after fqn is a dot, then fqn is a namespace
		if pkg[len(fqn)] == '.' {
			return true
		}
	}
	return false
}

func isAggregateDescriptor(d protoreflect.Descriptor) bool {
	if isSentinelDescriptor(d) {
		// this indicates the name matched a package, not a
		// descriptor, but a package is an aggregate, so
		// we return true
		return true
	}
	switch d.(type) {
	case protoreflect.MessageDescriptor, protoreflect.EnumDescriptor, protoreflect.ServiceDescriptor:
		return true
	default:
		return false
	}
}

func isSentinelDescriptor(d protoreflect.Descriptor) bool {
	_, ok := d.(*sentinelDescriptor)
	return ok
}

func newSentinelDescriptor(name string) protoreflect.Descriptor {
	return &sentinelDescriptor{name: name}
}

// sentinelDescriptor is a placeholder descriptor. It is used instead of nil to
// distinguish between two situations:
//  1. The given name could not be found.
//  2. The given name *cannot* be a valid result so stop searching.
//
// In these cases, attempts to resolve an element name will return nil for the
// first case and will return a sentinelDescriptor in the second. The sentinel
// contains the fully-qualified name which caused the search to stop (which may
// be a prefix of the actual name being resolved).
type sentinelDescriptor struct {
	protoreflect.Descriptor
	name string
}

func (p *sentinelDescriptor) ParentFile() protoreflect.FileDescriptor {
	return nil
}

func (p *sentinelDescriptor) Parent() protoreflect.Descriptor {
	return nil
}

func (p *sentinelDescriptor) Index() int {
	return 0
}

func (p *sentinelDescriptor) Syntax() protoreflect.Syntax {
	return 0
}

func (p *sentinelDescriptor) Name() protoreflect.Name {
	return protoreflect.Name(p.name)
}

func (p *sentinelDescriptor) FullName() protoreflect.FullName {
	return protoreflect.FullName(p.name)
}

func (p *sentinelDescriptor) IsPlaceholder() bool {
	return false
}

func (p *sentinelDescriptor) Options() protoreflect.ProtoMessage {
	return nil
}

var _ protoreflect.Descriptor = (*sentinelDescriptor)(nil)
