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

package io.netty.handler.codec.quic;

import io.netty.channel.RecvByteBufAllocator;
import java.util.concurrent.TimeUnit;
import io.netty.channel.ConnectTimeoutException;
import java.nio.channels.AlreadyConnectedException;
import java.nio.channels.ConnectionPendingException;
import io.netty.util.internal.logging.InternalLoggerFactory;
import io.netty.channel.ChannelOutboundInvoker;
import io.netty.channel.ChannelConfig;
import io.netty.channel.socket.DatagramPacket;
import io.netty.util.concurrent.ImmediateEventExecutor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.nio.channels.ClosedChannelException;
import java.nio.BufferUnderflowException;
import io.netty.channel.ChannelOutboundBuffer;
import io.netty.util.internal.StringUtil;
import io.netty.buffer.Unpooled;
import java.net.SocketAddress;
import io.netty.channel.EventLoop;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.channel.ChannelFuture;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.DefaultChannelPipeline;
import java.net.ConnectException;
import java.util.function.Function;
import java.io.File;
import io.netty.handler.ssl.SniCompletionEvent;
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLEngine;
import io.netty.util.concurrent.ImmediateExecutor;
import java.util.HashSet;
import io.netty.util.collection.LongObjectHashMap;
import org.jetbrains.annotations.Nullable;
import io.netty.channel.Channel;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import io.netty.channel.ChannelFutureListener;
import java.net.InetSocketAddress;
import java.util.concurrent.ScheduledFuture;
import io.netty.channel.ChannelPromise;
import io.netty.buffer.ByteBuf;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.nio.ByteBuffer;
import java.util.Set;
import io.netty.util.AttributeKey;
import io.netty.channel.ChannelOption;
import java.util.Map;
import io.netty.channel.ChannelHandler;
import io.netty.util.collection.LongObjectMap;
import io.netty.channel.ChannelMetadata;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.channel.AbstractChannel;

final class QuicheQuicChannel extends AbstractChannel implements QuicChannel
{
    private static final InternalLogger logger;
    private static final String QLOG_FILE_EXTENSION = ".qlog";
    private static final ChannelMetadata METADATA;
    private long[] readableStreams;
    private long[] writableStreams;
    private final LongObjectMap<QuicheQuicStreamChannel> streams;
    private final QuicheQuicChannelConfig config;
    private final boolean server;
    private final QuicStreamIdGenerator idGenerator;
    private final ChannelHandler streamHandler;
    private final Map.Entry<ChannelOption<?>, Object>[] streamOptionsArray;
    private final Map.Entry<AttributeKey<?>, Object>[] streamAttrsArray;
    private final TimeoutHandler timeoutHandler;
    private final QuicConnectionIdGenerator connectionIdAddressGenerator;
    private final QuicResetTokenGenerator resetTokenGenerator;
    private final Set<ByteBuffer> sourceConnectionIds;
    private Consumer<QuicheQuicChannel> freeTask;
    private Executor sslTaskExecutor;
    private boolean inFireChannelReadCompleteQueue;
    private boolean fireChannelReadCompletePending;
    private ByteBuf finBuffer;
    private ByteBuf outErrorCodeBuffer;
    private ChannelPromise connectPromise;
    private ScheduledFuture<?> connectTimeoutFuture;
    private QuicConnectionAddress connectAddress;
    private CloseData closeData;
    private QuicConnectionCloseEvent connectionCloseEvent;
    private QuicConnectionStats statsAtClose;
    private boolean supportsDatagram;
    private boolean recvDatagramPending;
    private boolean datagramReadable;
    private boolean recvStreamPending;
    private boolean streamReadable;
    private boolean handshakeCompletionNotified;
    private boolean earlyDataReadyNotified;
    private int reantranceGuard;
    private static final int IN_RECV = 2;
    private static final int IN_CONNECTION_SEND = 4;
    private static final int IN_HANDLE_WRITABLE_STREAMS = 8;
    private volatile ChannelState state;
    private volatile boolean timedOut;
    private volatile String traceId;
    private volatile QuicheQuicConnection connection;
    private volatile InetSocketAddress local;
    private volatile InetSocketAddress remote;
    private final ChannelFutureListener continueSendingListener;
    private static final AtomicLongFieldUpdater<QuicheQuicChannel> UNI_STREAMS_LEFT_UPDATER;
    private volatile long uniStreamsLeft;
    private static final AtomicLongFieldUpdater<QuicheQuicChannel> BIDI_STREAMS_LEFT_UPDATER;
    private volatile long bidiStreamsLeft;
    private static final int MAX_ARRAY_LEN = 128;
    
    private QuicheQuicChannel(final Channel parent, final boolean server, @Nullable final ByteBuffer key, final InetSocketAddress local, final InetSocketAddress remote, final boolean supportsDatagram, final ChannelHandler streamHandler, final Map.Entry<ChannelOption<?>, Object>[] streamOptionsArray, final Map.Entry<AttributeKey<?>, Object>[] streamAttrsArray, @Nullable final Consumer<QuicheQuicChannel> freeTask, @Nullable final Executor sslTaskExecutor, @Nullable final QuicConnectionIdGenerator connectionIdAddressGenerator, @Nullable final QuicResetTokenGenerator resetTokenGenerator) {
        super(parent);
        this.readableStreams = new long[4];
        this.writableStreams = new long[4];
        this.streams = new LongObjectHashMap<QuicheQuicStreamChannel>();
        this.sourceConnectionIds = new HashSet<ByteBuffer>();
        this.state = ChannelState.OPEN;
        this.continueSendingListener = (f -> {
            if (this.connectionSend(this.connection) != SendResult.NONE) {
                this.flushParent();
            }
            return;
        });
        this.config = new QuicheQuicChannelConfig(this);
        this.freeTask = freeTask;
        this.server = server;
        this.idGenerator = new QuicStreamIdGenerator(server);
        this.connectionIdAddressGenerator = connectionIdAddressGenerator;
        this.resetTokenGenerator = resetTokenGenerator;
        if (key != null) {
            this.sourceConnectionIds.add(key);
        }
        this.supportsDatagram = supportsDatagram;
        this.local = local;
        this.remote = remote;
        this.streamHandler = streamHandler;
        this.streamOptionsArray = streamOptionsArray;
        this.streamAttrsArray = streamAttrsArray;
        this.timeoutHandler = new TimeoutHandler();
        this.sslTaskExecutor = ((sslTaskExecutor == null) ? ImmediateExecutor.INSTANCE : sslTaskExecutor);
    }
    
    static QuicheQuicChannel forClient(final Channel parent, final InetSocketAddress local, final InetSocketAddress remote, final ChannelHandler streamHandler, final Map.Entry<ChannelOption<?>, Object>[] streamOptionsArray, final Map.Entry<AttributeKey<?>, Object>[] streamAttrsArray) {
        return new QuicheQuicChannel(parent, false, null, local, remote, false, streamHandler, streamOptionsArray, streamAttrsArray, null, null, null, null);
    }
    
    static QuicheQuicChannel forServer(final Channel parent, final ByteBuffer key, final InetSocketAddress local, final InetSocketAddress remote, final boolean supportsDatagram, final ChannelHandler streamHandler, final Map.Entry<ChannelOption<?>, Object>[] streamOptionsArray, final Map.Entry<AttributeKey<?>, Object>[] streamAttrsArray, final Consumer<QuicheQuicChannel> freeTask, final Executor sslTaskExecutor, final QuicConnectionIdGenerator connectionIdAddressGenerator, final QuicResetTokenGenerator resetTokenGenerator) {
        return new QuicheQuicChannel(parent, true, key, local, remote, supportsDatagram, streamHandler, streamOptionsArray, streamAttrsArray, freeTask, sslTaskExecutor, connectionIdAddressGenerator, resetTokenGenerator);
    }
    
    private static long[] growIfNeeded(final long[] array, final int maxLength) {
        if (maxLength <= array.length) {
            return array;
        }
        if (array.length == 128) {
            return array;
        }
        return new long[Math.min(128, array.length + 4)];
    }
    
    @Override
    public boolean isTimedOut() {
        return this.timedOut;
    }
    
    @Override
    public SSLEngine sslEngine() {
        final QuicheQuicConnection connection = this.connection;
        return (connection == null) ? null : connection.engine();
    }
    
    private void notifyAboutHandshakeCompletionIfNeeded(final QuicheQuicConnection conn, @Nullable final SSLHandshakeException cause) {
        if (this.handshakeCompletionNotified) {
            return;
        }
        if (cause != null) {
            this.pipeline().fireUserEventTriggered((Object)new SslHandshakeCompletionEvent(cause));
            return;
        }
        if (conn.isFreed()) {
            return;
        }
        switch (this.connection.engine().getHandshakeStatus()) {
            case NOT_HANDSHAKING:
            case FINISHED: {
                this.handshakeCompletionNotified = true;
                this.pipeline().fireUserEventTriggered((Object)SslHandshakeCompletionEvent.SUCCESS);
                break;
            }
        }
    }
    
    @Override
    public long peerAllowedStreams(final QuicStreamType type) {
        switch (type) {
            case BIDIRECTIONAL: {
                return this.bidiStreamsLeft;
            }
            case UNIDIRECTIONAL: {
                return this.uniStreamsLeft;
            }
            default: {
                return 0L;
            }
        }
    }
    
    void attachQuicheConnection(final QuicheQuicConnection connection) {
        this.connection = connection;
        final byte[] traceId = Quiche.quiche_conn_trace_id(connection.address());
        if (traceId != null) {
            this.traceId = new String(traceId);
        }
        connection.init(this.local, this.remote, sniHostname -> this.pipeline().fireUserEventTriggered((Object)new SniCompletionEvent(sniHostname)));
        final QLogConfiguration configuration = this.config.getQLogConfiguration();
        if (configuration != null) {
            final File file = new File(configuration.path());
            String fileName;
            if (file.isDirectory()) {
                file.mkdir();
                if (this.traceId != null) {
                    fileName = configuration.path() + File.separatorChar + this.traceId + "-" + this.id().asShortText() + ".qlog";
                }
                else {
                    fileName = configuration.path() + File.separatorChar + this.id().asShortText() + ".qlog";
                }
            }
            else {
                fileName = configuration.path();
            }
            if (!Quiche.quiche_conn_set_qlog_path(connection.address(), fileName, configuration.logTitle(), configuration.logDescription())) {
                QuicheQuicChannel.logger.info("Unable to create qlog file: {} ", fileName);
            }
        }
    }
    
    void connectNow(final Function<QuicChannel, ? extends QuicSslEngine> engineProvider, final Executor sslTaskExecutor, final Consumer<QuicheQuicChannel> freeTask, final long configAddr, final int localConnIdLength, final boolean supportsDatagram, final ByteBuffer fromSockaddrMemory, final ByteBuffer toSockaddrMemory) throws Exception {
        assert this.connection == null;
        assert this.traceId == null;
        assert this.sourceConnectionIds.isEmpty();
        this.sslTaskExecutor = sslTaskExecutor;
        this.freeTask = freeTask;
        QuicConnectionAddress address = this.connectAddress;
        if (address == QuicConnectionAddress.EPHEMERAL) {
            address = QuicConnectionAddress.random(localConnIdLength);
        }
        final ByteBuffer connectId = address.id();
        if (connectId.remaining() != localConnIdLength) {
            this.failConnectPromiseAndThrow(new IllegalArgumentException("connectionAddress has length " + connectId.remaining() + " instead of " + localConnIdLength));
        }
        final QuicSslEngine engine = (QuicSslEngine)engineProvider.apply(this);
        if (!(engine instanceof QuicheQuicSslEngine)) {
            this.failConnectPromiseAndThrow(new IllegalArgumentException("QuicSslEngine is not of type " + QuicheQuicSslEngine.class.getSimpleName()));
            return;
        }
        if (!engine.getUseClientMode()) {
            this.failConnectPromiseAndThrow(new IllegalArgumentException("QuicSslEngine is not create in client mode"));
        }
        final QuicheQuicSslEngine quicheEngine = (QuicheQuicSslEngine)engine;
        final ByteBuf idBuffer = this.alloc().directBuffer(connectId.remaining()).writeBytes(connectId.duplicate());
        try {
            final int fromSockaddrLen = SockaddrIn.setAddress(fromSockaddrMemory, this.local);
            final int toSockaddrLen = SockaddrIn.setAddress(toSockaddrMemory, this.remote);
            final QuicheQuicConnection connection = quicheEngine.createConnection(ssl -> Quiche.quiche_conn_new_with_tls(Quiche.readerMemoryAddress(idBuffer), idBuffer.readableBytes(), -1L, -1, Quiche.memoryAddressWithPosition(fromSockaddrMemory), fromSockaddrLen, Quiche.memoryAddressWithPosition(toSockaddrMemory), toSockaddrLen, configAddr, ssl, false));
            if (connection == null) {
                this.failConnectPromiseAndThrow(new ConnectException());
                return;
            }
            this.attachQuicheConnection(connection);
            final QuicClientSessionCache sessionCache = quicheEngine.ctx.getSessionCache();
            if (sessionCache != null) {
                final byte[] sessionBytes = sessionCache.getSession(quicheEngine.getSession().getPeerHost(), quicheEngine.getSession().getPeerPort());
                if (sessionBytes != null) {
                    Quiche.quiche_conn_set_session(connection.address(), sessionBytes);
                }
            }
            this.supportsDatagram = supportsDatagram;
            this.sourceConnectionIds.add(connectId);
        }
        finally {
            idBuffer.release();
        }
    }
    
    private void failConnectPromiseAndThrow(final Exception e) throws Exception {
        this.tryFailConnectPromise(e);
        throw e;
    }
    
    private boolean tryFailConnectPromise(final Exception e) {
        final ChannelPromise promise = this.connectPromise;
        if (promise != null) {
            this.connectPromise = null;
            promise.tryFailure(e);
            return true;
        }
        return false;
    }
    
    Set<ByteBuffer> sourceConnectionIds() {
        return this.sourceConnectionIds;
    }
    
    boolean markInFireChannelReadCompleteQueue() {
        return !this.inFireChannelReadCompleteQueue && (this.inFireChannelReadCompleteQueue = true);
    }
    
    private void failPendingConnectPromise() {
        final ChannelPromise promise = this.connectPromise;
        if (promise != null) {
            this.connectPromise = null;
            promise.tryFailure(new QuicClosedChannelException(this.connectionCloseEvent));
        }
    }
    
    void forceClose() {
        this.unsafe().close(this.voidPromise());
    }
    
    @Override
    protected DefaultChannelPipeline newChannelPipeline() {
        return new DefaultChannelPipeline(this) {
            @Override
            protected void onUnhandledInboundMessage(final ChannelHandlerContext ctx, final Object msg) {
                if (msg instanceof QuicStreamChannel) {
                    final QuicStreamChannel channel = (QuicStreamChannel)msg;
                    Quic.setupChannel(channel, QuicheQuicChannel.this.streamOptionsArray, QuicheQuicChannel.this.streamAttrsArray, QuicheQuicChannel.this.streamHandler, QuicheQuicChannel.logger);
                    ctx.channel().eventLoop().register(channel);
                }
                else {
                    super.onUnhandledInboundMessage(ctx, msg);
                }
            }
        };
    }
    
    @Override
    public QuicChannel flush() {
        super.flush();
        return this;
    }
    
    @Override
    public QuicChannel read() {
        super.read();
        return this;
    }
    
    @Override
    public Future<QuicStreamChannel> createStream(final QuicStreamType type, @Nullable final ChannelHandler handler, final Promise<QuicStreamChannel> promise) {
        if (this.eventLoop().inEventLoop()) {
            ((QuicChannelUnsafe)this.unsafe()).connectStream(type, handler, promise);
        }
        else {
            this.eventLoop().execute(() -> ((QuicChannelUnsafe)this.unsafe()).connectStream(type, handler, promise));
        }
        return promise;
    }
    
    @Override
    public ChannelFuture close(final boolean applicationClose, final int error, final ByteBuf reason, final ChannelPromise promise) {
        if (this.eventLoop().inEventLoop()) {
            this.close0(applicationClose, error, reason, promise);
        }
        else {
            this.eventLoop().execute(() -> this.close0(applicationClose, error, reason, promise));
        }
        return promise;
    }
    
    private void close0(final boolean applicationClose, final int error, ByteBuf reason, final ChannelPromise promise) {
        if (this.closeData == null) {
            if (!reason.hasMemoryAddress()) {
                final ByteBuf copy = this.alloc().directBuffer(reason.readableBytes()).writeBytes(reason);
                reason.release();
                reason = copy;
            }
            promise.addListener((GenericFutureListener<? extends Future<? super Void>>)(this.closeData = new CloseData(applicationClose, error, reason)));
        }
        else {
            reason.release();
        }
        this.close(promise);
    }
    
    @Override
    public String toString() {
        final String traceId = this.traceId;
        if (traceId == null) {
            return "()" + super.toString();
        }
        return '(' + traceId + ')' + super.toString();
    }
    
    @Override
    protected AbstractUnsafe newUnsafe() {
        return new QuicChannelUnsafe();
    }
    
    @Override
    protected boolean isCompatible(final EventLoop eventLoop) {
        return this.parent().eventLoop() == eventLoop;
    }
    
    @Nullable
    @Override
    protected QuicConnectionAddress localAddress0() {
        final QuicheQuicConnection connection = this.connection;
        return (connection == null) ? null : connection.sourceId();
    }
    
    @Nullable
    @Override
    protected QuicConnectionAddress remoteAddress0() {
        final QuicheQuicConnection connection = this.connection;
        return (connection == null) ? null : connection.destinationId();
    }
    
    @Nullable
    @Override
    public QuicConnectionAddress localAddress() {
        return this.localAddress0();
    }
    
    @Nullable
    @Override
    public QuicConnectionAddress remoteAddress() {
        return this.remoteAddress0();
    }
    
    @Nullable
    @Override
    public SocketAddress localSocketAddress() {
        return this.local;
    }
    
    @Nullable
    @Override
    public SocketAddress remoteSocketAddress() {
        return this.remote;
    }
    
    @Override
    protected void doBind(final SocketAddress socketAddress) {
        throw new UnsupportedOperationException();
    }
    
    @Override
    protected void doDisconnect() throws Exception {
        this.doClose();
    }
    
    @Override
    protected void doClose() throws Exception {
        if (this.state == ChannelState.CLOSED) {
            return;
        }
        this.state = ChannelState.CLOSED;
        final QuicheQuicConnection conn = this.connection;
        if (conn == null || conn.isFreed()) {
            if (this.closeData != null) {
                this.closeData.reason.release();
                this.closeData = null;
            }
            this.failPendingConnectPromise();
            return;
        }
        SendResult sendResult = this.connectionSend(conn);
        boolean app;
        int err;
        ByteBuf reason;
        if (this.closeData == null) {
            app = false;
            err = 0;
            reason = Unpooled.EMPTY_BUFFER;
        }
        else {
            app = this.closeData.applicationClose;
            err = this.closeData.err;
            reason = this.closeData.reason;
            this.closeData = null;
        }
        this.failPendingConnectPromise();
        try {
            final int res = Quiche.quiche_conn_close(conn.address(), app, err, Quiche.readerMemoryAddress(reason), reason.readableBytes());
            if (res < 0 && res != Quiche.QUICHE_ERR_DONE) {
                throw Quiche.convertToException(res);
            }
            if (this.connectionSend(conn) == SendResult.SOME) {
                sendResult = SendResult.SOME;
            }
        }
        finally {
            this.statsAtClose = this.collectStats0(conn, this.eventLoop().newPromise());
            try {
                this.timedOut = Quiche.quiche_conn_is_timed_out(conn.address());
                this.closeStreams();
                if (this.finBuffer != null) {
                    this.finBuffer.release();
                    this.finBuffer = null;
                }
                if (this.outErrorCodeBuffer != null) {
                    this.outErrorCodeBuffer.release();
                    this.outErrorCodeBuffer = null;
                }
            }
            finally {
                if (sendResult == SendResult.SOME) {
                    this.forceFlushParent();
                }
                else {
                    this.flushParent();
                }
                conn.free();
                if (this.freeTask != null) {
                    this.freeTask.accept(this);
                }
                this.timeoutHandler.cancel();
                this.local = null;
                this.remote = null;
            }
        }
    }
    
    @Override
    protected void doBeginRead() {
        this.recvDatagramPending = true;
        this.recvStreamPending = true;
        if (this.datagramReadable || this.streamReadable) {
            ((QuicChannelUnsafe)this.unsafe()).recv();
        }
    }
    
    @Override
    protected Object filterOutboundMessage(final Object msg) {
        if (msg instanceof ByteBuf) {
            return msg;
        }
        throw new UnsupportedOperationException("Unsupported message type: " + StringUtil.simpleClassName(msg));
    }
    
    @Override
    protected void doWrite(final ChannelOutboundBuffer channelOutboundBuffer) throws Exception {
        if (!this.supportsDatagram) {
            throw new UnsupportedOperationException("Datagram extension is not supported");
        }
        boolean sendSomething = false;
        boolean retry = false;
        final QuicheQuicConnection conn = this.connection;
        try {
            while (true) {
                final ByteBuf buffer = (ByteBuf)channelOutboundBuffer.current();
                if (buffer == null) {
                    return;
                }
                final int readable = buffer.readableBytes();
                if (readable == 0) {
                    channelOutboundBuffer.remove();
                }
                else {
                    int res;
                    if (!buffer.isDirect() || buffer.nioBufferCount() > 1) {
                        final ByteBuf tmpBuffer = this.alloc().directBuffer(readable);
                        try {
                            tmpBuffer.writeBytes(buffer, buffer.readerIndex(), readable);
                            res = sendDatagram(conn, tmpBuffer);
                        }
                        finally {
                            tmpBuffer.release();
                        }
                    }
                    else {
                        res = sendDatagram(conn, buffer);
                    }
                    if (res >= 0) {
                        channelOutboundBuffer.remove();
                        sendSomething = true;
                        retry = false;
                    }
                    else if (res == Quiche.QUICHE_ERR_BUFFER_TOO_SHORT) {
                        retry = false;
                        channelOutboundBuffer.remove(new BufferUnderflowException());
                    }
                    else {
                        if (res == Quiche.QUICHE_ERR_INVALID_STATE) {
                            throw new UnsupportedOperationException("Remote peer does not support Datagram extension");
                        }
                        if (res != Quiche.QUICHE_ERR_DONE) {
                            throw Quiche.convertToException(res);
                        }
                        if (retry) {
                            break;
                        }
                        sendSomething = false;
                        if (this.connectionSend(conn) != SendResult.NONE) {
                            this.forceFlushParent();
                        }
                        retry = true;
                    }
                }
            }
            while (channelOutboundBuffer.remove()) {}
        }
        finally {
            if (sendSomething && this.connectionSend(conn) != SendResult.NONE) {
                this.flushParent();
            }
        }
    }
    
    private static int sendDatagram(final QuicheQuicConnection conn, final ByteBuf buf) throws ClosedChannelException {
        return Quiche.quiche_conn_dgram_send(connectionAddressChecked(conn), Quiche.readerMemoryAddress(buf), buf.readableBytes());
    }
    
    @Override
    public QuicChannelConfig config() {
        return this.config;
    }
    
    @Override
    public boolean isOpen() {
        return this.state != ChannelState.CLOSED;
    }
    
    @Override
    public boolean isActive() {
        return this.state == ChannelState.ACTIVE;
    }
    
    @Override
    public ChannelMetadata metadata() {
        return QuicheQuicChannel.METADATA;
    }
    
    private void flushParent() {
        if (!this.inFireChannelReadCompleteQueue) {
            this.forceFlushParent();
        }
    }
    
    private void forceFlushParent() {
        this.parent().flush();
    }
    
    private static long connectionAddressChecked(@Nullable final QuicheQuicConnection conn) throws ClosedChannelException {
        if (conn == null || conn.isFreed()) {
            throw new ClosedChannelException();
        }
        return conn.address();
    }
    
    boolean freeIfClosed() {
        final QuicheQuicConnection conn = this.connection;
        if (conn == null || conn.isFreed()) {
            return true;
        }
        if (conn.isClosed()) {
            this.unsafe().close(this.newPromise());
            return true;
        }
        return false;
    }
    
    private void closeStreams() {
        if (this.streams.isEmpty()) {
            return;
        }
        ClosedChannelException closedChannelException;
        if (this.isTimedOut()) {
            closedChannelException = new QuicTimeoutClosedChannelException();
        }
        else {
            closedChannelException = new ClosedChannelException();
        }
        for (final QuicheQuicStreamChannel stream : this.streams.values().toArray(new QuicheQuicStreamChannel[0])) {
            stream.unsafe().close(closedChannelException, this.voidPromise());
        }
        this.streams.clear();
    }
    
    void streamPriority(final long streamId, final byte priority, final boolean incremental) throws Exception {
        final int res = Quiche.quiche_conn_stream_priority(connectionAddressChecked(this.connection), streamId, priority, incremental);
        if (res < 0 && res != Quiche.QUICHE_ERR_DONE) {
            throw Quiche.convertToException(res);
        }
    }
    
    void streamClosed(final long streamId) {
        this.streams.remove(streamId);
    }
    
    boolean isStreamLocalCreated(final long streamId) {
        return (streamId & 0x1L) == (this.server ? 1 : 0);
    }
    
    QuicStreamType streamType(final long streamId) {
        return ((streamId & 0x2L) == 0x0L) ? QuicStreamType.BIDIRECTIONAL : QuicStreamType.UNIDIRECTIONAL;
    }
    
    void streamShutdown(final long streamId, final boolean read, final boolean write, final int err, final ChannelPromise promise) {
        final QuicheQuicConnection conn = this.connection;
        long connectionAddress;
        try {
            connectionAddress = connectionAddressChecked(conn);
        }
        catch (final ClosedChannelException e) {
            promise.setFailure((Throwable)e);
            return;
        }
        int res = 0;
        if (read) {
            res |= Quiche.quiche_conn_stream_shutdown(connectionAddress, streamId, Quiche.QUICHE_SHUTDOWN_READ, err);
        }
        if (write) {
            res |= Quiche.quiche_conn_stream_shutdown(connectionAddress, streamId, Quiche.QUICHE_SHUTDOWN_WRITE, err);
        }
        if (this.connectionSend(conn) != SendResult.NONE) {
            this.forceFlushParent();
        }
        if (res < 0 && res != Quiche.QUICHE_ERR_DONE) {
            promise.setFailure((Throwable)Quiche.convertToException(res));
        }
        else {
            promise.setSuccess();
        }
    }
    
    void streamSendFin(final long streamId) throws Exception {
        final QuicheQuicConnection conn = this.connection;
        try {
            final int res = this.streamSend0(conn, streamId, Unpooled.EMPTY_BUFFER, true);
            if (res < 0 && res != Quiche.QUICHE_ERR_DONE) {
                throw Quiche.convertToException(res);
            }
        }
        finally {
            if (this.connectionSend(conn) != SendResult.NONE) {
                this.flushParent();
            }
        }
    }
    
    int streamSend(final long streamId, final ByteBuf buffer, final boolean fin) throws ClosedChannelException {
        final QuicheQuicConnection conn = this.connection;
        if (buffer.nioBufferCount() == 1) {
            return this.streamSend0(conn, streamId, buffer, fin);
        }
        final ByteBuffer[] nioBuffers = buffer.nioBuffers();
        final int lastIdx = nioBuffers.length - 1;
        int res = 0;
        for (final ByteBuffer nioBuffer : nioBuffers) {
            while (nioBuffer.hasRemaining()) {
                final int localRes = this.streamSend(conn, streamId, nioBuffer, false);
                if (localRes <= 0) {
                    return res;
                }
                res += localRes;
                nioBuffer.position();
            }
        }
        final int localRes2 = this.streamSend(conn, streamId, nioBuffers[lastIdx], fin);
        if (localRes2 > 0) {
            res += localRes2;
        }
        return res;
    }
    
    void connectionSendAndFlush() {
        if (this.inFireChannelReadCompleteQueue || (this.reantranceGuard & 0x8) != 0x0) {
            return;
        }
        if (this.connectionSend(this.connection) != SendResult.NONE) {
            this.flushParent();
        }
    }
    
    private int streamSend0(final QuicheQuicConnection conn, final long streamId, final ByteBuf buffer, final boolean fin) throws ClosedChannelException {
        return Quiche.quiche_conn_stream_send(connectionAddressChecked(conn), streamId, Quiche.readerMemoryAddress(buffer), buffer.readableBytes(), fin);
    }
    
    private int streamSend(final QuicheQuicConnection conn, final long streamId, final ByteBuffer buffer, final boolean fin) throws ClosedChannelException {
        return Quiche.quiche_conn_stream_send(connectionAddressChecked(conn), streamId, Quiche.memoryAddressWithPosition(buffer), buffer.remaining(), fin);
    }
    
    StreamRecvResult streamRecv(final long streamId, final ByteBuf buffer) throws Exception {
        final QuicheQuicConnection conn = this.connection;
        final long connAddr = connectionAddressChecked(conn);
        if (this.finBuffer == null) {
            this.finBuffer = this.alloc().directBuffer(1);
        }
        if (this.outErrorCodeBuffer == null) {
            this.outErrorCodeBuffer = this.alloc().directBuffer(8);
        }
        this.outErrorCodeBuffer.setLongLE(0, -1L);
        final int writerIndex = buffer.writerIndex();
        final int recvLen = Quiche.quiche_conn_stream_recv(connAddr, streamId, Quiche.writerMemoryAddress(buffer), buffer.writableBytes(), Quiche.writerMemoryAddress(this.finBuffer), Quiche.writerMemoryAddress(this.outErrorCodeBuffer));
        final long errorCode = this.outErrorCodeBuffer.getLongLE(0);
        if (recvLen == Quiche.QUICHE_ERR_DONE) {
            return StreamRecvResult.DONE;
        }
        if (recvLen < 0) {
            throw Quiche.convertToException(recvLen, errorCode);
        }
        buffer.writerIndex(writerIndex + recvLen);
        return this.finBuffer.getBoolean(0) ? StreamRecvResult.FIN : StreamRecvResult.OK;
    }
    
    void recv(final InetSocketAddress sender, final InetSocketAddress recipient, final ByteBuf buffer) {
        ((QuicChannelUnsafe)this.unsafe()).connectionRecv(sender, recipient, buffer);
    }
    
    List<ByteBuffer> retiredSourceConnectionId() {
        final QuicheQuicConnection connection = this.connection;
        if (connection == null || connection.isFreed()) {
            return Collections.emptyList();
        }
        final long connAddr = connection.address();
        assert connAddr != -1L;
        List<ByteBuffer> retiredSourceIds = null;
        while (true) {
            final byte[] retired = Quiche.quiche_conn_retired_scid_next(connAddr);
            if (retired == null) {
                break;
            }
            if (retiredSourceIds == null) {
                retiredSourceIds = new ArrayList<ByteBuffer>();
            }
            final ByteBuffer retiredId = ByteBuffer.wrap(retired);
            retiredSourceIds.add(retiredId);
            this.sourceConnectionIds.remove(retiredId);
        }
        if (retiredSourceIds == null) {
            return Collections.emptyList();
        }
        return retiredSourceIds;
    }
    
    List<ByteBuffer> newSourceConnectionIds() {
        if (this.connectionIdAddressGenerator != null && this.resetTokenGenerator != null) {
            final QuicheQuicConnection connection = this.connection;
            if (connection == null || connection.isFreed()) {
                return Collections.emptyList();
            }
            final long connAddr = connection.address();
            int left = Quiche.quiche_conn_scids_left(connAddr);
            if (left > 0) {
                final QuicConnectionAddress sourceAddr = connection.sourceId();
                if (sourceAddr == null) {
                    return Collections.emptyList();
                }
                final List<ByteBuffer> generatedIds = new ArrayList<ByteBuffer>(left);
                boolean sendAndFlush = false;
                final ByteBuffer key = sourceAddr.id();
                final ByteBuf connIdBuffer = this.alloc().directBuffer(key.remaining());
                final byte[] resetTokenArray = new byte[16];
                try {
                    do {
                        final ByteBuffer srcId = this.connectionIdAddressGenerator.newId(key.duplicate(), key.remaining()).asReadOnlyBuffer();
                        connIdBuffer.clear();
                        connIdBuffer.writeBytes(srcId.duplicate());
                        final ByteBuffer resetToken = this.resetTokenGenerator.newResetToken(srcId.duplicate());
                        resetToken.get(resetTokenArray);
                        final long result = Quiche.quiche_conn_new_scid(connAddr, Quiche.memoryAddress(connIdBuffer, 0, connIdBuffer.readableBytes()), connIdBuffer.readableBytes(), resetTokenArray, false, -1L);
                        if (result < 0L) {
                            break;
                        }
                        sendAndFlush = true;
                        generatedIds.add(srcId.duplicate());
                        this.sourceConnectionIds.add(srcId);
                    } while (--left > 0);
                }
                finally {
                    connIdBuffer.release();
                }
                if (sendAndFlush) {
                    this.connectionSendAndFlush();
                }
                return generatedIds;
            }
        }
        return Collections.emptyList();
    }
    
    void writable() {
        final QuicheQuicConnection conn = this.connection;
        SendResult result = this.connectionSend(conn);
        this.handleWritableStreams(conn);
        if (this.connectionSend(conn) == SendResult.SOME) {
            result = SendResult.SOME;
        }
        if (result == SendResult.SOME) {
            this.forceFlushParent();
        }
        this.freeIfClosed();
    }
    
    long streamCapacity(final long streamId) {
        final QuicheQuicConnection conn = this.connection;
        if (conn.isClosed()) {
            return 0L;
        }
        return Quiche.quiche_conn_stream_capacity(conn.address(), streamId);
    }
    
    private boolean handleWritableStreams(final QuicheQuicConnection conn) {
        if (conn.isFreed()) {
            return false;
        }
        this.reantranceGuard |= 0x8;
        try {
            final long connAddr = conn.address();
            boolean mayNeedWrite = false;
            if (Quiche.quiche_conn_is_established(connAddr) || Quiche.quiche_conn_is_in_early_data(connAddr)) {
                final long writableIterator = Quiche.quiche_conn_writable(connAddr);
                int totalWritable = 0;
                try {
                    int writable;
                    do {
                        writable = Quiche.quiche_stream_iter_next(writableIterator, this.writableStreams);
                        for (int i = 0; i < writable; ++i) {
                            final long streamId = this.writableStreams[i];
                            final QuicheQuicStreamChannel streamChannel = this.streams.get(streamId);
                            if (streamChannel != null) {
                                final long capacity = Quiche.quiche_conn_stream_capacity(connAddr, streamId);
                                if (streamChannel.writable(capacity)) {
                                    mayNeedWrite = true;
                                }
                            }
                        }
                        if (writable > 0) {
                            totalWritable += writable;
                        }
                    } while (writable >= this.writableStreams.length);
                }
                finally {
                    Quiche.quiche_stream_iter_free(writableIterator);
                }
                this.writableStreams = growIfNeeded(this.writableStreams, totalWritable);
            }
            return mayNeedWrite;
        }
        finally {
            this.reantranceGuard &= 0xFFFFFFF7;
        }
    }
    
    void recvComplete() {
        try {
            final QuicheQuicConnection conn = this.connection;
            if (conn.isFreed()) {
                this.forceFlushParent();
                return;
            }
            this.fireChannelReadCompleteIfNeeded();
            this.connectionSend(conn);
            this.forceFlushParent();
            this.freeIfClosed();
        }
        finally {
            this.inFireChannelReadCompleteQueue = false;
        }
    }
    
    private void fireChannelReadCompleteIfNeeded() {
        if (this.fireChannelReadCompletePending) {
            this.fireChannelReadCompletePending = false;
            this.pipeline().fireChannelReadComplete();
        }
    }
    
    private void fireExceptionEvents(final QuicheQuicConnection conn, final Throwable cause) {
        if (cause instanceof SSLHandshakeException) {
            this.notifyAboutHandshakeCompletionIfNeeded(conn, (SSLHandshakeException)cause);
        }
        this.pipeline().fireExceptionCaught(cause);
    }
    
    private boolean runTasksDirectly() {
        return this.sslTaskExecutor == null || this.sslTaskExecutor == ImmediateExecutor.INSTANCE || this.sslTaskExecutor == ImmediateEventExecutor.INSTANCE;
    }
    
    private void runAllTaskSend(final QuicheQuicConnection conn, final Runnable task) {
        this.sslTaskExecutor.execute(this.decorateTaskSend(conn, task));
    }
    
    private void runAll(final QuicheQuicConnection conn, Runnable task) {
        do {
            task.run();
        } while ((task = conn.sslTask()) != null);
    }
    
    private Runnable decorateTaskSend(final QuicheQuicConnection conn, final Runnable task) {
        return () -> {
            try {
                this.runAll(conn, task);
            }
            finally {
                this.eventLoop().execute(() -> {
                    if (this.connectionSend(conn) != SendResult.NONE) {
                        this.forceFlushParent();
                    }
                    this.freeIfClosed();
                });
            }
        };
    }
    
    private SendResult connectionSendSegments(final QuicheQuicConnection conn, final SegmentedDatagramPacketAllocator segmentedDatagramPacketAllocator) {
        if (conn.isClosed()) {
            return SendResult.NONE;
        }
        final List<ByteBuf> bufferList = new ArrayList<ByteBuf>(segmentedDatagramPacketAllocator.maxNumSegments());
        final long connAddr = conn.address();
        final int maxDatagramSize = Quiche.quiche_conn_max_send_udp_payload_size(connAddr);
        SendResult sendResult = SendResult.NONE;
        boolean close = false;
        while (true) {
            final int len = calculateSendBufferLength(connAddr, maxDatagramSize);
            final ByteBuf out = this.alloc().directBuffer(len);
            final ByteBuffer sendInfo = conn.nextSendInfo();
            final InetSocketAddress sendToAddress = this.remote;
            final int writerIndex = out.writerIndex();
            final int written = Quiche.quiche_conn_send(connAddr, Quiche.writerMemoryAddress(out), out.writableBytes(), Quiche.memoryAddressWithPosition(sendInfo));
            if (written == 0) {
                out.release();
            }
            else {
                boolean done;
                if (written < 0) {
                    done = true;
                    if (written != Quiche.QUICHE_ERR_DONE) {
                        close = Quiche.shouldClose(written);
                        final Exception e = Quiche.convertToException(written);
                        if (!this.tryFailConnectPromise(e)) {
                            this.fireExceptionEvents(conn, e);
                        }
                    }
                }
                else {
                    done = false;
                }
                final int size = bufferList.size();
                if (done) {
                    out.release();
                    switch (size) {
                        case 0: {
                            break;
                        }
                        case 1: {
                            this.parent().write(new DatagramPacket(bufferList.get(0), sendToAddress));
                            sendResult = SendResult.SOME;
                            break;
                        }
                        default: {
                            final int segmentSize = segmentSize(bufferList);
                            final ByteBuf compositeBuffer = Unpooled.wrappedBuffer((ByteBuf[])bufferList.toArray(new ByteBuf[0]));
                            this.parent().write(segmentedDatagramPacketAllocator.newPacket(compositeBuffer, segmentSize, sendToAddress));
                            sendResult = SendResult.SOME;
                            break;
                        }
                    }
                    bufferList.clear();
                    if (close) {
                        sendResult = SendResult.CLOSE;
                    }
                    return sendResult;
                }
                out.writerIndex(writerIndex + written);
                int segmentSize = -1;
                if (conn.isSendInfoChanged()) {
                    this.remote = QuicheSendInfo.getToAddress(sendInfo);
                    this.local = QuicheSendInfo.getFromAddress(sendInfo);
                    if (size > 0) {
                        segmentSize = segmentSize(bufferList);
                    }
                }
                else if (size > 0) {
                    final int lastReadable = segmentSize(bufferList);
                    if (lastReadable != out.readableBytes() || size == segmentedDatagramPacketAllocator.maxNumSegments()) {
                        segmentSize = lastReadable;
                    }
                }
                if (segmentSize != -1) {
                    boolean stop;
                    if (size == 1) {
                        stop = this.writePacket(new DatagramPacket(bufferList.get(0), sendToAddress), maxDatagramSize, len);
                    }
                    else {
                        final ByteBuf compositeBuffer2 = Unpooled.wrappedBuffer((ByteBuf[])bufferList.toArray(new ByteBuf[0]));
                        stop = this.writePacket(segmentedDatagramPacketAllocator.newPacket(compositeBuffer2, segmentSize, sendToAddress), maxDatagramSize, len);
                    }
                    bufferList.clear();
                    sendResult = SendResult.SOME;
                    if (stop) {
                        if (out.isReadable()) {
                            this.parent().write(new DatagramPacket(out, sendToAddress));
                        }
                        else {
                            out.release();
                        }
                        if (close) {
                            sendResult = SendResult.CLOSE;
                        }
                        return sendResult;
                    }
                }
                out.touch((Object)bufferList);
                bufferList.add(out);
            }
        }
    }
    
    private static int segmentSize(final List<ByteBuf> bufferList) {
        assert !bufferList.isEmpty();
        final int size = bufferList.size();
        return bufferList.get(size - 1).readableBytes();
    }
    
    private SendResult connectionSendSimple(final QuicheQuicConnection conn) {
        if (conn.isClosed()) {
            return SendResult.NONE;
        }
        final long connAddr = conn.address();
        SendResult sendResult = SendResult.NONE;
        boolean close = false;
        final int maxDatagramSize = Quiche.quiche_conn_max_send_udp_payload_size(connAddr);
        while (true) {
            final ByteBuffer sendInfo = conn.nextSendInfo();
            final int len = calculateSendBufferLength(connAddr, maxDatagramSize);
            final ByteBuf out = this.alloc().directBuffer(len);
            final int writerIndex = out.writerIndex();
            final int written = Quiche.quiche_conn_send(connAddr, Quiche.writerMemoryAddress(out), out.writableBytes(), Quiche.memoryAddressWithPosition(sendInfo));
            if (written == 0) {
                out.release();
            }
            else if (written < 0) {
                out.release();
                if (written != Quiche.QUICHE_ERR_DONE) {
                    close = Quiche.shouldClose(written);
                    final Exception e = Quiche.convertToException(written);
                    if (!this.tryFailConnectPromise(e)) {
                        this.fireExceptionEvents(conn, e);
                    }
                    break;
                }
                break;
            }
            else {
                if (conn.isSendInfoChanged()) {
                    this.remote = QuicheSendInfo.getToAddress(sendInfo);
                    this.local = QuicheSendInfo.getFromAddress(sendInfo);
                }
                out.writerIndex(writerIndex + written);
                final boolean stop = this.writePacket(new DatagramPacket(out, this.remote), maxDatagramSize, len);
                sendResult = SendResult.SOME;
                if (stop) {
                    break;
                }
                continue;
            }
        }
        if (close) {
            sendResult = SendResult.CLOSE;
        }
        return sendResult;
    }
    
    private boolean writePacket(final DatagramPacket packet, final int maxDatagramSize, final int len) {
        final ChannelFuture future = this.parent().write(packet);
        if (isSendWindowUsed(maxDatagramSize, len)) {
            future.addListener((GenericFutureListener<? extends Future<? super Void>>)this.continueSendingListener);
            return true;
        }
        return false;
    }
    
    private static boolean isSendWindowUsed(final int maxDatagramSize, final int len) {
        return len < maxDatagramSize;
    }
    
    private static int calculateSendBufferLength(final long connAddr, final int maxDatagramSize) {
        final int len = Math.min(maxDatagramSize, Quiche.quiche_conn_send_quantum(connAddr));
        if (len <= 0) {
            return 8;
        }
        return len;
    }
    
    private SendResult connectionSend(final QuicheQuicConnection conn) {
        if (conn.isFreed()) {
            return SendResult.NONE;
        }
        if ((this.reantranceGuard & 0x4) != 0x0) {
            this.notifyEarlyDataReadyIfNeeded(conn);
            return SendResult.NONE;
        }
        this.reantranceGuard |= 0x4;
        try {
            final SegmentedDatagramPacketAllocator segmentedDatagramPacketAllocator = this.config.getSegmentedDatagramPacketAllocator();
            SendResult sendResult;
            if (segmentedDatagramPacketAllocator.maxNumSegments() > 0) {
                sendResult = this.connectionSendSegments(conn, segmentedDatagramPacketAllocator);
            }
            else {
                sendResult = this.connectionSendSimple(conn);
            }
            Runnable task = conn.sslTask();
            if (task != null) {
                if (this.runTasksDirectly()) {
                    do {
                        task.run();
                        this.notifyEarlyDataReadyIfNeeded(conn);
                    } while ((task = conn.sslTask()) != null);
                    this.eventLoop().execute(new Runnable() {
                        @Override
                        public void run() {
                            if (QuicheQuicChannel.this.connectionSend(conn) != SendResult.NONE) {
                                QuicheQuicChannel.this.forceFlushParent();
                            }
                            QuicheQuicChannel.this.freeIfClosed();
                        }
                    });
                }
                else {
                    this.runAllTaskSend(conn, task);
                }
            }
            else {
                this.notifyEarlyDataReadyIfNeeded(conn);
            }
            this.timeoutHandler.scheduleTimeout();
            return sendResult;
        }
        finally {
            this.reantranceGuard &= 0xFFFFFFFB;
        }
    }
    
    void finishConnect() {
        assert !this.server;
        assert this.connection != null;
        if (this.connectionSend(this.connection) != SendResult.NONE) {
            this.flushParent();
        }
    }
    
    private void notifyEarlyDataReadyIfNeeded(final QuicheQuicConnection conn) {
        if (!this.server && !this.earlyDataReadyNotified && !conn.isFreed() && Quiche.quiche_conn_is_in_early_data(conn.address())) {
            this.earlyDataReadyNotified = true;
            this.pipeline().fireUserEventTriggered((Object)SslEarlyDataReadyEvent.INSTANCE);
        }
    }
    
    @Override
    public Future<QuicConnectionStats> collectStats(final Promise<QuicConnectionStats> promise) {
        if (this.eventLoop().inEventLoop()) {
            this.collectStats0(promise);
        }
        else {
            this.eventLoop().execute(() -> this.collectStats0(promise));
        }
        return promise;
    }
    
    private void collectStats0(final Promise<QuicConnectionStats> promise) {
        final QuicheQuicConnection conn = this.connection;
        if (conn.isFreed()) {
            promise.setSuccess(this.statsAtClose);
            return;
        }
        this.collectStats0(this.connection, promise);
    }
    
    @Nullable
    private QuicConnectionStats collectStats0(final QuicheQuicConnection connection, final Promise<QuicConnectionStats> promise) {
        final long[] stats = Quiche.quiche_conn_stats(connection.address());
        if (stats == null) {
            promise.setFailure(new IllegalStateException("native quiche_conn_stats(...) failed"));
            return null;
        }
        final QuicheQuicConnectionStats connStats = new QuicheQuicConnectionStats(stats);
        promise.setSuccess(connStats);
        return connStats;
    }
    
    @Override
    public Future<QuicConnectionPathStats> collectPathStats(final int pathIdx, final Promise<QuicConnectionPathStats> promise) {
        if (this.eventLoop().inEventLoop()) {
            this.collectPathStats0(pathIdx, promise);
        }
        else {
            this.eventLoop().execute(() -> this.collectPathStats0(pathIdx, promise));
        }
        return promise;
    }
    
    private void collectPathStats0(final int pathIdx, final Promise<QuicConnectionPathStats> promise) {
        final QuicheQuicConnection conn = this.connection;
        if (conn.isFreed()) {
            promise.setFailure(new IllegalStateException("Connection is closed"));
            return;
        }
        final Object[] stats = Quiche.quiche_conn_path_stats(this.connection.address(), pathIdx);
        if (stats == null) {
            promise.setFailure(new IllegalStateException("native quiche_conn_path_stats(...) failed"));
            return;
        }
        promise.setSuccess(new QuicheQuicConnectionPathStats(stats));
    }
    
    @Override
    public QuicTransportParameters peerTransportParameters() {
        return this.connection.peerParameters();
    }
    
    static {
        logger = InternalLoggerFactory.getInstance(QuicheQuicChannel.class);
        METADATA = new ChannelMetadata(false, 16);
        UNI_STREAMS_LEFT_UPDATER = AtomicLongFieldUpdater.newUpdater(QuicheQuicChannel.class, "uniStreamsLeft");
        BIDI_STREAMS_LEFT_UPDATER = AtomicLongFieldUpdater.newUpdater(QuicheQuicChannel.class, "bidiStreamsLeft");
    }
    
    enum StreamRecvResult
    {
        DONE, 
        FIN, 
        OK;
    }
    
    private enum ChannelState
    {
        OPEN, 
        ACTIVE, 
        CLOSED;
    }
    
    private enum SendResult
    {
        SOME, 
        NONE, 
        CLOSE;
    }
    
    private static final class CloseData implements ChannelFutureListener
    {
        final boolean applicationClose;
        final int err;
        final ByteBuf reason;
        
        CloseData(final boolean applicationClose, final int err, final ByteBuf reason) {
            this.applicationClose = applicationClose;
            this.err = err;
            this.reason = reason;
        }
        
        @Override
        public void operationComplete(final ChannelFuture future) {
            this.reason.release();
        }
    }
    
    private final class QuicChannelUnsafe extends AbstractUnsafe
    {
        void connectStream(final QuicStreamType type, @Nullable final ChannelHandler handler, final Promise<QuicStreamChannel> promise) {
            if (!promise.setUncancellable()) {
                return;
            }
            final long streamId = QuicheQuicChannel.this.idGenerator.nextStreamId(type == QuicStreamType.BIDIRECTIONAL);
            try {
                final int res = QuicheQuicChannel.this.streamSend0(QuicheQuicChannel.this.connection, streamId, Unpooled.EMPTY_BUFFER, false);
                if (res < 0 && res != Quiche.QUICHE_ERR_DONE) {
                    throw Quiche.convertToException(res);
                }
            }
            catch (final Exception e) {
                promise.setFailure(e);
                return;
            }
            if (type == QuicStreamType.UNIDIRECTIONAL) {
                QuicheQuicChannel.UNI_STREAMS_LEFT_UPDATER.decrementAndGet(QuicheQuicChannel.this);
            }
            else {
                QuicheQuicChannel.BIDI_STREAMS_LEFT_UPDATER.decrementAndGet(QuicheQuicChannel.this);
            }
            final QuicheQuicStreamChannel streamChannel = this.addNewStreamChannel(streamId);
            if (handler != null) {
                streamChannel.pipeline().addLast(handler);
            }
            QuicheQuicChannel.this.eventLoop().register(streamChannel).addListener(f -> {
                if (f.isSuccess()) {
                    promise.setSuccess(streamChannel);
                }
                else {
                    promise.setFailure(f.cause());
                    QuicheQuicChannel.this.streams.remove(streamId);
                }
            });
        }
        
        @Override
        public void connect(final SocketAddress remote, final SocketAddress local, final ChannelPromise channelPromise) {
            assert QuicheQuicChannel.this.eventLoop().inEventLoop();
            if (!channelPromise.setUncancellable()) {
                return;
            }
            if (QuicheQuicChannel.this.server) {
                channelPromise.setFailure((Throwable)new UnsupportedOperationException());
                return;
            }
            if (QuicheQuicChannel.this.connectPromise != null) {
                channelPromise.setFailure((Throwable)new ConnectionPendingException());
                return;
            }
            if (!(remote instanceof QuicConnectionAddress)) {
                channelPromise.setFailure((Throwable)new UnsupportedOperationException());
                return;
            }
            if (!QuicheQuicChannel.this.sourceConnectionIds.isEmpty()) {
                channelPromise.setFailure((Throwable)new AlreadyConnectedException());
                return;
            }
            QuicheQuicChannel.this.connectAddress = (QuicConnectionAddress)remote;
            QuicheQuicChannel.this.connectPromise = channelPromise;
            final int connectTimeoutMillis = QuicheQuicChannel.this.config().getConnectTimeoutMillis();
            if (connectTimeoutMillis > 0) {
                QuicheQuicChannel.this.connectTimeoutFuture = QuicheQuicChannel.this.eventLoop().schedule(() -> {
                    final ChannelPromise connectPromise = QuicheQuicChannel.this.connectPromise;
                    if (connectPromise != null && !connectPromise.isDone()) {
                        new ConnectTimeoutException("connection timed out: " + remote);
                        final ConnectTimeoutException ex;
                        final Object o;
                        if (((Promise)o).tryFailure(ex)) {
                            this.close(this.voidPromise());
                        }
                    }
                    return;
                }, (long)connectTimeoutMillis, TimeUnit.MILLISECONDS);
            }
            QuicheQuicChannel.this.connectPromise.addListener(future -> {
                if (future.isCancelled()) {
                    if (QuicheQuicChannel.this.connectTimeoutFuture != null) {
                        QuicheQuicChannel.this.connectTimeoutFuture.cancel(false);
                    }
                    QuicheQuicChannel.this.connectPromise = null;
                    this.close(this.voidPromise());
                }
                return;
            });
            QuicheQuicChannel.this.parent().connect(new QuicheQuicChannelAddress(QuicheQuicChannel.this)).addListener(f -> {
                final ChannelPromise connectPromise2 = QuicheQuicChannel.this.connectPromise;
                if (connectPromise2 != null && !f.isSuccess()) {
                    connectPromise2.tryFailure(f.cause());
                    QuicheQuicChannel.this.unsafe().closeForcibly();
                }
            });
        }
        
        private void fireConnectCloseEventIfNeeded(final QuicheQuicConnection conn) {
            if (QuicheQuicChannel.this.connectionCloseEvent == null && !conn.isFreed()) {
                QuicheQuicChannel.this.connectionCloseEvent = Quiche.quiche_conn_peer_error(conn.address());
                if (QuicheQuicChannel.this.connectionCloseEvent != null) {
                    QuicheQuicChannel.this.pipeline().fireUserEventTriggered((Object)QuicheQuicChannel.this.connectionCloseEvent);
                }
            }
        }
        
        void connectionRecv(final InetSocketAddress sender, final InetSocketAddress recipient, ByteBuf buffer) {
            final QuicheQuicConnection conn = QuicheQuicChannel.this.connection;
            if (conn.isFreed()) {
                return;
            }
            int bufferReadable = buffer.readableBytes();
            if (bufferReadable == 0) {
                return;
            }
            QuicheQuicChannel.this.reantranceGuard |= 0x2;
            boolean close = false;
            try {
                ByteBuf tmpBuffer = null;
                if (buffer.isReadOnly()) {
                    tmpBuffer = QuicheQuicChannel.this.alloc().directBuffer(buffer.readableBytes());
                    tmpBuffer.writeBytes(buffer);
                    buffer = tmpBuffer;
                }
                long memoryAddress = Quiche.readerMemoryAddress(buffer);
                final ByteBuffer recvInfo = conn.nextRecvInfo();
                QuicheRecvInfo.setRecvInfo(recvInfo, sender, recipient);
                QuicheQuicChannel.this.remote = sender;
                QuicheQuicChannel.this.local = recipient;
                try {
                    do {
                        final int res = Quiche.quiche_conn_recv(conn.address(), memoryAddress, bufferReadable, Quiche.memoryAddressWithPosition(recvInfo));
                        boolean done;
                        if (res < 0) {
                            done = true;
                            if (res != Quiche.QUICHE_ERR_DONE) {
                                close = Quiche.shouldClose(res);
                                final Exception e = Quiche.convertToException(res);
                                if (QuicheQuicChannel.this.tryFailConnectPromise(e)) {
                                    break;
                                }
                                QuicheQuicChannel.this.fireExceptionEvents(conn, e);
                            }
                        }
                        else {
                            done = false;
                        }
                        Runnable task = conn.sslTask();
                        if (task != null) {
                            if (QuicheQuicChannel.this.runTasksDirectly()) {
                                do {
                                    task.run();
                                } while ((task = conn.sslTask()) != null);
                                this.processReceived(conn);
                            }
                            else {
                                this.runAllTaskRecv(conn, task);
                            }
                        }
                        else {
                            this.processReceived(conn);
                        }
                        if (done) {
                            break;
                        }
                        memoryAddress += res;
                        bufferReadable -= res;
                    } while (bufferReadable > 0 && !conn.isFreed());
                }
                finally {
                    buffer.skipBytes((int)(memoryAddress - Quiche.readerMemoryAddress(buffer)));
                    if (tmpBuffer != null) {
                        tmpBuffer.release();
                    }
                }
                if (close) {
                    QuicheQuicChannel.this.unsafe().close(QuicheQuicChannel.this.newPromise());
                }
            }
            finally {
                QuicheQuicChannel.this.reantranceGuard &= 0xFFFFFFFD;
            }
        }
        
        private void processReceived(final QuicheQuicConnection conn) {
            if (this.handlePendingChannelActive(conn)) {
                return;
            }
            QuicheQuicChannel.this.notifyAboutHandshakeCompletionIfNeeded(conn, null);
            this.fireConnectCloseEventIfNeeded(conn);
            if (conn.isFreed()) {
                return;
            }
            final long connAddr = conn.address();
            if (Quiche.quiche_conn_is_established(connAddr) || Quiche.quiche_conn_is_in_early_data(connAddr)) {
                final long uniLeftOld = QuicheQuicChannel.this.uniStreamsLeft;
                final long bidiLeftOld = QuicheQuicChannel.this.bidiStreamsLeft;
                if (uniLeftOld == 0L || bidiLeftOld == 0L) {
                    final long uniLeft = Quiche.quiche_conn_peer_streams_left_uni(connAddr);
                    final long bidiLeft = Quiche.quiche_conn_peer_streams_left_bidi(connAddr);
                    QuicheQuicChannel.this.uniStreamsLeft = uniLeft;
                    QuicheQuicChannel.this.bidiStreamsLeft = bidiLeft;
                    if (uniLeftOld != uniLeft || bidiLeftOld != bidiLeft) {
                        QuicheQuicChannel.this.pipeline().fireUserEventTriggered((Object)QuicStreamLimitChangedEvent.INSTANCE);
                    }
                }
                this.handlePathEvents(conn);
                if (QuicheQuicChannel.this.handleWritableStreams(conn)) {
                    QuicheQuicChannel.this.flushParent();
                }
                QuicheQuicChannel.this.datagramReadable = true;
                QuicheQuicChannel.this.streamReadable = true;
                this.recvDatagram(conn);
                this.recvStream(conn);
            }
        }
        
        private void handlePathEvents(final QuicheQuicConnection conn) {
            long event;
            while (!conn.isFreed() && (event = Quiche.quiche_conn_path_event_next(conn.address())) > 0L) {
                try {
                    final int type = Quiche.quiche_path_event_type(event);
                    if (type == Quiche.QUICHE_PATH_EVENT_NEW) {
                        final Object[] ret = Quiche.quiche_path_event_new(event);
                        final InetSocketAddress local = (InetSocketAddress)ret[0];
                        final InetSocketAddress peer = (InetSocketAddress)ret[1];
                        QuicheQuicChannel.this.pipeline().fireUserEventTriggered((Object)new QuicPathEvent.New(local, peer));
                    }
                    else if (type == Quiche.QUICHE_PATH_EVENT_VALIDATED) {
                        final Object[] ret = Quiche.quiche_path_event_validated(event);
                        final InetSocketAddress local = (InetSocketAddress)ret[0];
                        final InetSocketAddress peer = (InetSocketAddress)ret[1];
                        QuicheQuicChannel.this.pipeline().fireUserEventTriggered((Object)new QuicPathEvent.Validated(local, peer));
                    }
                    else if (type == Quiche.QUICHE_PATH_EVENT_FAILED_VALIDATION) {
                        final Object[] ret = Quiche.quiche_path_event_failed_validation(event);
                        final InetSocketAddress local = (InetSocketAddress)ret[0];
                        final InetSocketAddress peer = (InetSocketAddress)ret[1];
                        QuicheQuicChannel.this.pipeline().fireUserEventTriggered((Object)new QuicPathEvent.FailedValidation(local, peer));
                    }
                    else if (type == Quiche.QUICHE_PATH_EVENT_CLOSED) {
                        final Object[] ret = Quiche.quiche_path_event_closed(event);
                        final InetSocketAddress local = (InetSocketAddress)ret[0];
                        final InetSocketAddress peer = (InetSocketAddress)ret[1];
                        QuicheQuicChannel.this.pipeline().fireUserEventTriggered((Object)new QuicPathEvent.Closed(local, peer));
                    }
                    else if (type == Quiche.QUICHE_PATH_EVENT_REUSED_SOURCE_CONNECTION_ID) {
                        final Object[] ret = Quiche.quiche_path_event_reused_source_connection_id(event);
                        final Long seq = (Long)ret[0];
                        final InetSocketAddress localOld = (InetSocketAddress)ret[1];
                        final InetSocketAddress peerOld = (InetSocketAddress)ret[2];
                        final InetSocketAddress local2 = (InetSocketAddress)ret[3];
                        final InetSocketAddress peer2 = (InetSocketAddress)ret[4];
                        QuicheQuicChannel.this.pipeline().fireUserEventTriggered((Object)new QuicPathEvent.ReusedSourceConnectionId(seq, localOld, peerOld, local2, peer2));
                    }
                    else {
                        if (type != Quiche.QUICHE_PATH_EVENT_PEER_MIGRATED) {
                            continue;
                        }
                        final Object[] ret = Quiche.quiche_path_event_peer_migrated(event);
                        final InetSocketAddress local = (InetSocketAddress)ret[0];
                        final InetSocketAddress peer = (InetSocketAddress)ret[1];
                        QuicheQuicChannel.this.pipeline().fireUserEventTriggered((Object)new QuicPathEvent.PeerMigrated(local, peer));
                    }
                }
                finally {
                    Quiche.quiche_path_event_free(event);
                }
            }
        }
        
        private void runAllTaskRecv(final QuicheQuicConnection conn, final Runnable task) {
            QuicheQuicChannel.this.sslTaskExecutor.execute(this.decorateTaskRecv(conn, task));
        }
        
        private Runnable decorateTaskRecv(final QuicheQuicConnection conn, final Runnable task) {
            return () -> {
                try {
                    QuicheQuicChannel.this.runAll(conn, task);
                }
                finally {
                    QuicheQuicChannel.this.eventLoop().execute(() -> {
                        if (!conn.isFreed()) {
                            this.processReceived(conn);
                            if (QuicheQuicChannel.this.connectionSend(conn) != SendResult.NONE) {
                                QuicheQuicChannel.this.forceFlushParent();
                            }
                            QuicheQuicChannel.this.freeIfClosed();
                        }
                    });
                }
            };
        }
        
        void recv() {
            final QuicheQuicConnection conn = QuicheQuicChannel.this.connection;
            if ((QuicheQuicChannel.this.reantranceGuard & 0x2) != 0x0 || conn.isFreed()) {
                return;
            }
            final long connAddr = conn.address();
            if (!Quiche.quiche_conn_is_established(connAddr) && !Quiche.quiche_conn_is_in_early_data(connAddr)) {
                return;
            }
            QuicheQuicChannel.this.reantranceGuard |= 0x2;
            try {
                this.recvDatagram(conn);
                this.recvStream(conn);
            }
            finally {
                QuicheQuicChannel.this.fireChannelReadCompleteIfNeeded();
                QuicheQuicChannel.this.reantranceGuard &= 0xFFFFFFFD;
            }
        }
        
        private void recvStream(final QuicheQuicConnection conn) {
            if (conn.isFreed()) {
                return;
            }
            final long connAddr = conn.address();
            final long readableIterator = Quiche.quiche_conn_readable(connAddr);
            int totalReadable = 0;
            if (readableIterator != -1L) {
                try {
                    if (QuicheQuicChannel.this.recvStreamPending && QuicheQuicChannel.this.streamReadable) {
                        while (true) {
                            final int readable = Quiche.quiche_stream_iter_next(readableIterator, QuicheQuicChannel.this.readableStreams);
                            for (int i = 0; i < readable; ++i) {
                                final long streamId = QuicheQuicChannel.this.readableStreams[i];
                                QuicheQuicStreamChannel streamChannel = QuicheQuicChannel.this.streams.get(streamId);
                                if (streamChannel == null) {
                                    QuicheQuicChannel.this.recvStreamPending = false;
                                    QuicheQuicChannel.this.fireChannelReadCompletePending = true;
                                    streamChannel = this.addNewStreamChannel(streamId);
                                    streamChannel.readable();
                                    QuicheQuicChannel.this.pipeline().fireChannelRead((Object)streamChannel);
                                }
                                else {
                                    streamChannel.readable();
                                }
                            }
                            if (readable < QuicheQuicChannel.this.readableStreams.length) {
                                break;
                            }
                            if (readable <= 0) {
                                continue;
                            }
                            totalReadable += readable;
                        }
                        QuicheQuicChannel.this.streamReadable = false;
                    }
                }
                finally {
                    Quiche.quiche_stream_iter_free(readableIterator);
                }
                QuicheQuicChannel.this.readableStreams = growIfNeeded(QuicheQuicChannel.this.readableStreams, totalReadable);
            }
        }
        
        private void recvDatagram(final QuicheQuicConnection conn) {
            if (!QuicheQuicChannel.this.supportsDatagram) {
                return;
            }
            while (QuicheQuicChannel.this.recvDatagramPending && QuicheQuicChannel.this.datagramReadable && !conn.isFreed()) {
                final RecvByteBufAllocator.Handle recvHandle = this.recvBufAllocHandle();
                recvHandle.reset(QuicheQuicChannel.this.config());
                int numMessagesRead = 0;
                do {
                    final long connAddr = conn.address();
                    final int len = Quiche.quiche_conn_dgram_recv_front_len(connAddr);
                    if (len == Quiche.QUICHE_ERR_DONE) {
                        QuicheQuicChannel.this.datagramReadable = false;
                        return;
                    }
                    final ByteBuf datagramBuffer = QuicheQuicChannel.this.alloc().directBuffer(len);
                    recvHandle.attemptedBytesRead(datagramBuffer.writableBytes());
                    final int writerIndex = datagramBuffer.writerIndex();
                    final long memoryAddress = Quiche.writerMemoryAddress(datagramBuffer);
                    final int written = Quiche.quiche_conn_dgram_recv(connAddr, memoryAddress, datagramBuffer.writableBytes());
                    if (written < 0) {
                        datagramBuffer.release();
                        if (written == Quiche.QUICHE_ERR_DONE) {
                            QuicheQuicChannel.this.datagramReadable = false;
                            break;
                        }
                        QuicheQuicChannel.this.pipeline().fireExceptionCaught((Throwable)Quiche.convertToException(written));
                    }
                    recvHandle.lastBytesRead(written);
                    recvHandle.incMessagesRead(1);
                    ++numMessagesRead;
                    datagramBuffer.writerIndex(writerIndex + written);
                    QuicheQuicChannel.this.recvDatagramPending = false;
                    QuicheQuicChannel.this.fireChannelReadCompletePending = true;
                    QuicheQuicChannel.this.pipeline().fireChannelRead((Object)datagramBuffer);
                } while (recvHandle.continueReading() && !conn.isFreed());
                recvHandle.readComplete();
                if (numMessagesRead > 0) {
                    QuicheQuicChannel.this.fireChannelReadCompleteIfNeeded();
                }
            }
        }
        
        private boolean handlePendingChannelActive(final QuicheQuicConnection conn) {
            if (conn.isFreed() || QuicheQuicChannel.this.state == ChannelState.CLOSED) {
                return true;
            }
            if (QuicheQuicChannel.this.server) {
                if (QuicheQuicChannel.this.state == ChannelState.OPEN && Quiche.quiche_conn_is_established(conn.address())) {
                    QuicheQuicChannel.this.state = ChannelState.ACTIVE;
                    QuicheQuicChannel.this.pipeline().fireChannelActive();
                    QuicheQuicChannel.this.notifyAboutHandshakeCompletionIfNeeded(conn, null);
                    this.fireDatagramExtensionEvent(conn);
                }
            }
            else if (QuicheQuicChannel.this.connectPromise != null && Quiche.quiche_conn_is_established(conn.address())) {
                final ChannelPromise promise = QuicheQuicChannel.this.connectPromise;
                QuicheQuicChannel.this.connectPromise = null;
                QuicheQuicChannel.this.state = ChannelState.ACTIVE;
                final boolean promiseSet = promise.trySuccess();
                QuicheQuicChannel.this.pipeline().fireChannelActive();
                QuicheQuicChannel.this.notifyAboutHandshakeCompletionIfNeeded(conn, null);
                this.fireDatagramExtensionEvent(conn);
                if (!promiseSet) {
                    this.fireConnectCloseEventIfNeeded(conn);
                    this.close(this.voidPromise());
                    return true;
                }
            }
            return false;
        }
        
        private void fireDatagramExtensionEvent(final QuicheQuicConnection conn) {
            if (conn.isClosed()) {
                return;
            }
            final long connAddr = conn.address();
            final int len = Quiche.quiche_conn_dgram_max_writable_len(connAddr);
            if (len != Quiche.QUICHE_ERR_DONE) {
                QuicheQuicChannel.this.pipeline().fireUserEventTriggered((Object)new QuicDatagramExtensionEvent(len));
            }
        }
        
        private QuicheQuicStreamChannel addNewStreamChannel(final long streamId) {
            final QuicheQuicStreamChannel streamChannel = new QuicheQuicStreamChannel(QuicheQuicChannel.this, streamId);
            final QuicheQuicStreamChannel old = QuicheQuicChannel.this.streams.put(streamId, streamChannel);
            assert old == null;
            streamChannel.writable(QuicheQuicChannel.this.streamCapacity(streamId));
            return streamChannel;
        }
    }
    
    private final class TimeoutHandler implements Runnable
    {
        private ScheduledFuture<?> timeoutFuture;
        
        @Override
        public void run() {
            final QuicheQuicConnection conn = QuicheQuicChannel.this.connection;
            if (conn.isFreed()) {
                return;
            }
            if (!QuicheQuicChannel.this.freeIfClosed()) {
                final long connAddr = conn.address();
                this.timeoutFuture = null;
                Quiche.quiche_conn_on_timeout(connAddr);
                if (!QuicheQuicChannel.this.freeIfClosed()) {
                    if (QuicheQuicChannel.this.connectionSend(conn) != SendResult.NONE) {
                        QuicheQuicChannel.this.flushParent();
                    }
                    final boolean closed = QuicheQuicChannel.this.freeIfClosed();
                    if (!closed) {
                        this.scheduleTimeout();
                    }
                }
            }
        }
        
        void scheduleTimeout() {
            final QuicheQuicConnection conn = QuicheQuicChannel.this.connection;
            if (conn.isFreed()) {
                this.cancel();
                return;
            }
            if (conn.isClosed()) {
                this.cancel();
                QuicheQuicChannel.this.unsafe().close(QuicheQuicChannel.this.newPromise());
                return;
            }
            final long nanos = Quiche.quiche_conn_timeout_as_nanos(conn.address());
            if (nanos < 0L || nanos == Long.MAX_VALUE) {
                this.cancel();
                return;
            }
            if (this.timeoutFuture == null) {
                this.timeoutFuture = QuicheQuicChannel.this.eventLoop().schedule((Runnable)this, nanos, TimeUnit.NANOSECONDS);
            }
            else {
                final long remaining = this.timeoutFuture.getDelay(TimeUnit.NANOSECONDS);
                if (remaining <= 0L) {
                    this.cancel();
                    this.run();
                }
                else if (remaining > nanos) {
                    this.cancel();
                    this.timeoutFuture = QuicheQuicChannel.this.eventLoop().schedule((Runnable)this, nanos, TimeUnit.NANOSECONDS);
                }
            }
        }
        
        void cancel() {
            if (this.timeoutFuture != null) {
                this.timeoutFuture.cancel(false);
                this.timeoutFuture = null;
            }
        }
    }
}
