#include "http2_settings.h"

#include <util/generic/strbuf.h>

namespace NSrvKernel::NHTTP2 {

    // TSettings =======================================================================================================

    TChunkPtr TSettings::Write() const noexcept {
        struct TSettingValue {
            ESettingIds Id;
            ui32 Value;
        } const settingValues[] = {
            {ESettingIds::HEADER_TABLE_SIZE, HeaderTableSize},
            {ESettingIds::MAX_CONCURRENT_STREAMS, MaxConcurrentStreams},
            {ESettingIds::INITIAL_WINDOW_SIZE, InitialWindowSize},
            {ESettingIds::MAX_FRAME_SIZE, MaxFrameSize},
            {ESettingIds::MAX_HEADER_LIST_SIZE, MaxHeaderListSize}
        };
        TStackRegion<RFC_SETTING_SIZE * Y_ARRAY_SIZE(settingValues), TOutputRegion> settingsOut;
        auto& output = settingsOut.GetRegion();
        for (auto setting : settingValues) {
            output.WriteUInt<2>((ui16) setting.Id);
            output.WriteUInt<4>((ui32) setting.Value);
        }
        return settingsOut.CopyToChunk();
    }

    void TSettings::PrintTo(IOutputStream& out) const {
        Y_HTTP2_PRINT_OBJ(out,
                          HeaderTableSize, MaxConcurrentStreams, InitialWindowSize, MaxFrameSize, MaxHeaderListSize);
    }


    // TClientSettings =================================================================================================

    TError TClientSettings::Validate(const TFrameHeading& heading, const TSettings& serverSettings) noexcept {
        Y_VERIFY(heading.IsSettings());

        Y_REQUIRE(!heading.StreamId,
            TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::InvalidFrame));

        if (heading.HasFlagAck()) {
            Y_REQUIRE(!heading.Length,
                TConnectionError(EErrorCode::FRAME_SIZE_ERROR, TUnspecifiedReason()));
        } else {
            Y_REQUIRE(!(heading.Length % RFC_SETTING_SIZE),
                TConnectionError(EErrorCode::FRAME_SIZE_ERROR, TUnspecifiedReason()));

            Y_REQUIRE(heading.Length <= serverSettings.MaxFrameSize,
                TConnectionError(EErrorCode::FRAME_SIZE_ERROR, TUnspecifiedReason()));
        }
        return {};
    }

#define Y_HTTP2_ON_CLIENT_SETTING(logger, settingField, settingValue) \
    if (settingValue != settingField) { \
        Y_HTTP2_EVENT(logger, "Setting change", settingField, settingValue); settingField = settingValue; \
    }

    TError TClientSettings::Parse(
        TLogger& logger, TStringBuf data,
        THPackEncoder& hpackEncoder, const TAuxServerSettings& auxServerSettings) noexcept
    {
        Y_HTTP2_FUNC_E(logger);

        // RFC 7540: In order to provide such synchronization timepoints, the recipient of
        //           a SETTINGS frame in which the ACK flag is not set MUST apply the
        //           updated parameters as soon as possible upon receipt.
        //
        //           The values in the SETTINGS frame MUST be processed in the order they
        //           appear, with no other frame processing between values.  Unsupported
        //           parameters MUST be ignored.
        TInputRegion reg{data};

        while (reg) {
            const ui16 settingsId = reg.ReadUInt<2, ui16>();
            const ui32 settingsValue = reg.ReadUInt<4, ui32>();

            switch (settingsId) {
            case (ui16) ESettingIds::HEADER_TABLE_SIZE:
                // RFC 7541: A decoder can limit the amount of state memory used by setting an
                //           appropriate value for the maximum size of the dynamic table.  In
                //           HTTP/2, this is realized by setting an appropriate value for the
                //           SETTINGS_HEADER_TABLE_SIZE parameter.  An encoder can limit the
                //           amount of state memory it uses by signaling a lower dynamic table
                //           size than the decoder allows (see Section 6.3).
                //
                // Impl: We also explicitly limit the encoder header table size.
                hpackEncoder.UpdateHeaderTableSize(std::min(settingsValue, auxServerSettings.EncoderTableSizeMax));
                Y_HTTP2_ON_CLIENT_SETTING(logger, HeaderTableSize, settingsValue);
                break;
            case (ui16) ESettingIds::ENABLE_PUSH:
                Y_REQUIRE(IsIn<ui32>({0, 1}, settingsValue),
                    TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::InvalidSetting));

                Y_HTTP2_ON_CLIENT_SETTING(logger, EnablePush, settingsValue);
                break;
            case (ui16) ESettingIds::MAX_CONCURRENT_STREAMS:
                Y_HTTP2_ON_CLIENT_SETTING(logger, MaxConcurrentStreams, settingsValue);
                break;
            case (ui16) ESettingIds::INITIAL_WINDOW_SIZE:
                // RFC 7540: SETTINGS_INITIAL_WINDOW_SIZE (0x4):  Indicates the sender's initial
                //           window size (in octets) for stream-level flow control.  The
                //           initial value is 2^16-1 (65,535) octets.
                //
                //           This setting affects the window size of all streams (see
                //           Section 6.9.2).
                //
                //           Values above the maximum flow-control window size of 2^31-1 MUST
                //           be treated as a connection error (Section 5.4.1) of type
                //           FLOW_CONTROL_ERROR.
                Y_REQUIRE(settingsValue <= RFC_WINDOW_SIZE_MAX,
                    TConnectionError(EErrorCode::FLOW_CONTROL_ERROR, EFlowControlError::WindowOverflow));

                Y_HTTP2_ON_CLIENT_SETTING(logger, InitialWindowSize, settingsValue);
                break;
            case (ui16) ESettingIds::MAX_FRAME_SIZE:
                Y_REQUIRE(settingsValue >= RFC_MAX_FRAME_SIZE_MIN,
                    TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::InvalidSetting));

                Y_REQUIRE(settingsValue <= RFC_MAX_FRAME_SIZE_MAX,
                    TConnectionError(EErrorCode::PROTOCOL_ERROR, EConnProtocolError::InvalidSetting));

                Y_HTTP2_ON_CLIENT_SETTING(logger, MaxFrameSize, settingsValue);
                MaxFrameSize = std::min(MaxFrameSize, auxServerSettings.ServerFrameSizeMax);
                break;
            case (ui16) ESettingIds::MAX_HEADER_LIST_SIZE:
                Y_HTTP2_ON_CLIENT_SETTING(logger, MaxHeaderListSize, settingsValue);
                break;
            default:
                break;
            }
        }
        return {};
    }


    // TServerSettings =================================================================================================

    TServerSettings::TServerSettings(const TSettings& settings) noexcept
        : Pending_(settings)
    {
        // BALANCER-2174 only delay applying of the stricter limits
        Current_.HeaderTableSize = std::max(Current_.HeaderTableSize, settings.HeaderTableSize);
        Current_.MaxFrameSize = std::max(Current_.MaxFrameSize, settings.MaxFrameSize);
        Current_.InitialWindowSize = std::max(Current_.InitialWindowSize, settings.InitialWindowSize);

        // RFC 7540: For any given request, a lower limit than what is advertised MAY
        //           be enforced.  The initial value of this setting is unlimited.
        Current_.MaxHeaderListSize = settings.MaxHeaderListSize;
        // RFC 7540: An endpoint that wishes to reduce the value of
        //           SETTINGS_MAX_CONCURRENT_STREAMS to a value that is below the current
        //           number of open streams can either close streams that exceed the new
        //           value or allow streams to complete.
        Current_.MaxConcurrentStreams = settings.MaxConcurrentStreams;
    }

    const TSettings& TServerSettings::GetCurrent() const noexcept {
        return Current_;
    }

    bool TServerSettings::HasPending() const noexcept {
        return (bool)Pending_;
    }

    TSettings& TServerSettings::ProvidePending() noexcept {
        if (!Pending_) {
            Pending_ = Current_;
        }
        return *Pending_;
    }

    TChunkPtr TServerSettings::WritePending() noexcept {
        Y_VERIFY(Pending_);

        const auto& pending = *Pending_;
        auto res = pending.Write();
        WaitingAck_.emplace_back(*Pending_);
        Pending_.Clear();
        return res;
    }

    void TServerSettings::AckNextWaiting() noexcept {
        if (WaitingAck_) {
            Current_ = WaitingAck_.front();
            WaitingAck_.pop_front();
        }
    }

    void TServerSettings::PrintTo(IOutputStream& out) const {
        Y_HTTP2_PRINT_OBJ(out, Current_, Pending_, WaitingAck_.size());
    }


    // TAuxServerSettings ==============================================================================================

    void TAuxServerSettings::PrintTo(IOutputStream& out) const {
        Y_HTTP2_PRINT_OBJ(
            out,
            ClientRTTEstimateMax,
            ServerFrameSizeMax,
            ServerDataFrameSizeMax,
            FramesNoYieldMax,
            ClientHeadersCountMax,
            EncoderTableSizeMax,
            StreamSendBufferLoMax,
            StreamSendBufferHiMax,
            StreamSendBufferLoadStep,
            ConnDataSendBufferMax,
            ConnCtrlSendBufferMax,
            SockSendBufferSizeMax,
            StreamsIdleMax,
            StreamsClosedMax,
            StreamsPrioQueueType,
            GoAwayDebugDataEnabled,
            EdgePrioFixEnabled
        );
    }


    // THPackSettings ==================================================================================================

    THPackEncoderSettings GetHPackEncoderSettings(
        const TSettings& settings, const TAuxServerSettings& auxSettings) noexcept
    {
        Y_VERIFY(settings.HeaderTableSize <= IMPL_HEADER_TABLE_SIZE_LIMIT);

        THPackEncoderSettings res;
        res.MaxFrameSize = settings.MaxFrameSize;
        res.MaxHeaderTableSize = std::min(settings.HeaderTableSize, auxSettings.EncoderTableSizeMax);
        res.DefaultHeaderRule = EHPackHeaderRule::Index;
        return res;
    }

    THPackDecoderSettings GetHPackDecoderSettings(
        const TSettings& settings, const TAuxServerSettings& auxSettings) noexcept
    {
        Y_VERIFY(settings.HeaderTableSize <= IMPL_HEADER_TABLE_SIZE_LIMIT);

        THPackDecoderSettings res;
        res.MaxHeaderListSize = settings.MaxHeaderListSize;
        res.MaxHeaderTableSize = settings.HeaderTableSize;
        res.MaxHeadersCount = auxSettings.ClientHeadersCountMax;
        return res;
    }
}

Y_HTTP2_GEN_PRINT(TSettings)
Y_HTTP2_GEN_PRINT(TServerSettings)
Y_HTTP2_GEN_PRINT(TAuxServerSettings)
