// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Immutable;

namespace System.Reflection.Metadata.Ecma335
{
    // TODO: debug metadata blobs
    // TODO: revisit ctors (public vs internal vs static factories)?

    public readonly struct BlobEncoder
    {
        public BlobBuilder Builder { get; }

        public BlobEncoder(BlobBuilder builder)
        {
            if (builder == null)
            {
                Throw.BuilderArgumentNull();
            }

            Builder = builder;
        }

        /// <summary>
        /// Encodes Field Signature blob.
        /// </summary>
        /// <returns>Encoder of the field type.</returns>
        public SignatureTypeEncoder FieldSignature()
        {
            Builder.WriteByte((byte)SignatureKind.Field);
            return new SignatureTypeEncoder(Builder);
        }

        /// <summary>
        /// Encodes Method Specification Signature blob.
        /// </summary>
        /// <param name="genericArgumentCount">Number of generic arguments.</param>
        /// <returns>Encoder of generic arguments.</returns>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="genericArgumentCount"/> is not in range [0, 0xffff].</exception>
        public GenericTypeArgumentsEncoder MethodSpecificationSignature(int genericArgumentCount)
        {
            if (unchecked((uint)genericArgumentCount) > ushort.MaxValue) 
            {
                Throw.ArgumentOutOfRange(nameof(genericArgumentCount));
            }

            Builder.WriteByte((byte)SignatureKind.MethodSpecification);
            Builder.WriteCompressedInteger(genericArgumentCount);

            return new GenericTypeArgumentsEncoder(Builder);
        }

        /// <summary>
        /// Encodes Method Signature blob.
        /// </summary>
        /// <param name="convention">Calling convention.</param>
        /// <param name="genericParameterCount">Number of generic parameters.</param>
        /// <param name="isInstanceMethod">True to encode an instance method signature, false to encode a static method signature.</param>
        /// <returns>An Encoder of the rest of the signature including return value and parameters.</returns>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="genericParameterCount"/> is not in range [0, 0xffff].</exception>
        public MethodSignatureEncoder MethodSignature(
            SignatureCallingConvention convention = SignatureCallingConvention.Default,
            int genericParameterCount = 0, 
            bool isInstanceMethod = false)
        {
            if (unchecked((uint)genericParameterCount) > ushort.MaxValue)
            {
                Throw.ArgumentOutOfRange(nameof(genericParameterCount));
            }

            var attributes = 
                (genericParameterCount != 0 ? SignatureAttributes.Generic : 0) | 
                (isInstanceMethod ? SignatureAttributes.Instance : 0);

            Builder.WriteByte(new SignatureHeader(SignatureKind.Method, convention, attributes).RawValue);

            if (genericParameterCount != 0)
            {
                Builder.WriteCompressedInteger(genericParameterCount);
            }

            return new MethodSignatureEncoder(Builder, hasVarArgs: convention == SignatureCallingConvention.VarArgs);
        }

        /// <summary>
        /// Encodes Property Signature blob.
        /// </summary>
        /// <param name="isInstanceProperty">True to encode an instance property signature, false to encode a static property signature.</param>
        /// <returns>An Encoder of the rest of the signature including return value and parameters, which has the same structure as Method Signature.</returns>
        public MethodSignatureEncoder PropertySignature(bool isInstanceProperty = false)
        {
            Builder.WriteByte(new SignatureHeader(SignatureKind.Property, SignatureCallingConvention.Default, (isInstanceProperty ? SignatureAttributes.Instance : 0)).RawValue);
            return new MethodSignatureEncoder(Builder, hasVarArgs: false);
        }

        /// <summary>
        /// Encodes Custom Attribute Signature blob.
        /// Returns a pair of encoders that must be used in the order they appear in the parameter list.
        /// </summary>
        /// <param name="fixedArguments">Use first, to encode fixed arguments.</param>
        /// <param name="namedArguments">Use second, to encode named arguments.</param>
        public void CustomAttributeSignature(out FixedArgumentsEncoder fixedArguments, out CustomAttributeNamedArgumentsEncoder namedArguments)
        {
            Builder.WriteUInt16(0x0001);

            fixedArguments = new FixedArgumentsEncoder(Builder);
            namedArguments = new CustomAttributeNamedArgumentsEncoder(Builder);
        }

        /// <summary>
        /// Encodes Custom Attribute Signature blob.
        /// </summary>
        /// <param name="fixedArguments">Called first, to encode fixed arguments.</param>
        /// <param name="namedArguments">Called second, to encode named arguments.</param>
        /// <exception cref="ArgumentNullException"><paramref name="fixedArguments"/> or <paramref name="namedArguments"/> is null.</exception>
        public void CustomAttributeSignature(Action<FixedArgumentsEncoder> fixedArguments, Action<CustomAttributeNamedArgumentsEncoder> namedArguments)
        {
            if (fixedArguments == null) Throw.ArgumentNull(nameof(fixedArguments));
            if (namedArguments == null) Throw.ArgumentNull(nameof(namedArguments));

            FixedArgumentsEncoder fixedArgumentsEncoder;
            CustomAttributeNamedArgumentsEncoder namedArgumentsEncoder;
            CustomAttributeSignature(out fixedArgumentsEncoder, out namedArgumentsEncoder);
            fixedArguments(fixedArgumentsEncoder);
            namedArguments(namedArgumentsEncoder);
        }

        /// <summary>
        /// Encodes Local Variable Signature.
        /// </summary>
        /// <param name="variableCount">Number of local variables.</param>
        /// <returns>Encoder of a sequence of local variables.</returns>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="variableCount"/> is not in range [0, 0x1fffffff].</exception>
        public LocalVariablesEncoder LocalVariableSignature(int variableCount)
        {
            if (unchecked((uint)variableCount) > BlobWriterImpl.MaxCompressedIntegerValue)
            {
                Throw.ArgumentOutOfRange(nameof(variableCount));
            }

            Builder.WriteByte((byte)SignatureKind.LocalVariables);
            Builder.WriteCompressedInteger(variableCount);
            return new LocalVariablesEncoder(Builder);
        }

        /// <summary>
        /// Encodes Type Specification Signature.
        /// </summary>
        /// <returns>
        /// Type encoder of the structured type represented by the Type Specification (it shall not encode a primitive type).
        /// </returns>
        public SignatureTypeEncoder TypeSpecificationSignature()
        {
            return new SignatureTypeEncoder(Builder);
        }

        /// <summary>
        /// Encodes a Permission Set blob.
        /// </summary>
        /// <param name="attributeCount">Number of attributes in the set.</param>
        /// <returns>Permission Set encoder.</returns>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="attributeCount"/> is not in range [0, 0x1fffffff].</exception>
        public PermissionSetEncoder PermissionSetBlob(int attributeCount)
        {
            if (unchecked((uint)attributeCount) > BlobWriterImpl.MaxCompressedIntegerValue)
            {
                Throw.ArgumentOutOfRange(nameof(attributeCount));
            }

            Builder.WriteByte((byte)'.');
            Builder.WriteCompressedInteger(attributeCount);
            return new PermissionSetEncoder(Builder);
        }

        /// <summary>
        /// Encodes Permission Set arguments.
        /// </summary>
        /// <param name="argumentCount">Number of arguments in the set.</param>
        /// <returns>Encoder of the arguments of the set.</returns>
        public NamedArgumentsEncoder PermissionSetArguments(int argumentCount)
        {
            if (unchecked((uint)argumentCount) > BlobWriterImpl.MaxCompressedIntegerValue)
            {
                Throw.ArgumentOutOfRange(nameof(argumentCount));
            }

            Builder.WriteCompressedInteger(argumentCount);
            return new NamedArgumentsEncoder(Builder);
        }
    }

    public readonly struct MethodSignatureEncoder
    {
        public BlobBuilder Builder { get; }
        public bool HasVarArgs { get; }

        public MethodSignatureEncoder(BlobBuilder builder, bool hasVarArgs)
        {
            Builder = builder;
            HasVarArgs = hasVarArgs;
        }

        /// <summary>
        /// Encodes return type and parameters.
        /// Returns a pair of encoders that must be used in the order they appear in the parameter list.
        /// </summary>
        /// <param name="parameterCount">Number of parameters.</param>
        /// <param name="returnType">Use first, to encode the return types.</param>
        /// <param name="parameters">Use second, to encode the actual parameters.</param>
        public void Parameters(int parameterCount, out ReturnTypeEncoder returnType, out ParametersEncoder parameters)
        {
            if (unchecked((uint)parameterCount) > BlobWriterImpl.MaxCompressedIntegerValue)
            {
                Throw.ArgumentOutOfRange(nameof(parameterCount));
            }

            Builder.WriteCompressedInteger(parameterCount);

            returnType = new ReturnTypeEncoder(Builder);
            parameters = new ParametersEncoder(Builder, hasVarArgs: HasVarArgs);
        }

        /// <summary>
        /// Encodes return type and parameters.
        /// </summary>
        /// <param name="parameterCount">Number of parameters.</param>
        /// <param name="returnType">Called first, to encode the return type.</param>
        /// <param name="parameters">Called second, to encode the actual parameters.</param>
        /// <exception cref="ArgumentNullException"><paramref name="returnType"/> or <paramref name="parameters"/> is null.</exception>
        public void Parameters(int parameterCount, Action<ReturnTypeEncoder> returnType, Action<ParametersEncoder> parameters)
        {
            if (returnType == null) Throw.ArgumentNull(nameof(returnType));
            if (parameters == null) Throw.ArgumentNull(nameof(parameters));

            ReturnTypeEncoder returnTypeEncoder;
            ParametersEncoder parametersEncoder;
            Parameters(parameterCount, out returnTypeEncoder, out parametersEncoder);
            returnType(returnTypeEncoder);
            parameters(parametersEncoder);
        }
    }

    public readonly struct LocalVariablesEncoder
    {
        public BlobBuilder Builder { get; }

        public LocalVariablesEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        public LocalVariableTypeEncoder AddVariable()
        {
            return new LocalVariableTypeEncoder(Builder);
        }
    }

    public readonly struct LocalVariableTypeEncoder
    {
        public BlobBuilder Builder { get; }

        public LocalVariableTypeEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        public CustomModifiersEncoder CustomModifiers()
        {
            return new CustomModifiersEncoder(Builder);
        }

        public SignatureTypeEncoder Type(bool isByRef = false, bool isPinned = false)
        {
            if (isPinned)
            {
                Builder.WriteByte((byte)SignatureTypeCode.Pinned);
            }

            if (isByRef)
            {
                Builder.WriteByte((byte)SignatureTypeCode.ByReference);
            }

            return new SignatureTypeEncoder(Builder);
        }

        public void TypedReference()
        {
            Builder.WriteByte((byte)SignatureTypeCode.TypedReference);
        }
    }

    public readonly struct ParameterTypeEncoder
    {
        public BlobBuilder Builder { get; }

        public ParameterTypeEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        public CustomModifiersEncoder CustomModifiers()
        {
            return new CustomModifiersEncoder(Builder);
        }

        public SignatureTypeEncoder Type(bool isByRef = false)
        {
            if (isByRef)
            {
                Builder.WriteByte((byte)SignatureTypeCode.ByReference);
            }

            return new SignatureTypeEncoder(Builder);
        }

        public void TypedReference()
        {
            Builder.WriteByte((byte)SignatureTypeCode.TypedReference);
        }
    }

    public readonly struct PermissionSetEncoder
    {
        public BlobBuilder Builder { get; }

        public PermissionSetEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        public PermissionSetEncoder AddPermission(string typeName, ImmutableArray<byte> encodedArguments)
        {
            if (typeName == null)
            {
                Throw.ArgumentNull(nameof(typeName));
            }

            if (encodedArguments.IsDefault)
            {
                Throw.ArgumentNull(nameof(encodedArguments));
            }

            if (encodedArguments.Length > BlobWriterImpl.MaxCompressedIntegerValue)
            {
                Throw.BlobTooLarge(nameof(encodedArguments));
            }

            Builder.WriteSerializedString(typeName);
            Builder.WriteCompressedInteger(encodedArguments.Length);
            Builder.WriteBytes(encodedArguments);
            return this;
        }

        public PermissionSetEncoder AddPermission(string typeName, BlobBuilder encodedArguments)
        {
            if (typeName == null)
            {
                Throw.ArgumentNull(nameof(typeName));
            }

            if (encodedArguments == null)
            {
                Throw.ArgumentNull(nameof(encodedArguments));
            }

            if (encodedArguments.Count > BlobWriterImpl.MaxCompressedIntegerValue)
            {
                Throw.BlobTooLarge(nameof(encodedArguments));
            }

            Builder.WriteSerializedString(typeName);
            Builder.WriteCompressedInteger(encodedArguments.Count);
            encodedArguments.WriteContentTo(Builder);
            return this;
        }
    }

    public readonly struct GenericTypeArgumentsEncoder
    {
        public BlobBuilder Builder { get; }

        public GenericTypeArgumentsEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        public SignatureTypeEncoder AddArgument()
        {
            return new SignatureTypeEncoder(Builder);
        }
    }

    public readonly struct FixedArgumentsEncoder
    {
        public BlobBuilder Builder { get; }

        public FixedArgumentsEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        public LiteralEncoder AddArgument()
        {
            return new LiteralEncoder(Builder);
        }
    }

    public readonly struct LiteralEncoder
    {
        public BlobBuilder Builder { get; }

        public LiteralEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        public VectorEncoder Vector()
        {
            return new VectorEncoder(Builder);
        }

        /// <summary>
        /// Encodes the type and the items of a vector literal.
        /// Returns a pair of encoders that must be used in the order they appear in the parameter list.
        /// </summary>        
        /// <param name="arrayType">Use first, to encode the type of the vector.</param>
        /// <param name="vector">Use second, to encode the items of the vector.</param>
        public void TaggedVector(out CustomAttributeArrayTypeEncoder arrayType, out VectorEncoder vector)
        {
            arrayType = new CustomAttributeArrayTypeEncoder(Builder);
            vector = new VectorEncoder(Builder);
        }

        /// <summary>
        /// Encodes the type and the items of a vector literal.
        /// </summary>
        /// <param name="arrayType">Called first, to encode the type of the vector.</param>
        /// <param name="vector">Called second, to encode the items of the vector.</param>
        /// <exception cref="ArgumentNullException"><paramref name="arrayType"/> or <paramref name="vector"/> is null.</exception>
        public void TaggedVector(Action<CustomAttributeArrayTypeEncoder> arrayType, Action<VectorEncoder> vector)
        {
            if (arrayType == null) Throw.ArgumentNull(nameof(arrayType));
            if (vector == null) Throw.ArgumentNull(nameof(vector));

            CustomAttributeArrayTypeEncoder arrayTypeEncoder;
            VectorEncoder vectorEncoder;
            TaggedVector(out arrayTypeEncoder, out vectorEncoder);
            arrayType(arrayTypeEncoder);
            vector(vectorEncoder);
        }

        /// <summary>
        /// Encodes a scalar literal.
        /// </summary>
        /// <returns>Encoder of the literal value.</returns>
        public ScalarEncoder Scalar()
        {
            return new ScalarEncoder(Builder);
        }

        /// <summary>
        /// Encodes the type and the value of a literal.
        /// Returns a pair of encoders that must be used in the order they appear in the parameter list.
        /// </summary>
        /// <param name="type">Called first, to encode the type of the literal.</param>
        /// <param name="scalar">Called second, to encode the value of the literal.</param>
        public void TaggedScalar(out CustomAttributeElementTypeEncoder type, out ScalarEncoder scalar)
        {
            type = new CustomAttributeElementTypeEncoder(Builder);
            scalar = new ScalarEncoder(Builder);
        }

        /// <summary>
        /// Encodes the type and the value of a literal.
        /// </summary>
        /// <param name="type">Called first, to encode the type of the literal.</param>
        /// <param name="scalar">Called second, to encode the value of the literal.</param>
        /// <exception cref="ArgumentNullException"><paramref name="type"/> or <paramref name="scalar"/> is null.</exception>
        public void TaggedScalar(Action<CustomAttributeElementTypeEncoder> type, Action<ScalarEncoder> scalar)
        {
            if (type == null) Throw.ArgumentNull(nameof(type));
            if (scalar == null) Throw.ArgumentNull(nameof(scalar));

            CustomAttributeElementTypeEncoder typeEncoder;
            ScalarEncoder scalarEncoder;
            TaggedScalar(out typeEncoder, out scalarEncoder);
            type(typeEncoder);
            scalar(scalarEncoder);
        }
    }

    public readonly struct ScalarEncoder
    {
        public BlobBuilder Builder { get; }

        public ScalarEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        /// <summary>
        /// Encodes <c>null</c> literal of type <see cref="Array"/>.
        /// </summary>
        public void NullArray()
        {
            Builder.WriteInt32(-1);
        }

        /// <summary>
        /// Encodes constant literal.
        /// </summary>
        /// <param name="value">
        /// Constant of type 
        /// <see cref="bool"/>,
        /// <see cref="byte"/>,
        /// <see cref="sbyte"/>,
        /// <see cref="short"/>,
        /// <see cref="ushort"/>,
        /// <see cref="int"/>,
        /// <see cref="uint"/>,
        /// <see cref="long"/>,
        /// <see cref="ulong"/>,
        /// <see cref="float"/>,
        /// <see cref="double"/>,
        /// <see cref="char"/> (encoded as two-byte Unicode character),
        /// <see cref="string"/> (encoded as SerString), or
        /// <see cref="Enum"/> (encoded as the underlying integer value).
        /// </param>
        /// <exception cref="ArgumentException">Unexpected constant type.</exception>
        public void Constant(object value)
        {
            string str = value as string;
            if (str != null || value == null)
            {
                String(str);
            }
            else
            {
                Builder.WriteConstant(value);
            }
        }

        /// <summary>
        /// Encodes literal of type <see cref="Type"/> (possibly null).
        /// </summary>
        /// <param name="serializedTypeName">The name of the type, or null.</param>
        /// <exception cref="ArgumentException"><paramref name="serializedTypeName"/> is empty.</exception>
        public void SystemType(string serializedTypeName)
        {
            if (serializedTypeName != null && serializedTypeName.Length == 0)
            {
                Throw.ArgumentEmptyString(nameof(serializedTypeName));
            }

            String(serializedTypeName);
        }

        private void String(string value)
        {
            Builder.WriteSerializedString(value);
        }
    }

    public readonly struct LiteralsEncoder
    {
        public BlobBuilder Builder { get; }

        public LiteralsEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        public LiteralEncoder AddLiteral()
        {
            return new LiteralEncoder(Builder);
        }
    }

    public readonly struct VectorEncoder
    {
        public BlobBuilder Builder { get; }

        public VectorEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        public LiteralsEncoder Count(int count)
        {
            if (count < 0)
            {
                Throw.ArgumentOutOfRange(nameof(count));
            }

            Builder.WriteUInt32((uint)count);
            return new LiteralsEncoder(Builder);
        }
    }

    public readonly struct NameEncoder
    {
        public BlobBuilder Builder { get; }

        public NameEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        public void Name(string name)
        {
            if (name == null) Throw.ArgumentNull(nameof(name));
            if (name.Length == 0) Throw.ArgumentEmptyString(nameof(name));

            Builder.WriteSerializedString(name);
        }
    }

    public readonly struct CustomAttributeNamedArgumentsEncoder
    {
        public BlobBuilder Builder { get; }

        public CustomAttributeNamedArgumentsEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        public NamedArgumentsEncoder Count(int count)
        {
            if (unchecked((uint)count) > ushort.MaxValue)
            {
                Throw.ArgumentOutOfRange(nameof(count));
            }
            
            Builder.WriteUInt16((ushort)count);
            return new NamedArgumentsEncoder(Builder);
        }
    }

    public readonly struct NamedArgumentsEncoder
    {
        public BlobBuilder Builder { get; }

        public NamedArgumentsEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        /// <summary>
        /// Encodes a named argument (field or property).
        /// Returns a triplet of encoders that must be used in the order they appear in the parameter list.
        /// </summary>
        /// <param name="isField">True to encode a field, false to encode a property.</param>
        /// <param name="type">Use first, to encode the type of the argument.</param>
        /// <param name="name">Use second, to encode the name of the field or property.</param>
        /// <param name="literal">Use third, to encode the literal value of the argument.</param>
        public void AddArgument(bool isField, out NamedArgumentTypeEncoder type, out NameEncoder name, out LiteralEncoder literal)
        {
            Builder.WriteByte(isField ? (byte)CustomAttributeNamedArgumentKind.Field : (byte)CustomAttributeNamedArgumentKind.Property);
            type = new NamedArgumentTypeEncoder(Builder);
            name = new NameEncoder(Builder);
            literal = new LiteralEncoder(Builder);
        }

        /// <summary>
        /// Encodes a named argument (field or property).
        /// </summary>
        /// <param name="isField">True to encode a field, false to encode a property.</param>
        /// <param name="type">Called first, to encode the type of the argument.</param>
        /// <param name="name">Called second, to encode the name of the field or property.</param>
        /// <param name="literal">Called third, to encode the literal value of the argument.</param>
        /// <exception cref="ArgumentNullException"><paramref name="type"/>, <paramref name="name"/> or <paramref name="literal"/> is null.</exception>
        public void AddArgument(bool isField, Action<NamedArgumentTypeEncoder> type, Action<NameEncoder> name, Action<LiteralEncoder> literal)
        {
            if (type == null) Throw.ArgumentNull(nameof(type));
            if (name == null) Throw.ArgumentNull(nameof(name));
            if (literal == null) Throw.ArgumentNull(nameof(literal));

            NamedArgumentTypeEncoder typeEncoder;
            NameEncoder nameEncoder;
            LiteralEncoder literalEncoder;
            AddArgument(isField, out typeEncoder, out nameEncoder, out literalEncoder);
            type(typeEncoder);
            name(nameEncoder);
            literal(literalEncoder);
        }
    }

    public readonly struct NamedArgumentTypeEncoder
    {
        public BlobBuilder Builder { get; }

        public NamedArgumentTypeEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        public CustomAttributeElementTypeEncoder ScalarType()
        {
            return new CustomAttributeElementTypeEncoder(Builder);
        }

        public void Object()
        {
            Builder.WriteByte((byte)SerializationTypeCode.TaggedObject);
        }

        public CustomAttributeArrayTypeEncoder SZArray()
        {
            return new CustomAttributeArrayTypeEncoder(Builder);
        }
    }

    public readonly struct CustomAttributeArrayTypeEncoder
    {
        public BlobBuilder Builder { get; }

        public CustomAttributeArrayTypeEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        public void ObjectArray()
        {
            Builder.WriteByte((byte)SerializationTypeCode.SZArray);
            Builder.WriteByte((byte)SerializationTypeCode.TaggedObject);
        }

        public CustomAttributeElementTypeEncoder ElementType()
        {
            Builder.WriteByte((byte)SerializationTypeCode.SZArray);
            return new CustomAttributeElementTypeEncoder(Builder);
        }
    }

    public readonly struct CustomAttributeElementTypeEncoder
    {
        public BlobBuilder Builder { get; }

        public CustomAttributeElementTypeEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        private void WriteTypeCode(SerializationTypeCode value)
        {
            Builder.WriteByte((byte)value);
        }

        public void Boolean() => WriteTypeCode(SerializationTypeCode.Boolean);
        public void Char() => WriteTypeCode(SerializationTypeCode.Char);
        public void SByte() => WriteTypeCode(SerializationTypeCode.SByte);
        public void Byte() => WriteTypeCode(SerializationTypeCode.Byte);
        public void Int16() => WriteTypeCode(SerializationTypeCode.Int16);
        public void UInt16() => WriteTypeCode(SerializationTypeCode.UInt16);
        public void Int32() => WriteTypeCode(SerializationTypeCode.Int32);
        public void UInt32() => WriteTypeCode(SerializationTypeCode.UInt32);
        public void Int64() => WriteTypeCode(SerializationTypeCode.Int64);
        public void UInt64() => WriteTypeCode(SerializationTypeCode.UInt64);
        public void Single() => WriteTypeCode(SerializationTypeCode.Single);
        public void Double() => WriteTypeCode(SerializationTypeCode.Double);
        public void String() => WriteTypeCode(SerializationTypeCode.String);

        public void PrimitiveType(PrimitiveSerializationTypeCode type)
        {
            switch (type)
            {
                case PrimitiveSerializationTypeCode.Boolean:
                case PrimitiveSerializationTypeCode.Byte:
                case PrimitiveSerializationTypeCode.SByte:
                case PrimitiveSerializationTypeCode.Char:
                case PrimitiveSerializationTypeCode.Int16:
                case PrimitiveSerializationTypeCode.UInt16:
                case PrimitiveSerializationTypeCode.Int32:
                case PrimitiveSerializationTypeCode.UInt32:
                case PrimitiveSerializationTypeCode.Int64:
                case PrimitiveSerializationTypeCode.UInt64:
                case PrimitiveSerializationTypeCode.Single:
                case PrimitiveSerializationTypeCode.Double:
                case PrimitiveSerializationTypeCode.String:
                    WriteTypeCode((SerializationTypeCode)type);
                    return;

                default:
                    Throw.ArgumentOutOfRange(nameof(type));
                    return;
            }
        }

        public void SystemType()
        {
            WriteTypeCode(SerializationTypeCode.Type);
        }

        public void Enum(string enumTypeName)
        {
            if (enumTypeName == null) Throw.ArgumentNull(nameof(enumTypeName));
            if (enumTypeName.Length == 0) Throw.ArgumentEmptyString(nameof(enumTypeName));

            WriteTypeCode(SerializationTypeCode.Enum);
            Builder.WriteSerializedString(enumTypeName);
        }
    }

    public readonly struct SignatureTypeEncoder
    {
        public BlobBuilder Builder { get; }

        public SignatureTypeEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        private void WriteTypeCode(SignatureTypeCode value)
        {
            Builder.WriteByte((byte)value);
        }

        private void ClassOrValue(bool isValueType)
        {
            Builder.WriteByte(isValueType ? (byte)SignatureTypeKind.ValueType : (byte)SignatureTypeKind.Class);
        }

        public void Boolean() => WriteTypeCode(SignatureTypeCode.Boolean);
        public void Char() => WriteTypeCode(SignatureTypeCode.Char);
        public void SByte() => WriteTypeCode(SignatureTypeCode.SByte);
        public void Byte() => WriteTypeCode(SignatureTypeCode.Byte);
        public void Int16() => WriteTypeCode(SignatureTypeCode.Int16);
        public void UInt16() => WriteTypeCode(SignatureTypeCode.UInt16);
        public void Int32() => WriteTypeCode(SignatureTypeCode.Int32);
        public void UInt32() => WriteTypeCode(SignatureTypeCode.UInt32);
        public void Int64() => WriteTypeCode(SignatureTypeCode.Int64);
        public void UInt64() => WriteTypeCode(SignatureTypeCode.UInt64);
        public void Single() => WriteTypeCode(SignatureTypeCode.Single);
        public void Double() => WriteTypeCode(SignatureTypeCode.Double);
        public void String() => WriteTypeCode(SignatureTypeCode.String);
        public void IntPtr() => WriteTypeCode(SignatureTypeCode.IntPtr);
        public void UIntPtr() => WriteTypeCode(SignatureTypeCode.UIntPtr);
        public void Object() => WriteTypeCode(SignatureTypeCode.Object);

        /// <summary>
        /// Writes primitive type code.
        /// </summary>
        /// <param name="type">Any primitive type code except for <see cref="PrimitiveTypeCode.TypedReference"/> and <see cref="PrimitiveTypeCode.Void"/>.</param>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="type"/> is not valid in this context.</exception>
        public void PrimitiveType(PrimitiveTypeCode type)
        {
            switch (type)
            {
                case PrimitiveTypeCode.Boolean:
                case PrimitiveTypeCode.Byte:
                case PrimitiveTypeCode.SByte:
                case PrimitiveTypeCode.Char:
                case PrimitiveTypeCode.Int16:
                case PrimitiveTypeCode.UInt16:
                case PrimitiveTypeCode.Int32:
                case PrimitiveTypeCode.UInt32:
                case PrimitiveTypeCode.Int64:
                case PrimitiveTypeCode.UInt64:
                case PrimitiveTypeCode.Single:
                case PrimitiveTypeCode.Double:
                case PrimitiveTypeCode.IntPtr:
                case PrimitiveTypeCode.UIntPtr:
                case PrimitiveTypeCode.String:
                case PrimitiveTypeCode.Object:
                    Builder.WriteByte((byte)type);
                    return;

                case PrimitiveTypeCode.TypedReference:
                case PrimitiveTypeCode.Void:
                default:
                    Throw.ArgumentOutOfRange(nameof(type));
                    return;
            }
        }

        /// <summary>
        /// Encodes an array type.
        /// Returns a pair of encoders that must be used in the order they appear in the parameter list.
        /// </summary>
        /// <param name="elementType">Use first, to encode the type of the element.</param>
        /// <param name="arrayShape">Use second, to encode the shape of the array.</param>
        public void Array(out SignatureTypeEncoder elementType, out ArrayShapeEncoder arrayShape)
        {
            Builder.WriteByte((byte)SignatureTypeCode.Array);
            elementType = this;
            arrayShape = new ArrayShapeEncoder(Builder);
        }

        /// <summary>
        /// Encodes an array type.
        /// </summary>
        /// <param name="elementType">Called first, to encode the type of the element.</param>
        /// <param name="arrayShape">Called second, to encode the shape of the array.</param>
        /// <exception cref="ArgumentNullException"><paramref name="elementType"/> or <paramref name="arrayShape"/> is null.</exception>
        public void Array(Action<SignatureTypeEncoder> elementType, Action<ArrayShapeEncoder> arrayShape)
        {
            if (elementType == null) Throw.ArgumentNull(nameof(elementType));
            if (arrayShape == null) Throw.ArgumentNull(nameof(arrayShape));

            SignatureTypeEncoder elementTypeEncoder;
            ArrayShapeEncoder arrayShapeEncoder;
            Array(out elementTypeEncoder, out arrayShapeEncoder);
            elementType(elementTypeEncoder);
            arrayShape(arrayShapeEncoder);
        }

        /// <summary>
        /// Encodes a reference to a type.
        /// </summary>
        /// <param name="type"><see cref="TypeDefinitionHandle"/> or <see cref="TypeReferenceHandle"/>.</param>
        /// <param name="isValueType">True to mark the type as value type, false to mark it as a reference type in the signature.</param>
        /// <exception cref="ArgumentException"><paramref name="type"/> doesn't have the expected handle kind.</exception>
        public void Type(EntityHandle type, bool isValueType)
        {
            // Get the coded index before we start writing anything (might throw argument exception):
            // Note: We don't allow TypeSpec as per https://github.com/dotnet/corefx/blob/master/src/System.Reflection.Metadata/specs/Ecma-335-Issues.md#proposed-specification-change
            int codedIndex = CodedIndex.TypeDefOrRef(type);

            ClassOrValue(isValueType);
            Builder.WriteCompressedInteger(codedIndex);
        }

        /// <summary>
        /// Starts a function pointer signature.
        /// </summary>
        /// <param name="convention">Calling convention.</param>
        /// <param name="attributes">Function pointer attributes.</param>
        /// <param name="genericParameterCount">Generic parameter count.</param>
        /// <exception cref="ArgumentException"><paramref name="attributes"/> is invalid.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="genericParameterCount"/> is not in range [0, 0xffff].</exception>
        public MethodSignatureEncoder FunctionPointer(
            SignatureCallingConvention convention = SignatureCallingConvention.Default, 
            FunctionPointerAttributes attributes = FunctionPointerAttributes.None, 
            int genericParameterCount = 0)
        {
            // Spec:
            // The EXPLICITTHIS (0x40) bit can be set only in signatures for function pointers.
            // If EXPLICITTHIS (0x40) in the signature is set, then HASTHIS (0x20) shall also be set.

            if (attributes != FunctionPointerAttributes.None &&
                attributes != FunctionPointerAttributes.HasThis &&
                attributes != FunctionPointerAttributes.HasExplicitThis)
            {
                throw new ArgumentException(SR.InvalidSignature, nameof(attributes));
            }

            if (unchecked((uint)genericParameterCount) > ushort.MaxValue)
            {
                Throw.ArgumentOutOfRange(nameof(genericParameterCount));
            }

            Builder.WriteByte((byte)SignatureTypeCode.FunctionPointer);
            Builder.WriteByte(new SignatureHeader(SignatureKind.Method, convention, (SignatureAttributes)attributes).RawValue);

            if (genericParameterCount != 0)
            {
                Builder.WriteCompressedInteger(genericParameterCount);
            }

            return new MethodSignatureEncoder(Builder, hasVarArgs: convention == SignatureCallingConvention.VarArgs);
        }

        /// <summary>
        /// Starts a generic instantiation signature.
        /// </summary>
        /// <param name="genericType"><see cref="TypeDefinitionHandle"/> or <see cref="TypeReferenceHandle"/>.</param>
        /// <param name="genericArgumentCount">Generic argument count.</param>
        /// <param name="isValueType">True to mark the type as value type, false to mark it as a reference type in the signature.</param>
        /// <exception cref="ArgumentException"><paramref name="genericType"/> doesn't have the expected handle kind.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="genericArgumentCount"/> is not in range [1, 0xffff].</exception>
        public GenericTypeArgumentsEncoder GenericInstantiation(EntityHandle genericType, int genericArgumentCount, bool isValueType)
        {
            if (unchecked((uint)(genericArgumentCount - 1)) > ushort.MaxValue - 1)
            {
                Throw.ArgumentOutOfRange(nameof(genericArgumentCount));
            }

            // Get the coded index before we start writing anything (might throw argument exception):
            // Note: We don't allow TypeSpec as per https://github.com/dotnet/corefx/blob/master/src/System.Reflection.Metadata/specs/Ecma-335-Issues.md#proposed-specification-change
            int codedIndex = CodedIndex.TypeDefOrRef(genericType);

            Builder.WriteByte((byte)SignatureTypeCode.GenericTypeInstance);
            ClassOrValue(isValueType);
            Builder.WriteCompressedInteger(codedIndex);
            Builder.WriteCompressedInteger(genericArgumentCount);
            return new GenericTypeArgumentsEncoder(Builder);
        }

        /// <summary>
        /// Encodes a reference to type parameter of a containing generic method.
        /// </summary>
        /// <param name="parameterIndex">Parameter index.</param>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="parameterIndex"/> is not in range [0, 0xffff].</exception>
        public void GenericMethodTypeParameter(int parameterIndex)
        {
            if (unchecked((uint)parameterIndex) > ushort.MaxValue)
            {
                Throw.ArgumentOutOfRange(nameof(parameterIndex));
            }

            Builder.WriteByte((byte)SignatureTypeCode.GenericMethodParameter);
            Builder.WriteCompressedInteger(parameterIndex);
        }

        /// <summary>
        /// Encodes a reference to type parameter of a containing generic type.
        /// </summary>
        /// <param name="parameterIndex">Parameter index.</param>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="parameterIndex"/> is not in range [0, 0xffff].</exception>
        public void GenericTypeParameter(int parameterIndex)
        {
            if (unchecked((uint)parameterIndex) > ushort.MaxValue)
            {
                Throw.ArgumentOutOfRange(nameof(parameterIndex));
            }

            Builder.WriteByte((byte)SignatureTypeCode.GenericTypeParameter);
            Builder.WriteCompressedInteger(parameterIndex);
        }

        /// <summary>
        /// Starts pointer signature.
        /// </summary>
        public SignatureTypeEncoder Pointer()
        {
            Builder.WriteByte((byte)SignatureTypeCode.Pointer);
            return this;
        }

        /// <summary>
        /// Encodes <code>void*</code>.
        /// </summary>
        public void VoidPointer()
        {
            Builder.WriteByte((byte)SignatureTypeCode.Pointer);
            Builder.WriteByte((byte)SignatureTypeCode.Void);
        }

        /// <summary>
        /// Starts SZ array (vector) signature.
        /// </summary>
        public SignatureTypeEncoder SZArray()
        {
            Builder.WriteByte((byte)SignatureTypeCode.SZArray);
            return this;
        }

        /// <summary>
        /// Starts a signature of a type with custom modifiers.
        /// </summary>
        public CustomModifiersEncoder CustomModifiers()
        {
            return new CustomModifiersEncoder(Builder);
        }
    }

    public readonly struct CustomModifiersEncoder
    {
        public BlobBuilder Builder { get; }

        public CustomModifiersEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        /// <summary>
        /// Encodes a custom modifier.
        /// </summary>
        /// <param name="type"><see cref="TypeDefinitionHandle"/>, <see cref="TypeReferenceHandle"/> or <see cref="TypeSpecificationHandle"/>.</param>
        /// <param name="isOptional">Is optional modifier.</param>
        /// <returns>Encoder of subsequent modifiers.</returns>
        /// <exception cref="ArgumentException"><paramref name="type"/> is nil or of an unexpected kind.</exception>
        public CustomModifiersEncoder AddModifier(EntityHandle type, bool isOptional)
        {
            if (type.IsNil)
            {
                Throw.InvalidArgument_Handle(nameof(type));
            }

            if (isOptional)
            {
                Builder.WriteByte((byte)SignatureTypeCode.OptionalModifier);
            }
            else
            {
                Builder.WriteByte((byte)SignatureTypeCode.RequiredModifier);
            }

            Builder.WriteCompressedInteger(CodedIndex.TypeDefOrRefOrSpec(type));
            return this;
        }
    }

    public readonly struct ArrayShapeEncoder
    {
        public BlobBuilder Builder { get; }

        public ArrayShapeEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        /// <summary>
        /// Encodes array shape.
        /// </summary>
        /// <param name="rank">The number of dimensions in the array (shall be 1 or more).</param>
        /// <param name="sizes">
        /// Dimension sizes. The array may be shorter than <paramref name="rank"/> but not longer.
        /// </param>
        /// <param name="lowerBounds">
        /// Dimension lower bounds, or <c>default(<see cref="ImmutableArray{Int32}"/>)</c> to set all <paramref name="rank"/> lower bounds to 0. 
        /// The array may be shorter than <paramref name="rank"/> but not longer.
        /// </param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="rank"/> is outside of range [1, 0xffff],
        /// smaller than <paramref name="sizes"/>.Length, or
        /// smaller than <paramref name="lowerBounds"/>.Length.
        /// </exception>
        /// <exception cref="ArgumentNullException"><paramref name="sizes"/> is null.</exception>
        public void Shape(int rank, ImmutableArray<int> sizes, ImmutableArray<int> lowerBounds)
        {
            // The specification doesn't impose a limit on the max number of array dimensions. 
            // The CLR supports <64. More than 0xffff is causing crashes in various tools (ildasm).
            if (unchecked((uint)(rank - 1)) > ushort.MaxValue - 1)
            {
                Throw.ArgumentOutOfRange(nameof(rank));
            }

            if (sizes.IsDefault)
            {
                Throw.ArgumentNull(nameof(sizes));
            }

            // rank
            Builder.WriteCompressedInteger(rank);

            // sizes
            if (sizes.Length > rank)
            {
                Throw.ArgumentOutOfRange(nameof(rank));
            }

            Builder.WriteCompressedInteger(sizes.Length);
            foreach (int size in sizes)
            {
                Builder.WriteCompressedInteger(size);
            }

            // lower bounds
            if (lowerBounds.IsDefault) // TODO: remove -- update Roslyn
            {
                Builder.WriteCompressedInteger(rank);
                for (int i = 0; i < rank; i++)
                {
                    Builder.WriteCompressedSignedInteger(0);
                }
            }
            else
            {
                if (lowerBounds.Length > rank)
                {
                    Throw.ArgumentOutOfRange(nameof(rank));
                }

                Builder.WriteCompressedInteger(lowerBounds.Length);
                foreach (int lowerBound in lowerBounds)
                {
                    Builder.WriteCompressedSignedInteger(lowerBound);
                }
            }
        }
    }

    public readonly struct ReturnTypeEncoder
    {
        public BlobBuilder Builder { get; }

        public ReturnTypeEncoder(BlobBuilder builder)
        {
            Builder = builder;
        }

        public CustomModifiersEncoder CustomModifiers()
        {
            return new CustomModifiersEncoder(Builder);
        }

        public SignatureTypeEncoder Type(bool isByRef = false)
        {
            if (isByRef)
            {
                Builder.WriteByte((byte)SignatureTypeCode.ByReference);
            }

            return new SignatureTypeEncoder(Builder);
        }

        public void TypedReference()
        {
            Builder.WriteByte((byte)SignatureTypeCode.TypedReference);
        }

        public void Void()
        {
            Builder.WriteByte((byte)SignatureTypeCode.Void);
        }
    }

    public readonly struct ParametersEncoder
    {
        public BlobBuilder Builder { get; }
        public bool HasVarArgs { get; }

        public ParametersEncoder(BlobBuilder builder, bool hasVarArgs = false)
        {
            Builder = builder;
            HasVarArgs = hasVarArgs;
        }

        public ParameterTypeEncoder AddParameter()
        {
            return new ParameterTypeEncoder(Builder);
        }

        public ParametersEncoder StartVarArgs()
        {
            if (!HasVarArgs)
            {
                Throw.SignatureNotVarArg();
            }

            Builder.WriteByte((byte)SignatureTypeCode.Sentinel);
            return new ParametersEncoder(Builder, hasVarArgs: false);
        }
    }
}
