// Copyright 2021 Google LLC
//
// 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
//
//     https://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 adapt

import (
	"encoding/base64"
	"fmt"
	"sort"
	"strings"

	"cloud.google.com/go/bigquery/storage/apiv1/storagepb"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protodesc"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/types/descriptorpb"
	"google.golang.org/protobuf/types/known/durationpb"
	"google.golang.org/protobuf/types/known/timestamppb"
	"google.golang.org/protobuf/types/known/wrapperspb"
)

var bqModeToFieldLabelMapProto2 = map[storagepb.TableFieldSchema_Mode]descriptorpb.FieldDescriptorProto_Label{
	storagepb.TableFieldSchema_NULLABLE: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL,
	storagepb.TableFieldSchema_REPEATED: descriptorpb.FieldDescriptorProto_LABEL_REPEATED,
	storagepb.TableFieldSchema_REQUIRED: descriptorpb.FieldDescriptorProto_LABEL_REQUIRED,
}

var bqModeToFieldLabelMapProto3 = map[storagepb.TableFieldSchema_Mode]descriptorpb.FieldDescriptorProto_Label{
	storagepb.TableFieldSchema_NULLABLE: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL,
	storagepb.TableFieldSchema_REPEATED: descriptorpb.FieldDescriptorProto_LABEL_REPEATED,
	storagepb.TableFieldSchema_REQUIRED: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL,
}

func convertModeToLabel(mode storagepb.TableFieldSchema_Mode, useProto3 bool) *descriptorpb.FieldDescriptorProto_Label {
	if useProto3 {
		return bqModeToFieldLabelMapProto3[mode].Enum()
	}
	return bqModeToFieldLabelMapProto2[mode].Enum()
}

// Allows conversion between BQ schema type and FieldDescriptorProto's type.
var bqTypeToFieldTypeMap = map[storagepb.TableFieldSchema_Type]descriptorpb.FieldDescriptorProto_Type{
	storagepb.TableFieldSchema_BIGNUMERIC: descriptorpb.FieldDescriptorProto_TYPE_BYTES,
	storagepb.TableFieldSchema_BOOL:       descriptorpb.FieldDescriptorProto_TYPE_BOOL,
	storagepb.TableFieldSchema_BYTES:      descriptorpb.FieldDescriptorProto_TYPE_BYTES,
	storagepb.TableFieldSchema_DATE:       descriptorpb.FieldDescriptorProto_TYPE_INT32,
	storagepb.TableFieldSchema_DATETIME:   descriptorpb.FieldDescriptorProto_TYPE_INT64,
	storagepb.TableFieldSchema_DOUBLE:     descriptorpb.FieldDescriptorProto_TYPE_DOUBLE,
	storagepb.TableFieldSchema_GEOGRAPHY:  descriptorpb.FieldDescriptorProto_TYPE_STRING,
	storagepb.TableFieldSchema_INT64:      descriptorpb.FieldDescriptorProto_TYPE_INT64,
	storagepb.TableFieldSchema_NUMERIC:    descriptorpb.FieldDescriptorProto_TYPE_BYTES,
	storagepb.TableFieldSchema_STRING:     descriptorpb.FieldDescriptorProto_TYPE_STRING,
	storagepb.TableFieldSchema_STRUCT:     descriptorpb.FieldDescriptorProto_TYPE_MESSAGE,
	storagepb.TableFieldSchema_TIME:       descriptorpb.FieldDescriptorProto_TYPE_INT64,
	storagepb.TableFieldSchema_TIMESTAMP:  descriptorpb.FieldDescriptorProto_TYPE_INT64,
	storagepb.TableFieldSchema_RANGE:      descriptorpb.FieldDescriptorProto_TYPE_MESSAGE,
	storagepb.TableFieldSchema_JSON:       descriptorpb.FieldDescriptorProto_TYPE_STRING,
}

var allowedRangeTypes = []storagepb.TableFieldSchema_Type{
	storagepb.TableFieldSchema_DATE,
	storagepb.TableFieldSchema_DATETIME,
	storagepb.TableFieldSchema_TIMESTAMP,
}

// Primitive types which can leverage packed encoding when repeated/arrays.
//
// Note: many/most of these aren't used when doing schema to proto conversion, but
// are included for completeness.
var packedTypes = []descriptorpb.FieldDescriptorProto_Type{
	descriptorpb.FieldDescriptorProto_TYPE_INT32,
	descriptorpb.FieldDescriptorProto_TYPE_INT64,
	descriptorpb.FieldDescriptorProto_TYPE_UINT32,
	descriptorpb.FieldDescriptorProto_TYPE_UINT64,
	descriptorpb.FieldDescriptorProto_TYPE_SINT32,
	descriptorpb.FieldDescriptorProto_TYPE_SINT64,
	descriptorpb.FieldDescriptorProto_TYPE_FIXED32,
	descriptorpb.FieldDescriptorProto_TYPE_FIXED64,
	descriptorpb.FieldDescriptorProto_TYPE_SFIXED32,
	descriptorpb.FieldDescriptorProto_TYPE_SFIXED64,
	descriptorpb.FieldDescriptorProto_TYPE_FLOAT,
	descriptorpb.FieldDescriptorProto_TYPE_DOUBLE,
	descriptorpb.FieldDescriptorProto_TYPE_BOOL,
	descriptorpb.FieldDescriptorProto_TYPE_ENUM,
}

// For TableFieldSchema OPTIONAL mode, we use the wrapper types to allow for the
// proper representation of NULL values, as proto3 semantics would just use default value.
var bqTypeToWrapperMap = map[storagepb.TableFieldSchema_Type]string{
	storagepb.TableFieldSchema_BIGNUMERIC: ".google.protobuf.BytesValue",
	storagepb.TableFieldSchema_BOOL:       ".google.protobuf.BoolValue",
	storagepb.TableFieldSchema_BYTES:      ".google.protobuf.BytesValue",
	storagepb.TableFieldSchema_DATE:       ".google.protobuf.Int32Value",
	storagepb.TableFieldSchema_DATETIME:   ".google.protobuf.Int64Value",
	storagepb.TableFieldSchema_DOUBLE:     ".google.protobuf.DoubleValue",
	storagepb.TableFieldSchema_GEOGRAPHY:  ".google.protobuf.StringValue",
	storagepb.TableFieldSchema_INT64:      ".google.protobuf.Int64Value",
	storagepb.TableFieldSchema_NUMERIC:    ".google.protobuf.BytesValue",
	storagepb.TableFieldSchema_STRING:     ".google.protobuf.StringValue",
	storagepb.TableFieldSchema_TIME:       ".google.protobuf.Int64Value",
	storagepb.TableFieldSchema_TIMESTAMP:  ".google.protobuf.Int64Value",
	storagepb.TableFieldSchema_JSON:       ".google.protobuf.StringValue",
}

// filename used by well known types proto
var wellKnownTypesWrapperName = "google/protobuf/wrappers.proto"

// filename used by timestamp and duration proto
var extraWellKnownTypesPerTypeName = map[string]struct {
	name           string
	fileDescriptor *descriptorpb.FileDescriptorProto
}{
	".google.protobuf.Timestamp": {
		name:           "google/protobuf/timestamp.proto",
		fileDescriptor: protodesc.ToFileDescriptorProto(timestamppb.File_google_protobuf_timestamp_proto),
	},
	".google.protobuf.Duration": {
		name:           "google/protobuf/duration.proto",
		fileDescriptor: protodesc.ToFileDescriptorProto(durationpb.File_google_protobuf_duration_proto),
	},
}

var rangeTypesPrefix = "rangemessage_range_"

// dependencyCache is used to reduce the number of unique messages we generate by caching based on the tableschema.
//
// Keys are based on the base64-encoded serialized tableschema value.
type dependencyCache struct {
	// keyed by element type
	rangeTypes map[storagepb.TableFieldSchema_Type]protoreflect.MessageDescriptor
	// general cache
	msgs map[string]protoreflect.MessageDescriptor
}

func newDependencyCache() *dependencyCache {
	return &dependencyCache{
		rangeTypes: make(map[storagepb.TableFieldSchema_Type]protoreflect.MessageDescriptor),
		msgs:       make(map[string]protoreflect.MessageDescriptor),
	}
}

func (dm *dependencyCache) get(schema *storagepb.TableSchema) protoreflect.MessageDescriptor {
	if dm == nil {
		return nil
	}
	b, err := proto.Marshal(schema)
	if err != nil {
		return nil
	}
	encoded := base64.StdEncoding.EncodeToString(b)
	if desc, ok := dm.msgs[encoded]; ok {
		return desc
	}
	return nil
}

func (dm *dependencyCache) getFileDescriptorProtos() []*descriptorpb.FileDescriptorProto {
	var fdpList []*descriptorpb.FileDescriptorProto
	// emit encountered messages.
	for _, d := range dm.msgs {
		if fd := d.ParentFile(); fd != nil {
			fdp := protodesc.ToFileDescriptorProto(fd)
			fdpList = append(fdpList, fdp)
		}
	}
	// emit any range value types used.
	for _, d := range dm.rangeTypes {
		if fd := d.ParentFile(); fd != nil {
			fdp := protodesc.ToFileDescriptorProto(fd)
			fdpList = append(fdpList, fdp)
		}
	}
	return fdpList
}

func (dm *dependencyCache) add(schema *storagepb.TableSchema, descriptor protoreflect.MessageDescriptor) error {
	if dm == nil {
		return fmt.Errorf("cache is nil")
	}
	b, err := proto.Marshal(schema)
	if err != nil {
		return fmt.Errorf("failed to serialize tableschema: %w", err)
	}
	encoded := base64.StdEncoding.EncodeToString(b)
	dm.msgs[encoded] = descriptor
	return nil
}

func (dm *dependencyCache) addRangeByElementType(typ storagepb.TableFieldSchema_Type, cfg *customConfig) (protoreflect.MessageDescriptor, error) {
	if md, present := dm.rangeTypes[typ]; present {
		// already added, do nothing.
		return md, nil
	}
	// Not yet present.  Build the message.
	allowed := false
	for _, a := range allowedRangeTypes {
		if typ == a {
			allowed = true
		}
	}
	if !allowed {
		return nil, fmt.Errorf("range does not support %q as a valid element type", typ.String())
	}
	ts := &storagepb.TableSchema{
		Fields: []*storagepb.TableFieldSchema{
			{
				Name: "start",
				Type: typ,
				Mode: storagepb.TableFieldSchema_NULLABLE,
			},
			{
				Name: "end",
				Type: typ,
				Mode: storagepb.TableFieldSchema_NULLABLE,
			},
		},
	}
	// we put the range types outside the hierarchical namespace as they're effectively BQ-specific well-known types.
	msgTypeName := fmt.Sprintf("%s%s", rangeTypesPrefix, strings.ToLower(typ.String()))
	// use a new dependency cache, as we don't want to taint the main one due to matching schema
	md, err := storageSchemaToDescriptorInternal(ts, msgTypeName, newDependencyCache(), cfg)
	if err != nil {
		return nil, fmt.Errorf("failed to generate range descriptor %q: %v", msgTypeName, err)
	}
	dm.rangeTypes[typ] = md
	return md, nil
}

func (dm *dependencyCache) getRange(typ storagepb.TableFieldSchema_Type) protoreflect.MessageDescriptor {
	md, ok := dm.rangeTypes[typ]
	if !ok {
		return nil
	}
	return md
}

// StorageSchemaToProto2Descriptor builds a protoreflect.Descriptor for a given table schema using proto2 syntax.
func StorageSchemaToProto2Descriptor(inSchema *storagepb.TableSchema, scope string) (protoreflect.Descriptor, error) {
	return StorageSchemaToProtoDescriptorWithOptions(inSchema, scope, withProto2())
}

// StorageSchemaToProto3Descriptor builds a protoreflect.Descriptor for a given table schema using proto3 syntax.
//
// NOTE: Currently the write API doesn't yet support proto3 behaviors (default value, wrapper types, etc), but this is provided for
// completeness.
func StorageSchemaToProto3Descriptor(inSchema *storagepb.TableSchema, scope string) (protoreflect.Descriptor, error) {
	return StorageSchemaToProtoDescriptorWithOptions(inSchema, scope, withProto3())
}

// StorageSchemaToProtoDescriptorWithOptions builds a protoreflect.Descriptor for a given table schema with
// extra configuration options. Uses proto2 syntax by default.
func StorageSchemaToProtoDescriptorWithOptions(inSchema *storagepb.TableSchema, scope string, opts ...ProtoConversionOption) (protoreflect.Descriptor, error) {
	dc := newDependencyCache()
	cfg := &customConfig{
		useProto3:             false,
		protoMappingOverrides: protoMappingOverrides{},
	}
	for _, opt := range opts {
		opt.applyCustomClientOpt(cfg)
	}
	// TODO: b/193064992 tracks support for wrapper types.  In the interim, disable wrapper usage.
	return storageSchemaToDescriptorInternal(inSchema, scope, dc, cfg)
}

// Internal implementation of the conversion code.
func storageSchemaToDescriptorInternal(inSchema *storagepb.TableSchema, scope string, cache *dependencyCache, cfg *customConfig) (protoreflect.MessageDescriptor, error) {
	if inSchema == nil {
		return nil, newConversionError(scope, fmt.Errorf("no input schema was provided"))
	}

	var fields []*descriptorpb.FieldDescriptorProto
	var deps []protoreflect.FileDescriptor
	var fNumber int32

	for _, f := range inSchema.GetFields() {
		fNumber = fNumber + 1
		currentScope := fmt.Sprintf("%s__%s", scope, f.GetName())

		if f.Type == storagepb.TableFieldSchema_STRUCT {
			// If we're dealing with a STRUCT type, we must deal with sub messages.
			// As multiple submessages may share the same type definition, we use a dependency cache
			// and interrogate it / populate it as we're going.
			foundDesc := cache.get(&storagepb.TableSchema{Fields: f.GetFields()})
			if foundDesc != nil {
				// check to see if we already have this in current dependency list
				haveDep := false
				for _, dep := range deps {
					if messageDependsOnFile(foundDesc, dep) {
						haveDep = true
						break
					}
				}
				// If dep is missing, add to current dependencies.
				if !haveDep {
					deps = append(deps, foundDesc.ParentFile())
				}
				// Construct field descriptor for the message.
				fdp := tableFieldSchemaToFieldDescriptorProto(f, fNumber, string(foundDesc.FullName()), cfg)
				fields = append(fields, fdp)
			} else {
				// Wrap the current struct's fields in a TableSchema outer message, and then build the submessage.
				ts := &storagepb.TableSchema{
					Fields: f.GetFields(),
				}
				desc, err := storageSchemaToDescriptorInternal(ts, currentScope, cache, cfg)
				if err != nil {
					return nil, newConversionError(currentScope, fmt.Errorf("couldn't convert message: %w", err))
				}
				// Now that we have the submessage definition, we append it both to the local dependencies, as well
				// as inserting it into the cache for possible reuse elsewhere.
				deps = append(deps, desc.ParentFile())
				err = cache.add(ts, desc)
				if err != nil {
					return nil, newConversionError(currentScope, fmt.Errorf("failed to add descriptor to dependency cache: %w", err))
				}
				fdp := tableFieldSchemaToFieldDescriptorProto(f, fNumber, currentScope, cfg)
				fields = append(fields, fdp)
			}
		} else {
			if f.Type == storagepb.TableFieldSchema_RANGE {
				// Range handling is a special case of general struct handling.
				ret := f.GetRangeElementType()
				if ret == nil {
					return nil, fmt.Errorf("field %q is a RANGE, but doesn't include RangeElementType info", f.GetName())
				}
				foundDesc, err := cache.addRangeByElementType(ret.GetType(), cfg)
				if err != nil {
					return nil, err
				}
				if foundDesc != nil {
					haveDep := false
					for _, dep := range deps {
						if messageDependsOnFile(foundDesc, dep) {
							haveDep = true
							break
						}
					}
					// If dep is missing, add to current dependencies.
					if !haveDep {
						deps = append(deps, foundDesc.ParentFile())
					}
				}
			}
			fd := tableFieldSchemaToFieldDescriptorProto(f, fNumber, currentScope, cfg)
			fields = append(fields, fd)
		}
	}
	// Start constructing a DescriptorProto.
	dp := &descriptorpb.DescriptorProto{
		Name:  proto.String(scope),
		Field: fields,
	}

	// Use the local dependencies to generate a list of filenames.
	depNames := []string{wellKnownTypesWrapperName}
	for _, override := range cfg.protoMappingOverrides {
		if dep, found := extraWellKnownTypesPerTypeName[override.TypeName]; found {
			depNames = append(depNames, dep.name)
		}
	}
	for _, d := range deps {
		depNames = append(depNames, d.ParentFile().Path())
	}

	// Now, construct a FileDescriptorProto.
	fdp := &descriptorpb.FileDescriptorProto{
		MessageType: []*descriptorpb.DescriptorProto{dp},
		Name:        proto.String(fmt.Sprintf("%s.proto", scope)),
		Syntax:      proto.String("proto3"),
		Dependency:  depNames,
	}
	if !cfg.useProto3 {
		fdp.Syntax = proto.String("proto2")
	}

	// We'll need a FileDescriptorSet as we have a FileDescriptorProto for the current
	// descriptor we're building, but we need to include all the referenced dependencies.

	fdpList := []*descriptorpb.FileDescriptorProto{
		fdp,
		protodesc.ToFileDescriptorProto(wrapperspb.File_google_protobuf_wrappers_proto),
	}
	for _, override := range cfg.protoMappingOverrides {
		if dep, found := extraWellKnownTypesPerTypeName[override.TypeName]; found {
			fdpList = append(fdpList, dep.fileDescriptor)
		}
	}
	fdpList = append(fdpList, cache.getFileDescriptorProtos()...)

	fds := &descriptorpb.FileDescriptorSet{
		File: fdpList,
	}

	// Load the set into a registry, then interrogate it for the descriptor corresponding to the top level message.
	files, err := protodesc.NewFiles(fds)
	if err != nil {
		return nil, err
	}
	found, err := files.FindDescriptorByName(protoreflect.FullName(scope))
	if err != nil {
		return nil, err
	}
	return found.(protoreflect.MessageDescriptor), nil
}

// messageDependsOnFile checks if the given message descriptor already belongs to the file descriptor.
// To check for that, first we check if the message descriptor parent file is the same as the file descriptor.
// If not, check if the message descriptor belongs is contained as a child of the file descriptor.
func messageDependsOnFile(msg protoreflect.MessageDescriptor, file protoreflect.FileDescriptor) bool {
	parentFile := msg.ParentFile()
	parentFileName := parentFile.FullName()
	if parentFileName != "" {
		if parentFileName == file.FullName() {
			return true
		}
	}
	fileMessages := file.Messages()
	for i := 0; i < fileMessages.Len(); i++ {
		childMsg := fileMessages.Get(i)
		if msg.FullName() == childMsg.FullName() {
			return true
		}
	}
	return false
}

// tableFieldSchemaToFieldDescriptorProto builds individual field descriptors for a proto message.
//
// For proto3, in cases where the mode is nullable we use the well known wrapper types.
// For proto2, we propagate the mode->label annotation as expected.
//
// Messages are always nullable, and repeated fields are as well.
func tableFieldSchemaToFieldDescriptorProto(field *storagepb.TableFieldSchema, idx int32, scope string, cfg *customConfig) *descriptorpb.FieldDescriptorProto {
	name := field.GetName()
	var fdp *descriptorpb.FieldDescriptorProto

	if field.GetType() == storagepb.TableFieldSchema_STRUCT {
		fdp = &descriptorpb.FieldDescriptorProto{
			Name:     proto.String(name),
			Number:   proto.Int32(idx),
			TypeName: proto.String(scope),
			Label:    convertModeToLabel(field.GetMode(), cfg.useProto3),
		}
	} else if field.GetType() == storagepb.TableFieldSchema_RANGE {
		fdp = &descriptorpb.FieldDescriptorProto{
			Name:     proto.String(name),
			Number:   proto.Int32(idx),
			TypeName: proto.String(fmt.Sprintf("%s%s", rangeTypesPrefix, strings.ToLower(field.GetRangeElementType().GetType().String()))),
			Label:    convertModeToLabel(field.GetMode(), cfg.useProto3),
		}
	} else {
		typeName, outType, label := resolveType(scope, field, cfg)
		fdp = &descriptorpb.FieldDescriptorProto{
			Name:     proto.String(name),
			Number:   proto.Int32(idx),
			Type:     outType.Enum(),
			TypeName: typeName,
			Label:    label,
		}

		// Special case: proto2 repeated fields may benefit from using packed annotation.
		if field.GetMode() == storagepb.TableFieldSchema_REPEATED && !cfg.useProto3 {
			for _, v := range packedTypes {
				if outType == v {
					fdp.Options = &descriptorpb.FieldOptions{
						Packed: proto.Bool(true),
					}
					break
				}
			}
		}
	}
	if nameRequiresAnnotation(name) {
		// Use a prefix + base64 encoded name when annotations bear the actual name.
		// Base 64 standard encoding may also contain certain characters (+,/,=) which
		// we remove from the generated name.
		encoded := strings.Trim(base64.StdEncoding.EncodeToString([]byte(name)), "+/=")
		fdp.Name = proto.String(fmt.Sprintf("col_%s", encoded))
		opts := fdp.GetOptions()
		if opts == nil {
			fdp.Options = &descriptorpb.FieldOptions{}
		}
		proto.SetExtension(fdp.Options, storagepb.E_ColumnName, name)
	}
	return fdp
}

func resolveType(scope string, field *storagepb.TableFieldSchema, cfg *customConfig) (*string, descriptorpb.FieldDescriptorProto_Type, *descriptorpb.FieldDescriptorProto_Label) {
	path := strings.TrimPrefix(strings.ReplaceAll(scope, "__", "."), "root.")
	if override := cfg.protoMappingOverrides.getByField(field, path); override != nil {
		var typeName *string
		if override.TypeName != "" {
			typeName = proto.String(override.TypeName)
		}
		return typeName, override.Type, convertModeToLabel(field.GetMode(), cfg.useProto3)
	}
	// For (REQUIRED||REPEATED) fields for proto3, or all cases for proto2, we can use the expected scalar types.
	if field.GetMode() != storagepb.TableFieldSchema_NULLABLE || !cfg.useProto3 {
		return nil, bqTypeToFieldTypeMap[field.GetType()], convertModeToLabel(field.GetMode(), cfg.useProto3)
	}
	// For NULLABLE proto3 fields, use a wrapper type.
	return proto.String(bqTypeToWrapperMap[field.GetType()]), descriptorpb.FieldDescriptorProto_TYPE_MESSAGE, descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
}

// nameRequiresAnnotation determines whether a field name requires unicode-annotation.
func nameRequiresAnnotation(in string) bool {
	return !protoreflect.Name(in).IsValid()
}

// NormalizeDescriptor builds a self-contained DescriptorProto suitable for communicating schema
// information with the BigQuery Storage write API.  It's primarily used for cases where users are
// interested in sending data using a predefined protocol buffer message.
//
// The storage API accepts a single DescriptorProto for decoding message data.  In many cases, a message
// is comprised of multiple independent messages, from the same .proto file or from multiple sources.  Rather
// than being forced to communicate all these messages independently, what this method does is rewrite the
// DescriptorProto to inline all messages as nested submessages.  As the backend only cares about the types
// and not the namespaces when decoding, this is sufficient for the needs of the API's representation.
//
// In addition to nesting messages, this method also handles some encapsulation of enum types to avoid possible
// conflicts due to ambiguities, and clears oneof indices as oneof isn't a concept that maps into BigQuery
// schemas.
//
// To enable proto3 usage, this function will also rewrite proto3 descriptors into equivalent proto2 form.
// Such rewrites include setting the appropriate default values for proto3 fields.
func NormalizeDescriptor(in protoreflect.MessageDescriptor) (*descriptorpb.DescriptorProto, error) {
	return normalizeDescriptorInternal(in, newStringSet(), newStringSet(), newStringSet(), nil)
}

func normalizeDescriptorInternal(in protoreflect.MessageDescriptor, visitedTypes, enumTypes, structTypes *stringSet, root *descriptorpb.DescriptorProto) (*descriptorpb.DescriptorProto, error) {
	if in == nil {
		return nil, fmt.Errorf("no messagedescriptor provided")
	}
	resultDP := &descriptorpb.DescriptorProto{}
	if root == nil {
		root = resultDP
	}
	fullProtoName := string(in.FullName())
	resultDP.Name = proto.String(normalizeName(fullProtoName))
	visitedTypes.add(fullProtoName)
	for i := 0; i < in.Fields().Len(); i++ {
		inField := in.Fields().Get(i)
		resultFDP := protodesc.ToFieldDescriptorProto(inField)
		// For messages without explicit presence, use default values to match implicit presence behavior.
		if !inField.HasPresence() && inField.Cardinality() != protoreflect.Repeated {
			switch resultFDP.GetType() {
			case descriptorpb.FieldDescriptorProto_TYPE_BOOL:
				resultFDP.DefaultValue = proto.String("false")
			case descriptorpb.FieldDescriptorProto_TYPE_BYTES, descriptorpb.FieldDescriptorProto_TYPE_STRING:
				resultFDP.DefaultValue = proto.String("")
			case descriptorpb.FieldDescriptorProto_TYPE_ENUM:
				// Resolve the proto3 default value.  The default value should be the value name.
				defValue := inField.Enum().Values().ByNumber(inField.Default().Enum())
				resultFDP.DefaultValue = proto.String(string(defValue.Name()))
			case descriptorpb.FieldDescriptorProto_TYPE_DOUBLE,
				descriptorpb.FieldDescriptorProto_TYPE_FLOAT,
				descriptorpb.FieldDescriptorProto_TYPE_INT64,
				descriptorpb.FieldDescriptorProto_TYPE_UINT64,
				descriptorpb.FieldDescriptorProto_TYPE_INT32,
				descriptorpb.FieldDescriptorProto_TYPE_FIXED64,
				descriptorpb.FieldDescriptorProto_TYPE_FIXED32,
				descriptorpb.FieldDescriptorProto_TYPE_UINT32,
				descriptorpb.FieldDescriptorProto_TYPE_SFIXED32,
				descriptorpb.FieldDescriptorProto_TYPE_SFIXED64,
				descriptorpb.FieldDescriptorProto_TYPE_SINT32,
				descriptorpb.FieldDescriptorProto_TYPE_SINT64:
				resultFDP.DefaultValue = proto.String("0")
			}
		}
		// Clear proto3 optional annotation, as the backend converter can
		// treat this as a proto2 optional.
		if resultFDP.Proto3Optional != nil {
			resultFDP.Proto3Optional = nil
		}
		if resultFDP.OneofIndex != nil {
			resultFDP.OneofIndex = nil
		}
		if inField.Kind() == protoreflect.MessageKind || inField.Kind() == protoreflect.GroupKind {
			// Handle fields that reference messages.
			// Groups are a proto2-ism which predated nested messages.
			msgFullName := string(inField.Message().FullName())
			if !skipNormalization(msgFullName) {
				// for everything but well known types, normalize.
				normName := normalizeName(string(msgFullName))
				if structTypes.contains(msgFullName) {
					resultFDP.TypeName = proto.String(normName)
				} else {
					if visitedTypes.contains(msgFullName) {
						return nil, fmt.Errorf("recursive type not supported: %s", inField.FullName())
					}
					visitedTypes.add(msgFullName)
					dp, err := normalizeDescriptorInternal(inField.Message(), visitedTypes, enumTypes, structTypes, root)
					if err != nil {
						return nil, fmt.Errorf("error converting message %s: %v", inField.FullName(), err)
					}
					root.NestedType = append(root.NestedType, dp)
					visitedTypes.delete(msgFullName)
					lastNested := root.GetNestedType()[len(root.GetNestedType())-1].GetName()
					resultFDP.TypeName = proto.String(lastNested)
				}
			}
		}
		if inField.Kind() == protoreflect.EnumKind {
			// For enums, in order to avoid value conflict, we will always define
			// a enclosing struct called enum_full_name_E that includes the actual
			// enum.
			enumFullName := string(inField.Enum().FullName())
			enclosingTypeName := normalizeName(enumFullName) + "_E"
			enumName := string(inField.Enum().Name())
			actualFullName := fmt.Sprintf("%s.%s", enclosingTypeName, enumName)
			if enumTypes.contains(enumFullName) {
				resultFDP.TypeName = proto.String(actualFullName)
			} else {
				enumDP := protodesc.ToEnumDescriptorProto(inField.Enum())
				enumDP.Name = proto.String(enumName)
				// Ensure values in enum are sorted.
				vals := enumDP.GetValue()
				sort.SliceStable(vals, func(i, j int) bool {
					return vals[i].GetNumber() < vals[j].GetNumber()
				})
				// Append wrapped enum to nested types.
				root.NestedType = append(root.NestedType, &descriptorpb.DescriptorProto{
					Name:     proto.String(enclosingTypeName),
					EnumType: []*descriptorpb.EnumDescriptorProto{enumDP},
				})
				resultFDP.TypeName = proto.String(actualFullName)
				enumTypes.add(enumFullName)
			}
		}
		resultDP.Field = append(resultDP.Field, resultFDP)
	}
	// To reduce comparison jitter, order the common slices fields where possible.
	//
	// First, fields are sorted by ID number.
	fields := resultDP.GetField()
	sort.SliceStable(fields, func(i, j int) bool {
		return fields[i].GetNumber() < fields[j].GetNumber()
	})
	// Then, sort nested messages in NestedType by name.
	nested := resultDP.GetNestedType()
	sort.SliceStable(nested, func(i, j int) bool {
		return nested[i].GetName() < nested[j].GetName()
	})
	structTypes.add(fullProtoName)
	return resultDP, nil
}

type stringSet struct {
	m map[string]struct{}
}

func (s *stringSet) contains(k string) bool {
	_, ok := s.m[k]
	return ok
}

func (s *stringSet) add(k string) {
	s.m[k] = struct{}{}
}

func (s *stringSet) delete(k string) {
	delete(s.m, k)
}

func newStringSet() *stringSet {
	return &stringSet{
		m: make(map[string]struct{}),
	}
}

func normalizeName(in string) string {
	return strings.Replace(in, ".", "_", -1)
}

// These types don't get normalized into the fully-contained structure.
var normalizationSkipList = []string{
	/*
		TODO: when backend supports resolving well known types, this list should be enabled.
		"google.protobuf.DoubleValue",
		"google.protobuf.FloatValue",
		"google.protobuf.Int64Value",
		"google.protobuf.UInt64Value",
		"google.protobuf.Int32Value",
		"google.protobuf.Uint32Value",
		"google.protobuf.BoolValue",
		"google.protobuf.StringValue",
		"google.protobuf.BytesValue",
	*/
}

func skipNormalization(fullName string) bool {
	for _, v := range normalizationSkipList {
		if v == fullName {
			return true
		}
	}
	return false
}
