#region Copyright notice and license
// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc.  All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#endregion

using Google.Protobuf.Collections;
using Google.Protobuf.Compatibility;
using System;

namespace Google.Protobuf.Reflection
{
    /// <summary>
    /// Descriptor for a field or extension within a message in a .proto file.
    /// </summary>
    public sealed class FieldDescriptor : DescriptorBase, IComparable<FieldDescriptor>
    {
        private EnumDescriptor enumType;
        private MessageDescriptor extendeeType;
        private MessageDescriptor messageType;
        private IFieldAccessor accessor;

        /// <summary>
        /// Get the field's containing message type, or <c>null</c> if it is a field defined at the top level of a file as an extension.
        /// </summary>
        public MessageDescriptor ContainingType { get; }

        /// <summary>
        /// Returns the oneof containing this field, or <c>null</c> if it is not part of a oneof.
        /// </summary>
        public OneofDescriptor ContainingOneof { get; }

        /// <summary>
        /// Returns the oneof containing this field if it's a "real" oneof, or <c>null</c> if either this
        /// field is not part of a oneof, or the oneof is synthetic.
        /// </summary>
        public OneofDescriptor RealContainingOneof => ContainingOneof?.IsSynthetic == false ? ContainingOneof : null;

        /// <summary>
        /// The effective JSON name for this field. This is usually the lower-camel-cased form of the field name,
        /// but can be overridden using the <c>json_name</c> option in the .proto file.
        /// </summary>
        public string JsonName { get; }

        /// <summary>
        /// The name of the property in the <c>ContainingType.ClrType</c> class.
        /// </summary>
        public string PropertyName { get; }

        /// <summary>
        /// Indicates whether this field supports presence, either implicitly (e.g. due to it being a message
        /// type field) or explicitly via Has/Clear members. If this returns true, it is safe to call
        /// <see cref="IFieldAccessor.Clear(IMessage)"/> and <see cref="IFieldAccessor.HasValue(IMessage)"/>
        /// on this field's accessor with a suitable message.
        /// </summary>
        public bool HasPresence =>
            Extension != null ? !Extension.IsRepeated
            : IsRepeated ? false
            : IsMap ? false
            : FieldType == FieldType.Message ? true
            : FieldType == FieldType.Group ? true
            // This covers "real oneof members" and "proto3 optional fields"
            : ContainingOneof != null ? true
            : Features.FieldPresence != FeatureSet.Types.FieldPresence.Implicit;

        internal FieldDescriptorProto Proto { get; }

        /// <summary>
        /// Returns a clone of the underlying <see cref="FieldDescriptorProto"/> describing this field.
        /// Note that a copy is taken every time this method is called, so clients using it frequently
        /// (and not modifying it) may want to cache the returned value.
        /// </summary>
        /// <returns>A protobuf representation of this field descriptor.</returns>
        public FieldDescriptorProto ToProto() => Proto.Clone();

        /// <summary>
        /// An extension identifier for this field, or <c>null</c> if this field isn't an extension.
        /// </summary>
        public Extension Extension { get; }

        internal FieldDescriptor(FieldDescriptorProto proto, FileDescriptor file,
                                 MessageDescriptor parent, int index, string propertyName, Extension extension)
            : base(file, file.ComputeFullName(parent, proto.Name), index,
                  GetDirectParentFeatures(proto, file, parent).MergedWith(InferFeatures(file, proto)).MergedWith(proto.Options?.Features))
        {
            Proto = proto;
            if (proto.HasType)
            {
                FieldType = GetFieldTypeFromProtoType(proto.Type);
                if (FieldType == FieldType.Message &&
                    Features.MessageEncoding == FeatureSet.Types.MessageEncoding.Delimited)
                {
                    FieldType = FieldType.Group;
                }
            }

            if (FieldNumber <= 0)
            {
                throw new DescriptorValidationException(this, "Field numbers must be positive integers.");
            }
            ContainingType = parent;
            if (proto.HasOneofIndex)
            {
                if (proto.OneofIndex < 0 || proto.OneofIndex >= parent.Proto.OneofDecl.Count)
                {
                    throw new DescriptorValidationException(this,
                        $"FieldDescriptorProto.oneof_index is out of range for type {parent.Name}");
                }
                ContainingOneof = parent.Oneofs[proto.OneofIndex];
            }

            file.DescriptorPool.AddSymbol(this);
            // We can't create the accessor until we've cross-linked, unfortunately, as we
            // may not know whether the type of the field is a map or not. Remember the property name
            // for later.
            // We could trust the generated code and check whether the type of the property is
            // a MapField, but that feels a tad nasty.
            PropertyName = propertyName;
            Extension = extension;
            JsonName =  Proto.JsonName == "" ? JsonFormatter.ToJsonName(Proto.Name) : Proto.JsonName;
        }

        /// <summary>
        /// Returns the features from the direct parent:
        /// - The file for top-level extensions
        /// - The oneof for one-of fields
        /// - Otherwise the message
        /// </summary>
        private static FeatureSetDescriptor GetDirectParentFeatures(FieldDescriptorProto proto, FileDescriptor file, MessageDescriptor parent) =>
            parent is null ? file.Features
            // Ignore invalid oneof indexes here; they'll be validated later anyway.
            : proto.OneofIndex >= 0 && proto.OneofIndex < parent.Proto.OneofDecl.Count ? parent.Oneofs[proto.OneofIndex].Features
            : parent.Features;

        /// <summary>
        /// Returns a feature set with inferred features for the given field, or null if no features
        /// need to be inferred.
        /// </summary>
        private static FeatureSet InferFeatures(FileDescriptor file, FieldDescriptorProto proto)
        {
            if ((int) file.Edition >= (int) Edition._2023)
            {
                return null;
            }
            // This is lazily initialized, as most fields won't need it.
            FeatureSet features = null;
            if (proto.Label == FieldDescriptorProto.Types.Label.Required)
            {
                features ??= new FeatureSet();
                features.FieldPresence = FeatureSet.Types.FieldPresence.LegacyRequired;
            }
            if (proto.Type == FieldDescriptorProto.Types.Type.Group)
            {
                features ??= new FeatureSet();
                features.MessageEncoding = FeatureSet.Types.MessageEncoding.Delimited;
            }
            if (file.Edition == Edition.Proto2 && (proto.Options?.Packed ?? false))
            {
                features ??= new FeatureSet();
                features.RepeatedFieldEncoding = FeatureSet.Types.RepeatedFieldEncoding.Packed;
            }
            if (file.Edition == Edition.Proto3 && !(proto.Options?.Packed ?? true))
            {
                features ??= new FeatureSet();
                features.RepeatedFieldEncoding = FeatureSet.Types.RepeatedFieldEncoding.Expanded;
            }
            return features;
        }

        /// <summary>
        /// The brief name of the descriptor's target.
        /// </summary>
        public override string Name => Proto.Name;

        /// <summary>
        /// Returns the accessor for this field.
        /// </summary>
        /// <remarks>
        /// <para>
        /// While a <see cref="FieldDescriptor"/> describes the field, it does not provide
        /// any way of obtaining or changing the value of the field within a specific message;
        /// that is the responsibility of the accessor.
        /// </para>
        /// <para>
        /// In descriptors for generated code, the value returned by this property will be non-null for all
        /// regular fields. However, if a message containing a map field is introspected, the list of nested messages will include
        /// an auto-generated nested key/value pair message for the field. This is not represented in any
        /// generated type, and the value of the map field itself is represented by a dictionary in the
        /// reflection API. There are never instances of those "hidden" messages, so no accessor is provided
        /// and this property will return null.
        /// </para>
        /// <para>
        /// In dynamically loaded descriptors, the value returned by this property will current be null;
        /// if and when dynamic messages are supported, it will return a suitable accessor to work with
        /// them.
        /// </para>
        /// </remarks>
        public IFieldAccessor Accessor => accessor;

        /// <summary>
        /// Maps a field type as included in the .proto file to a FieldType.
        /// </summary>
        private static FieldType GetFieldTypeFromProtoType(FieldDescriptorProto.Types.Type type)
        {
            return type switch
            {
                FieldDescriptorProto.Types.Type.Double => FieldType.Double,
                FieldDescriptorProto.Types.Type.Float => FieldType.Float,
                FieldDescriptorProto.Types.Type.Int64 => FieldType.Int64,
                FieldDescriptorProto.Types.Type.Uint64 => FieldType.UInt64,
                FieldDescriptorProto.Types.Type.Int32 => FieldType.Int32,
                FieldDescriptorProto.Types.Type.Fixed64 => FieldType.Fixed64,
                FieldDescriptorProto.Types.Type.Fixed32 => FieldType.Fixed32,
                FieldDescriptorProto.Types.Type.Bool => FieldType.Bool,
                FieldDescriptorProto.Types.Type.String => FieldType.String,
                FieldDescriptorProto.Types.Type.Group => FieldType.Group,
                FieldDescriptorProto.Types.Type.Message => FieldType.Message,
                FieldDescriptorProto.Types.Type.Bytes => FieldType.Bytes,
                FieldDescriptorProto.Types.Type.Uint32 => FieldType.UInt32,
                FieldDescriptorProto.Types.Type.Enum => FieldType.Enum,
                FieldDescriptorProto.Types.Type.Sfixed32 => FieldType.SFixed32,
                FieldDescriptorProto.Types.Type.Sfixed64 => FieldType.SFixed64,
                FieldDescriptorProto.Types.Type.Sint32 => FieldType.SInt32,
                FieldDescriptorProto.Types.Type.Sint64 => FieldType.SInt64,
                _ => throw new ArgumentException("Invalid type specified"),
            };
        }

        /// <summary>
        /// Returns <c>true</c> if this field is a repeated field; <c>false</c> otherwise.
        /// </summary>
        public bool IsRepeated => Proto.Label == FieldDescriptorProto.Types.Label.Repeated;

        /// <summary>
        /// Returns <c>true</c> if this field is a required field; <c>false</c> otherwise.
        /// </summary>
        public bool IsRequired => Features.FieldPresence == FeatureSet.Types.FieldPresence.LegacyRequired;

        /// <summary>
        /// Returns <c>true</c> if this field is a map field; <c>false</c> otherwise.
        /// </summary>
        public bool IsMap => FieldType == FieldType.Message && messageType.IsMapEntry;

        /// <summary>
        /// Returns <c>true</c> if this field is a packed, repeated field; <c>false</c> otherwise.
        /// </summary>
        public bool IsPacked => Features.RepeatedFieldEncoding == FeatureSet.Types.RepeatedFieldEncoding.Packed;

        /// <summary>
        /// Returns <c>true</c> if this field extends another message type; <c>false</c> otherwise.
        /// </summary>
        public bool IsExtension => Proto.HasExtendee;

        /// <summary>
        /// Returns the type of the field.
        /// </summary>
        public FieldType FieldType { get; private set; }

        /// <summary>
        /// Returns the field number declared in the proto file.
        /// </summary>
        public int FieldNumber => Proto.Number;

        /// <summary>
        /// Compares this descriptor with another one, ordering in "canonical" order
        /// which simply means ascending order by field number. <paramref name="other"/>
        /// must be a field of the same type, i.e. the <see cref="ContainingType"/> of
        /// both fields must be the same.
        /// </summary>
        public int CompareTo(FieldDescriptor other)
        {
            if (other.ContainingType != ContainingType)
            {
                throw new ArgumentException("FieldDescriptors can only be compared to other FieldDescriptors " +
                                            "for fields of the same message type.");
            }
            return FieldNumber - other.FieldNumber;
        }

        /// <summary>
        /// For enum fields, returns the field's type.
        /// </summary>
        public EnumDescriptor EnumType
        {
            get
            {
                if (FieldType != FieldType.Enum)
                {
                    throw new InvalidOperationException("EnumType is only valid for enum fields.");
                }
                return enumType;
            }
        }

        /// <summary>
        /// For embedded message and group fields, returns the field's type.
        /// </summary>
        public MessageDescriptor MessageType
        {
            get
            {
                if (FieldType != FieldType.Message && FieldType != FieldType.Group)
                {
                    throw new InvalidOperationException("MessageType is only valid for message or group fields.");
                }
                return messageType;
            }
        }

        /// <summary>
        /// For extension fields, returns the extended type
        /// </summary>
        public MessageDescriptor ExtendeeType
        {
            get
            {
                if (!Proto.HasExtendee)
                {
                    throw new InvalidOperationException("ExtendeeType is only valid for extension fields.");
                }
                return extendeeType;
            }
        }

        /// <summary>
        /// The (possibly empty) set of custom options for this field.
        /// </summary>
        [Obsolete("CustomOptions are obsolete. Use the GetOptions() method.")]
        public CustomOptions CustomOptions => new CustomOptions(Proto.Options?._extensions?.ValuesByNumber);

        /// <summary>
        /// The <c>FieldOptions</c>, defined in <c>descriptor.proto</c>.
        /// If the options message is not present (i.e. there are no options), <c>null</c> is returned.
        /// Custom options can be retrieved as extensions of the returned message.
        /// NOTE: A defensive copy is created each time this property is retrieved.
        /// </summary>
        public FieldOptions GetOptions()
        {
            var clone = Proto.Options?.Clone();
            if (clone is null)
            {
                return null;
            }
            // Clients should be using feature accessor methods, not accessing features on the
            // options proto.
            clone.Features = null;
            return clone;
        }

        /// <summary>
        /// Gets a single value field option for this descriptor
        /// </summary>
        [Obsolete("GetOption is obsolete. Use the GetOptions() method.")]
        public T GetOption<T>(Extension<FieldOptions, T> extension)
        {
            var value = Proto.Options.GetExtension(extension);
            return value is IDeepCloneable<T> ? (value as IDeepCloneable<T>).Clone() : value;
        }

        /// <summary>
        /// Gets a repeated value field option for this descriptor
        /// </summary>
         [Obsolete("GetOption is obsolete. Use the GetOptions() method.")]
        public RepeatedField<T> GetOption<T>(RepeatedExtension<FieldOptions, T> extension)
        {
            return Proto.Options.GetExtension(extension).Clone();
        }

        /// <summary>
        /// Look up and cross-link all field types etc.
        /// </summary>
        internal void CrossLink()
        {
            if (Proto.HasTypeName)
            {
                IDescriptor typeDescriptor =
                    File.DescriptorPool.LookupSymbol(Proto.TypeName, this);

                // In most cases, the type will be specified in the descriptor proto. This may be
                // guaranteed in descriptor.proto in the future (with respect to spring 2024), but
                // we may still see older descriptors created by old versions of protoc, and there
                // may be some code creating descriptor protos directly. This code effectively
                // maintains backward compatibility, but we don't expect it to be a path taken
                // often at all.
                if (!Proto.HasType)
                {
                    // Choose field type based on symbol.
                    if (typeDescriptor is MessageDescriptor)
                    {
                        FieldType =
                            Features.MessageEncoding == FeatureSet.Types.MessageEncoding.Delimited
                            ? FieldType.Group
                            : FieldType.Message;
                    }
                    else if (typeDescriptor is EnumDescriptor)
                    {
                        FieldType = FieldType.Enum;
                    }
                    else
                    {
                        throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not a type.");
                    }
                }

                if (FieldType == FieldType.Message || FieldType == FieldType.Group)
                {
                    if (typeDescriptor is not MessageDescriptor m)
                    {
                        throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not a message type.");
                    }
                    messageType = m;
                    if (m.Proto.Options?.MapEntry == true || ContainingType?.Proto.Options?.MapEntry == true)
                    {
                        // Maps can't inherit delimited encoding.
                        FieldType = FieldType.Message;
                    }

                    if (Proto.HasDefaultValue)
                    {
                        throw new DescriptorValidationException(this, "Messages can't have default values.");
                    }
                }
                else if (FieldType == FieldType.Enum)
                {
                    if (typeDescriptor is not EnumDescriptor e)
                    {
                        throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not an enum type.");
                    }
                    enumType = e;
                }
                else
                {
                    throw new DescriptorValidationException(this, "Field with primitive type has type_name.");
                }
            }
            else
            {
                if (FieldType == FieldType.Message || FieldType == FieldType.Enum)
                {
                    throw new DescriptorValidationException(this, "Field with message or enum type missing type_name.");
                }
            }

            if (Proto.HasExtendee)
            {
                extendeeType = File.DescriptorPool.LookupSymbol(Proto.Extendee, this) as MessageDescriptor;
            }

            // Note: no attempt to perform any default value parsing

            File.DescriptorPool.AddFieldByNumber(this);

            if (ContainingType != null && ContainingType.Proto.Options != null && ContainingType.Proto.Options.MessageSetWireFormat)
            {
                throw new DescriptorValidationException(this, "MessageSet format is not supported.");
            }
            accessor = CreateAccessor();
        }

        private IFieldAccessor CreateAccessor()
        {
            if (Extension != null)
            {
                return new ExtensionAccessor(this);
            }

            // If we're given no property name, that's because we really don't want an accessor.
            // This could be because it's a map message, or it could be that we're loading a FileDescriptor dynamically.
            // TODO: Support dynamic messages.
            if (PropertyName == null)
            {
                return null;
            }

            var property = ContainingType.ClrType.GetProperty(PropertyName);
            if (property == null)
            {
                throw new DescriptorValidationException(this, $"Property {PropertyName} not found in {ContainingType.ClrType}");
            }
            return IsMap ? new MapFieldAccessor(property, this)
                : IsRepeated ? new RepeatedFieldAccessor(property, this)
                : (IFieldAccessor) new SingleFieldAccessor(ContainingType.ClrType, property, this);
        }
    }
}
