package dev.crucifixbeta.mod.modules.impl.movement;

import asm.accessors.IPlayerPositionLookS2CPacket;
import dev.crucifixbeta.api.events.eventbus.EventListener;
import dev.crucifixbeta.api.events.impl.PacketEvent;
import dev.crucifixbeta.api.events.impl.UpdateEvent;
import dev.crucifixbeta.api.utils.math.Timer;
import dev.crucifixbeta.core.impl.CommandManager;
import dev.crucifixbeta.mod.modules.Module;
import dev.crucifixbeta.mod.modules.settings.impl.BooleanSetting;
import dev.crucifixbeta.mod.modules.settings.impl.EnumSetting;
import dev.crucifixbeta.mod.modules.settings.impl.SliderSetting;
import java.util.ArrayDeque;
import java.util.Iterator;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket;
import net.minecraft.network.packet.c2s.play.TeleportConfirmC2SPacket;
import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket.Full;
import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket.PositionAndOnGround;
import net.minecraft.network.packet.s2c.play.PlayerPositionLookS2CPacket;
import net.minecraft.network.packet.s2c.play.PositionFlag;
import net.minecraft.util.math.Vec3d;

@Environment(EnvType.CLIENT)
public class AntiBounce extends Module {
   public static AntiBounce INSTANCE;
   private static final double NORMAL_TELEPORT_DISTANCE;
   private static final double MIN_INTERCEPT_DISTANCE;
   private final EnumSetting<AntiBounce.Mode> mode = this.add(new EnumSetting<>("Mode", AntiBounce.Mode.Grim));
   private final BooleanSetting cancelAll = this.add(new BooleanSetting("CancelAll", false, () -> this.mode.is(AntiBounce.Mode.Cancel)));
   private final SliderSetting cancelDistance = this.add(
      new SliderSetting("CancelDist", 5.0, 0.0, 50.0, 0.5, () -> this.mode.is(AntiBounce.Mode.Cancel) && !this.cancelAll.getValue())
   );
   private final BooleanSetting confirmOnly = this.add(new BooleanSetting("ConfirmOnly", true, () -> this.mode.is(AntiBounce.Mode.Confirm)));
   private final BooleanSetting modifyRotation = this.add(new BooleanSetting("ModifyRotation", true, () -> this.mode.is(AntiBounce.Mode.Modify)));
   private final BooleanSetting modifyPosition = this.add(new BooleanSetting("ModifyPosition", true, () -> this.mode.is(AntiBounce.Mode.Modify)));
   private final SliderSetting grimThreshold = this.add(
      new SliderSetting("GrimThreshold", 0.5, 0.1, 10.0, 0.1, () -> this.mode.is(AntiBounce.Mode.Grim))
   );
   private final BooleanSetting grimRecord = this.add(new BooleanSetting("GrimRecord", true, () -> this.mode.is(AntiBounce.Mode.Grim)));
   private final SliderSetting grimExpire = this.add(
      new SliderSetting("GrimExpire", 30.0, 5.0, 60.0, 1.0, () -> this.mode.is(AntiBounce.Mode.Grim)).setSuffix("s")
   );
   private final SliderSetting resendDelay = this.add(
      new SliderSetting("ResendDelay", 0.0, 0.0, 500.0, 10.0, () -> this.mode.is(AntiBounce.Mode.Resend)).setSuffix("ms")
   );
   private final BooleanSetting resendConfirm = this.add(new BooleanSetting("ResendConfirm", true, () -> this.mode.is(AntiBounce.Mode.Resend)));
   private final SliderSetting resendCount = this.add(
      new SliderSetting("ResendCount", 1.0, 1.0, 5.0, 1.0, () -> this.mode.is(AntiBounce.Mode.Resend))
   );
   private final BooleanSetting onlyWhenFlying = this.add(new BooleanSetting("OnlyFlying", false));
   private final BooleanSetting debug = this.add(new BooleanSetting("Debug", false));
   private final ArrayDeque<AntiBounce.TimeVec3d> recentMoves = new ArrayDeque<>();
   private final Timer resendTimer = new Timer();
   private Vec3d lastPlayerPos = Vec3d.ZERO;
   private float lastPlayerYaw = 0.0F;
   private float lastPlayerPitch = 0.0F;
   private int setbackCount = 0;

   public AntiBounce() {
      super("AntiBounce", Module.Category.Movement);
      this.setChinese("\u9632\u56de\u5f39");
      INSTANCE = this;
   }

   @Override
   public String getInfo() {
      return this.mode.getValue().name();
   }

   @Override
   public void onEnable() {
      this.resetState();
   }

   @Override
   public void onDisable() {
      this.resetState();
   }

   private void resetState() {
      this.recentMoves.clear();
      this.lastPlayerPos = Vec3d.ZERO;
      this.lastPlayerYaw = 0.0F;
      this.lastPlayerPitch = 0.0F;
      this.setbackCount = 0;
      this.resendTimer.reset();
   }

   @EventListener
   public void onUpdate(UpdateEvent event) {
      if (!nullCheck()) {
         this.lastPlayerPos = mc.player.getPos();
         this.lastPlayerYaw = mc.player.getYaw();
         this.lastPlayerPitch = mc.player.getPitch();
         this.pruneRecentMoves();
      }
   }

   @EventListener(
      priority = -100
   )
   public void onPacketSend(PacketEvent.Send event) {
      if (!nullCheck()) {
         if (event.getPacket() instanceof PlayerMoveC2SPacket var3 && (var3 instanceof PositionAndOnGround || var3 instanceof Full)) {
            this.recentMoves.addLast(new AntiBounce.TimeVec3d(new Vec3d(var3.getX(0.0), var3.getY(0.0), var3.getZ(0.0)), System.currentTimeMillis()));
            this.pruneRecentMoves();
         }
      }
   }

   @EventListener(
      priority = 200
   )
   public void onPacketReceive(PacketEvent.Receive event) {
      if (!nullCheck()) {
         if (event.getPacket() instanceof PlayerPositionLookS2CPacket var3) {
            if (!this.onlyWhenFlying.getValue() || !mc.player.isOnGround()) {
               this.setbackCount++;
               switch ((AntiBounce.Mode)this.mode.getValue()) {
                  case Cancel:
                     this.handleCancel(event, var3);
                     break;
                  case Confirm:
                     this.handleConfirm(event, var3);
                     break;
                  case Modify:
                     this.handleModify(event, var3);
                     break;
                  case Grim:
                     this.handleGrim(event, var3);
                     break;
                  case Resend:
                     this.handleResend(event, var3);
               }
            }
         }
      }
   }

   private void handleCancel(PacketEvent.Receive event, PlayerPositionLookS2CPacket packet) {
      Vec3d var3 = this.resolvePacketPos(packet);
      double var4 = mc.player.getPos().distanceTo(var3);
      if (!this.cancelAll.getValue() && !this.shouldIntercept(packet, var3, this.cancelDistance.getValue())) {
         this.debugPass("Cancel", packet, var4);
      } else if (this.cancelAll.getValue() && this.isLikelyLegitTeleport(var3, this.cancelDistance.getValue())) {
         this.debugPass("Cancel", packet, var4);
      } else {
         event.cancel();
         this.debugBlock("Cancel", packet, var4);
      }
   }

   private void handleConfirm(PacketEvent.Receive event, PlayerPositionLookS2CPacket packet) {
      Vec3d var3 = this.resolvePacketPos(packet);
      double var4 = mc.player.getPos().distanceTo(var3);
      double var6 = Math.max(1.0, this.grimThreshold.getValue());
      if (!this.shouldIntercept(packet, var3, var6)) {
         this.debugPass("Confirm", packet, var4);
      } else {
         event.cancel();
         if (this.confirmOnly.getValue()) {
            mc.getNetworkHandler().sendPacket(new TeleportConfirmC2SPacket(packet.getTeleportId()));
         }

         this.debugBlock("Confirm", packet, var4);
      }
   }

   private void handleModify(PacketEvent.Receive event, PlayerPositionLookS2CPacket packet) {
      Vec3d var3 = this.resolvePacketPos(packet);
      double var4 = mc.player.getPos().distanceTo(var3);
      double var6 = Math.max(1.0, this.grimThreshold.getValue());
      if (this.modifyPosition.getValue() && this.shouldIntercept(packet, var3, var6)) {
         event.cancel();
         mc.getNetworkHandler().sendPacket(new TeleportConfirmC2SPacket(packet.getTeleportId()));
         this.debugBlock("Modify", packet, var4);
      } else {
         if (this.modifyRotation.getValue()) {
            ((IPlayerPositionLookS2CPacket)packet).setYaw(this.lastPlayerYaw);
            ((IPlayerPositionLookS2CPacket)packet).setPitch(this.lastPlayerPitch);
         }

         this.debugPass("Modify", packet, var4);
      }
   }

   private void handleGrim(PacketEvent.Receive event, PlayerPositionLookS2CPacket packet) {
      Vec3d var3 = this.resolvePacketPos(packet);
      double var4 = mc.player.getPos().distanceTo(var3);
      if (!this.shouldIntercept(packet, var3, this.grimThreshold.getValue())) {
         this.debugPass("Grim", packet, var4);
      } else {
         event.cancel();
         mc.getNetworkHandler().sendPacket(new TeleportConfirmC2SPacket(packet.getTeleportId()));
         this.debugBlock("Grim", packet, var4);
      }
   }

   private void handleResend(PacketEvent.Receive event, PlayerPositionLookS2CPacket packet) {
      Vec3d var3 = mc.player.getPos();
      float var4 = mc.player.getYaw();
      float var5 = mc.player.getPitch();
      if (this.resendConfirm.getValue()) {
         mc.getNetworkHandler().sendPacket(new TeleportConfirmC2SPacket(packet.getTeleportId()));
      }

      if (!(this.resendDelay.getValue() > 0.0) || this.resendTimer.passedMs(this.resendDelay.getValue())) {
         this.resendTimer.reset();
         int var6 = this.resendCount.getValueInt();

         for (int var7 = 0; var7 < var6; var7++) {
            mc.getNetworkHandler().sendPacket(new Full(var3.x, var3.y, var3.z, var4, var5, mc.player.isOnGround()));
         }

         mc.player.setPosition(var3);
         mc.player.setYaw(var4);
         mc.player.setPitch(var5);
         if (this.debug.getValue()) {
            this.sendMessage("§b[Resend] resent " + var6 + " move packets before setback");
         }
      }
   }

   private boolean shouldIntercept(PlayerPositionLookS2CPacket packet, Vec3d targetPos, double threshold) {
      double var5 = mc.player.getPos().distanceTo(targetPos);
      if (var5 < 0.05) {
         return false;
      } else if (this.isLikelyLegitTeleport(targetPos, threshold)) {
         return false;
      } else {
         return !this.hasRecentMoveMatch(targetPos, Math.max(0.2, threshold)) ? false : !this.hasRotationChange(packet);
      }
   }

   private boolean isLikelyLegitTeleport(Vec3d targetPos, double threshold) {
      double var4 = mc.player.getPos().distanceTo(targetPos);
      return var4 > Math.max(8.0, threshold * 2.0);
   }

   private boolean hasRecentMoveMatch(Vec3d targetPos, double threshold) {
      for (AntiBounce.TimeVec3d var5 : this.recentMoves) {
         if (var5.pos.distanceTo(targetPos) <= threshold) {
            return true;
         }
      }

      return false;
   }

   private boolean hasRotationChange(PlayerPositionLookS2CPacket packet) {
      return Math.abs(packet.getYaw()) > 0.001F
         || Math.abs(packet.getPitch()) > 0.001F
         || packet.getFlags().contains(PositionFlag.X_ROT)
         || packet.getFlags().contains(PositionFlag.Y_ROT);
   }

   private Vec3d resolvePacketPos(PlayerPositionLookS2CPacket packet) {
      double var2 = packet.getFlags().contains(PositionFlag.X) ? mc.player.getX() + packet.getX() : packet.getX();
      double var4 = packet.getFlags().contains(PositionFlag.Y) ? mc.player.getY() + packet.getY() : packet.getY();
      double var6 = packet.getFlags().contains(PositionFlag.Z) ? mc.player.getZ() + packet.getZ() : packet.getZ();
      return new Vec3d(var2, var4, var6);
   }

   private void pruneRecentMoves() {
      long var1 = (long)(this.grimExpire.getValue() * 1000.0);
      long var3 = System.currentTimeMillis();
      Iterator var5 = this.recentMoves.iterator();

      while (var5.hasNext()) {
         if (var3 - ((AntiBounce.TimeVec3d)var5.next()).time > var1) {
            var5.remove();
         }
      }

      while (this.recentMoves.size() > 80) {
         this.recentMoves.removeFirst();
      }
   }

   private void debugPass(String modeName, PlayerPositionLookS2CPacket packet, double dist) {
      if (this.debug.getValue()) {
         this.sendMessage("§7[" + modeName + "] pass tpId=" + packet.getTeleportId() + " dist=" + String.format("%.2f", dist));
      }
   }

   private void debugBlock(String modeName, PlayerPositionLookS2CPacket packet, double dist) {
      if (this.debug.getValue()) {
         this.sendMessage("§e[" + modeName + "] block tpId=" + packet.getTeleportId() + " dist=" + String.format("%.2f", dist));
      }
   }

   @Override
   public void sendMessage(String message) {
      CommandManager.sendMessage(message);
   }

   static {

      MIN_INTERCEPT_DISTANCE = 0.05;
      NORMAL_TELEPORT_DISTANCE = 8.0;
   }
@Environment(EnvType.CLIENT)
   public static enum Mode {
      Cancel,
      Confirm,
      Modify,
      Grim,
      Resend;

}

   @Environment(EnvType.CLIENT)
   private static class TimeVec3d {
      final Vec3d pos;
      final long time;

      TimeVec3d(Vec3d pos, long time) {
         this.pos = pos;
         this.time = time;
      }
   }
}
