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

package com.hypixel.hytale.server.core.io.netty;

import io.netty.util.AttributeKey;
import io.netty.channel.ReflectiveChannelFactory;
import io.netty.channel.ChannelException;
import io.netty.util.internal.StringUtil;
import io.netty.util.internal.ObjectUtil;
import java.lang.reflect.Constructor;
import io.netty.channel.ChannelFactory;
import io.netty.handler.logging.LogLevel;
import com.hypixel.hytale.logger.backend.HytaleLoggerBackend;
import java.util.Iterator;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.Universe;
import java.util.logging.Level;
import java.net.InetSocketAddress;
import java.util.Objects;
import javax.annotation.Nullable;
import java.net.SocketAddress;
import io.netty.handler.codec.quic.QuicStreamChannel;
import io.netty.handler.codec.quic.QuicChannel;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.kqueue.KQueueDatagramChannel;
import io.netty.channel.epoll.EpollDatagramChannel;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.SocketProtocolFamily;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.kqueue.KQueueServerSocketChannel;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.ServerChannel;
import java.util.concurrent.ThreadFactory;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.kqueue.KQueue;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.Epoll;
import com.hypixel.hytale.server.core.util.concurrent.ThreadUtil;
import io.netty.util.internal.SystemPropertyUtil;
import io.netty.channel.EventLoopGroup;
import com.hypixel.hytale.server.core.io.PacketHandler;
import io.netty.channel.ChannelHandler;
import javax.annotation.Nonnull;
import io.netty.channel.Channel;
import io.netty.handler.logging.LoggingHandler;
import com.hypixel.hytale.logger.HytaleLogger;

public class NettyUtil
{
    public static final HytaleLogger CONNECTION_EXCEPTION_LOGGER;
    public static final HytaleLogger PACKET_LOGGER;
    public static final String PACKET_DECODER = "packetDecoder";
    public static final String PACKET_ARRAY_ENCODER = "packetArrayEncoder";
    public static final PacketArrayEncoder PACKET_ARRAY_ENCODER_INSTANCE;
    public static final String PACKET_ENCODER = "packetEncoder";
    public static final String LOGGER_KEY = "logger";
    public static final LoggingHandler LOGGER;
    public static final String HANDLER = "handler";
    public static final String RATE_LIMIT = "rateLimit";
    
    public static void init() {
    }
    
    private static void injectLogger(@Nonnull final Channel channel) {
        if (channel.pipeline().get("logger") == null) {
            channel.pipeline().addAfter("packetArrayEncoder", "logger", NettyUtil.LOGGER);
        }
    }
    
    private static void uninjectLogger(@Nonnull final Channel channel) {
        channel.pipeline().remove("logger");
    }
    
    public static void setChannelHandler(@Nonnull final Channel channel, @Nonnull final PacketHandler packetHandler) {
        final ChannelHandler oldHandler = channel.pipeline().replace("handler", "handler", new PlayerChannelHandler(packetHandler));
        PacketHandler oldPlayerConnection = null;
        if (oldHandler instanceof final PlayerChannelHandler playerChannelHandler) {
            oldPlayerConnection = playerChannelHandler.getHandler();
            oldPlayerConnection.unregistered(packetHandler);
        }
        packetHandler.registered(oldPlayerConnection);
    }
    
    @Nonnull
    public static EventLoopGroup getEventLoopGroup(final String name) {
        return getEventLoopGroup(0, name);
    }
    
    @Nonnull
    public static EventLoopGroup getEventLoopGroup(int nThreads, final String name) {
        if (nThreads == 0) {
            nThreads = Math.max(1, SystemPropertyUtil.getInt("server.io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));
        }
        final ThreadFactory factory = ThreadUtil.daemonCounted(name + " - %d");
        if (Epoll.isAvailable()) {
            return new EpollEventLoopGroup(nThreads, factory);
        }
        if (KQueue.isAvailable()) {
            return new KQueueEventLoopGroup(nThreads, factory);
        }
        return new NioEventLoopGroup(nThreads, factory);
    }
    
    @Nonnull
    public static Class<? extends ServerChannel> getServerChannel() {
        if (Epoll.isAvailable()) {
            return EpollServerSocketChannel.class;
        }
        if (KQueue.isAvailable()) {
            return KQueueServerSocketChannel.class;
        }
        return NioServerSocketChannel.class;
    }
    
    @Nonnull
    public static ReflectiveChannelFactory<? extends DatagramChannel> getDatagramChannelFactory(final SocketProtocolFamily family) {
        if (Epoll.isAvailable()) {
            return new ReflectiveChannelFactory<DatagramChannel>(EpollDatagramChannel.class, family);
        }
        if (KQueue.isAvailable()) {
            return new ReflectiveChannelFactory<DatagramChannel>(KQueueDatagramChannel.class, family);
        }
        return new ReflectiveChannelFactory<DatagramChannel>(NioDatagramChannel.class, family);
    }
    
    public static String formatRemoteAddress(final Channel channel) {
        if (channel instanceof final QuicChannel quicChannel) {
            return String.valueOf(quicChannel.remoteAddress()) + " (" + String.valueOf(quicChannel.remoteSocketAddress());
        }
        if (channel instanceof final QuicStreamChannel quicStreamChannel) {
            return String.valueOf(quicStreamChannel.parent().localAddress()) + " (" + String.valueOf(quicStreamChannel.parent().remoteSocketAddress()) + ", streamId=" + quicStreamChannel.remoteAddress().streamId();
        }
        return channel.remoteAddress().toString();
    }
    
    public static String formatLocalAddress(final Channel channel) {
        if (channel instanceof final QuicChannel quicChannel) {
            return String.valueOf(quicChannel.localAddress()) + " (" + String.valueOf(quicChannel.localSocketAddress());
        }
        if (channel instanceof final QuicStreamChannel quicStreamChannel) {
            return String.valueOf(quicStreamChannel.parent().localAddress()) + " (" + String.valueOf(quicStreamChannel.parent().localSocketAddress()) + ", streamId=" + quicStreamChannel.localAddress().streamId();
        }
        return channel.localAddress().toString();
    }
    
    @Nullable
    public static SocketAddress getRemoteSocketAddress(final Channel channel) {
        if (channel instanceof final QuicChannel quicChannel) {
            return quicChannel.remoteSocketAddress();
        }
        if (channel instanceof final QuicStreamChannel quicStreamChannel) {
            return quicStreamChannel.parent().remoteSocketAddress();
        }
        return channel.remoteAddress();
    }
    
    public static boolean isFromSameOrigin(final Channel channel1, final Channel channel2) {
        final SocketAddress remoteSocketAddress1 = getRemoteSocketAddress(channel1);
        final SocketAddress remoteSocketAddress2 = getRemoteSocketAddress(channel2);
        if (remoteSocketAddress1 == null || remoteSocketAddress2 == null) {
            return false;
        }
        if (Objects.equals(remoteSocketAddress1, remoteSocketAddress2)) {
            return true;
        }
        if (!remoteSocketAddress1.getClass().equals(remoteSocketAddress2.getClass())) {
            return false;
        }
        if (remoteSocketAddress1 instanceof final InetSocketAddress remoteInetSocketAddress1) {
            if (remoteSocketAddress2 instanceof final InetSocketAddress remoteInetSocketAddress2) {
                return (remoteInetSocketAddress1.getAddress().isLoopbackAddress() && remoteInetSocketAddress2.getAddress().isLoopbackAddress()) || remoteInetSocketAddress1.getAddress().equals(remoteInetSocketAddress2.getAddress());
            }
        }
        return false;
    }
    
    static {
        CONNECTION_EXCEPTION_LOGGER = HytaleLogger.get("ConnectionExceptionLogging");
        PACKET_LOGGER = HytaleLogger.get("PacketLogging");
        final HytaleLoggerBackend loggerBackend = HytaleLoggerBackend.getLogger(NettyUtil.PACKET_LOGGER.getName());
        loggerBackend.setOnLevelChange((oldLevel, newLevel) -> {
            final Universe universe = Universe.get();
            if (universe != null) {
                if (newLevel == Level.OFF) {
                    for (final PlayerRef p2 : universe.getPlayers()) {
                        uninjectLogger(p2.getPacketHandler().getChannel());
                    }
                }
                else {
                    for (final PlayerRef p3 : universe.getPlayers()) {
                        injectLogger(p3.getPacketHandler().getChannel());
                    }
                }
            }
            return;
        });
        NettyUtil.PACKET_LOGGER.setLevel(Level.OFF);
        loggerBackend.loadLogLevel();
        NettyUtil.CONNECTION_EXCEPTION_LOGGER.setLevel(Level.ALL);
        PACKET_ARRAY_ENCODER_INSTANCE = new PacketArrayEncoder();
        LOGGER = new LoggingHandler("PacketLogging", LogLevel.INFO);
    }
    
    public static class ReflectiveChannelFactory<T extends Channel> implements ChannelFactory<T>
    {
        @Nonnull
        private final Constructor<? extends T> constructor;
        private final SocketProtocolFamily family;
        
        public ReflectiveChannelFactory(@Nonnull final Class<? extends T> clazz, final SocketProtocolFamily family) {
            ObjectUtil.checkNotNull(clazz, "clazz");
            try {
                this.constructor = clazz.getConstructor(SocketProtocolFamily.class);
                this.family = family;
            }
            catch (final NoSuchMethodException e) {
                throw new IllegalArgumentException("Class " + StringUtil.simpleClassName(clazz) + " does not have a public non-arg constructor", (Throwable)e);
            }
        }
        
        @Nonnull
        @Override
        public T newChannel() {
            try {
                return (T)this.constructor.newInstance(this.family);
            }
            catch (final Throwable t) {
                throw new ChannelException("Unable to create Channel from class " + String.valueOf(this.constructor.getDeclaringClass()), t);
            }
        }
        
        @Nonnull
        public String getSimpleName() {
            return StringUtil.simpleClassName(this.constructor.getDeclaringClass()) + "(" + String.valueOf(this.family);
        }
        
        @Nonnull
        @Override
        public String toString() {
            return StringUtil.simpleClassName(io.netty.channel.ReflectiveChannelFactory.class) + "(" + StringUtil.simpleClassName(this.constructor.getDeclaringClass()) + ".class, " + String.valueOf(this.family);
        }
    }
    
    record TimeoutContext(@Nonnull String stage, long connectionStartNs, @Nonnull String playerIdentifier) {
        public static final AttributeKey<TimeoutContext> KEY;
        
        public static void init(@Nonnull final Channel channel, @Nonnull final String stage, @Nonnull final String identifier) {
            channel.attr(TimeoutContext.KEY).set(new TimeoutContext(stage, System.nanoTime(), identifier));
        }
        
        public static void update(@Nonnull final Channel channel, @Nonnull final String stage, @Nonnull final String identifier) {
            final TimeoutContext existing = get(channel);
            channel.attr(TimeoutContext.KEY).set(new TimeoutContext(stage, existing.connectionStartNs, identifier));
        }
        
        public static void update(@Nonnull final Channel channel, @Nonnull final String stage) {
            final TimeoutContext existing = get(channel);
            channel.attr(TimeoutContext.KEY).set(new TimeoutContext(stage, existing.connectionStartNs, existing.playerIdentifier));
        }
        
        @Nonnull
        public static TimeoutContext get(@Nonnull final Channel channel) {
            final TimeoutContext context = channel.attr(TimeoutContext.KEY).get();
            if (context == null) {
                throw new IllegalStateException("TimeoutContext not initialized - this indicates a bug in the connection flow");
            }
            return context;
        }
        
        @Nonnull
        public String stage() {
            return this.stage;
        }
        
        @Nonnull
        public String playerIdentifier() {
            return this.playerIdentifier;
        }
        
        static {
            KEY = AttributeKey.newInstance("TIMEOUT_CONTEXT");
        }
    }
}
