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

using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
    internal static class Http2FrameReader
    {
        /* https://tools.ietf.org/html/rfc7540#section-4.1
            +-----------------------------------------------+
            |                 Length (24)                   |
            +---------------+---------------+---------------+
            |   Type (8)    |   Flags (8)   |
            +-+-------------+---------------+-------------------------------+
            |R|                 Stream Identifier (31)                      |
            +=+=============================================================+
            |                   Frame Payload (0...)                      ...
            +---------------------------------------------------------------+
        */
        public const int HeaderLength = 9;

        private const int TypeOffset = 3;
        private const int FlagsOffset = 4;
        private const int StreamIdOffset = 5;

        public const int SettingSize = 6; // 2 bytes for the id, 4 bytes for the value.

        public static bool TryReadFrame(ref ReadOnlySequence<byte> buffer, Http2Frame frame, uint maxFrameSize, out ReadOnlySequence<byte> framePayload)
        {
            framePayload = ReadOnlySequence<byte>.Empty;

            if (buffer.Length < HeaderLength)
            {
                return false;
            }

            var headerSlice = buffer.Slice(0, HeaderLength);
            var header = headerSlice.ToSpan();

            var payloadLength = (int)Bitshifter.ReadUInt24BigEndian(header);
            if (payloadLength > maxFrameSize)
            {
                throw new Http2ConnectionErrorException(SharedStrings.FormatHttp2ErrorFrameOverLimit(payloadLength, maxFrameSize), Http2ErrorCode.FRAME_SIZE_ERROR);
            }

            // Make sure the whole frame is buffered
            var frameLength = HeaderLength + payloadLength;
            if (buffer.Length < frameLength)
            {
                return false;
            }

            frame.PayloadLength = payloadLength;
            frame.Type = (Http2FrameType)header[TypeOffset];
            frame.Flags = header[FlagsOffset];
            frame.StreamId = (int)Bitshifter.ReadUInt31BigEndian(header.Slice(StreamIdOffset));

            var extendedHeaderLength = ReadExtendedFields(frame, buffer);

            // The remaining payload minus the extra fields
            framePayload = buffer.Slice(HeaderLength + extendedHeaderLength, payloadLength - extendedHeaderLength);
            buffer = buffer.Slice(framePayload.End);

            return true;
        }

        private static int ReadExtendedFields(Http2Frame frame, in ReadOnlySequence<byte> readableBuffer)
        {
            // Copy in any extra fields for the given frame type
            var extendedHeaderLength = GetPayloadFieldsLength(frame);

            if (extendedHeaderLength > frame.PayloadLength)
            {
                throw new Http2ConnectionErrorException(
                    SharedStrings.FormatHttp2ErrorUnexpectedFrameLength(frame.Type, expectedLength: extendedHeaderLength), Http2ErrorCode.FRAME_SIZE_ERROR);
            }

            var extendedHeaders = readableBuffer.Slice(HeaderLength, extendedHeaderLength).ToSpan();

            // Parse frame type specific fields
            switch (frame.Type)
            {
                /*
                    +---------------+
                    |Pad Length? (8)|
                    +---------------+-----------------------------------------------+
                    |                            Data (*)                         ...
                    +---------------------------------------------------------------+
                    |                           Padding (*)                       ...
                    +---------------------------------------------------------------+
                */
                case Http2FrameType.DATA: // Variable 0 or 1
                    frame.DataPadLength = frame.DataHasPadding ? extendedHeaders[0] : (byte)0;
                    break;

                /* https://tools.ietf.org/html/rfc7540#section-6.2
                    +---------------+
                    |Pad Length? (8)|
                    +-+-------------+-----------------------------------------------+
                    |E|                 Stream Dependency? (31)                     |
                    +-+-------------+-----------------------------------------------+
                    |  Weight? (8)  |
                    +-+-------------+-----------------------------------------------+
                    |                   Header Block Fragment (*)                 ...
                    +---------------------------------------------------------------+
                    |                           Padding (*)                       ...
                    +---------------------------------------------------------------+
                */
                case Http2FrameType.HEADERS:
                    if (frame.HeadersHasPadding)
                    {
                        frame.HeadersPadLength = extendedHeaders[0];
                        extendedHeaders = extendedHeaders.Slice(1);
                    }
                    else
                    {
                        frame.HeadersPadLength = 0;
                    }

                    if (frame.HeadersHasPriority)
                    {
                        frame.HeadersStreamDependency = (int)Bitshifter.ReadUInt31BigEndian(extendedHeaders);
                        frame.HeadersPriorityWeight = extendedHeaders.Slice(4)[0];
                    }
                    else
                    {
                        frame.HeadersStreamDependency = 0;
                        frame.HeadersPriorityWeight = 0;
                    }
                    break;

                /* https://tools.ietf.org/html/rfc7540#section-6.8
                    +-+-------------------------------------------------------------+
                    |R|                  Last-Stream-ID (31)                        |
                    +-+-------------------------------------------------------------+
                    |                      Error Code (32)                          |
                    +---------------------------------------------------------------+
                    |                  Additional Debug Data (*)                    |
                    +---------------------------------------------------------------+
                */
                case Http2FrameType.GOAWAY:
                    frame.GoAwayLastStreamId = (int)Bitshifter.ReadUInt31BigEndian(extendedHeaders);
                    frame.GoAwayErrorCode = (Http2ErrorCode)BinaryPrimitives.ReadUInt32BigEndian(extendedHeaders.Slice(4));
                    break;

                /* https://tools.ietf.org/html/rfc7540#section-6.3
                    +-+-------------------------------------------------------------+
                    |E|                  Stream Dependency (31)                     |
                    +-+-------------+-----------------------------------------------+
                    |   Weight (8)  |
                    +-+-------------+
                */
                case Http2FrameType.PRIORITY:
                    frame.PriorityStreamDependency = (int)Bitshifter.ReadUInt31BigEndian(extendedHeaders);
                    frame.PriorityWeight = extendedHeaders.Slice(4)[0];
                    break;

                /* https://tools.ietf.org/html/rfc7540#section-6.4
                    +---------------------------------------------------------------+
                    |                        Error Code (32)                        |
                    +---------------------------------------------------------------+
                */
                case Http2FrameType.RST_STREAM:
                    frame.RstStreamErrorCode = (Http2ErrorCode)BinaryPrimitives.ReadUInt32BigEndian(extendedHeaders);
                    break;

                /* https://tools.ietf.org/html/rfc7540#section-6.9
                    +-+-------------------------------------------------------------+
                    |R|              Window Size Increment (31)                     |
                    +-+-------------------------------------------------------------+
                */
                case Http2FrameType.WINDOW_UPDATE:
                    frame.WindowUpdateSizeIncrement = (int)Bitshifter.ReadUInt31BigEndian(extendedHeaders);
                    break;

                case Http2FrameType.PING: // Opaque payload 8 bytes long
                case Http2FrameType.SETTINGS: // Settings are general payload
                case Http2FrameType.CONTINUATION: // None
                case Http2FrameType.PUSH_PROMISE: // Not implemented frames are ignored at this phase
                default:
                    return 0;
            }

            return extendedHeaderLength;
        }

        // The length in bytes of additional fields stored in the payload section.
        // This may be variable based on flags, but should be no more than 8 bytes.
        public static int GetPayloadFieldsLength(Http2Frame frame)
        {
            switch (frame.Type)
            {
                // TODO: Extract constants
                case Http2FrameType.DATA: // Variable 0 or 1
                    return frame.DataHasPadding ? 1 : 0;
                case Http2FrameType.HEADERS:
                    return (frame.HeadersHasPadding ? 1 : 0) + (frame.HeadersHasPriority ? 5 : 0); // Variable 0 to 6
                case Http2FrameType.GOAWAY:
                    return 8; // Last stream id and error code.
                case Http2FrameType.PRIORITY:
                    return 5; // Stream dependency and weight
                case Http2FrameType.RST_STREAM:
                    return 4; // Error code
                case Http2FrameType.WINDOW_UPDATE:
                    return 4; // Update size
                case Http2FrameType.PING: // 8 bytes of opaque data
                case Http2FrameType.SETTINGS: // Settings are general payload
                case Http2FrameType.CONTINUATION: // None
                case Http2FrameType.PUSH_PROMISE: // Not implemented frames are ignored at this phase
                default:
                    return 0;
            }
        }

        public static IList<Http2PeerSetting> ReadSettings(in ReadOnlySequence<byte> payload)
        {
            var data = payload.ToSpan();
            Debug.Assert(data.Length % SettingSize == 0, "Invalid settings payload length");
            var settingsCount = data.Length / SettingSize;

            var settings = new Http2PeerSetting[settingsCount];
            for (int i = 0; i < settings.Length; i++)
            {
                settings[i] = ReadSetting(data);
                data = data.Slice(SettingSize);
            }
            return settings;
        }

        private static Http2PeerSetting ReadSetting(ReadOnlySpan<byte> payload)
        {
            var id = (Http2SettingsParameter)BinaryPrimitives.ReadUInt16BigEndian(payload);
            var value = BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(2));

            return new Http2PeerSetting(id, value);
        }
    }
}
