// 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.

#nullable disable

extern alias DSR;
using System.Collections.Immutable;
using System.Linq;
using DSR::Microsoft.DiaSymReader;
using Microsoft.CodeAnalysis.Debugging;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
using Xunit;

namespace Roslyn.Test.Utilities
{
    internal sealed class MethodDebugInfoBytes
    {
        public readonly ImmutableArray<byte> Bytes;
        public readonly ISymUnmanagedMethod Method;

        public MethodDebugInfoBytes(ImmutableArray<byte> bytes, ISymUnmanagedMethod method)
        {
            this.Bytes = bytes;
            this.Method = method;
        }

        /// <remarks>
        /// This is a helper class for creating mostly-correct <see cref="MethodDebugInfoBytes"/> objects (e.g. circular forwards, extra records, etc).
        /// To create totally broken objects (e.g. corrupted bytes, alternate scope structures, etc), construct <see cref="MethodDebugInfoBytes"/> objects directly.
        /// </remarks>
        internal sealed class Builder
        {
            private const byte Version = 4;
            private const byte Padding = 0;

            private readonly ISymUnmanagedMethod _method;

            private ArrayBuilder<byte> _bytesBuilder;
            private int _recordCount;

            public Builder(string[][] importStringGroups = null, bool suppressUsingInfo = false, ISymUnmanagedConstant[] constants = null)
            {
                _bytesBuilder = ArrayBuilder<byte>.GetInstance();
                if (importStringGroups != null && !suppressUsingInfo)
                {
                    var groupSizes = importStringGroups.Select(g => (short)g.Length).ToArray();
                    AddUsingInfo(groupSizes);
                }

                var namespaces = importStringGroups == null
                    ? default(ImmutableArray<ISymUnmanagedNamespace>)
                    : [.. importStringGroups.SelectMany(names => names.Select(name => (ISymUnmanagedNamespace)new MockSymUnmanagedNamespace(name)))];
                var childScope = new MockSymUnmanagedScope(default(ImmutableArray<ISymUnmanagedScope>), namespaces, constants);
                var rootScope = new MockSymUnmanagedScope([childScope], default(ImmutableArray<ISymUnmanagedNamespace>));
                _method = new MockSymUnmanagedMethod(rootScope);
            }

            public Builder AddUsingInfo(params short[] groupSizes)
            {
                var numGroupSizes = groupSizes.Length;
                var recordSize = BitArithmeticUtilities.Align(4 + 4 + 2 + 2 * numGroupSizes, 4); // Record size, including header.

                // Record header
                _bytesBuilder.Add(Version);
                _bytesBuilder.Add((byte)CustomDebugInfoKind.UsingGroups);
                _bytesBuilder.Add(Padding);
                _bytesBuilder.Add(Padding);
                _bytesBuilder.Add4(recordSize);

                // Record body
                _bytesBuilder.Add2((short)numGroupSizes);
                foreach (var groupSize in groupSizes)
                {
                    _bytesBuilder.Add2(groupSize);
                }

                if ((_bytesBuilder.Count % 4) != 0)
                {
                    _bytesBuilder.Add2(0);
                }

                Assert.Equal(0, _bytesBuilder.Count % 4);
                _recordCount++;
                return this;
            }

            public Builder AddForward(int targetToken)
            {
                return AddForward(targetToken, isModuleLevel: false);
            }

            public Builder AddModuleForward(int targetToken)
            {
                return AddForward(targetToken, isModuleLevel: true);
            }

            private Builder AddForward(int targetToken, bool isModuleLevel)
            {
                // Record header
                _bytesBuilder.Add(Version);
                _bytesBuilder.Add((byte)(isModuleLevel ? CustomDebugInfoKind.ForwardModuleInfo : CustomDebugInfoKind.ForwardMethodInfo));
                _bytesBuilder.Add(Padding);
                _bytesBuilder.Add(Padding);
                _bytesBuilder.Add4(12); // Record size, including header.

                // Record body
                _bytesBuilder.Add4(targetToken);

                Assert.Equal(0, _bytesBuilder.Count % 4);
                _recordCount++;
                return this;
            }

            public MethodDebugInfoBytes Build()
            {
                // Global header
                _bytesBuilder.Insert(0, Version);
                _bytesBuilder.Insert(1, (byte)_recordCount);
                _bytesBuilder.Insert(2, Padding);
                _bytesBuilder.Insert(3, Padding);

                Assert.Equal(0, _bytesBuilder.Count % 4);

                var info = new MethodDebugInfoBytes(_bytesBuilder.ToImmutableAndFree(), _method);
                _bytesBuilder = null; // We'll blow up if any other methods are called.
                return info;
            }
        }
    }
}
