// 
// Decompiled by Procyon v0.6.0
// 

package io.netty.handler.codec.http3;

import io.netty.channel.PendingWriteQueue;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.channel.ChannelHandler;
import java.net.SocketAddress;
import java.util.function.BiFunction;
import java.util.Iterator;
import java.util.Map;
import io.netty.buffer.Unpooled;
import io.netty.util.ReferenceCountUtil;
import io.netty.channel.ChannelPromise;
import java.util.function.BiConsumer;
import io.netty.handler.codec.quic.QuicStreamChannel;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.quic.QuicStreamFrame;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.internal.ObjectUtil;
import io.netty.channel.ChannelOutboundHandler;
import io.netty.handler.codec.ByteToMessageDecoder;

final class Http3FrameCodec extends ByteToMessageDecoder implements ChannelOutboundHandler
{
    private final Http3FrameTypeValidator validator;
    private final long maxHeaderListSize;
    private final QpackDecoder qpackDecoder;
    private final QpackEncoder qpackEncoder;
    private final Http3RequestStreamCodecState encodeState;
    private final Http3RequestStreamCodecState decodeState;
    private boolean firstFrame;
    private boolean error;
    private long type;
    private int payLoadLength;
    private QpackAttributes qpackAttributes;
    private ReadResumptionListener readResumptionListener;
    private WriteResumptionListener writeResumptionListener;
    
    static Http3FrameCodecFactory newFactory(final QpackDecoder qpackDecoder, final long maxHeaderListSize, final QpackEncoder qpackEncoder) {
        ObjectUtil.checkNotNull(qpackEncoder, "qpackEncoder");
        ObjectUtil.checkNotNull(qpackDecoder, "qpackDecoder");
        return (validator, encodeState, decodeState) -> new Http3FrameCodec(validator, qpackDecoder, maxHeaderListSize, qpackEncoder, encodeState, decodeState);
    }
    
    Http3FrameCodec(final Http3FrameTypeValidator validator, final QpackDecoder qpackDecoder, final long maxHeaderListSize, final QpackEncoder qpackEncoder, final Http3RequestStreamCodecState encodeState, final Http3RequestStreamCodecState decodeState) {
        this.firstFrame = true;
        this.type = -1L;
        this.payLoadLength = -1;
        this.validator = ObjectUtil.checkNotNull(validator, "validator");
        this.qpackDecoder = ObjectUtil.checkNotNull(qpackDecoder, "qpackDecoder");
        this.maxHeaderListSize = ObjectUtil.checkPositive(maxHeaderListSize, "maxHeaderListSize");
        this.qpackEncoder = ObjectUtil.checkNotNull(qpackEncoder, "qpackEncoder");
        this.encodeState = ObjectUtil.checkNotNull(encodeState, "encodeState");
        this.decodeState = ObjectUtil.checkNotNull(decodeState, "decodeState");
    }
    
    @Override
    public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
        this.qpackAttributes = Http3.getQpackAttributes(ctx.channel().parent());
        assert this.qpackAttributes != null;
        this.initReadResumptionListenerIfRequired(ctx);
        super.handlerAdded(ctx);
    }
    
    @Override
    public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
        if (this.writeResumptionListener != null) {
            this.writeResumptionListener.drain();
        }
        super.channelInactive(ctx);
    }
    
    @Override
    protected void handlerRemoved0(final ChannelHandlerContext ctx) throws Exception {
        if (this.writeResumptionListener != null) {
            this.writeResumptionListener.drain();
        }
        super.handlerRemoved0(ctx);
    }
    
    @Override
    public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
        ByteBuf buffer;
        if (msg instanceof QuicStreamFrame) {
            final QuicStreamFrame streamFrame = (QuicStreamFrame)msg;
            buffer = streamFrame.content().retain();
            streamFrame.release();
        }
        else {
            buffer = (ByteBuf)msg;
        }
        super.channelRead(ctx, buffer);
    }
    
    @Override
    public void channelReadComplete(final ChannelHandlerContext ctx) throws Exception {
        assert this.readResumptionListener != null;
        if (this.readResumptionListener.readCompleted()) {
            super.channelReadComplete(ctx);
        }
    }
    
    private void connectionError(final ChannelHandlerContext ctx, final Http3ErrorCode code, final String msg, final boolean fireException) {
        this.error = true;
        Http3CodecUtils.connectionError(ctx, code, msg, fireException);
    }
    
    private void connectionError(final ChannelHandlerContext ctx, final Http3Exception exception, final boolean fireException) {
        this.error = true;
        Http3CodecUtils.connectionError(ctx, exception, fireException);
    }
    
    @Override
    protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final List<Object> out) {
        assert this.readResumptionListener != null;
        if (!in.isReadable() || this.readResumptionListener.isSuspended()) {
            return;
        }
        if (this.error) {
            in.skipBytes(in.readableBytes());
            return;
        }
        if (this.type == -1L) {
            final int typeLen = Http3CodecUtils.numBytesForVariableLengthInteger(in.getByte(in.readerIndex()));
            if (in.readableBytes() < typeLen) {
                return;
            }
            final long localType = Http3CodecUtils.readVariableLengthInteger(in, typeLen);
            if (Http3CodecUtils.isReservedHttp2FrameType(localType)) {
                this.connectionError(ctx, Http3ErrorCode.H3_FRAME_UNEXPECTED, "Reserved type for HTTP/2 received.", true);
                return;
            }
            try {
                this.validator.validate(localType, this.firstFrame);
            }
            catch (final Http3Exception e) {
                this.connectionError(ctx, e, true);
                return;
            }
            this.type = localType;
            this.firstFrame = false;
            if (!in.isReadable()) {
                return;
            }
        }
        if (this.payLoadLength == -1) {
            final int payloadLen = Http3CodecUtils.numBytesForVariableLengthInteger(in.getByte(in.readerIndex()));
            assert payloadLen <= 8;
            if (in.readableBytes() < payloadLen) {
                return;
            }
            final long len = Http3CodecUtils.readVariableLengthInteger(in, payloadLen);
            if (len > 2147483647L) {
                this.connectionError(ctx, Http3ErrorCode.H3_EXCESSIVE_LOAD, "Received an invalid frame len.", true);
                return;
            }
            this.payLoadLength = (int)len;
        }
        final int read = this.decodeFrame(ctx, this.type, this.payLoadLength, in, out);
        if (read >= 0) {
            if (read == this.payLoadLength) {
                this.type = -1L;
                this.payLoadLength = -1;
            }
            else {
                this.payLoadLength -= read;
            }
        }
    }
    
    private static int skipBytes(final ByteBuf in, final int payLoadLength) {
        in.skipBytes(payLoadLength);
        return payLoadLength;
    }
    
    private int decodeFrame(final ChannelHandlerContext ctx, final long longType, final int payLoadLength, final ByteBuf in, final List<Object> out) {
        if (longType > 2147483647L && !Http3CodecUtils.isReservedFrameType(longType)) {
            return skipBytes(in, payLoadLength);
        }
        final int type = (int)longType;
        switch (type) {
            case 0: {
                final int readable = in.readableBytes();
                if (readable == 0 && payLoadLength > 0) {
                    return 0;
                }
                final int length = Math.min(readable, payLoadLength);
                out.add(new DefaultHttp3DataFrame(in.readRetainedSlice(length)));
                return length;
            }
            case 1: {
                if (!this.enforceMaxPayloadLength(ctx, in, type, payLoadLength, this.maxHeaderListSize, Http3ErrorCode.H3_EXCESSIVE_LOAD)) {
                    return 0;
                }
                assert this.qpackAttributes != null;
                if (!this.qpackAttributes.dynamicTableDisabled() && !this.qpackAttributes.decoderStreamAvailable()) {
                    assert this.readResumptionListener != null;
                    this.readResumptionListener.suspended();
                    return 0;
                }
                else {
                    final Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame();
                    if (this.decodeHeaders(ctx, headersFrame.headers(), in, payLoadLength, this.decodeState.receivedFinalHeaders())) {
                        out.add(headersFrame);
                        return payLoadLength;
                    }
                    return -1;
                }
                break;
            }
            case 3: {
                if (!this.enforceMaxPayloadLength(ctx, in, type, payLoadLength, 8L, Http3ErrorCode.H3_FRAME_ERROR)) {
                    return 0;
                }
                final int pushIdLen = Http3CodecUtils.numBytesForVariableLengthInteger(in.getByte(in.readerIndex()));
                out.add(new DefaultHttp3CancelPushFrame(Http3CodecUtils.readVariableLengthInteger(in, pushIdLen)));
                return payLoadLength;
            }
            case 4: {
                if (!this.enforceMaxPayloadLength(ctx, in, type, payLoadLength, 256L, Http3ErrorCode.H3_EXCESSIVE_LOAD)) {
                    return 0;
                }
                final Http3SettingsFrame settingsFrame = this.decodeSettings(ctx, in, payLoadLength);
                if (settingsFrame != null) {
                    out.add(settingsFrame);
                }
                return payLoadLength;
            }
            case 5: {
                if (!this.enforceMaxPayloadLength(ctx, in, type, payLoadLength, Math.max(this.maxHeaderListSize, this.maxHeaderListSize + 8L), Http3ErrorCode.H3_EXCESSIVE_LOAD)) {
                    return 0;
                }
                assert this.qpackAttributes != null;
                if (!this.qpackAttributes.dynamicTableDisabled() && !this.qpackAttributes.decoderStreamAvailable()) {
                    assert this.readResumptionListener != null;
                    this.readResumptionListener.suspended();
                    return 0;
                }
                else {
                    final int readerIdx = in.readerIndex();
                    final int pushPromiseIdLen = Http3CodecUtils.numBytesForVariableLengthInteger(in.getByte(in.readerIndex()));
                    final Http3PushPromiseFrame pushPromiseFrame = new DefaultHttp3PushPromiseFrame(Http3CodecUtils.readVariableLengthInteger(in, pushPromiseIdLen));
                    if (this.decodeHeaders(ctx, pushPromiseFrame.headers(), in, payLoadLength - pushPromiseIdLen, false)) {
                        out.add(pushPromiseFrame);
                        return payLoadLength;
                    }
                    in.readerIndex(readerIdx);
                    return -1;
                }
                break;
            }
            case 7: {
                if (!this.enforceMaxPayloadLength(ctx, in, type, payLoadLength, 8L, Http3ErrorCode.H3_FRAME_ERROR)) {
                    return 0;
                }
                final int idLen = Http3CodecUtils.numBytesForVariableLengthInteger(in.getByte(in.readerIndex()));
                out.add(new DefaultHttp3GoAwayFrame(Http3CodecUtils.readVariableLengthInteger(in, idLen)));
                return payLoadLength;
            }
            case 13: {
                if (!this.enforceMaxPayloadLength(ctx, in, type, payLoadLength, 8L, Http3ErrorCode.H3_FRAME_ERROR)) {
                    return 0;
                }
                final int pidLen = Http3CodecUtils.numBytesForVariableLengthInteger(in.getByte(in.readerIndex()));
                out.add(new DefaultHttp3MaxPushIdFrame(Http3CodecUtils.readVariableLengthInteger(in, pidLen)));
                return payLoadLength;
            }
            default: {
                if (!Http3CodecUtils.isReservedFrameType(longType)) {
                    return skipBytes(in, payLoadLength);
                }
                if (in.readableBytes() < payLoadLength) {
                    return 0;
                }
                out.add(new DefaultHttp3UnknownFrame(longType, in.readRetainedSlice(payLoadLength)));
                return payLoadLength;
            }
        }
    }
    
    private boolean enforceMaxPayloadLength(final ChannelHandlerContext ctx, final ByteBuf in, final int type, final int payLoadLength, final long maxPayLoadLength, final Http3ErrorCode error) {
        if (payLoadLength > maxPayLoadLength) {
            this.connectionError(ctx, error, "Received an invalid frame len " + payLoadLength + " for frame of type " + type + '.', true);
            return false;
        }
        return in.readableBytes() >= payLoadLength;
    }
    
    @Nullable
    private Http3SettingsFrame decodeSettings(final ChannelHandlerContext ctx, final ByteBuf in, int payLoadLength) {
        final Http3SettingsFrame settingsFrame = new DefaultHttp3SettingsFrame();
        while (payLoadLength > 0) {
            final int keyLen = Http3CodecUtils.numBytesForVariableLengthInteger(in.getByte(in.readerIndex()));
            final long key = Http3CodecUtils.readVariableLengthInteger(in, keyLen);
            if (Http3CodecUtils.isReservedHttp2Setting(key)) {
                this.connectionError(ctx, Http3ErrorCode.H3_SETTINGS_ERROR, "Received a settings key that is reserved for HTTP/2.", true);
                return null;
            }
            payLoadLength -= keyLen;
            final int valueLen = Http3CodecUtils.numBytesForVariableLengthInteger(in.getByte(in.readerIndex()));
            final long value = Http3CodecUtils.readVariableLengthInteger(in, valueLen);
            payLoadLength -= valueLen;
            if (settingsFrame.put(key, value) != null) {
                this.connectionError(ctx, Http3ErrorCode.H3_SETTINGS_ERROR, "Received a duplicate settings key.", true);
                return null;
            }
        }
        return settingsFrame;
    }
    
    private boolean decodeHeaders(final ChannelHandlerContext ctx, final Http3Headers headers, final ByteBuf in, final int length, final boolean trailer) {
        try {
            final Http3HeadersSink sink = new Http3HeadersSink(headers, this.maxHeaderListSize, true, trailer);
            assert this.qpackAttributes != null;
            assert this.readResumptionListener != null;
            if (this.qpackDecoder.decode(this.qpackAttributes, ((QuicStreamChannel)ctx.channel()).streamId(), in, length, sink, this.readResumptionListener)) {
                sink.finish();
                return true;
            }
            this.readResumptionListener.suspended();
        }
        catch (final Http3Exception e) {
            this.connectionError(ctx, e.errorCode(), e.getMessage(), true);
        }
        catch (final QpackException e2) {
            this.connectionError(ctx, Http3ErrorCode.QPACK_DECOMPRESSION_FAILED, "Decompression of header block failed.", true);
        }
        catch (final Http3HeadersValidationException e3) {
            this.error = true;
            ctx.fireExceptionCaught((Throwable)e3);
            Http3CodecUtils.streamError(ctx, Http3ErrorCode.H3_MESSAGE_ERROR);
        }
        return false;
    }
    
    @Override
    public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) {
        assert this.qpackAttributes != null;
        if (this.writeResumptionListener != null) {
            this.writeResumptionListener.enqueue(msg, promise);
            return;
        }
        if ((msg instanceof Http3HeadersFrame || msg instanceof Http3PushPromiseFrame) && !this.qpackAttributes.dynamicTableDisabled() && !this.qpackAttributes.encoderStreamAvailable()) {
            (this.writeResumptionListener = WriteResumptionListener.newListener(ctx, this)).enqueue(msg, promise);
            return;
        }
        this.write0(ctx, msg, promise);
    }
    
    private void write0(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) {
        try {
            if (msg instanceof Http3DataFrame) {
                writeDataFrame(ctx, (Http3DataFrame)msg, promise);
            }
            else if (msg instanceof Http3HeadersFrame) {
                this.writeHeadersFrame(ctx, (Http3HeadersFrame)msg, promise);
            }
            else if (msg instanceof Http3CancelPushFrame) {
                writeCancelPushFrame(ctx, (Http3CancelPushFrame)msg, promise);
            }
            else if (msg instanceof Http3SettingsFrame) {
                writeSettingsFrame(ctx, (Http3SettingsFrame)msg, promise);
            }
            else if (msg instanceof Http3PushPromiseFrame) {
                this.writePushPromiseFrame(ctx, (Http3PushPromiseFrame)msg, promise);
            }
            else if (msg instanceof Http3GoAwayFrame) {
                writeGoAwayFrame(ctx, (Http3GoAwayFrame)msg, promise);
            }
            else if (msg instanceof Http3MaxPushIdFrame) {
                writeMaxPushIdFrame(ctx, (Http3MaxPushIdFrame)msg, promise);
            }
            else if (msg instanceof Http3UnknownFrame) {
                this.writeUnknownFrame(ctx, (Http3UnknownFrame)msg, promise);
            }
            else {
                unsupported(promise);
            }
        }
        finally {
            ReferenceCountUtil.release(msg);
        }
    }
    
    private static void writeDataFrame(final ChannelHandlerContext ctx, final Http3DataFrame frame, final ChannelPromise promise) {
        final ByteBuf out = ctx.alloc().directBuffer(16);
        Http3CodecUtils.writeVariableLengthInteger(out, frame.type());
        Http3CodecUtils.writeVariableLengthInteger(out, frame.content().readableBytes());
        final ByteBuf content = frame.content().retain();
        ctx.write(Unpooled.wrappedUnmodifiableBuffer(out, content), promise);
    }
    
    private void writeHeadersFrame(final ChannelHandlerContext ctx, final Http3HeadersFrame frame, final ChannelPromise promise) {
        assert this.qpackAttributes != null;
        final QuicStreamChannel channel = (QuicStreamChannel)ctx.channel();
        writeDynamicFrame(ctx, frame.type(), frame, (f, out) -> {
            this.qpackEncoder.encodeHeaders(this.qpackAttributes, out, ctx.alloc(), channel.streamId(), f.headers());
            return Boolean.valueOf(true);
        }, promise);
    }
    
    private static void writeCancelPushFrame(final ChannelHandlerContext ctx, final Http3CancelPushFrame frame, final ChannelPromise promise) {
        writeFrameWithId(ctx, frame.type(), frame.id(), promise);
    }
    
    private static void writeSettingsFrame(final ChannelHandlerContext ctx, final Http3SettingsFrame frame, final ChannelPromise promise) {
        writeDynamicFrame(ctx, frame.type(), frame, (f, out) -> {
            f.iterator();
            final Iterator iterator;
            while (iterator.hasNext()) {
                final Map.Entry<Long, Long> e = iterator.next();
                final Long key = e.getKey();
                if (Http3CodecUtils.isReservedHttp2Setting(key)) {
                    final Http3Exception exception = new Http3Exception(Http3ErrorCode.H3_SETTINGS_ERROR, "Received a settings key that is reserved for HTTP/2.");
                    promise.setFailure((Throwable)exception);
                    Http3CodecUtils.connectionError(ctx, exception, false);
                    return Boolean.valueOf(false);
                }
                else {
                    final Long value = e.getValue();
                    final int keyLen = Http3CodecUtils.numBytesForVariableLengthInteger(key);
                    final int valueLen = Http3CodecUtils.numBytesForVariableLengthInteger(value);
                    Http3CodecUtils.writeVariableLengthInteger(out, key, keyLen);
                    Http3CodecUtils.writeVariableLengthInteger(out, value, valueLen);
                }
            }
            return Boolean.valueOf(true);
        }, promise);
    }
    
    private static <T extends Http3Frame> void writeDynamicFrame(final ChannelHandlerContext ctx, final long type, final T frame, final BiFunction<T, ByteBuf, Boolean> writer, final ChannelPromise promise) {
        final ByteBuf out = ctx.alloc().directBuffer();
        final int initialWriterIndex = out.writerIndex();
        final int payloadStartIndex = initialWriterIndex + 16;
        out.writerIndex(payloadStartIndex);
        if (writer.apply(frame, out)) {
            final int finalWriterIndex = out.writerIndex();
            final int payloadLength = finalWriterIndex - payloadStartIndex;
            final int len = Http3CodecUtils.numBytesForVariableLengthInteger(payloadLength);
            out.writerIndex(payloadStartIndex - len);
            Http3CodecUtils.writeVariableLengthInteger(out, payloadLength, len);
            final int typeLength = Http3CodecUtils.numBytesForVariableLengthInteger(type);
            final int startIndex = payloadStartIndex - len - typeLength;
            out.writerIndex(startIndex);
            Http3CodecUtils.writeVariableLengthInteger(out, type, typeLength);
            out.setIndex(startIndex, finalWriterIndex);
            ctx.write(out, promise);
        }
        else {
            out.release();
        }
    }
    
    private void writePushPromiseFrame(final ChannelHandlerContext ctx, final Http3PushPromiseFrame frame, final ChannelPromise promise) {
        assert this.qpackAttributes != null;
        final QuicStreamChannel channel = (QuicStreamChannel)ctx.channel();
        writeDynamicFrame(ctx, frame.type(), frame, (f, out) -> {
            final long id = f.id();
            Http3CodecUtils.writeVariableLengthInteger(out, id);
            this.qpackEncoder.encodeHeaders(this.qpackAttributes, out, ctx.alloc(), channel.streamId(), f.headers());
            return Boolean.valueOf(true);
        }, promise);
    }
    
    private static void writeGoAwayFrame(final ChannelHandlerContext ctx, final Http3GoAwayFrame frame, final ChannelPromise promise) {
        writeFrameWithId(ctx, frame.type(), frame.id(), promise);
    }
    
    private static void writeMaxPushIdFrame(final ChannelHandlerContext ctx, final Http3MaxPushIdFrame frame, final ChannelPromise promise) {
        writeFrameWithId(ctx, frame.type(), frame.id(), promise);
    }
    
    private static void writeFrameWithId(final ChannelHandlerContext ctx, final long type, final long id, final ChannelPromise promise) {
        final ByteBuf out = ctx.alloc().directBuffer(24);
        Http3CodecUtils.writeVariableLengthInteger(out, type);
        Http3CodecUtils.writeVariableLengthInteger(out, Http3CodecUtils.numBytesForVariableLengthInteger(id));
        Http3CodecUtils.writeVariableLengthInteger(out, id);
        ctx.write(out, promise);
    }
    
    private void writeUnknownFrame(final ChannelHandlerContext ctx, final Http3UnknownFrame frame, final ChannelPromise promise) {
        final long type = frame.type();
        if (Http3CodecUtils.isReservedHttp2FrameType(type)) {
            final Http3Exception exception = new Http3Exception(Http3ErrorCode.H3_FRAME_UNEXPECTED, "Reserved type for HTTP/2 send.");
            promise.setFailure((Throwable)exception);
            this.connectionError(ctx, exception.errorCode(), exception.getMessage(), false);
            return;
        }
        if (!Http3CodecUtils.isReservedFrameType(type)) {
            final Http3Exception exception = new Http3Exception(Http3ErrorCode.H3_FRAME_UNEXPECTED, "Non reserved type for HTTP/3 send.");
            promise.setFailure((Throwable)exception);
            return;
        }
        final ByteBuf out = ctx.alloc().directBuffer();
        Http3CodecUtils.writeVariableLengthInteger(out, type);
        Http3CodecUtils.writeVariableLengthInteger(out, frame.content().readableBytes());
        final ByteBuf content = frame.content().retain();
        ctx.write(Unpooled.wrappedUnmodifiableBuffer(out, content), promise);
    }
    
    private void initReadResumptionListenerIfRequired(final ChannelHandlerContext ctx) {
        if (this.readResumptionListener == null) {
            this.readResumptionListener = new ReadResumptionListener(ctx, this);
        }
    }
    
    private static void unsupported(final ChannelPromise promise) {
        promise.setFailure((Throwable)new UnsupportedOperationException());
    }
    
    @Override
    public void bind(final ChannelHandlerContext ctx, final SocketAddress localAddress, final ChannelPromise promise) {
        ctx.bind(localAddress, promise);
    }
    
    @Override
    public void connect(final ChannelHandlerContext ctx, final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
        ctx.connect(remoteAddress, localAddress, promise);
    }
    
    @Override
    public void disconnect(final ChannelHandlerContext ctx, final ChannelPromise promise) {
        ctx.disconnect(promise);
    }
    
    @Override
    public void close(final ChannelHandlerContext ctx, final ChannelPromise promise) {
        ctx.close(promise);
    }
    
    @Override
    public void deregister(final ChannelHandlerContext ctx, final ChannelPromise promise) {
        ctx.deregister(promise);
    }
    
    @Override
    public void read(final ChannelHandlerContext ctx) {
        assert this.readResumptionListener != null;
        if (this.readResumptionListener.readRequested()) {
            ctx.read();
        }
    }
    
    @Override
    public void flush(final ChannelHandlerContext ctx) {
        if (this.writeResumptionListener != null) {
            this.writeResumptionListener.enqueueFlush();
        }
        else {
            ctx.flush();
        }
    }
    
    private static final class ReadResumptionListener implements Runnable, GenericFutureListener<Future<? super QuicStreamChannel>>
    {
        private static final int STATE_SUSPENDED = 128;
        private static final int STATE_READ_PENDING = 64;
        private static final int STATE_READ_COMPLETE_PENDING = 32;
        private final ChannelHandlerContext ctx;
        private final Http3FrameCodec codec;
        private byte state;
        
        ReadResumptionListener(final ChannelHandlerContext ctx, final Http3FrameCodec codec) {
            this.ctx = ctx;
            this.codec = codec;
            assert codec.qpackAttributes != null;
            if (!codec.qpackAttributes.dynamicTableDisabled() && !codec.qpackAttributes.decoderStreamAvailable()) {
                codec.qpackAttributes.whenDecoderStreamAvailable(this);
            }
        }
        
        void suspended() {
            assert !this.codec.qpackAttributes.dynamicTableDisabled();
            this.setState(128);
        }
        
        boolean readCompleted() {
            if (this.hasState(128)) {
                this.setState(32);
                return false;
            }
            return true;
        }
        
        boolean readRequested() {
            if (this.hasState(128)) {
                this.setState(64);
                return false;
            }
            return true;
        }
        
        boolean isSuspended() {
            return this.hasState(128);
        }
        
        @Override
        public void operationComplete(final Future<? super QuicStreamChannel> future) {
            if (future.isSuccess()) {
                this.resume();
            }
            else {
                this.ctx.fireExceptionCaught(future.cause());
            }
        }
        
        @Override
        public void run() {
            this.resume();
        }
        
        private void resume() {
            this.unsetState(128);
            try {
                this.codec.channelRead(this.ctx, Unpooled.EMPTY_BUFFER);
                if (this.hasState(32)) {
                    this.unsetState(32);
                    this.codec.channelReadComplete(this.ctx);
                }
                if (this.hasState(64)) {
                    this.unsetState(64);
                    this.codec.read(this.ctx);
                }
            }
            catch (final Exception e) {
                this.ctx.fireExceptionCaught((Throwable)e);
            }
        }
        
        private void setState(final int toSet) {
            this.state |= (byte)toSet;
        }
        
        private boolean hasState(final int toCheck) {
            return (this.state & toCheck) == toCheck;
        }
        
        private void unsetState(final int toUnset) {
            this.state &= (byte)~toUnset;
        }
    }
    
    private static final class WriteResumptionListener implements GenericFutureListener<Future<? super QuicStreamChannel>>
    {
        private static final Object FLUSH;
        private final PendingWriteQueue queue;
        private final ChannelHandlerContext ctx;
        private final Http3FrameCodec codec;
        
        private WriteResumptionListener(final ChannelHandlerContext ctx, final Http3FrameCodec codec) {
            this.ctx = ctx;
            this.codec = codec;
            this.queue = new PendingWriteQueue(ctx);
        }
        
        @Override
        public void operationComplete(final Future<? super QuicStreamChannel> future) {
            this.drain();
        }
        
        void enqueue(final Object msg, final ChannelPromise promise) {
            assert this.ctx.channel().eventLoop().inEventLoop();
            ReferenceCountUtil.touch(msg);
            this.queue.add(msg, promise);
        }
        
        void enqueueFlush() {
            assert this.ctx.channel().eventLoop().inEventLoop();
            this.queue.add(WriteResumptionListener.FLUSH, this.ctx.voidPromise());
        }
        
        void drain() {
            assert this.ctx.channel().eventLoop().inEventLoop();
            boolean flushSeen = false;
            try {
                while (true) {
                    final Object entry = this.queue.current();
                    if (entry == null) {
                        break;
                    }
                    if (entry == WriteResumptionListener.FLUSH) {
                        flushSeen = true;
                        this.queue.remove().trySuccess();
                    }
                    else {
                        this.codec.write0(this.ctx, ReferenceCountUtil.retain(entry), this.queue.remove());
                    }
                }
                this.codec.writeResumptionListener = null;
            }
            finally {
                if (flushSeen) {
                    this.codec.flush(this.ctx);
                }
            }
        }
        
        static WriteResumptionListener newListener(final ChannelHandlerContext ctx, final Http3FrameCodec codec) {
            final WriteResumptionListener listener = new WriteResumptionListener(ctx, codec);
            assert codec.qpackAttributes != null;
            codec.qpackAttributes.whenEncoderStreamAvailable(listener);
            return listener;
        }
        
        static {
            FLUSH = new Object();
        }
    }
    
    @FunctionalInterface
    interface Http3FrameCodecFactory
    {
        ChannelHandler newCodec(final Http3FrameTypeValidator p0, final Http3RequestStreamCodecState p1, final Http3RequestStreamCodecState p2);
    }
}
