// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.


// After the server pipe is connected, it forks off a thread to handle the connection, and creates
// a new instance of the pipe to listen for new clients. When it gets a request, it validates
// the security and elevation level of the client. If that fails, it disconnects the client. Otherwise,
// it handles the request, sends a response (described by Response class) back to the client, then
// disconnects the pipe and ends the thread.
namespace Microsoft.NET.Sdk.Razor.Tool
{
    /// <summary>
    /// Base class for all possible responses to a request.
    /// The ResponseType enum should list all possible response types
    /// and ReadResponse creates the appropriate response subclass based
    /// on the response type sent by the client.
    /// The format of a response is:
    ///
    /// Field Name       Field Type          Size (bytes)
    /// -------------------------------------------------
    /// responseLength   int (positive)      4
    /// responseType     enum ResponseType   4
    /// responseBody     Response subclass   variable
    /// </summary>
    internal abstract class ServerResponse
    {
        public enum ResponseType
        {
            // The client and server are using incompatible protocol versions.
            MismatchedVersion,

            // The build request completed on the server and the results are contained
            // in the message.
            Completed,

            // The shutdown request completed and the server process information is
            // contained in the message.
            Shutdown,

            // The request was rejected by the server.
            Rejected,
        }

        public abstract ResponseType Type { get; }

        public async Task WriteAsync(Stream outStream, CancellationToken cancellationToken)
        {
            using (var memoryStream = new MemoryStream())
            using (var writer = new BinaryWriter(memoryStream, Encoding.Unicode))
            {
                // Format the response
                ServerLogger.Log("Formatting Response");
                writer.Write((int)Type);

                AddResponseBody(writer);
                writer.Flush();

                cancellationToken.ThrowIfCancellationRequested();

                // Send the response to the client

                // Write the length of the response
                var length = checked((int)memoryStream.Length);

                ServerLogger.Log("Writing response length");
                // There is no way to know the number of bytes written to
                // the pipe stream. We just have to assume all of them are written.
                await outStream
                    .WriteAsync(BitConverter.GetBytes(length), 0, 4, cancellationToken)
                    .ConfigureAwait(false);

                // Write the response
                ServerLogger.Log("Writing response of size {0}", length);
                memoryStream.Position = 0;
                await memoryStream
                    .CopyToAsync(outStream, bufferSize: length, cancellationToken: cancellationToken)
                    .ConfigureAwait(false);
            }
        }

        protected abstract void AddResponseBody(BinaryWriter writer);

        /// <summary>
        /// May throw exceptions if there are pipe problems.
        /// </summary>
        /// <param name="stream"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public static async Task<ServerResponse> ReadAsync(Stream stream, CancellationToken cancellationToken = default(CancellationToken))
        {
            ServerLogger.Log("Reading response length");
            // Read the response length
            var lengthBuffer = new byte[4];
            await ServerProtocol.ReadAllAsync(stream, lengthBuffer, 4, cancellationToken).ConfigureAwait(false);
            var length = BitConverter.ToUInt32(lengthBuffer, 0);

            // Read the response
            ServerLogger.Log("Reading response of length {0}", length);
            var responseBuffer = new byte[length];
            await ServerProtocol.ReadAllAsync(
                stream,
                responseBuffer,
                responseBuffer.Length,
                cancellationToken)
                .ConfigureAwait(false);

            using (var reader = new BinaryReader(new MemoryStream(responseBuffer), Encoding.Unicode))
            {
                var responseType = (ResponseType)reader.ReadInt32();

                switch (responseType)
                {
                    case ResponseType.Completed:
                        return CompletedServerResponse.Create(reader);
                    case ResponseType.MismatchedVersion:
                        return new MismatchedVersionServerResponse();
                    case ResponseType.Shutdown:
                        return ShutdownServerResponse.Create(reader);
                    case ResponseType.Rejected:
                        return new RejectedServerResponse();
                    default:
                        throw new InvalidOperationException("Received invalid response type from server.");
                }
            }
        }
    }
}
