#region Copyright notice and license
// Protocol Buffers - Google's data interchange format
// Copyright 2017 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 System.Collections.Generic;
using Google.Protobuf.Collections;

namespace Google.Protobuf
{
    /// <summary>
    /// Represents a single field in an UnknownFieldSet.
    ///
    /// An UnknownField consists of four lists of values. The lists correspond
    /// to the four "wire types" used in the protocol buffer binary format.
    /// Normally, only one of the four lists will contain any values, since it
    /// is impossible to define a valid message type that declares two different
    /// types for the same field number. However, the code is designed to allow
    /// for the case where the same unknown field number is encountered using
    /// multiple different wire types.
    ///
    /// </summary>
    internal sealed class UnknownField
    {
        private List<ulong> varintList;
        private List<uint> fixed32List;
        private List<ulong> fixed64List;
        private List<ByteString> lengthDelimitedList;
        private List<UnknownFieldSet> groupList;

        /// <summary>
        /// Creates a new UnknownField.
        /// </summary>
        public UnknownField()
        {
        }

        /// <summary>
        /// Checks if two unknown field are equal.
        /// </summary>
        public override bool Equals(object other)
        {
            if (ReferenceEquals(this, other))
            {
                return true;
            }
            return other is UnknownField otherField
                && Lists.Equals(varintList, otherField.varintList)
                && Lists.Equals(fixed32List, otherField.fixed32List)
                && Lists.Equals(fixed64List, otherField.fixed64List)
                && Lists.Equals(lengthDelimitedList, otherField.lengthDelimitedList)
                && Lists.Equals(groupList, otherField.groupList);
        }

        /// <summary>
        /// Get the hash code of the unknown field.
        /// </summary>
        public override int GetHashCode()
        {
            int hash = 43;
            hash = hash * 47 + Lists.GetHashCode(varintList);
            hash = hash * 47 + Lists.GetHashCode(fixed32List);
            hash = hash * 47 + Lists.GetHashCode(fixed64List);
            hash = hash * 47 + Lists.GetHashCode(lengthDelimitedList);
            hash = hash * 47 + Lists.GetHashCode(groupList);
            return hash;
        }

        /// <summary>
        /// Serializes the field, including the field number, and writes it to
        /// <paramref name="output"/>
        /// </summary>
        /// <param name="fieldNumber">The unknown field number.</param>
        /// <param name="output">The write context to write to.</param>
        internal void WriteTo(int fieldNumber, ref WriteContext output)
        {
            if (varintList != null)
            {
                foreach (ulong value in varintList)
                {
                    output.WriteTag(fieldNumber, WireFormat.WireType.Varint);
                    output.WriteUInt64(value);
                }
            }
            if (fixed32List != null)
            {
                foreach (uint value in fixed32List)
                {
                    output.WriteTag(fieldNumber, WireFormat.WireType.Fixed32);
                    output.WriteFixed32(value);
                }
            }
            if (fixed64List != null)
            {
                foreach (ulong value in fixed64List)
                {
                    output.WriteTag(fieldNumber, WireFormat.WireType.Fixed64);
                    output.WriteFixed64(value);
                }
            }
            if (lengthDelimitedList != null)
            {
                foreach (ByteString value in lengthDelimitedList)
                {
                    output.WriteTag(fieldNumber, WireFormat.WireType.LengthDelimited);
                    output.WriteBytes(value);
                }
            }
            if (groupList != null)
            {
                foreach (UnknownFieldSet value in groupList)
                {
                    output.WriteTag(fieldNumber, WireFormat.WireType.StartGroup);
                    value.WriteTo(ref output);
                    output.WriteTag(fieldNumber, WireFormat.WireType.EndGroup);
                }
            }
        }

        /// <summary>
        /// Computes the number of bytes required to encode this field, including field
        /// number.
        /// </summary>
        internal int GetSerializedSize(int fieldNumber)
        {
            int result = 0;
            if (varintList != null)
            {
                result += CodedOutputStream.ComputeTagSize(fieldNumber) * varintList.Count;
                foreach (ulong value in varintList)
                {
                    result += CodedOutputStream.ComputeUInt64Size(value);
                }
            }
            if (fixed32List != null)
            {
                result += CodedOutputStream.ComputeTagSize(fieldNumber) * fixed32List.Count;
                result += CodedOutputStream.ComputeFixed32Size(1) * fixed32List.Count;
            }
            if (fixed64List != null)
            {
                result += CodedOutputStream.ComputeTagSize(fieldNumber) * fixed64List.Count;
                result += CodedOutputStream.ComputeFixed64Size(1) * fixed64List.Count;
            }
            if (lengthDelimitedList != null)
            {
                result += CodedOutputStream.ComputeTagSize(fieldNumber) * lengthDelimitedList.Count;
                foreach (ByteString value in lengthDelimitedList)
                {
                    result += CodedOutputStream.ComputeBytesSize(value);
                }
            }
            if (groupList != null)
            {
                result += CodedOutputStream.ComputeTagSize(fieldNumber) * 2 * groupList.Count;
                foreach (UnknownFieldSet value in groupList)
                {
                    result += value.CalculateSize();
                }
            }
            return result;
        }

        /// <summary>
        /// Merge the values in <paramref name="other" /> into this field.  For each list
        /// of values, <paramref name="other"/>'s values are append to the ones in this
        /// field.
        /// </summary>
        internal UnknownField MergeFrom(UnknownField other)
        {
            varintList = AddAll(varintList, other.varintList);
            fixed32List = AddAll(fixed32List, other.fixed32List);
            fixed64List = AddAll(fixed64List, other.fixed64List);
            lengthDelimitedList = AddAll(lengthDelimitedList, other.lengthDelimitedList);
            groupList = AddAll(groupList, other.groupList);
            return this;
        }

        /// <summary>
        /// Returns a new list containing all of the given specified values from
        /// both the <paramref name="current"/> and <paramref name="extras"/> lists.
        /// If <paramref name="current" /> is null and <paramref name="extras"/> is null or empty,
        /// null is returned. Otherwise, either a new list is created (if <paramref name="current" />
        /// is null) or the elements of <paramref name="extras"/> are added to <paramref name="current" />.
        /// </summary>
        private static List<T> AddAll<T>(List<T> current, IList<T> extras)
        {
            if (extras == null || extras.Count == 0)
            {
                return current;
            }
            if (current == null)
            {
                current = new List<T>(extras);
            }
            else
            {
                current.AddRange(extras);
            }
            return current;
        }

        /// <summary>
        /// Adds a varint value.
        /// </summary>
        internal UnknownField AddVarint(ulong value)
        {
            varintList = Add(varintList, value);
            return this;
        }

        /// <summary>
        /// Adds a fixed32 value.
        /// </summary>
        internal UnknownField AddFixed32(uint value)
        {
            fixed32List = Add(fixed32List, value);
            return this;
        }

        /// <summary>
        /// Adds a fixed64 value.
        /// </summary>
        internal UnknownField AddFixed64(ulong value)
        {
            fixed64List = Add(fixed64List, value);
            return this;
        }

        /// <summary>
        /// Adds a length-delimited value.
        /// </summary>
        internal UnknownField AddLengthDelimited(ByteString value)
        {
            lengthDelimitedList = Add(lengthDelimitedList, value);
            return this;
        }

        internal UnknownField AddGroup(UnknownFieldSet value)
        {
            groupList = Add(groupList, value);
            return this;
        }

        /// <summary>
        /// Adds <paramref name="value"/> to the <paramref name="list"/>, creating
        /// a new list if <paramref name="list"/> is null. The list is returned - either
        /// the original reference or the new list.
        /// </summary>
        private static List<T> Add<T>(List<T> list, T value)
        {
            if (list == null)
            {
                list = new List<T>();
            }
            list.Add(value);
            return list;
        }
    }
}
