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

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

import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.codecs.EnumCodec;
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
import java.util.concurrent.locks.ReentrantLock;
import com.hypixel.hytale.metrics.metric.Metric;
import com.hypixel.hytale.metrics.metric.HistoricMetric;
import it.unimi.dsi.fastutil.longs.LongPriorityQueue;
import it.unimi.dsi.fastutil.ints.IntPriorityQueue;
import java.util.concurrent.locks.Lock;
import com.hypixel.hytale.metrics.MetricsRegistry;
import io.netty.util.Attribute;
import com.google.common.flogger.LazyArgs;
import java.net.InetAddress;
import java.net.SocketAddress;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.unix.DomainSocketAddress;
import com.hypixel.hytale.common.util.NetworkUtil;
import java.net.InetSocketAddress;
import io.netty.handler.codec.quic.QuicStreamChannel;
import java.util.concurrent.TimeUnit;
import com.hypixel.hytale.common.util.FormatUtil;
import com.hypixel.hytale.server.core.io.handlers.login.PasswordPacketHandler;
import com.hypixel.hytale.server.core.io.handlers.login.AuthenticationPacketHandler;
import java.util.function.BooleanSupplier;
import java.time.Duration;
import com.hypixel.hytale.protocol.packets.connection.Pong;
import com.hypixel.hytale.protocol.packets.connection.Ping;
import com.hypixel.hytale.server.core.modules.time.WorldTimeResource;
import java.time.Instant;
import com.hypixel.hytale.protocol.io.PacketStatsRecorder;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import com.hypixel.hytale.protocol.io.netty.ProtocolUtil;
import com.hypixel.hytale.protocol.packets.connection.Disconnect;
import com.hypixel.hytale.protocol.packets.connection.DisconnectType;
import com.hypixel.hytale.server.core.io.netty.NettyUtil;
import com.hypixel.hytale.protocol.CachedPacket;
import com.hypixel.hytale.server.core.io.adapter.PacketAdapters;
import io.netty.channel.ChannelHandlerContext;
import com.hypixel.hytale.protocol.Packet;
import java.util.logging.Level;
import com.hypixel.hytale.protocol.packets.connection.PongType;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledFuture;
import java.security.SecureRandom;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import com.hypixel.hytale.server.core.auth.PlayerAuthentication;
import javax.annotation.Nonnull;
import io.netty.channel.Channel;
import io.netty.util.AttributeKey;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.receiver.IPacketReceiver;

public abstract class PacketHandler implements IPacketReceiver
{
    public static final int MAX_PACKET_ID = 512;
    private static final HytaleLogger LOGIN_TIMING_LOGGER;
    private static final AttributeKey<Long> LOGIN_START_ATTRIBUTE_KEY;
    @Nonnull
    protected final Channel channel;
    @Nonnull
    protected final ProtocolVersion protocolVersion;
    @Nullable
    protected PlayerAuthentication auth;
    protected boolean queuePackets;
    protected final AtomicInteger queuedPackets;
    protected final SecureRandom pingIdRandom;
    @Nonnull
    protected final PingInfo[] pingInfo;
    private float pingTimer;
    protected boolean registered;
    private ScheduledFuture<?> timeoutTask;
    @Nullable
    protected Throwable clientReadyForChunksFutureStack;
    @Nullable
    protected CompletableFuture<Void> clientReadyForChunksFuture;
    @Nonnull
    protected final DisconnectReason disconnectReason;
    
    public PacketHandler(@Nonnull final Channel channel, @Nonnull final ProtocolVersion protocolVersion) {
        this.queuedPackets = new AtomicInteger();
        this.pingIdRandom = new SecureRandom();
        this.disconnectReason = new DisconnectReason();
        this.channel = channel;
        this.protocolVersion = protocolVersion;
        this.pingInfo = new PingInfo[PongType.VALUES.length];
        for (final PongType pingType : PongType.VALUES) {
            this.pingInfo[pingType.ordinal()] = new PingInfo(pingType);
        }
    }
    
    @Nonnull
    public Channel getChannel() {
        return this.channel;
    }
    
    @Deprecated(forRemoval = true)
    public void setCompressionEnabled(final boolean compressionEnabled) {
        HytaleLogger.getLogger().at(Level.INFO).log(this.getIdentifier() + " compression now handled by encoder");
    }
    
    @Deprecated(forRemoval = true)
    public boolean isCompressionEnabled() {
        return true;
    }
    
    @Nonnull
    public abstract String getIdentifier();
    
    @Nonnull
    public ProtocolVersion getProtocolVersion() {
        return this.protocolVersion;
    }
    
    public final void registered(@Nullable final PacketHandler oldHandler) {
        this.registered = true;
        this.registered0(oldHandler);
    }
    
    protected void registered0(@Nullable final PacketHandler oldHandler) {
    }
    
    public final void unregistered(@Nullable final PacketHandler newHandler) {
        this.registered = false;
        this.clearTimeout();
        this.unregistered0(newHandler);
    }
    
    protected void unregistered0(@Nullable final PacketHandler newHandler) {
    }
    
    public void handle(@Nonnull final Packet packet) {
        this.accept(packet);
    }
    
    public abstract void accept(@Nonnull final Packet p0);
    
    public void logCloseMessage() {
        HytaleLogger.getLogger().at(Level.INFO).log("%s was closed.", this.getIdentifier());
    }
    
    public void closed(final ChannelHandlerContext ctx) {
        this.clearTimeout();
    }
    
    public void setQueuePackets(final boolean queuePackets) {
        this.queuePackets = queuePackets;
    }
    
    public void tryFlush() {
        if (this.queuedPackets.getAndSet(0) > 0) {
            this.channel.flush();
        }
    }
    
    public void write(@Nonnull final Packet... packets) {
        final Packet[] cachedPackets = new Packet[packets.length];
        this.handleOutboundAndCachePackets(packets, cachedPackets);
        if (this.queuePackets) {
            this.channel.write(cachedPackets, this.channel.voidPromise());
            this.queuedPackets.getAndIncrement();
        }
        else {
            this.channel.writeAndFlush(cachedPackets, this.channel.voidPromise());
        }
    }
    
    public void write(@Nonnull final Packet[] packets, @Nonnull final Packet finalPacket) {
        final Packet[] cachedPackets = new Packet[packets.length + 1];
        this.handleOutboundAndCachePackets(packets, cachedPackets);
        cachedPackets[cachedPackets.length - 1] = this.handleOutboundAndCachePacket(finalPacket);
        if (this.queuePackets) {
            this.channel.write(cachedPackets, this.channel.voidPromise());
            this.queuedPackets.getAndIncrement();
        }
        else {
            this.channel.writeAndFlush(cachedPackets, this.channel.voidPromise());
        }
    }
    
    @Override
    public void write(@Nonnull final Packet packet) {
        this.writePacket(packet, true);
    }
    
    @Override
    public void writeNoCache(@Nonnull final Packet packet) {
        this.writePacket(packet, false);
    }
    
    public void writePacket(@Nonnull final Packet packet, final boolean cache) {
        if (PacketAdapters.__handleOutbound(this, packet)) {
            return;
        }
        Packet toSend;
        if (cache) {
            toSend = this.handleOutboundAndCachePacket(packet);
        }
        else {
            toSend = packet;
        }
        if (this.queuePackets) {
            this.channel.write(toSend, this.channel.voidPromise());
            this.queuedPackets.getAndIncrement();
        }
        else {
            this.channel.writeAndFlush(toSend, this.channel.voidPromise());
        }
    }
    
    private void handleOutboundAndCachePackets(@Nonnull final Packet[] packets, @Nonnull final Packet[] cachedPackets) {
        for (int i = 0; i < packets.length; ++i) {
            final Packet packet = packets[i];
            if (!PacketAdapters.__handleOutbound(this, packet)) {
                cachedPackets[i] = this.handleOutboundAndCachePacket(packet);
            }
        }
    }
    
    @Nonnull
    private Packet handleOutboundAndCachePacket(@Nonnull final Packet packet) {
        if (packet instanceof CachedPacket) {
            return packet;
        }
        return CachedPacket.cache(packet);
    }
    
    public void disconnect(@Nonnull final String message) {
        this.disconnectReason.setServerDisconnectReason(message);
        HytaleLogger.getLogger().at(Level.INFO).log("Disconnecting %s with the message: %s", NettyUtil.formatRemoteAddress(this.channel), message);
        this.disconnect0(message);
    }
    
    protected void disconnect0(@Nonnull final String message) {
        this.channel.writeAndFlush(new Disconnect(message, DisconnectType.Disconnect)).addListener((GenericFutureListener<? extends Future<? super Void>>)ProtocolUtil.CLOSE_ON_COMPLETE);
    }
    
    @Nullable
    public PacketStatsRecorder getPacketStatsRecorder() {
        return this.channel.attr(PacketStatsRecorder.CHANNEL_KEY).get();
    }
    
    @Nonnull
    public PingInfo getPingInfo(@Nonnull final PongType pongType) {
        return this.pingInfo[pongType.ordinal()];
    }
    
    public long getOperationTimeoutThreshold() {
        final double average = this.getPingInfo(PongType.Tick).getPingMetricSet().getAverage(0);
        return PingInfo.TIME_UNIT.toMillis(Math.round(average * 2.0)) + 3000L;
    }
    
    public void tickPing(final float dt) {
        this.pingTimer -= dt;
        if (this.pingTimer <= 0.0f) {
            this.pingTimer = 1.0f;
            this.sendPing();
        }
    }
    
    public void sendPing() {
        final int id = this.pingIdRandom.nextInt();
        final Instant nowInstant = Instant.now();
        final long nowTimestamp = System.nanoTime();
        for (final PingInfo info : this.pingInfo) {
            info.recordSent(id, nowTimestamp);
        }
        this.writeNoCache(new Ping(id, WorldTimeResource.instantToInstantData(nowInstant), (int)this.getPingInfo(PongType.Raw).getPingMetricSet().getLastValue(), (int)this.getPingInfo(PongType.Direct).getPingMetricSet().getLastValue(), (int)this.getPingInfo(PongType.Tick).getPingMetricSet().getLastValue()));
    }
    
    public void handlePong(@Nonnull final Pong packet) {
        this.pingInfo[packet.type.ordinal()].handlePacket(packet);
    }
    
    protected void initStage(@Nonnull final String stage, @Nonnull final Duration timeout, @Nonnull final BooleanSupplier condition) {
        NettyUtil.TimeoutContext.init(this.channel, stage, this.getIdentifier());
        this.setStageTimeout(stage, timeout, condition);
    }
    
    protected void enterStage(@Nonnull final String stage, @Nonnull final Duration timeout, @Nonnull final BooleanSupplier condition) {
        NettyUtil.TimeoutContext.update(this.channel, stage, this.getIdentifier());
        this.updatePacketTimeout(timeout);
        this.setStageTimeout(stage, timeout, condition);
    }
    
    protected void enterStage(@Nonnull final String stage, @Nonnull final Duration timeout) {
        NettyUtil.TimeoutContext.update(this.channel, stage, this.getIdentifier());
        this.updatePacketTimeout(timeout);
    }
    
    protected void continueStage(@Nonnull final String stage, @Nonnull final Duration timeout, @Nonnull final BooleanSupplier condition) {
        NettyUtil.TimeoutContext.update(this.channel, stage);
        this.updatePacketTimeout(timeout);
        this.setStageTimeout(stage, timeout, condition);
    }
    
    private void setStageTimeout(@Nonnull final String stageId, @Nonnull final Duration timeout, @Nonnull final BooleanSupplier meets) {
        if (this.timeoutTask != null) {
            this.timeoutTask.cancel(false);
        }
        if (!(this instanceof AuthenticationPacketHandler) && this instanceof PasswordPacketHandler && this.auth == null) {
            return;
        }
        logConnectionTimings(this.channel, "Entering stage '" + stageId, Level.FINEST);
        final long timeoutMillis = timeout.toMillis();
        this.timeoutTask = this.channel.eventLoop().schedule(() -> {
            if (!(!this.channel.isOpen())) {
                if (!meets.getAsBoolean()) {
                    final NettyUtil.TimeoutContext context = this.channel.attr(NettyUtil.TimeoutContext.KEY).get();
                    final String duration = (context != null) ? FormatUtil.nanosToString(System.nanoTime() - context.connectionStartNs()) : "unknown";
                    HytaleLogger.getLogger().at(Level.WARNING).log("Stage timeout for %s at stage '%s' after %s connected", this.getIdentifier(), stageId, duration);
                    this.disconnect("Either you took too long to login or we took too long to process your request! Retry again in a moment.");
                }
            }
        }, timeoutMillis, TimeUnit.MILLISECONDS);
    }
    
    private void updatePacketTimeout(@Nonnull final Duration timeout) {
        this.channel.attr(ProtocolUtil.PACKET_TIMEOUT_KEY).set(timeout);
    }
    
    protected void clearTimeout() {
        if (this.timeoutTask != null) {
            this.timeoutTask.cancel(false);
        }
        if (this.clientReadyForChunksFuture != null) {
            this.clientReadyForChunksFuture.cancel(true);
            this.clientReadyForChunksFuture = null;
            this.clientReadyForChunksFutureStack = null;
        }
    }
    
    @Nullable
    public PlayerAuthentication getAuth() {
        return this.auth;
    }
    
    public boolean stillActive() {
        return this.channel.isActive();
    }
    
    public int getQueuedPacketsCount() {
        return this.queuedPackets.get();
    }
    
    public boolean isLocalConnection() {
        final Channel channel = this.channel;
        SocketAddress socketAddress;
        if (channel instanceof final QuicStreamChannel quicStreamChannel) {
            socketAddress = quicStreamChannel.parent().remoteSocketAddress();
        }
        else {
            socketAddress = this.channel.remoteAddress();
        }
        if (socketAddress instanceof final InetSocketAddress inetSocketAddress) {
            final InetAddress address = inetSocketAddress.getAddress();
            return NetworkUtil.addressMatchesAny(address, NetworkUtil.AddressType.ANY_LOCAL, NetworkUtil.AddressType.LOOPBACK);
        }
        return socketAddress instanceof DomainSocketAddress || socketAddress instanceof LocalAddress;
    }
    
    public boolean isLANConnection() {
        final Channel channel = this.channel;
        SocketAddress socketAddress;
        if (channel instanceof final QuicStreamChannel quicStreamChannel) {
            socketAddress = quicStreamChannel.parent().remoteSocketAddress();
        }
        else {
            socketAddress = this.channel.remoteAddress();
        }
        if (socketAddress instanceof final InetSocketAddress inetSocketAddress) {
            final InetAddress address = inetSocketAddress.getAddress();
            return NetworkUtil.addressMatchesAny(address);
        }
        return socketAddress instanceof DomainSocketAddress || socketAddress instanceof LocalAddress;
    }
    
    @Nonnull
    public DisconnectReason getDisconnectReason() {
        return this.disconnectReason;
    }
    
    public void setClientReadyForChunksFuture(@Nonnull final CompletableFuture<Void> clientReadyFuture) {
        if (this.clientReadyForChunksFuture != null) {
            throw new IllegalStateException("Tried to hook client ready but something is already waiting for it!", this.clientReadyForChunksFutureStack);
        }
        HytaleLogger.getLogger().at(Level.WARNING).log("%s Added future for ClientReady packet?", this.getIdentifier());
        this.clientReadyForChunksFutureStack = new Throwable();
        this.clientReadyForChunksFuture = clientReadyFuture;
    }
    
    @Nullable
    public CompletableFuture<Void> getClientReadyForChunksFuture() {
        return this.clientReadyForChunksFuture;
    }
    
    public static void logConnectionTimings(@Nonnull final Channel channel, @Nonnull final String message, @Nonnull final Level level) {
        final Attribute<Long> loginStartAttribute = channel.attr(PacketHandler.LOGIN_START_ATTRIBUTE_KEY);
        final long now = System.nanoTime();
        final Long before = loginStartAttribute.getAndSet(now);
        if (before == null) {
            PacketHandler.LOGIN_TIMING_LOGGER.at(level).log(message);
        }
        else {
            PacketHandler.LOGIN_TIMING_LOGGER.at(level).log("%s took %s", message, LazyArgs.lazy(() -> FormatUtil.nanosToString(now - before)));
        }
    }
    
    static {
        (LOGIN_TIMING_LOGGER = HytaleLogger.get("LoginTiming")).setLevel(Level.ALL);
        LOGIN_START_ATTRIBUTE_KEY = AttributeKey.newInstance("LOGIN_START");
    }
    
    public static class PingInfo
    {
        public static final MetricsRegistry<PingInfo> METRICS_REGISTRY;
        public static final TimeUnit TIME_UNIT;
        public static final int ONE_SECOND_INDEX = 0;
        public static final int ONE_MINUTE_INDEX = 1;
        public static final int FIVE_MINUTE_INDEX = 2;
        public static final double PERCENTILE = 0.9900000095367432;
        public static final int PING_FREQUENCY = 1;
        public static final TimeUnit PING_FREQUENCY_UNIT;
        public static final int PING_FREQUENCY_MILLIS = 1000;
        public static final int PING_HISTORY_MILLIS = 15000;
        public static final int PING_HISTORY_LENGTH = 15;
        protected final PongType pingType;
        protected final Lock queueLock;
        protected final IntPriorityQueue pingIdQueue;
        protected final LongPriorityQueue pingTimestampQueue;
        protected final Lock pingLock;
        @Nonnull
        protected final HistoricMetric pingMetricSet;
        protected final Metric packetQueueMetric;
        
        public PingInfo(final PongType pingType) {
            this.queueLock = new ReentrantLock();
            this.pingIdQueue = new IntArrayFIFOQueue(15);
            this.pingTimestampQueue = new LongArrayFIFOQueue(15);
            this.pingLock = new ReentrantLock();
            this.packetQueueMetric = new Metric();
            this.pingType = pingType;
            this.pingMetricSet = HistoricMetric.builder(1000L, TimeUnit.MILLISECONDS).addPeriod(1L, TimeUnit.SECONDS).addPeriod(1L, TimeUnit.MINUTES).addPeriod(5L, TimeUnit.MINUTES).build();
        }
        
        protected void recordSent(final int id, final long timestamp) {
            this.queueLock.lock();
            try {
                this.pingIdQueue.enqueue(id);
                this.pingTimestampQueue.enqueue(timestamp);
            }
            finally {
                this.queueLock.unlock();
            }
        }
        
        protected void handlePacket(@Nonnull final Pong packet) {
            if (packet.type != this.pingType) {
                throw new IllegalArgumentException("Got packet for " + String.valueOf(packet.type) + " but expected " + String.valueOf(this.pingType));
            }
            this.queueLock.lock();
            int nextIdToHandle;
            long sentTimestamp;
            try {
                nextIdToHandle = this.pingIdQueue.dequeueInt();
                sentTimestamp = this.pingTimestampQueue.dequeueLong();
            }
            finally {
                this.queueLock.unlock();
            }
            if (packet.id != nextIdToHandle) {
                throw new IllegalArgumentException(String.valueOf(packet.id));
            }
            final long nanoTime = System.nanoTime();
            final long pingValue = nanoTime - sentTimestamp;
            if (pingValue <= 0L) {
                throw new IllegalArgumentException(String.format("Ping must be received after its sent! %s", pingValue));
            }
            this.pingLock.lock();
            try {
                this.pingMetricSet.add(nanoTime, PingInfo.TIME_UNIT.convert(pingValue, TimeUnit.NANOSECONDS));
                this.packetQueueMetric.add(packet.packetQueueSize);
            }
            finally {
                this.pingLock.unlock();
            }
        }
        
        public PongType getPingType() {
            return this.pingType;
        }
        
        @Nonnull
        public Metric getPacketQueueMetric() {
            return this.packetQueueMetric;
        }
        
        @Nonnull
        public HistoricMetric getPingMetricSet() {
            return this.pingMetricSet;
        }
        
        public void clear() {
            this.pingLock.lock();
            try {
                this.packetQueueMetric.clear();
                this.pingMetricSet.clear();
            }
            finally {
                this.pingLock.unlock();
            }
        }
        
        static {
            METRICS_REGISTRY = new MetricsRegistry<PingInfo>().register("PingType", pingInfo -> pingInfo.pingType, (Codec<PongType>)new EnumCodec<PongType>(PongType.class)).register("PingMetrics", PingInfo::getPingMetricSet, HistoricMetric.METRICS_CODEC).register("PacketQueueMin", pingInfo -> pingInfo.packetQueueMetric.getMin(), (Codec<Long>)Codec.LONG).register("PacketQueueAvg", pingInfo -> pingInfo.packetQueueMetric.getAverage(), (Codec<Double>)Codec.DOUBLE).register("PacketQueueMax", pingInfo -> pingInfo.packetQueueMetric.getMax(), (Codec<Long>)Codec.LONG);
            TIME_UNIT = TimeUnit.MICROSECONDS;
            PING_FREQUENCY_UNIT = TimeUnit.SECONDS;
        }
    }
    
    public static class DisconnectReason
    {
        @Nullable
        private String serverDisconnectReason;
        @Nullable
        private DisconnectType clientDisconnectType;
        
        protected DisconnectReason() {
        }
        
        @Nullable
        public String getServerDisconnectReason() {
            return this.serverDisconnectReason;
        }
        
        public void setServerDisconnectReason(final String serverDisconnectReason) {
            this.serverDisconnectReason = serverDisconnectReason;
            this.clientDisconnectType = null;
        }
        
        @Nullable
        public DisconnectType getClientDisconnectType() {
            return this.clientDisconnectType;
        }
        
        public void setClientDisconnectType(final DisconnectType clientDisconnectType) {
            this.clientDisconnectType = clientDisconnectType;
            this.serverDisconnectReason = null;
        }
        
        @Nonnull
        @Override
        public String toString() {
            return "DisconnectReason{serverDisconnectReason='" + this.serverDisconnectReason + "', clientDisconnectType=" + String.valueOf(this.clientDisconnectType);
        }
    }
}
