package net.mcreator.puzzle_code.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.block.state.BlockState;
import net.minecraft.world.level.block.Blocks;
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.NbtUtils;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.HolderLookup;

import net.mcreator.puzzle_code.PuzzleCodeMod;

import java.util.function.Supplier;

@EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD)
public class PuzzleCodeModVariables {
	public static final DeferredRegister<AttachmentType<?>> ATTACHMENT_TYPES = DeferredRegister.create(NeoForgeRegistries.Keys.ATTACHMENT_TYPES, PuzzleCodeMod.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) {
		PuzzleCodeMod.addNetworkMessage(SavedDataSyncMessage.TYPE, SavedDataSyncMessage.STREAM_CODEC, SavedDataSyncMessage::handleData);
		PuzzleCodeMod.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.IsSwitchedYellow = original.IsSwitchedYellow;
				clone.YellowSwitcherEffectPower = original.YellowSwitcherEffectPower;
				clone.positionSetXPos = original.positionSetXPos;
				clone.positionSetYPos = original.positionSetYPos;
				clone.positionSetZPos = original.positionSetZPos;
				clone.positionSetXPos2 = original.positionSetXPos2;
				clone.positionSetYPos2 = original.positionSetYPos2;
				clone.positionSetZPos2 = original.positionSetZPos2;
				clone.NBTVerifierNBT = original.NBTVerifierNBT;
				clone.NBTVerifierCurrentValue = original.NBTVerifierCurrentValue;
				clone.NBTVerifierVariableType = original.NBTVerifierVariableType;
				clone.NBTVerifierXPosBlock = original.NBTVerifierXPosBlock;
				clone.NBTVerifierYPosBlock = original.NBTVerifierYPosBlock;
				clone.NBTVerifierZPosBlock = original.NBTVerifierZPosBlock;
			}
			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 = "puzzle_code_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 = "puzzle_code_mapvars";
		public boolean IsSwitchedBlue = false;
		public boolean IsTimedSwitched = false;
		public BlockState ABlockIsBrokenBlockstate = Blocks.AIR.defaultBlockState();
		public double ABlockIsBrokenX_Pos = 0.0;
		public double ABlockIsBrokenY_Pos = 0.0;
		public double ABlockIsBrokenZ_Pos = 0.0;
		public BlockState ABlockIsPlacedBlockstate = Blocks.AIR.defaultBlockState();
		public double ABlockIsPlacedX_Pos = 0;
		public double ABlockIsPlacedY_Pos = 0;
		public double ABlockIsPlacedZ_Pos = 0.0;
		public double TimedSwitchingTime = 0.0;
		public double ABlockIsRightClickedX_Pos = 0.0;
		public double ABlockIsRightClickedY_Pos = 0.0;
		public double ABlockIsRightClickedZ_Pos = 0.0;
		public BlockState ABlockIsRightClickedBlockstate = Blocks.AIR.defaultBlockState();
		public double ABlockIsLeftClickedX_Pos = 0.0;
		public double ABlockIsLeftClickedY_Pos = 0;
		public double ABlockIsLeftClickedZ_Pos = 0;
		public BlockState ABlockIsLeftClickedBlockstate = Blocks.AIR.defaultBlockState();

		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) {
			IsSwitchedBlue = nbt.getBoolean("IsSwitchedBlue");
			IsTimedSwitched = nbt.getBoolean("IsTimedSwitched");
			ABlockIsBrokenBlockstate = NbtUtils.readBlockState(lookupProvider.lookupOrThrow(BuiltInRegistries.BLOCK.key()), nbt.getCompound("ABlockIsBrokenBlockstate"));
			ABlockIsBrokenX_Pos = nbt.getDouble("ABlockIsBrokenX_Pos");
			ABlockIsBrokenY_Pos = nbt.getDouble("ABlockIsBrokenY_Pos");
			ABlockIsBrokenZ_Pos = nbt.getDouble("ABlockIsBrokenZ_Pos");
			ABlockIsPlacedBlockstate = NbtUtils.readBlockState(lookupProvider.lookupOrThrow(BuiltInRegistries.BLOCK.key()), nbt.getCompound("ABlockIsPlacedBlockstate"));
			ABlockIsPlacedX_Pos = nbt.getDouble("ABlockIsPlacedX_Pos");
			ABlockIsPlacedY_Pos = nbt.getDouble("ABlockIsPlacedY_Pos");
			ABlockIsPlacedZ_Pos = nbt.getDouble("ABlockIsPlacedZ_Pos");
			TimedSwitchingTime = nbt.getDouble("TimedSwitchingTime");
			ABlockIsRightClickedX_Pos = nbt.getDouble("ABlockIsRightClickedX_Pos");
			ABlockIsRightClickedY_Pos = nbt.getDouble("ABlockIsRightClickedY_Pos");
			ABlockIsRightClickedZ_Pos = nbt.getDouble("ABlockIsRightClickedZ_Pos");
			ABlockIsRightClickedBlockstate = NbtUtils.readBlockState(lookupProvider.lookupOrThrow(BuiltInRegistries.BLOCK.key()), nbt.getCompound("ABlockIsRightClickedBlockstate"));
			ABlockIsLeftClickedX_Pos = nbt.getDouble("ABlockIsLeftClickedX_Pos");
			ABlockIsLeftClickedY_Pos = nbt.getDouble("ABlockIsLeftClickedY_Pos");
			ABlockIsLeftClickedZ_Pos = nbt.getDouble("ABlockIsLeftClickedZ_Pos");
			ABlockIsLeftClickedBlockstate = NbtUtils.readBlockState(lookupProvider.lookupOrThrow(BuiltInRegistries.BLOCK.key()), nbt.getCompound("ABlockIsLeftClickedBlockstate"));
		}

		@Override
		public CompoundTag save(CompoundTag nbt, HolderLookup.Provider lookupProvider) {
			nbt.putBoolean("IsSwitchedBlue", IsSwitchedBlue);
			nbt.putBoolean("IsTimedSwitched", IsTimedSwitched);
			nbt.put("ABlockIsBrokenBlockstate", NbtUtils.writeBlockState(ABlockIsBrokenBlockstate));
			nbt.putDouble("ABlockIsBrokenX_Pos", ABlockIsBrokenX_Pos);
			nbt.putDouble("ABlockIsBrokenY_Pos", ABlockIsBrokenY_Pos);
			nbt.putDouble("ABlockIsBrokenZ_Pos", ABlockIsBrokenZ_Pos);
			nbt.put("ABlockIsPlacedBlockstate", NbtUtils.writeBlockState(ABlockIsPlacedBlockstate));
			nbt.putDouble("ABlockIsPlacedX_Pos", ABlockIsPlacedX_Pos);
			nbt.putDouble("ABlockIsPlacedY_Pos", ABlockIsPlacedY_Pos);
			nbt.putDouble("ABlockIsPlacedZ_Pos", ABlockIsPlacedZ_Pos);
			nbt.putDouble("TimedSwitchingTime", TimedSwitchingTime);
			nbt.putDouble("ABlockIsRightClickedX_Pos", ABlockIsRightClickedX_Pos);
			nbt.putDouble("ABlockIsRightClickedY_Pos", ABlockIsRightClickedY_Pos);
			nbt.putDouble("ABlockIsRightClickedZ_Pos", ABlockIsRightClickedZ_Pos);
			nbt.put("ABlockIsRightClickedBlockstate", NbtUtils.writeBlockState(ABlockIsRightClickedBlockstate));
			nbt.putDouble("ABlockIsLeftClickedX_Pos", ABlockIsLeftClickedX_Pos);
			nbt.putDouble("ABlockIsLeftClickedY_Pos", ABlockIsLeftClickedY_Pos);
			nbt.putDouble("ABlockIsLeftClickedZ_Pos", ABlockIsLeftClickedZ_Pos);
			nbt.put("ABlockIsLeftClickedBlockstate", NbtUtils.writeBlockState(ABlockIsLeftClickedBlockstate));
			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(PuzzleCodeMod.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 boolean IsSwitchedYellow = false;
		public double YellowSwitcherEffectPower = -1.0;
		public double positionSetXPos = 0;
		public double positionSetYPos = 0;
		public double positionSetZPos = 0;
		public double positionSetXPos2 = 0;
		public double positionSetYPos2 = 0;
		public double positionSetZPos2 = 0.0;
		public String NBTVerifierNBT = "none";
		public String NBTVerifierCurrentValue = "none";
		public String NBTVerifierVariableType = "Number";
		public double NBTVerifierXPosBlock = 0;
		public double NBTVerifierYPosBlock = 0;
		public double NBTVerifierZPosBlock = 0;

		@Override
		public CompoundTag serializeNBT(HolderLookup.Provider lookupProvider) {
			CompoundTag nbt = new CompoundTag();
			nbt.putBoolean("IsSwitchedYellow", IsSwitchedYellow);
			nbt.putDouble("YellowSwitcherEffectPower", YellowSwitcherEffectPower);
			nbt.putDouble("positionSetXPos", positionSetXPos);
			nbt.putDouble("positionSetYPos", positionSetYPos);
			nbt.putDouble("positionSetZPos", positionSetZPos);
			nbt.putDouble("positionSetXPos2", positionSetXPos2);
			nbt.putDouble("positionSetYPos2", positionSetYPos2);
			nbt.putDouble("positionSetZPos2", positionSetZPos2);
			nbt.putString("NBTVerifierNBT", NBTVerifierNBT);
			nbt.putString("NBTVerifierCurrentValue", NBTVerifierCurrentValue);
			nbt.putString("NBTVerifierVariableType", NBTVerifierVariableType);
			nbt.putDouble("NBTVerifierXPosBlock", NBTVerifierXPosBlock);
			nbt.putDouble("NBTVerifierYPosBlock", NBTVerifierYPosBlock);
			nbt.putDouble("NBTVerifierZPosBlock", NBTVerifierZPosBlock);
			return nbt;
		}

		@Override
		public void deserializeNBT(HolderLookup.Provider lookupProvider, CompoundTag nbt) {
			IsSwitchedYellow = nbt.getBoolean("IsSwitchedYellow");
			YellowSwitcherEffectPower = nbt.getDouble("YellowSwitcherEffectPower");
			positionSetXPos = nbt.getDouble("positionSetXPos");
			positionSetYPos = nbt.getDouble("positionSetYPos");
			positionSetZPos = nbt.getDouble("positionSetZPos");
			positionSetXPos2 = nbt.getDouble("positionSetXPos2");
			positionSetYPos2 = nbt.getDouble("positionSetYPos2");
			positionSetZPos2 = nbt.getDouble("positionSetZPos2");
			NBTVerifierNBT = nbt.getString("NBTVerifierNBT");
			NBTVerifierCurrentValue = nbt.getString("NBTVerifierCurrentValue");
			NBTVerifierVariableType = nbt.getString("NBTVerifierVariableType");
			NBTVerifierXPosBlock = nbt.getDouble("NBTVerifierXPosBlock");
			NBTVerifierYPosBlock = nbt.getDouble("NBTVerifierYPosBlock");
			NBTVerifierZPosBlock = nbt.getDouble("NBTVerifierZPosBlock");
		}

		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(PuzzleCodeMod.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;
				});
			}
		}
	}
}
