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

package io.netty.handler.codec.quic;

import io.netty.channel.DefaultAddressedEnvelope;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.net.SocketAddress;
import io.netty.channel.ChannelPromise;
import java.net.InetSocketAddress;
import io.netty.channel.socket.DatagramPacket;
import io.netty.channel.ChannelHandlerContext;
import java.util.Iterator;
import org.jetbrains.annotations.Nullable;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.HashSet;
import io.netty.buffer.ByteBuf;
import io.netty.channel.MessageSizeEstimator;
import java.util.function.Consumer;
import java.util.Queue;
import java.util.Set;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.channel.ChannelDuplexHandler;

abstract class QuicheQuicCodec extends ChannelDuplexHandler
{
    private static final InternalLogger LOGGER;
    private final ConnectionIdChannelMap connectionIdToChannel;
    private final Set<QuicheQuicChannel> channels;
    private final Queue<QuicheQuicChannel> needsFireChannelReadComplete;
    private final Queue<QuicheQuicChannel> delayedRemoval;
    private final Consumer<QuicheQuicChannel> freeTask;
    private final FlushStrategy flushStrategy;
    private final int localConnIdLength;
    private final QuicheConfig config;
    private MessageSizeEstimator.Handle estimatorHandle;
    private QuicHeaderParser headerParser;
    private QuicHeaderParser.QuicHeaderProcessor parserCallback;
    private int pendingBytes;
    private int pendingPackets;
    private boolean inChannelReadComplete;
    private boolean delayRemoval;
    private ByteBuf senderSockaddrMemory;
    private ByteBuf recipientSockaddrMemory;
    
    QuicheQuicCodec(final QuicheConfig config, final int localConnIdLength, final FlushStrategy flushStrategy) {
        this.connectionIdToChannel = new ConnectionIdChannelMap();
        this.channels = new HashSet<QuicheQuicChannel>();
        this.needsFireChannelReadComplete = new ArrayDeque<QuicheQuicChannel>();
        this.delayedRemoval = new ArrayDeque<QuicheQuicChannel>();
        this.freeTask = this::removeChannel;
        this.config = config;
        this.localConnIdLength = localConnIdLength;
        this.flushStrategy = flushStrategy;
    }
    
    @Override
    public final boolean isSharable() {
        return false;
    }
    
    @Nullable
    protected final QuicheQuicChannel getChannel(final ByteBuffer key) {
        return this.connectionIdToChannel.get(key);
    }
    
    private void addMapping(final QuicheQuicChannel channel, final ByteBuffer id) {
        final QuicheQuicChannel ch = this.connectionIdToChannel.put(id, channel);
        assert ch == null;
    }
    
    private void removeMapping(final QuicheQuicChannel channel, final ByteBuffer id) {
        final QuicheQuicChannel ch = this.connectionIdToChannel.remove(id);
        assert ch == channel;
    }
    
    private void processDelayedRemoval() {
        while (true) {
            final QuicheQuicChannel toBeRemoved = this.delayedRemoval.poll();
            if (toBeRemoved == null) {
                break;
            }
            this.removeChannel(toBeRemoved);
        }
    }
    
    private void removeChannel(final QuicheQuicChannel channel) {
        if (this.delayRemoval) {
            final boolean added = this.delayedRemoval.offer(channel);
            assert added;
        }
        else {
            final boolean removed = this.channels.remove(channel);
            if (removed) {
                for (final ByteBuffer id : channel.sourceConnectionIds()) {
                    final QuicheQuicChannel ch = this.connectionIdToChannel.remove(id);
                    assert ch == channel;
                }
            }
        }
    }
    
    protected final void addChannel(final QuicheQuicChannel channel) {
        final boolean added = this.channels.add(channel);
        assert added;
        for (final ByteBuffer id : channel.sourceConnectionIds()) {
            final QuicheQuicChannel ch = this.connectionIdToChannel.put(id.duplicate(), channel);
            assert ch == null;
        }
    }
    
    @Override
    public final void handlerAdded(final ChannelHandlerContext ctx) {
        this.senderSockaddrMemory = Quiche.allocateNativeOrder(Quiche.SIZEOF_SOCKADDR_STORAGE);
        this.recipientSockaddrMemory = Quiche.allocateNativeOrder(Quiche.SIZEOF_SOCKADDR_STORAGE);
        this.headerParser = new QuicHeaderParser(this.localConnIdLength);
        this.parserCallback = new QuicCodecHeaderProcessor(ctx);
        this.estimatorHandle = ctx.channel().config().getMessageSizeEstimator().newHandle();
        this.handlerAdded(ctx, this.localConnIdLength);
    }
    
    protected void handlerAdded(final ChannelHandlerContext ctx, final int localConnIdLength) {
    }
    
    @Override
    public void handlerRemoved(final ChannelHandlerContext ctx) {
        try {
            for (final QuicheQuicChannel ch : this.channels.toArray(new QuicheQuicChannel[0])) {
                ch.forceClose();
            }
            if (this.pendingPackets > 0) {
                this.flushNow(ctx);
            }
        }
        finally {
            this.channels.clear();
            this.connectionIdToChannel.clear();
            this.needsFireChannelReadComplete.clear();
            this.delayedRemoval.clear();
            this.config.free();
            if (this.senderSockaddrMemory != null) {
                this.senderSockaddrMemory.release();
            }
            if (this.recipientSockaddrMemory != null) {
                this.recipientSockaddrMemory.release();
            }
            if (this.headerParser != null) {
                this.headerParser.close();
                this.headerParser = null;
            }
        }
    }
    
    @Override
    public final void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
        final DatagramPacket packet = (DatagramPacket)msg;
        try {
            final ByteBuf buffer = ((DefaultAddressedEnvelope<ByteBuf, A>)msg).content();
            if (!buffer.isDirect()) {
                final ByteBuf direct = ctx.alloc().directBuffer(buffer.readableBytes());
                try {
                    direct.writeBytes(buffer, buffer.readerIndex(), buffer.readableBytes());
                    this.handleQuicPacket(((DefaultAddressedEnvelope<M, InetSocketAddress>)packet).sender(), ((DefaultAddressedEnvelope<M, InetSocketAddress>)packet).recipient(), direct);
                }
                finally {
                    direct.release();
                }
            }
            else {
                this.handleQuicPacket(((DefaultAddressedEnvelope<M, InetSocketAddress>)packet).sender(), ((DefaultAddressedEnvelope<M, InetSocketAddress>)packet).recipient(), buffer);
            }
        }
        finally {
            packet.release();
        }
    }
    
    private void handleQuicPacket(final InetSocketAddress sender, final InetSocketAddress recipient, final ByteBuf buffer) {
        try {
            this.headerParser.parse(sender, recipient, buffer, this.parserCallback);
        }
        catch (final Exception e) {
            QuicheQuicCodec.LOGGER.debug("Error while processing QUIC packet", e);
        }
    }
    
    @Nullable
    protected abstract QuicheQuicChannel quicPacketRead(final ChannelHandlerContext p0, final InetSocketAddress p1, final InetSocketAddress p2, final QuicPacketType p3, final long p4, final ByteBuf p5, final ByteBuf p6, final ByteBuf p7, final ByteBuf p8, final ByteBuf p9, final Consumer<QuicheQuicChannel> p10, final int p11, final QuicheConfig p12) throws Exception;
    
    @Override
    public final void channelReadComplete(final ChannelHandlerContext ctx) {
        this.inChannelReadComplete = true;
        try {
            while (true) {
                final QuicheQuicChannel channel = this.needsFireChannelReadComplete.poll();
                if (channel == null) {
                    break;
                }
                channel.recvComplete();
            }
        }
        finally {
            this.inChannelReadComplete = false;
            if (this.pendingPackets > 0) {
                this.flushNow(ctx);
            }
        }
    }
    
    @Override
    public final void channelWritabilityChanged(final ChannelHandlerContext ctx) {
        if (ctx.channel().isWritable()) {
            this.delayRemoval = true;
            try {
                for (final QuicheQuicChannel channel : this.channels) {
                    channel.writable();
                }
            }
            finally {
                this.delayRemoval = false;
                this.processDelayedRemoval();
            }
        }
        else {
            ctx.flush();
        }
        ctx.fireChannelWritabilityChanged();
    }
    
    @Override
    public final void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) {
        ++this.pendingPackets;
        final int size = this.estimatorHandle.size(msg);
        if (size > 0) {
            this.pendingBytes += size;
        }
        try {
            ctx.write(msg, promise);
        }
        finally {
            this.flushIfNeeded(ctx);
        }
    }
    
    @Override
    public final void flush(final ChannelHandlerContext ctx) {
        if (this.inChannelReadComplete) {
            this.flushIfNeeded(ctx);
        }
        else if (this.pendingPackets > 0) {
            this.flushNow(ctx);
        }
    }
    
    @Override
    public void connect(final ChannelHandlerContext ctx, final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) throws Exception {
        if (remoteAddress instanceof QuicheQuicChannelAddress) {
            final QuicheQuicChannelAddress addr = (QuicheQuicChannelAddress)remoteAddress;
            final QuicheQuicChannel channel = addr.channel;
            this.connectQuicChannel(channel, remoteAddress, localAddress, this.senderSockaddrMemory, this.recipientSockaddrMemory, this.freeTask, this.localConnIdLength, this.config, promise);
        }
        else {
            ctx.connect(remoteAddress, localAddress, promise);
        }
    }
    
    protected abstract void connectQuicChannel(final QuicheQuicChannel p0, final SocketAddress p1, final SocketAddress p2, final ByteBuf p3, final ByteBuf p4, final Consumer<QuicheQuicChannel> p5, final int p6, final QuicheConfig p7, final ChannelPromise p8);
    
    private void flushIfNeeded(final ChannelHandlerContext ctx) {
        if (this.flushStrategy.shouldFlushNow(this.pendingPackets, this.pendingBytes)) {
            this.flushNow(ctx);
        }
    }
    
    private void flushNow(final ChannelHandlerContext ctx) {
        this.pendingBytes = 0;
        this.pendingPackets = 0;
        ctx.flush();
    }
    
    static {
        LOGGER = InternalLoggerFactory.getInstance(QuicheQuicCodec.class);
    }
    
    private final class QuicCodecHeaderProcessor implements QuicHeaderParser.QuicHeaderProcessor
    {
        private final ChannelHandlerContext ctx;
        
        QuicCodecHeaderProcessor(final ChannelHandlerContext ctx) {
            this.ctx = ctx;
        }
        
        @Override
        public void process(final InetSocketAddress sender, final InetSocketAddress recipient, final ByteBuf buffer, final QuicPacketType type, final long version, final ByteBuf scid, final ByteBuf dcid, final ByteBuf token) throws Exception {
            final QuicheQuicChannel channel = QuicheQuicCodec.this.quicPacketRead(this.ctx, sender, recipient, type, version, scid, dcid, token, QuicheQuicCodec.this.senderSockaddrMemory, QuicheQuicCodec.this.recipientSockaddrMemory, QuicheQuicCodec.this.freeTask, QuicheQuicCodec.this.localConnIdLength, QuicheQuicCodec.this.config);
            if (channel != null) {
                if (channel.markInFireChannelReadCompleteQueue()) {
                    QuicheQuicCodec.this.needsFireChannelReadComplete.add(channel);
                }
                channel.recv(sender, recipient, buffer);
                for (final ByteBuffer retiredSourceConnectionId : channel.retiredSourceConnectionId()) {
                    QuicheQuicCodec.this.removeMapping(channel, retiredSourceConnectionId);
                }
                for (final ByteBuffer newSourceConnectionId : channel.newSourceConnectionIds()) {
                    QuicheQuicCodec.this.addMapping(channel, newSourceConnectionId);
                }
            }
        }
    }
}
