package net.mcreator.gts.network;

import net.neoforged.neoforge.registries.NeoForgeRegistries;
import net.neoforged.neoforge.registries.DeferredRegister;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import net.neoforged.neoforge.network.PacketDistributor;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import net.neoforged.neoforge.common.util.INBTSerializable;
import net.neoforged.neoforge.attachment.AttachmentType;
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.bus.api.SubscribeEvent;

import net.minecraft.world.level.saveddata.SavedData;
import net.minecraft.world.level.ServerLevelAccessor;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.Level;
import net.minecraft.world.entity.Entity;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.network.protocol.PacketFlow;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.chat.Component;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.core.HolderLookup;

import net.mcreator.gts.GtsMod;

import java.util.function.Supplier;

@EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD)
public class GtsModVariables {
	public static final DeferredRegister<AttachmentType<?>> ATTACHMENT_TYPES = DeferredRegister.create(NeoForgeRegistries.Keys.ATTACHMENT_TYPES, GtsMod.MODID);
	public static final Supplier<AttachmentType<PlayerVariables>> PLAYER_VARIABLES = ATTACHMENT_TYPES.register("player_variables", () -> AttachmentType.serializable(() -> new PlayerVariables()).build());

	@SubscribeEvent
	public static void init(FMLCommonSetupEvent event) {
		GtsMod.addNetworkMessage(SavedDataSyncMessage.TYPE, SavedDataSyncMessage.STREAM_CODEC, SavedDataSyncMessage::handleData);
		GtsMod.addNetworkMessage(PlayerVariablesSyncMessage.TYPE, PlayerVariablesSyncMessage.STREAM_CODEC, PlayerVariablesSyncMessage::handleData);
	}

	@EventBusSubscriber
	public static class EventBusVariableHandlers {
		@SubscribeEvent
		public static void onPlayerLoggedInSyncPlayerVariables(PlayerEvent.PlayerLoggedInEvent event) {
			if (event.getEntity() instanceof ServerPlayer player)
				player.getData(PLAYER_VARIABLES).syncPlayerVariables(event.getEntity());
		}

		@SubscribeEvent
		public static void onPlayerRespawnedSyncPlayerVariables(PlayerEvent.PlayerRespawnEvent event) {
			if (event.getEntity() instanceof ServerPlayer player)
				player.getData(PLAYER_VARIABLES).syncPlayerVariables(event.getEntity());
		}

		@SubscribeEvent
		public static void onPlayerChangedDimensionSyncPlayerVariables(PlayerEvent.PlayerChangedDimensionEvent event) {
			if (event.getEntity() instanceof ServerPlayer player)
				player.getData(PLAYER_VARIABLES).syncPlayerVariables(event.getEntity());
		}

		@SubscribeEvent
		public static void clonePlayer(PlayerEvent.Clone event) {
			PlayerVariables original = event.getOriginal().getData(PLAYER_VARIABLES);
			PlayerVariables clone = new PlayerVariables();
			if (!event.isWasDeath()) {
				clone.Player_Speed = original.Player_Speed;
			}
			event.getEntity().setData(PLAYER_VARIABLES, clone);
		}

		@SubscribeEvent
		public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) {
			if (event.getEntity() instanceof ServerPlayer player) {
				SavedData mapdata = MapVariables.get(event.getEntity().level());
				SavedData worlddata = WorldVariables.get(event.getEntity().level());
				if (mapdata != null)
					PacketDistributor.sendToPlayer(player, new SavedDataSyncMessage(0, mapdata));
				if (worlddata != null)
					PacketDistributor.sendToPlayer(player, new SavedDataSyncMessage(1, worlddata));
			}
		}

		@SubscribeEvent
		public static void onPlayerChangedDimension(PlayerEvent.PlayerChangedDimensionEvent event) {
			if (event.getEntity() instanceof ServerPlayer player) {
				SavedData worlddata = WorldVariables.get(event.getEntity().level());
				if (worlddata != null)
					PacketDistributor.sendToPlayer(player, new SavedDataSyncMessage(1, worlddata));
			}
		}
	}

	public static class WorldVariables extends SavedData {
		public static final String DATA_NAME = "gts_worldvars";

		public static WorldVariables load(CompoundTag tag, HolderLookup.Provider lookupProvider) {
			WorldVariables data = new WorldVariables();
			data.read(tag, lookupProvider);
			return data;
		}

		public void read(CompoundTag nbt, HolderLookup.Provider lookupProvider) {
		}

		@Override
		public CompoundTag save(CompoundTag nbt, HolderLookup.Provider lookupProvider) {
			return nbt;
		}

		public void syncData(LevelAccessor world) {
			this.setDirty();
			if (world instanceof ServerLevel level)
				PacketDistributor.sendToPlayersInDimension(level, new SavedDataSyncMessage(1, this));
		}

		static WorldVariables clientSide = new WorldVariables();

		public static WorldVariables get(LevelAccessor world) {
			if (world instanceof ServerLevel level) {
				return level.getDataStorage().computeIfAbsent(new SavedData.Factory<>(WorldVariables::new, WorldVariables::load), DATA_NAME);
			} else {
				return clientSide;
			}
		}
	}

	public static class MapVariables extends SavedData {
		public static final String DATA_NAME = "gts_mapvars";
		public boolean DisableStep = false;
		public boolean DisableKick = false;
		public boolean DisableSit = false;
		public boolean DisableGunSlap = false;
		public boolean DisableGunSmash = false;
		public boolean DisableThrow = false;
		public boolean DisableVore = false;
		public boolean DisableSniper = false;
		public boolean DisableSpit = false;
		public boolean DisableDrone = false;
		public boolean DisableUpperBreach = false;
		public boolean DisableMiddleBreach = false;
		public boolean DisableLowerBreach = false;
		public boolean DisableTrappedBreach = false;
		public boolean DisableShowOff = false;
		public boolean DisableBelch = false;
		public boolean DisableStomachSound = false;
		public boolean DisableClothingDamage = false;
		public boolean DisableRaidWaveSizeChange = false;
		public double Toki_TurnSpeed = 30.0;
		public boolean DisableLeavesBreakWalk = false;
		public boolean DisableLockOnTarget = true;
		public boolean DisableSubmission = false;
		public double GtsNaturalSpawnHeight = 3.0;
		public boolean DisableNaturalSpawn = false;
		public boolean DisableCandyStep = false;
		public boolean DisableCandyShockWave = false;
		public boolean DisableCandyKick = false;
		public boolean DisableCandySwipe = false;
		public boolean DisableCandySmash = false;
		public boolean DisableCandyBreach = false;
		public boolean DisableCandyVore = false;
		public boolean DisableCandyHeartMagic = false;
		public boolean DisableCandySpit = false;
		public boolean DisableCandyThunderMagic = false;
		public boolean Version211 = true;
		public boolean DisableTamedGiantessFriendlyFire = false;
		public boolean DisableTamedGiantessBreach = false;
		public boolean DisableTamedGiantessMobAggro = false;
		public boolean DisableEatDependsOnHealth = false;
		public boolean ChancedBasedGrabEscape = false;
		public boolean DisableAraTailSwipe = false;
		public boolean DisableAraTailSqueeze = false;
		public boolean DisableAraStep = false;
		public boolean DisableAraLowGrab = false;
		public boolean DisableAraTailWaistSlap = false;
		public boolean DisableAraWaistGrab = false;
		public boolean DisableAraChomp = false;
		public boolean DisableAraAboveAttack = false;
		public boolean DisableAraBreach = false;
		public boolean DisableAraEat = false;
		public boolean DisableGtsBiomeSpawn = false;

		public static MapVariables load(CompoundTag tag, HolderLookup.Provider lookupProvider) {
			MapVariables data = new MapVariables();
			data.read(tag, lookupProvider);
			return data;
		}

		public void read(CompoundTag nbt, HolderLookup.Provider lookupProvider) {
			DisableStep = nbt.getBoolean("DisableStep");
			DisableKick = nbt.getBoolean("DisableKick");
			DisableSit = nbt.getBoolean("DisableSit");
			DisableGunSlap = nbt.getBoolean("DisableGunSlap");
			DisableGunSmash = nbt.getBoolean("DisableGunSmash");
			DisableThrow = nbt.getBoolean("DisableThrow");
			DisableVore = nbt.getBoolean("DisableVore");
			DisableSniper = nbt.getBoolean("DisableSniper");
			DisableSpit = nbt.getBoolean("DisableSpit");
			DisableDrone = nbt.getBoolean("DisableDrone");
			DisableUpperBreach = nbt.getBoolean("DisableUpperBreach");
			DisableMiddleBreach = nbt.getBoolean("DisableMiddleBreach");
			DisableLowerBreach = nbt.getBoolean("DisableLowerBreach");
			DisableTrappedBreach = nbt.getBoolean("DisableTrappedBreach");
			DisableShowOff = nbt.getBoolean("DisableShowOff");
			DisableBelch = nbt.getBoolean("DisableBelch");
			DisableStomachSound = nbt.getBoolean("DisableStomachSound");
			DisableClothingDamage = nbt.getBoolean("DisableClothingDamage");
			DisableRaidWaveSizeChange = nbt.getBoolean("DisableRaidWaveSizeChange");
			Toki_TurnSpeed = nbt.getDouble("Toki_TurnSpeed");
			DisableLeavesBreakWalk = nbt.getBoolean("DisableLeavesBreakWalk");
			DisableLockOnTarget = nbt.getBoolean("DisableLockOnTarget");
			DisableSubmission = nbt.getBoolean("DisableSubmission");
			GtsNaturalSpawnHeight = nbt.getDouble("GtsNaturalSpawnHeight");
			DisableNaturalSpawn = nbt.getBoolean("DisableNaturalSpawn");
			DisableCandyStep = nbt.getBoolean("DisableCandyStep");
			DisableCandyShockWave = nbt.getBoolean("DisableCandyShockWave");
			DisableCandyKick = nbt.getBoolean("DisableCandyKick");
			DisableCandySwipe = nbt.getBoolean("DisableCandySwipe");
			DisableCandySmash = nbt.getBoolean("DisableCandySmash");
			DisableCandyBreach = nbt.getBoolean("DisableCandyBreach");
			DisableCandyVore = nbt.getBoolean("DisableCandyVore");
			DisableCandyHeartMagic = nbt.getBoolean("DisableCandyHeartMagic");
			DisableCandySpit = nbt.getBoolean("DisableCandySpit");
			DisableCandyThunderMagic = nbt.getBoolean("DisableCandyThunderMagic");
			Version211 = nbt.getBoolean("Version211");
			DisableTamedGiantessFriendlyFire = nbt.getBoolean("DisableTamedGiantessFriendlyFire");
			DisableTamedGiantessBreach = nbt.getBoolean("DisableTamedGiantessBreach");
			DisableTamedGiantessMobAggro = nbt.getBoolean("DisableTamedGiantessMobAggro");
			DisableEatDependsOnHealth = nbt.getBoolean("DisableEatDependsOnHealth");
			ChancedBasedGrabEscape = nbt.getBoolean("ChancedBasedGrabEscape");
			DisableAraTailSwipe = nbt.getBoolean("DisableAraTailSwipe");
			DisableAraTailSqueeze = nbt.getBoolean("DisableAraTailSqueeze");
			DisableAraStep = nbt.getBoolean("DisableAraStep");
			DisableAraLowGrab = nbt.getBoolean("DisableAraLowGrab");
			DisableAraTailWaistSlap = nbt.getBoolean("DisableAraTailWaistSlap");
			DisableAraWaistGrab = nbt.getBoolean("DisableAraWaistGrab");
			DisableAraChomp = nbt.getBoolean("DisableAraChomp");
			DisableAraAboveAttack = nbt.getBoolean("DisableAraAboveAttack");
			DisableAraBreach = nbt.getBoolean("DisableAraBreach");
			DisableAraEat = nbt.getBoolean("DisableAraEat");
			DisableGtsBiomeSpawn = nbt.getBoolean("DisableGtsBiomeSpawn");
		}

		@Override
		public CompoundTag save(CompoundTag nbt, HolderLookup.Provider lookupProvider) {
			nbt.putBoolean("DisableStep", DisableStep);
			nbt.putBoolean("DisableKick", DisableKick);
			nbt.putBoolean("DisableSit", DisableSit);
			nbt.putBoolean("DisableGunSlap", DisableGunSlap);
			nbt.putBoolean("DisableGunSmash", DisableGunSmash);
			nbt.putBoolean("DisableThrow", DisableThrow);
			nbt.putBoolean("DisableVore", DisableVore);
			nbt.putBoolean("DisableSniper", DisableSniper);
			nbt.putBoolean("DisableSpit", DisableSpit);
			nbt.putBoolean("DisableDrone", DisableDrone);
			nbt.putBoolean("DisableUpperBreach", DisableUpperBreach);
			nbt.putBoolean("DisableMiddleBreach", DisableMiddleBreach);
			nbt.putBoolean("DisableLowerBreach", DisableLowerBreach);
			nbt.putBoolean("DisableTrappedBreach", DisableTrappedBreach);
			nbt.putBoolean("DisableShowOff", DisableShowOff);
			nbt.putBoolean("DisableBelch", DisableBelch);
			nbt.putBoolean("DisableStomachSound", DisableStomachSound);
			nbt.putBoolean("DisableClothingDamage", DisableClothingDamage);
			nbt.putBoolean("DisableRaidWaveSizeChange", DisableRaidWaveSizeChange);
			nbt.putDouble("Toki_TurnSpeed", Toki_TurnSpeed);
			nbt.putBoolean("DisableLeavesBreakWalk", DisableLeavesBreakWalk);
			nbt.putBoolean("DisableLockOnTarget", DisableLockOnTarget);
			nbt.putBoolean("DisableSubmission", DisableSubmission);
			nbt.putDouble("GtsNaturalSpawnHeight", GtsNaturalSpawnHeight);
			nbt.putBoolean("DisableNaturalSpawn", DisableNaturalSpawn);
			nbt.putBoolean("DisableCandyStep", DisableCandyStep);
			nbt.putBoolean("DisableCandyShockWave", DisableCandyShockWave);
			nbt.putBoolean("DisableCandyKick", DisableCandyKick);
			nbt.putBoolean("DisableCandySwipe", DisableCandySwipe);
			nbt.putBoolean("DisableCandySmash", DisableCandySmash);
			nbt.putBoolean("DisableCandyBreach", DisableCandyBreach);
			nbt.putBoolean("DisableCandyVore", DisableCandyVore);
			nbt.putBoolean("DisableCandyHeartMagic", DisableCandyHeartMagic);
			nbt.putBoolean("DisableCandySpit", DisableCandySpit);
			nbt.putBoolean("DisableCandyThunderMagic", DisableCandyThunderMagic);
			nbt.putBoolean("Version211", Version211);
			nbt.putBoolean("DisableTamedGiantessFriendlyFire", DisableTamedGiantessFriendlyFire);
			nbt.putBoolean("DisableTamedGiantessBreach", DisableTamedGiantessBreach);
			nbt.putBoolean("DisableTamedGiantessMobAggro", DisableTamedGiantessMobAggro);
			nbt.putBoolean("DisableEatDependsOnHealth", DisableEatDependsOnHealth);
			nbt.putBoolean("ChancedBasedGrabEscape", ChancedBasedGrabEscape);
			nbt.putBoolean("DisableAraTailSwipe", DisableAraTailSwipe);
			nbt.putBoolean("DisableAraTailSqueeze", DisableAraTailSqueeze);
			nbt.putBoolean("DisableAraStep", DisableAraStep);
			nbt.putBoolean("DisableAraLowGrab", DisableAraLowGrab);
			nbt.putBoolean("DisableAraTailWaistSlap", DisableAraTailWaistSlap);
			nbt.putBoolean("DisableAraWaistGrab", DisableAraWaistGrab);
			nbt.putBoolean("DisableAraChomp", DisableAraChomp);
			nbt.putBoolean("DisableAraAboveAttack", DisableAraAboveAttack);
			nbt.putBoolean("DisableAraBreach", DisableAraBreach);
			nbt.putBoolean("DisableAraEat", DisableAraEat);
			nbt.putBoolean("DisableGtsBiomeSpawn", DisableGtsBiomeSpawn);
			return nbt;
		}

		public void syncData(LevelAccessor world) {
			this.setDirty();
			if (world instanceof Level && !world.isClientSide())
				PacketDistributor.sendToAllPlayers(new SavedDataSyncMessage(0, this));
		}

		static MapVariables clientSide = new MapVariables();

		public static MapVariables get(LevelAccessor world) {
			if (world instanceof ServerLevelAccessor serverLevelAcc) {
				return serverLevelAcc.getLevel().getServer().getLevel(Level.OVERWORLD).getDataStorage().computeIfAbsent(new SavedData.Factory<>(MapVariables::new, MapVariables::load), DATA_NAME);
			} else {
				return clientSide;
			}
		}
	}

	public record SavedDataSyncMessage(int dataType, SavedData data) implements CustomPacketPayload {
		public static final Type<SavedDataSyncMessage> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(GtsMod.MODID, "saved_data_sync"));
		public static final StreamCodec<RegistryFriendlyByteBuf, SavedDataSyncMessage> STREAM_CODEC = StreamCodec.of((RegistryFriendlyByteBuf buffer, SavedDataSyncMessage message) -> {
			buffer.writeInt(message.dataType);
			if (message.data != null)
				buffer.writeNbt(message.data.save(new CompoundTag(), buffer.registryAccess()));
		}, (RegistryFriendlyByteBuf buffer) -> {
			int dataType = buffer.readInt();
			CompoundTag nbt = buffer.readNbt();
			SavedData data = null;
			if (nbt != null) {
				data = dataType == 0 ? new MapVariables() : new WorldVariables();
				if (data instanceof MapVariables mapVariables)
					mapVariables.read(nbt, buffer.registryAccess());
				else if (data instanceof WorldVariables worldVariables)
					worldVariables.read(nbt, buffer.registryAccess());
			}
			return new SavedDataSyncMessage(dataType, data);
		});

		@Override
		public Type<SavedDataSyncMessage> type() {
			return TYPE;
		}

		public static void handleData(final SavedDataSyncMessage message, final IPayloadContext context) {
			if (context.flow() == PacketFlow.CLIENTBOUND && message.data != null) {
				context.enqueueWork(() -> {
					if (message.dataType == 0)
						MapVariables.clientSide.read(message.data.save(new CompoundTag(), context.player().registryAccess()), context.player().registryAccess());
					else
						WorldVariables.clientSide.read(message.data.save(new CompoundTag(), context.player().registryAccess()), context.player().registryAccess());
				}).exceptionally(e -> {
					context.connection().disconnect(Component.literal(e.getMessage()));
					return null;
				});
			}
		}
	}

	public static class PlayerVariables implements INBTSerializable<CompoundTag> {
		public double Player_Speed = 0;

		@Override
		public CompoundTag serializeNBT(HolderLookup.Provider lookupProvider) {
			CompoundTag nbt = new CompoundTag();
			nbt.putDouble("Player_Speed", Player_Speed);
			return nbt;
		}

		@Override
		public void deserializeNBT(HolderLookup.Provider lookupProvider, CompoundTag nbt) {
			Player_Speed = nbt.getDouble("Player_Speed");
		}

		public void syncPlayerVariables(Entity entity) {
			if (entity instanceof ServerPlayer serverPlayer)
				PacketDistributor.sendToPlayer(serverPlayer, new PlayerVariablesSyncMessage(this));
		}
	}

	public record PlayerVariablesSyncMessage(PlayerVariables data) implements CustomPacketPayload {
		public static final Type<PlayerVariablesSyncMessage> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(GtsMod.MODID, "player_variables_sync"));
		public static final StreamCodec<RegistryFriendlyByteBuf, PlayerVariablesSyncMessage> STREAM_CODEC = StreamCodec
				.of((RegistryFriendlyByteBuf buffer, PlayerVariablesSyncMessage message) -> buffer.writeNbt(message.data().serializeNBT(buffer.registryAccess())), (RegistryFriendlyByteBuf buffer) -> {
					PlayerVariablesSyncMessage message = new PlayerVariablesSyncMessage(new PlayerVariables());
					message.data.deserializeNBT(buffer.registryAccess(), buffer.readNbt());
					return message;
				});

		@Override
		public Type<PlayerVariablesSyncMessage> type() {
			return TYPE;
		}

		public static void handleData(final PlayerVariablesSyncMessage message, final IPayloadContext context) {
			if (context.flow() == PacketFlow.CLIENTBOUND && message.data != null) {
				context.enqueueWork(() -> context.player().getData(PLAYER_VARIABLES).deserializeNBT(context.player().registryAccess(), message.data.serializeNBT(context.player().registryAccess()))).exceptionally(e -> {
					context.connection().disconnect(Component.literal(e.getMessage()));
					return null;
				});
			}
		}
	}
}
