#region Copyright notice and license
// Copyright 2015 gRPC authors.
//
// 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.
#endregion

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

using Grpc.Core.Internal;
using Grpc.Core.Utils;

namespace Grpc.Core
{
    /// <summary>
    /// A collection of metadata entries that can be exchanged during a call.
    /// gRPC supports these types of metadata:
    /// <list type="bullet">
    /// <item><term>Request headers</term><description>are sent by the client at the beginning of a remote call before any request messages are sent.</description></item>
    /// <item><term>Response headers</term><description>are sent by the server at the beginning of a remote call handler before any response messages are sent.</description></item>
    /// <item><term>Response trailers</term><description>are sent by the server at the end of a remote call along with resulting call status.</description></item>
    /// </list>
    /// </summary>
    public sealed class Metadata : IList<Metadata.Entry>
    {
        /// <summary>
        /// All binary headers should have this suffix.
        /// </summary>
        public const string BinaryHeaderSuffix = "-bin";

        /// <summary>
        /// An read-only instance of metadata containing no entries.
        /// </summary>
        public static readonly Metadata Empty = new Metadata().Freeze();

        /// <summary>
        /// To be used in initial metadata to request specific compression algorithm
        /// for given call. Direct selection of compression algorithms is an internal
        /// feature and is not part of public API.
        /// </summary>
        internal const string CompressionRequestAlgorithmMetadataKey = "grpc-internal-encoding-request";

        readonly List<Entry> entries;
        bool readOnly;

        /// <summary>
        /// Initializes a new instance of <c>Metadata</c>.
        /// </summary>
        public Metadata()
        {
            this.entries = new List<Entry>();
        }

        /// <summary>
        /// Makes this object read-only.
        /// </summary>
        /// <returns>this object</returns>
        internal Metadata Freeze()
        {
            this.readOnly = true;
            return this;
        }

        // TODO: add support for access by key

        #region IList members


        /// <summary>
        /// <see cref="T:IList`1"/>
        /// </summary>
        public int IndexOf(Metadata.Entry item)
        {
            return entries.IndexOf(item);
        }

        /// <summary>
        /// <see cref="T:IList`1"/>
        /// </summary>
        public void Insert(int index, Metadata.Entry item)
        {
            GrpcPreconditions.CheckNotNull(item);
            CheckWriteable();
            entries.Insert(index, item);
        }

        /// <summary>
        /// <see cref="T:IList`1"/>
        /// </summary>
        public void RemoveAt(int index)
        {
            CheckWriteable();
            entries.RemoveAt(index);
        }

        /// <summary>
        /// <see cref="T:IList`1"/>
        /// </summary>
        public Metadata.Entry this[int index]
        {
            get
            {
                return entries[index];
            }

            set
            {
                GrpcPreconditions.CheckNotNull(value);
                CheckWriteable();
                entries[index] = value;
            }
        }

        /// <summary>
        /// <see cref="T:IList`1"/>
        /// </summary>
        public void Add(Metadata.Entry item)
        {
            GrpcPreconditions.CheckNotNull(item);
            CheckWriteable();
            entries.Add(item);
        }

        /// <summary>
        /// <see cref="T:IList`1"/>
        /// </summary>
        public void Add(string key, string value)
        {
            Add(new Entry(key, value));
        }

        /// <summary>
        /// <see cref="T:IList`1"/>
        /// </summary>
        public void Add(string key, byte[] valueBytes)
        {
            Add(new Entry(key, valueBytes));
        }

        /// <summary>
        /// <see cref="T:IList`1"/>
        /// </summary>
        public void Clear()
        {
            CheckWriteable();
            entries.Clear();
        }

        /// <summary>
        /// <see cref="T:IList`1"/>
        /// </summary>
        public bool Contains(Metadata.Entry item)
        {
            return entries.Contains(item);
        }

        /// <summary>
        /// <see cref="T:IList`1"/>
        /// </summary>
        public void CopyTo(Metadata.Entry[] array, int arrayIndex)
        {
            entries.CopyTo(array, arrayIndex);
        }

        /// <summary>
        /// <see cref="T:IList`1"/>
        /// </summary>
        public int Count
        {
            get { return entries.Count; }
        }

        /// <summary>
        /// <see cref="T:IList`1"/>
        /// </summary>
        public bool IsReadOnly
        {
            get { return readOnly; }
        }

        /// <summary>
        /// <see cref="T:IList`1"/>
        /// </summary>
        public bool Remove(Metadata.Entry item)
        {
            CheckWriteable();
            return entries.Remove(item);
        }

        /// <summary>
        /// <see cref="T:IList`1"/>
        /// </summary>
        public IEnumerator<Metadata.Entry> GetEnumerator()
        {
            return entries.GetEnumerator();
        }

        IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return entries.GetEnumerator();
        }

        private void CheckWriteable()
        {
            GrpcPreconditions.CheckState(!readOnly, "Object is read only");
        }

        #endregion

        /// <summary>
        /// Metadata entry
        /// </summary>
        public class Entry
        {
            private static readonly Regex ValidKeyRegex = new Regex("^[.a-z0-9_-]+$");

            readonly string key;
            readonly string value;
            readonly byte[] valueBytes;

            private Entry(string key, string value, byte[] valueBytes)
            {
                this.key = key;
                this.value = value;
                this.valueBytes = valueBytes;
            }

            /// <summary>
            /// Initializes a new instance of the <see cref="Grpc.Core.Metadata.Entry"/> struct with a binary value.
            /// </summary>
            /// <param name="key">Metadata key, needs to have suffix indicating a binary valued metadata entry.</param>
            /// <param name="valueBytes">Value bytes.</param>
            public Entry(string key, byte[] valueBytes)
            {
                this.key = NormalizeKey(key);
                GrpcPreconditions.CheckArgument(HasBinaryHeaderSuffix(this.key),
                    "Key for binary valued metadata entry needs to have suffix indicating binary value.");
                this.value = null;
                GrpcPreconditions.CheckNotNull(valueBytes, "valueBytes");
                this.valueBytes = new byte[valueBytes.Length];
                Buffer.BlockCopy(valueBytes, 0, this.valueBytes, 0, valueBytes.Length);  // defensive copy to guarantee immutability
            }

            /// <summary>
            /// Initializes a new instance of the <see cref="Grpc.Core.Metadata.Entry"/> struct holding an ASCII value.
            /// </summary>
            /// <param name="key">Metadata key, must not use suffix indicating a binary valued metadata entry.</param>
            /// <param name="value">Value string. Only ASCII characters are allowed.</param>
            public Entry(string key, string value)
            {
                this.key = NormalizeKey(key);
                GrpcPreconditions.CheckArgument(!HasBinaryHeaderSuffix(this.key),
                    "Key for ASCII valued metadata entry cannot have suffix indicating binary value.");
                this.value = GrpcPreconditions.CheckNotNull(value, "value");
                this.valueBytes = null;
            }

            /// <summary>
            /// Gets the metadata entry key.
            /// </summary>
            public string Key
            {
                get
                {
                    return this.key;
                }
            }

            /// <summary>
            /// Gets the binary value of this metadata entry.
            /// </summary>
            public byte[] ValueBytes
            {
                get
                {
                    if (valueBytes == null)
                    {
                        return MarshalUtils.GetBytesASCII(value);
                    }

                    // defensive copy to guarantee immutability
                    var bytes = new byte[valueBytes.Length];
                    Buffer.BlockCopy(valueBytes, 0, bytes, 0, valueBytes.Length);
                    return bytes;
                }
            }

            /// <summary>
            /// Gets the string value of this metadata entry.
            /// </summary>
            public string Value
            {
                get
                {
                    GrpcPreconditions.CheckState(!IsBinary, "Cannot access string value of a binary metadata entry");
                    return value ?? MarshalUtils.GetStringASCII(valueBytes);
                }
            }

            /// <summary>
            /// Returns <c>true</c> if this entry is a binary-value entry.
            /// </summary>
            public bool IsBinary
            {
                get
                {
                    return value == null;
                }
            }

            /// <summary>
            /// Returns a <see cref="System.String"/> that represents the current <see cref="Grpc.Core.Metadata.Entry"/>.
            /// </summary>
            public override string ToString()
            {
                if (IsBinary)
                {
                    return string.Format("[Entry: key={0}, valueBytes={1}]", key, valueBytes);
                }
                
                return string.Format("[Entry: key={0}, value={1}]", key, value);
            }

            /// <summary>
            /// Gets the serialized value for this entry. For binary metadata entries, this leaks
            /// the internal <c>valueBytes</c> byte array and caller must not change contents of it.
            /// </summary>
            internal byte[] GetSerializedValueUnsafe()
            {
                return valueBytes ?? MarshalUtils.GetBytesASCII(value);
            }

            /// <summary>
            /// Creates a binary value or ascii value metadata entry from data received from the native layer.
            /// We trust C core to give us well-formed data, so we don't perform any checks or defensive copying.
            /// </summary>
            internal static Entry CreateUnsafe(string key, byte[] valueBytes)
            {
                if (HasBinaryHeaderSuffix(key))
                {
                    return new Entry(key, null, valueBytes);
                }
                return new Entry(key, MarshalUtils.GetStringASCII(valueBytes), null);
            }

            private static string NormalizeKey(string key)
            {
                var normalized = GrpcPreconditions.CheckNotNull(key, "key").ToLowerInvariant();
                GrpcPreconditions.CheckArgument(ValidKeyRegex.IsMatch(normalized), 
                    "Metadata entry key not valid. Keys can only contain lowercase alphanumeric characters, underscores, hyphens and dots.");
                return normalized;
            }

            /// <summary>
            /// Returns <c>true</c> if the key has "-bin" binary header suffix.
            /// </summary>
            private static bool HasBinaryHeaderSuffix(string key)
            {
                // We don't use just string.EndsWith because its implementation is extremely slow
                // on CoreCLR and we've seen significant differences in gRPC benchmarks caused by it.
                // See https://github.com/dotnet/coreclr/issues/5612

                int len = key.Length;
                if (len >= 4 &&
                    key[len - 4] == '-' &&
                    key[len - 3] == 'b' &&
                    key[len - 2] == 'i' &&
                    key[len - 1] == 'n')
                {
                    return true;
                }
                return false;
            }
        }
    }
}