// Copyright 2020-2022 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 protoutil contains useful functions for interacting with descriptors.
// For now these include only functions for efficiently converting descriptors
// produced by the compiler to descriptor protos.
//
// Despite the fact that descriptor protos are mutable, calling code should NOT
// mutate any of the protos returned from this package. For efficiency, some
// protos returned from this package may be part of internal state of a compiler
// result, and mutating the proto could corrupt or invalidate parts of that
// result.
package protoutil

import (
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protodesc"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/types/descriptorpb"
)

// DescriptorProtoWrapper is a protoreflect.Descriptor that wraps an
// underlying descriptor proto. It provides the same interface as
// Descriptor but with one extra operation, to efficiently query for
// the underlying descriptor proto.
//
// Descriptors that implement this will also implement another method
// whose specified return type is the concrete type returned by the
// AsProto method. The name of this method varies by the type of this
// descriptor:
//
//	 Descriptor Type        Other Method Name
//	---------------------+------------------------------------
//	 FileDescriptor      |  FileDescriptorProto()
//	 MessageDescriptor   |  MessageDescriptorProto()
//	 FieldDescriptor     |  FieldDescriptorProto()
//	 OneofDescriptor     |  OneOfDescriptorProto()
//	 EnumDescriptor      |  EnumDescriptorProto()
//	 EnumValueDescriptor |  EnumValueDescriptorProto()
//	 ServiceDescriptor   |  ServiceDescriptorProto()
//	 MethodDescriptor    |  MethodDescriptorProto()
//
// For example, a DescriptorProtoWrapper that implements FileDescriptor
// returns a *descriptorpb.FileDescriptorProto value from its AsProto
// method and also provides a method with the following signature:
//
//	FileDescriptorProto() *descriptorpb.FileDescriptorProto
type DescriptorProtoWrapper interface {
	protoreflect.Descriptor
	// AsProto returns the underlying descriptor proto. The concrete
	// type of the proto message depends on the type of this
	// descriptor:
	//    Descriptor Type        Proto Message Type
	//   ---------------------+------------------------------------
	//    FileDescriptor      |  *descriptorpb.FileDescriptorProto
	//    MessageDescriptor   |  *descriptorpb.DescriptorProto
	//    FieldDescriptor     |  *descriptorpb.FieldDescriptorProto
	//    OneofDescriptor     |  *descriptorpb.OneofDescriptorProto
	//    EnumDescriptor      |  *descriptorpb.EnumDescriptorProto
	//    EnumValueDescriptor |  *descriptorpb.EnumValueDescriptorProto
	//    ServiceDescriptor   |  *descriptorpb.ServiceDescriptorProto
	//    MethodDescriptor    |  *descriptorpb.MethodDescriptorProto
	AsProto() proto.Message
}

// ProtoFromDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromDescriptor(d protoreflect.Descriptor) proto.Message {
	switch d := d.(type) {
	case protoreflect.FileDescriptor:
		return ProtoFromFileDescriptor(d)
	case protoreflect.MessageDescriptor:
		return ProtoFromMessageDescriptor(d)
	case protoreflect.FieldDescriptor:
		return ProtoFromFieldDescriptor(d)
	case protoreflect.OneofDescriptor:
		return ProtoFromOneofDescriptor(d)
	case protoreflect.EnumDescriptor:
		return ProtoFromEnumDescriptor(d)
	case protoreflect.EnumValueDescriptor:
		return ProtoFromEnumValueDescriptor(d)
	case protoreflect.ServiceDescriptor:
		return ProtoFromServiceDescriptor(d)
	case protoreflect.MethodDescriptor:
		return ProtoFromMethodDescriptor(d)
	default:
		// WTF??
		if res, ok := d.(DescriptorProtoWrapper); ok {
			return res.AsProto()
		}
		return nil
	}
}

// ProtoFromFileDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For file descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. File descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromFileDescriptor(d protoreflect.FileDescriptor) *descriptorpb.FileDescriptorProto {
	if imp, ok := d.(protoreflect.FileImport); ok {
		d = imp.FileDescriptor
	}
	type canProto interface {
		FileDescriptorProto() *descriptorpb.FileDescriptorProto
	}
	if res, ok := d.(canProto); ok {
		return res.FileDescriptorProto()
	}
	if res, ok := d.(DescriptorProtoWrapper); ok {
		if fd, ok := res.AsProto().(*descriptorpb.FileDescriptorProto); ok {
			return fd
		}
	}
	return protodesc.ToFileDescriptorProto(d)
}

// ProtoFromMessageDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For message descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Message descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromMessageDescriptor(d protoreflect.MessageDescriptor) *descriptorpb.DescriptorProto {
	type canProto interface {
		MessageDescriptorProto() *descriptorpb.DescriptorProto
	}
	if res, ok := d.(canProto); ok {
		return res.MessageDescriptorProto()
	}
	if res, ok := d.(DescriptorProtoWrapper); ok {
		if md, ok := res.AsProto().(*descriptorpb.DescriptorProto); ok {
			return md
		}
	}
	return protodesc.ToDescriptorProto(d)
}

// ProtoFromFieldDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For field descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Field descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromFieldDescriptor(d protoreflect.FieldDescriptor) *descriptorpb.FieldDescriptorProto {
	type canProto interface {
		FieldDescriptorProto() *descriptorpb.FieldDescriptorProto
	}
	if res, ok := d.(canProto); ok {
		return res.FieldDescriptorProto()
	}
	if res, ok := d.(DescriptorProtoWrapper); ok {
		if fd, ok := res.AsProto().(*descriptorpb.FieldDescriptorProto); ok {
			return fd
		}
	}
	return protodesc.ToFieldDescriptorProto(d)
}

// ProtoFromOneofDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For oneof descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Oneof descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromOneofDescriptor(d protoreflect.OneofDescriptor) *descriptorpb.OneofDescriptorProto {
	type canProto interface {
		OneofDescriptorProto() *descriptorpb.OneofDescriptorProto
	}
	if res, ok := d.(canProto); ok {
		return res.OneofDescriptorProto()
	}
	if res, ok := d.(DescriptorProtoWrapper); ok {
		if ood, ok := res.AsProto().(*descriptorpb.OneofDescriptorProto); ok {
			return ood
		}
	}
	return protodesc.ToOneofDescriptorProto(d)
}

// ProtoFromEnumDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For enum descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Enum descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromEnumDescriptor(d protoreflect.EnumDescriptor) *descriptorpb.EnumDescriptorProto {
	type canProto interface {
		EnumDescriptorProto() *descriptorpb.EnumDescriptorProto
	}
	if res, ok := d.(canProto); ok {
		return res.EnumDescriptorProto()
	}
	if res, ok := d.(DescriptorProtoWrapper); ok {
		if ed, ok := res.AsProto().(*descriptorpb.EnumDescriptorProto); ok {
			return ed
		}
	}
	return protodesc.ToEnumDescriptorProto(d)
}

// ProtoFromEnumValueDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For enum value descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Enum value descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromEnumValueDescriptor(d protoreflect.EnumValueDescriptor) *descriptorpb.EnumValueDescriptorProto {
	type canProto interface {
		EnumValueDescriptorProto() *descriptorpb.EnumValueDescriptorProto
	}
	if res, ok := d.(canProto); ok {
		return res.EnumValueDescriptorProto()
	}
	if res, ok := d.(DescriptorProtoWrapper); ok {
		if ed, ok := res.AsProto().(*descriptorpb.EnumValueDescriptorProto); ok {
			return ed
		}
	}
	return protodesc.ToEnumValueDescriptorProto(d)
}

// ProtoFromServiceDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For service descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Service descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromServiceDescriptor(d protoreflect.ServiceDescriptor) *descriptorpb.ServiceDescriptorProto {
	type canProto interface {
		ServiceDescriptorProto() *descriptorpb.ServiceDescriptorProto
	}
	if res, ok := d.(canProto); ok {
		return res.ServiceDescriptorProto()
	}
	if res, ok := d.(DescriptorProtoWrapper); ok {
		if sd, ok := res.AsProto().(*descriptorpb.ServiceDescriptorProto); ok {
			return sd
		}
	}
	return protodesc.ToServiceDescriptorProto(d)
}

// ProtoFromMethodDescriptor extracts a descriptor proto from the given "rich"
// descriptor. For method descriptors generated by the compiler, this is an
// inexpensive and non-lossy operation. Method descriptors from other sources
// however may be expensive (to re-create a proto) and even lossy.
func ProtoFromMethodDescriptor(d protoreflect.MethodDescriptor) *descriptorpb.MethodDescriptorProto {
	type canProto interface {
		MethodDescriptorProto() *descriptorpb.MethodDescriptorProto
	}
	if res, ok := d.(canProto); ok {
		return res.MethodDescriptorProto()
	}
	if res, ok := d.(DescriptorProtoWrapper); ok {
		if md, ok := res.AsProto().(*descriptorpb.MethodDescriptorProto); ok {
			return md
		}
	}
	return protodesc.ToMethodDescriptorProto(d)
}
