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

package com.hypixel.hytale.server.core.command.commands.debug.stresstest;

import io.netty.bootstrap.AbstractBootstrap;
import java.util.concurrent.Executors;
import com.hypixel.hytale.server.core.util.PositionUtil;
import com.hypixel.hytale.protocol.Direction;
import com.hypixel.hytale.protocol.Position;
import com.hypixel.hytale.protocol.InstantData;
import com.hypixel.hytale.protocol.packets.player.ClientMovement;
import com.hypixel.hytale.protocol.ModelTransform;
import com.hypixel.hytale.protocol.ComponentUpdate;
import com.hypixel.hytale.protocol.EntityUpdate;
import com.hypixel.hytale.protocol.packets.player.ClientReady;
import com.hypixel.hytale.protocol.packets.player.SetClientId;
import com.hypixel.hytale.protocol.TeleportAck;
import com.hypixel.hytale.protocol.packets.player.ClientTeleport;
import com.hypixel.hytale.protocol.ComponentUpdateType;
import com.hypixel.hytale.protocol.packets.entities.EntityUpdates;
import com.hypixel.hytale.protocol.packets.setup.PlayerOptions;
import java.util.Random;
import com.hypixel.hytale.server.core.cosmetics.CosmeticsModule;
import com.hypixel.hytale.protocol.packets.setup.ViewRadius;
import com.hypixel.hytale.protocol.packets.setup.RequestAssets;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.channel.ChannelFutureListener;
import com.hypixel.hytale.protocol.packets.connection.Disconnect;
import com.hypixel.hytale.protocol.packets.connection.DisconnectType;
import com.hypixel.hytale.protocol.HostAddress;
import com.hypixel.hytale.protocol.packets.connection.Connect;
import com.hypixel.hytale.protocol.packets.connection.ClientType;
import java.util.UUID;
import java.nio.charset.StandardCharsets;
import io.netty.channel.ChannelHandlerContext;
import com.hypixel.hytale.protocol.packets.connection.Pong;
import com.hypixel.hytale.protocol.packets.connection.PongType;
import com.hypixel.hytale.server.core.modules.time.WorldTimeResource;
import java.time.Instant;
import java.net.SocketException;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
import java.net.SocketAddress;
import com.hypixel.hytale.server.core.io.netty.NettyUtil;
import com.hypixel.hytale.protocol.io.netty.PacketEncoder;
import io.netty.channel.ChannelHandler;
import com.hypixel.hytale.protocol.io.netty.PacketDecoder;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.Channel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.channel.kqueue.KQueueSocketChannel;
import io.netty.channel.kqueue.KQueue;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.epoll.Epoll;
import io.netty.bootstrap.Bootstrap;
import java.util.logging.Level;
import com.hypixel.hytale.server.core.io.ServerManager;
import java.util.concurrent.ThreadLocalRandom;
import com.hypixel.hytale.math.vector.Vector3f;
import com.hypixel.hytale.math.vector.Vector3d;
import javax.annotation.Nullable;
import io.netty.channel.socket.SocketChannel;
import com.hypixel.hytale.protocol.MovementStates;
import com.hypixel.hytale.protocol.packets.connection.Ping;
import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue;
import java.util.concurrent.ScheduledFuture;
import javax.annotation.Nonnull;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.protocol.Asset;
import io.netty.channel.EventLoopGroup;
import java.util.concurrent.ScheduledExecutorService;
import com.hypixel.hytale.protocol.Packet;
import io.netty.channel.SimpleChannelInboundHandler;

public class Bot extends SimpleChannelInboundHandler<Packet>
{
    private static final ScheduledExecutorService EXECUTOR;
    private static final EventLoopGroup WORKER_GROUP;
    public static final Asset[] EMPTY_ASSET_ARRAY;
    @Nonnull
    private final HytaleLogger logger;
    private final String name;
    @Nonnull
    private final BotConfig config;
    @Nonnull
    private final ScheduledFuture<?> tickFuture;
    private final ObjectArrayFIFOQueue<Ping> pingPackets;
    private final MovementStates movementStates;
    @Nullable
    private SocketChannel channel;
    private int id;
    private Vector3d pos;
    private final Vector3f rotation;
    private final Vector3d destination;
    private final Vector3d temp;
    private final Vector3f targetRotation;
    
    public Bot(final String name, @Nonnull final BotConfig config, final int tickStepNanos) throws InterruptedException, SocketException {
        this.pingPackets = new ObjectArrayFIFOQueue<Ping>();
        this.movementStates = new MovementStates();
        this.id = -1;
        this.rotation = new Vector3f();
        this.destination = new Vector3d();
        this.temp = new Vector3d();
        this.targetRotation = new Vector3f();
        this.logger = HytaleLogger.get(name);
        this.name = name;
        this.config = config;
        this.destination.assign(config.spawn.getPosition());
        this.destination.y = ThreadLocalRandom.current().nextDouble(config.flyYHeight.getX(), config.flyYHeight.getY());
        final InetSocketAddress address = ServerManager.get().getLocalOrPublicAddress();
        this.logger.at(Level.INFO).log("Booting Bot! Connecting to %s", address);
        ((AbstractBootstrap<Bootstrap, C>)((AbstractBootstrap<Bootstrap, C>)((AbstractBootstrap<Bootstrap, C>)new Bootstrap()).group(Bot.WORKER_GROUP).channel((Class<? extends Channel>)(Epoll.isAvailable() ? EpollSocketChannel.class : (KQueue.isAvailable() ? KQueueSocketChannel.class : NioSocketChannel.class)))).option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)).handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(@Nonnull final SocketChannel channel) {
                Bot.this.channel = channel;
                channel.pipeline().addLast("packetDecoder", new PacketDecoder());
                channel.pipeline().addLast("packetEncoder", new PacketEncoder());
                if (NettyUtil.PACKET_LOGGER.getLevel() != Level.OFF) {
                    channel.pipeline().addLast("logger", NettyUtil.LOGGER);
                }
                channel.pipeline().addLast("handler", Bot.this);
            }
        }).connect(address).sync();
        final float dt = (float)(tickStepNanos / 1.0E9);
        this.tickFuture = Bot.EXECUTOR.scheduleAtFixedRate(() -> {
            if (this.channel != null) {
                try {
                    this.tick(dt);
                }
                catch (final Throwable t) {
                    this.logger.at(Level.SEVERE).withCause(t).log("Exception ticking %s", name);
                }
            }
        }, tickStepNanos, tickStepNanos, TimeUnit.NANOSECONDS);
    }
    
    public void shutdown() {
        this.tickFuture.cancel(false);
        if (this.channel != null && !this.channel.isShutdown()) {
            try {
                this.channel.shutdown().await(1L, TimeUnit.SECONDS);
                this.channel = null;
            }
            catch (final InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public void tick(final float dt) {
        while (!this.pingPackets.isEmpty()) {
            final Ping packet = this.pingPackets.dequeue();
            this.channel.write(new Pong(packet.id, WorldTimeResource.instantToInstantData(Instant.now()), PongType.Tick, (short)0));
        }
        if (this.pos == null) {
            this.channel.flush();
            return;
        }
        final double movementDistance = this.config.flySpeed * dt;
        if (this.pos.distanceSquaredTo(this.destination) <= movementDistance * movementDistance) {
            final ThreadLocalRandom random = ThreadLocalRandom.current();
            final double randX = random.nextDouble(-this.config.radius, this.config.radius);
            final double randY = random.nextDouble(this.config.flyYHeight.getX(), this.config.flyYHeight.getY());
            final double randZ = random.nextDouble(-this.config.radius, this.config.radius);
            this.destination.assign(this.config.spawn.getPosition());
            this.destination.y = randY;
            this.destination.add(randX, 0.0, randZ);
        }
        this.temp.assign(this.destination).subtract(this.pos);
        Vector3f.lookAt(this.temp, this.targetRotation);
        Vector3f.lerpAngle(this.rotation, this.targetRotation, 0.3f, this.rotation);
        this.temp.normalize();
        this.temp.scale(movementDistance);
        this.pos.add(this.temp);
        this.movementStates.flying = true;
        this.channel.writeAndFlush(this.createMovementPacket());
    }
    
    @Override
    public void channelActive(@Nonnull final ChannelHandlerContext ctx) {
        final UUID uuid = UUID.nameUUIDFromBytes(("BOT|" + this.name).getBytes(StandardCharsets.UTF_8));
        ctx.writeAndFlush(new Connect(1789265863, 2, "bot", ClientType.Game, uuid, this.name, null, "en", null, null));
        this.logger.at(Level.INFO).log("Connected!");
    }
    
    @Override
    public void channelInactive(final ChannelHandlerContext ctx) {
        this.logger.at(Level.INFO).log("Disconnected!");
        this.shutdown();
        StressTestStartCommand.BOTS.remove(this);
    }
    
    @Override
    public void exceptionCaught(@Nonnull final ChannelHandlerContext ctx, @Nonnull final Throwable cause) {
        this.logger.at(Level.WARNING).withCause(cause).log("Got exception from netty pipeline");
        if (ctx.channel().isWritable()) {
            ctx.channel().writeAndFlush(new Disconnect(cause.getMessage(), DisconnectType.Crash)).addListener((GenericFutureListener<? extends Future<? super Void>>)ChannelFutureListener.CLOSE);
        }
        else {
            ctx.channel().close();
        }
        this.shutdown();
        StressTestStartCommand.BOTS.remove(this);
    }
    
    public void channelRead0(@Nonnull final ChannelHandlerContext ctx, @Nonnull final Packet packet) {
        switch (packet.getId()) {
            case 20: {
                ctx.write(new RequestAssets(Bot.EMPTY_ASSET_ARRAY));
                ctx.write(new ViewRadius(this.config.viewRadius));
                ctx.writeAndFlush(new PlayerOptions(CosmeticsModule.get().generateRandomSkin(ThreadLocalRandom.current())));
                break;
            }
            case 161: {
                final EntityUpdates entityUpdates = (EntityUpdates)packet;
                final EntityUpdate entry = findEntityUpdate(entityUpdates, this.id);
                if (entry == null) {
                    return;
                }
                for (final ComponentUpdate update : entry.updates) {
                    if (update.type == ComponentUpdateType.Transform) {
                        this.updateModelTransform(update.transform);
                        break;
                    }
                }
                break;
            }
            case 109: {
                final ClientTeleport clientTeleport = (ClientTeleport)packet;
                final ModelTransform modelTransform = clientTeleport.modelTransform;
                if (modelTransform == null) {
                    return;
                }
                this.updateModelTransform(modelTransform);
                this.logger.at(Level.INFO).log("TP: %s (sending ack for teleportId: %s)", this.pos, clientTeleport.teleportId);
                final ClientMovement movement = this.createMovementPacket();
                movement.teleportAck = new TeleportAck(clientTeleport.teleportId);
                ctx.writeAndFlush(movement);
                break;
            }
            case 100: {
                this.id = ((SetClientId)packet).clientId;
                break;
            }
            case 104: {
                ctx.writeAndFlush(new ClientReady(true, this.id != -1));
                break;
            }
            case 1: {
                this.logger.at(Level.INFO).log("Disconnected for: %s %s", ((Disconnect)packet).reason, ((Disconnect)packet).type);
                ctx.close();
                break;
            }
            case 2: {
                final Ping ping = (Ping)packet;
                final InstantData instantData = WorldTimeResource.instantToInstantData(Instant.now());
                ctx.write(new Pong(ping.id, instantData, PongType.Raw, (short)0));
                ctx.writeAndFlush(new Pong(ping.id, instantData, PongType.Direct, (short)0));
                this.pingPackets.enqueue(ping);
                break;
            }
        }
    }
    
    public void updateModelTransform(@Nonnull final ModelTransform modelTransform) {
        final Position position = modelTransform.position;
        if (position != null) {
            if (this.pos == null) {
                this.pos = new Vector3d();
            }
            this.pos.assign(position.x, position.y, position.z);
        }
        final Direction lookOrientation = modelTransform.lookOrientation;
        if (lookOrientation != null) {
            this.updateRotation(lookOrientation);
        }
    }
    
    public void updateRotation(@Nonnull final Direction lookOrientation) {
        if (!Float.isNaN(lookOrientation.yaw)) {
            this.rotation.setYaw(lookOrientation.yaw);
        }
        if (!Float.isNaN(lookOrientation.pitch)) {
            this.rotation.setPitch(lookOrientation.pitch);
        }
        if (!Float.isNaN(lookOrientation.roll)) {
            this.rotation.setRoll(lookOrientation.roll);
        }
    }
    
    @Nonnull
    public ClientMovement createMovementPacket() {
        final ClientMovement movement = new ClientMovement();
        movement.absolutePosition = PositionUtil.toPositionPacket(this.pos);
        movement.lookOrientation = PositionUtil.toDirectionPacket(this.rotation);
        movement.bodyOrientation = PositionUtil.toDirectionPacket(this.rotation);
        movement.bodyOrientation.pitch = 0.0f;
        movement.movementStates = this.movementStates;
        return movement;
    }
    
    @Nonnull
    @Override
    public String toString() {
        return "Bot{name='" + this.name + "', id=" + this.id;
    }
    
    @Nullable
    public static EntityUpdate findEntityUpdate(@Nonnull final EntityUpdates bulkList, final int id) {
        if (bulkList.updates == null) {
            return null;
        }
        for (final EntityUpdate otherEntry : bulkList.updates) {
            if (otherEntry.networkId == id) {
                return otherEntry;
            }
        }
        return null;
    }
    
    static {
        EXECUTOR = Executors.newScheduledThreadPool(8);
        WORKER_GROUP = NettyUtil.getEventLoopGroup(8, "BotWorkerGroup");
        EMPTY_ASSET_ARRAY = new Asset[0];
    }
}
