package studio.halbear.hem;

import net.minecraftforge.fml.network.PacketDistributor;
import net.minecraftforge.fml.network.NetworkEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.event.AttachCapabilitiesEvent;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.common.util.FakePlayer;
import net.minecraftforge.common.capabilities.ICapabilitySerializable;
import net.minecraftforge.common.capabilities.CapabilityManager;
import net.minecraftforge.common.capabilities.CapabilityInject;
import net.minecraftforge.common.capabilities.Capability;

import net.minecraft.world.storage.WorldSavedData;
import net.minecraft.world.server.ServerWorld;
import net.minecraft.world.World;
import net.minecraft.world.IWorld;
import net.minecraft.world.IServerWorld;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.Direction;
import net.minecraft.network.PacketBuffer;
import net.minecraft.nbt.INBT;
import net.minecraft.nbt.CompoundNBT;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.Entity;
import net.minecraft.client.Minecraft;

import java.util.function.Supplier;

public class HemModVariables {
	public HemModVariables(HemModElements elements) {
		elements.addNetworkMessage(WorldSavedDataSyncMessage.class, WorldSavedDataSyncMessage::buffer, WorldSavedDataSyncMessage::new,
				WorldSavedDataSyncMessage::handler);
		elements.addNetworkMessage(PlayerVariablesSyncMessage.class, PlayerVariablesSyncMessage::buffer, PlayerVariablesSyncMessage::new,
				PlayerVariablesSyncMessage::handler);
		FMLJavaModLoadingContext.get().getModEventBus().addListener(this::init);
	}

	private void init(FMLCommonSetupEvent event) {
		CapabilityManager.INSTANCE.register(PlayerVariables.class, new PlayerVariablesStorage(), PlayerVariables::new);
	}

	@SubscribeEvent
	public void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) {
		if (!event.getPlayer().world.isRemote()) {
			WorldSavedData mapdata = MapVariables.get(event.getPlayer().world);
			WorldSavedData worlddata = WorldVariables.get(event.getPlayer().world);
			if (mapdata != null)
				HemMod.PACKET_HANDLER.send(PacketDistributor.PLAYER.with(() -> (ServerPlayerEntity) event.getPlayer()),
						new WorldSavedDataSyncMessage(0, mapdata));
			if (worlddata != null)
				HemMod.PACKET_HANDLER.send(PacketDistributor.PLAYER.with(() -> (ServerPlayerEntity) event.getPlayer()),
						new WorldSavedDataSyncMessage(1, worlddata));
		}
	}

	@SubscribeEvent
	public void onPlayerChangedDimension(PlayerEvent.PlayerChangedDimensionEvent event) {
		if (!event.getPlayer().world.isRemote()) {
			WorldSavedData worlddata = WorldVariables.get(event.getPlayer().world);
			if (worlddata != null)
				HemMod.PACKET_HANDLER.send(PacketDistributor.PLAYER.with(() -> (ServerPlayerEntity) event.getPlayer()),
						new WorldSavedDataSyncMessage(1, worlddata));
		}
	}

	public static class WorldVariables extends WorldSavedData {
		public static final String DATA_NAME = "hem_worldvars";
		public boolean EmberleafMilitaryDefeated = false;
		public double VehicleID = 0;

		public WorldVariables() {
			super(DATA_NAME);
		}

		public WorldVariables(String s) {
			super(s);
		}

		@Override
		public void read(CompoundNBT nbt) {
			EmberleafMilitaryDefeated = nbt.getBoolean("EmberleafMilitaryDefeated");
			VehicleID = nbt.getDouble("VehicleID");
		}

		@Override
		public CompoundNBT write(CompoundNBT nbt) {
			nbt.putBoolean("EmberleafMilitaryDefeated", EmberleafMilitaryDefeated);
			nbt.putDouble("VehicleID", VehicleID);
			return nbt;
		}

		public void syncData(IWorld world) {
			this.markDirty();
			if (world instanceof World && !world.isRemote())
				HemMod.PACKET_HANDLER.send(PacketDistributor.DIMENSION.with(((World) world)::getDimensionKey),
						new WorldSavedDataSyncMessage(1, this));
		}

		static WorldVariables clientSide = new WorldVariables();

		public static WorldVariables get(IWorld world) {
			if (world instanceof ServerWorld) {
				return ((ServerWorld) world).getSavedData().getOrCreate(WorldVariables::new, DATA_NAME);
			} else {
				return clientSide;
			}
		}
	}

	public static class MapVariables extends WorldSavedData {
		public static final String DATA_NAME = "hem_mapvars";

		public MapVariables() {
			super(DATA_NAME);
		}

		public MapVariables(String s) {
			super(s);
		}

		@Override
		public void read(CompoundNBT nbt) {
		}

		@Override
		public CompoundNBT write(CompoundNBT nbt) {
			return nbt;
		}

		public void syncData(IWorld world) {
			this.markDirty();
			if (world instanceof World && !world.isRemote())
				HemMod.PACKET_HANDLER.send(PacketDistributor.ALL.noArg(), new WorldSavedDataSyncMessage(0, this));
		}

		static MapVariables clientSide = new MapVariables();

		public static MapVariables get(IWorld world) {
			if (world instanceof IServerWorld) {
				return ((IServerWorld) world).getWorld().getServer().getWorld(World.OVERWORLD).getSavedData().getOrCreate(MapVariables::new,
						DATA_NAME);
			} else {
				return clientSide;
			}
		}
	}

	public static class WorldSavedDataSyncMessage {
		public int type;
		public WorldSavedData data;

		public WorldSavedDataSyncMessage(PacketBuffer buffer) {
			this.type = buffer.readInt();
			this.data = this.type == 0 ? new MapVariables() : new WorldVariables();
			this.data.read(buffer.readCompoundTag());
		}

		public WorldSavedDataSyncMessage(int type, WorldSavedData data) {
			this.type = type;
			this.data = data;
		}

		public static void buffer(WorldSavedDataSyncMessage message, PacketBuffer buffer) {
			buffer.writeInt(message.type);
			buffer.writeCompoundTag(message.data.write(new CompoundNBT()));
		}

		public static void handler(WorldSavedDataSyncMessage message, Supplier<NetworkEvent.Context> contextSupplier) {
			NetworkEvent.Context context = contextSupplier.get();
			context.enqueueWork(() -> {
				if (!context.getDirection().getReceptionSide().isServer()) {
					if (message.type == 0)
						MapVariables.clientSide = (MapVariables) message.data;
					else
						WorldVariables.clientSide = (WorldVariables) message.data;
				}
			});
			context.setPacketHandled(true);
		}
	}

	@CapabilityInject(PlayerVariables.class)
	public static Capability<PlayerVariables> PLAYER_VARIABLES_CAPABILITY = null;

	@SubscribeEvent
	public void onAttachCapabilities(AttachCapabilitiesEvent<Entity> event) {
		if (event.getObject() instanceof PlayerEntity && !(event.getObject() instanceof FakePlayer))
			event.addCapability(new ResourceLocation("hem", "player_variables"), new PlayerVariablesProvider());
	}

	private static class PlayerVariablesProvider implements ICapabilitySerializable<INBT> {
		private final LazyOptional<PlayerVariables> instance = LazyOptional.of(PLAYER_VARIABLES_CAPABILITY::getDefaultInstance);

		@Override
		public <T> LazyOptional<T> getCapability(Capability<T> cap, Direction side) {
			return cap == PLAYER_VARIABLES_CAPABILITY ? instance.cast() : LazyOptional.empty();
		}

		@Override
		public INBT serializeNBT() {
			return PLAYER_VARIABLES_CAPABILITY.getStorage().writeNBT(PLAYER_VARIABLES_CAPABILITY, this.instance.orElseThrow(RuntimeException::new),
					null);
		}

		@Override
		public void deserializeNBT(INBT nbt) {
			PLAYER_VARIABLES_CAPABILITY.getStorage().readNBT(PLAYER_VARIABLES_CAPABILITY, this.instance.orElseThrow(RuntimeException::new), null,
					nbt);
		}
	}

	private static class PlayerVariablesStorage implements Capability.IStorage<PlayerVariables> {
		@Override
		public INBT writeNBT(Capability<PlayerVariables> capability, PlayerVariables instance, Direction side) {
			CompoundNBT nbt = new CompoundNBT();
			nbt.putDouble("BodyTemperature", instance.BodyTemperature);
			nbt.putBoolean("DisableClientSideBlocks", instance.DisableClientSideBlocks);
			nbt.putBoolean("Eaten", instance.Eaten);
			nbt.putDouble("Fuel", instance.Fuel);
			nbt.putDouble("GoalMPH", instance.GoalMPH);
			nbt.putDouble("GooCooldown", instance.GooCooldown);
			nbt.putDouble("Heat", instance.Heat);
			nbt.putDouble("LastPlantTrapX", instance.LastPlantTrapX);
			nbt.putDouble("LastPlantTrapY", instance.LastPlantTrapY);
			nbt.putDouble("LastPlantTrapZ", instance.LastPlantTrapZ);
			nbt.putDouble("LastPosX", instance.LastPosX);
			nbt.putDouble("LastPosZ", instance.LastPosZ);
			nbt.putDouble("MPH", instance.MPH);
			nbt.putBoolean("PilotingVehicle", instance.PilotingVehicle);
			nbt.putDouble("PilotingVehicleID", instance.PilotingVehicleID);
			nbt.putDouble("PSI", instance.PSI);
			nbt.putDouble("Speed", instance.Speed);
			nbt.putBoolean("VehicleAccelerate", instance.VehicleAccelerate);
			nbt.putBoolean("VehicleBackward", instance.VehicleBackward);
			nbt.putBoolean("VehicleDecelerate", instance.VehicleDecelerate);
			nbt.putBoolean("VehicleForward", instance.VehicleForward);
			nbt.putBoolean("VehicleIncreasePressure", instance.VehicleIncreasePressure);
			nbt.putDouble("VehicleOdometer", instance.VehicleOdometer);
			nbt.putDouble("VehicleOdometerMetres", instance.VehicleOdometerMetres);
			nbt.putBoolean("VehicleReleasePressure", instance.VehicleReleasePressure);
			nbt.putBoolean("VehicleStrafeLeft", instance.VehicleStrafeLeft);
			nbt.putBoolean("VehicleStrafeRight", instance.VehicleStrafeRight);
			nbt.putBoolean("VehicleTurnLeft", instance.VehicleTurnLeft);
			nbt.putBoolean("VehicleTurnRight", instance.VehicleTurnRight);
			nbt.putDouble("VehicleYaw", instance.VehicleYaw);
			nbt.putDouble("Altimeter", instance.Altimeter);
			return nbt;
		}

		@Override
		public void readNBT(Capability<PlayerVariables> capability, PlayerVariables instance, Direction side, INBT inbt) {
			CompoundNBT nbt = (CompoundNBT) inbt;
			instance.BodyTemperature = nbt.getDouble("BodyTemperature");
			instance.DisableClientSideBlocks = nbt.getBoolean("DisableClientSideBlocks");
			instance.Eaten = nbt.getBoolean("Eaten");
			instance.Fuel = nbt.getDouble("Fuel");
			instance.GoalMPH = nbt.getDouble("GoalMPH");
			instance.GooCooldown = nbt.getDouble("GooCooldown");
			instance.Heat = nbt.getDouble("Heat");
			instance.LastPlantTrapX = nbt.getDouble("LastPlantTrapX");
			instance.LastPlantTrapY = nbt.getDouble("LastPlantTrapY");
			instance.LastPlantTrapZ = nbt.getDouble("LastPlantTrapZ");
			instance.LastPosX = nbt.getDouble("LastPosX");
			instance.LastPosZ = nbt.getDouble("LastPosZ");
			instance.MPH = nbt.getDouble("MPH");
			instance.PilotingVehicle = nbt.getBoolean("PilotingVehicle");
			instance.PilotingVehicleID = nbt.getDouble("PilotingVehicleID");
			instance.PSI = nbt.getDouble("PSI");
			instance.Speed = nbt.getDouble("Speed");
			instance.VehicleAccelerate = nbt.getBoolean("VehicleAccelerate");
			instance.VehicleBackward = nbt.getBoolean("VehicleBackward");
			instance.VehicleDecelerate = nbt.getBoolean("VehicleDecelerate");
			instance.VehicleForward = nbt.getBoolean("VehicleForward");
			instance.VehicleIncreasePressure = nbt.getBoolean("VehicleIncreasePressure");
			instance.VehicleOdometer = nbt.getDouble("VehicleOdometer");
			instance.VehicleOdometerMetres = nbt.getDouble("VehicleOdometerMetres");
			instance.VehicleReleasePressure = nbt.getBoolean("VehicleReleasePressure");
			instance.VehicleStrafeLeft = nbt.getBoolean("VehicleStrafeLeft");
			instance.VehicleStrafeRight = nbt.getBoolean("VehicleStrafeRight");
			instance.VehicleTurnLeft = nbt.getBoolean("VehicleTurnLeft");
			instance.VehicleTurnRight = nbt.getBoolean("VehicleTurnRight");
			instance.VehicleYaw = nbt.getDouble("VehicleYaw");
			instance.Altimeter = nbt.getDouble("Altimeter");
		}
	}

	public static class PlayerVariables {
		public double BodyTemperature = 37.0;
		public boolean DisableClientSideBlocks = false;
		public boolean Eaten = false;
		public double Fuel = 0;
		public double GoalMPH = 0;
		public double GooCooldown = 0;
		public double Heat = 0;
		public double LastPlantTrapX = 0;
		public double LastPlantTrapY = 0;
		public double LastPlantTrapZ = 0;
		public double LastPosX = 0;
		public double LastPosZ = 0;
		public double MPH = 0;
		public boolean PilotingVehicle = false;
		public double PilotingVehicleID = 0;
		public double PSI = 0;
		public double Speed = 0;
		public boolean VehicleAccelerate = false;
		public boolean VehicleBackward = false;
		public boolean VehicleDecelerate = false;
		public boolean VehicleForward = false;
		public boolean VehicleIncreasePressure = false;
		public double VehicleOdometer = 0;
		public double VehicleOdometerMetres = 0;
		public boolean VehicleReleasePressure = false;
		public boolean VehicleStrafeLeft = false;
		public boolean VehicleStrafeRight = false;
		public boolean VehicleTurnLeft = false;
		public boolean VehicleTurnRight = false;
		public double VehicleYaw = 0;
		public double Altimeter = 0;

		public void syncPlayerVariables(Entity entity) {
			if (entity instanceof ServerPlayerEntity)
				HemMod.PACKET_HANDLER.send(PacketDistributor.PLAYER.with(() -> (ServerPlayerEntity) entity), new PlayerVariablesSyncMessage(this));
		}
	}

	@SubscribeEvent
	public void onPlayerLoggedInSyncPlayerVariables(PlayerEvent.PlayerLoggedInEvent event) {
		if (!event.getPlayer().world.isRemote())
			((PlayerVariables) event.getPlayer().getCapability(PLAYER_VARIABLES_CAPABILITY, null).orElse(new PlayerVariables()))
					.syncPlayerVariables(event.getPlayer());
	}

	@SubscribeEvent
	public void onPlayerRespawnedSyncPlayerVariables(PlayerEvent.PlayerRespawnEvent event) {
		if (!event.getPlayer().world.isRemote())
			((PlayerVariables) event.getPlayer().getCapability(PLAYER_VARIABLES_CAPABILITY, null).orElse(new PlayerVariables()))
					.syncPlayerVariables(event.getPlayer());
	}

	@SubscribeEvent
	public void onPlayerChangedDimensionSyncPlayerVariables(PlayerEvent.PlayerChangedDimensionEvent event) {
		if (!event.getPlayer().world.isRemote())
			((PlayerVariables) event.getPlayer().getCapability(PLAYER_VARIABLES_CAPABILITY, null).orElse(new PlayerVariables()))
					.syncPlayerVariables(event.getPlayer());
	}

	@SubscribeEvent
	public void clonePlayer(PlayerEvent.Clone event) {
		PlayerVariables original = ((PlayerVariables) event.getOriginal().getCapability(PLAYER_VARIABLES_CAPABILITY, null)
				.orElse(new PlayerVariables()));
		PlayerVariables clone = ((PlayerVariables) event.getEntity().getCapability(PLAYER_VARIABLES_CAPABILITY, null).orElse(new PlayerVariables()));
		clone.BodyTemperature = original.BodyTemperature;
		clone.DisableClientSideBlocks = original.DisableClientSideBlocks;
		clone.Eaten = original.Eaten;
		clone.Fuel = original.Fuel;
		clone.GoalMPH = original.GoalMPH;
		clone.GooCooldown = original.GooCooldown;
		clone.Heat = original.Heat;
		clone.LastPlantTrapX = original.LastPlantTrapX;
		clone.LastPlantTrapY = original.LastPlantTrapY;
		clone.LastPlantTrapZ = original.LastPlantTrapZ;
		clone.LastPosX = original.LastPosX;
		clone.LastPosZ = original.LastPosZ;
		clone.MPH = original.MPH;
		clone.PilotingVehicle = original.PilotingVehicle;
		clone.PilotingVehicleID = original.PilotingVehicleID;
		clone.PSI = original.PSI;
		clone.Speed = original.Speed;
		clone.VehicleAccelerate = original.VehicleAccelerate;
		clone.VehicleBackward = original.VehicleBackward;
		clone.VehicleDecelerate = original.VehicleDecelerate;
		clone.VehicleForward = original.VehicleForward;
		clone.VehicleIncreasePressure = original.VehicleIncreasePressure;
		clone.VehicleOdometer = original.VehicleOdometer;
		clone.VehicleOdometerMetres = original.VehicleOdometerMetres;
		clone.VehicleReleasePressure = original.VehicleReleasePressure;
		clone.VehicleStrafeLeft = original.VehicleStrafeLeft;
		clone.VehicleStrafeRight = original.VehicleStrafeRight;
		clone.VehicleTurnLeft = original.VehicleTurnLeft;
		clone.VehicleTurnRight = original.VehicleTurnRight;
		clone.VehicleYaw = original.VehicleYaw;
		clone.Altimeter = original.Altimeter;
		if (!event.isWasDeath()) {
		}
	}

	public static class PlayerVariablesSyncMessage {
		public PlayerVariables data;

		public PlayerVariablesSyncMessage(PacketBuffer buffer) {
			this.data = new PlayerVariables();
			new PlayerVariablesStorage().readNBT(null, this.data, null, buffer.readCompoundTag());
		}

		public PlayerVariablesSyncMessage(PlayerVariables data) {
			this.data = data;
		}

		public static void buffer(PlayerVariablesSyncMessage message, PacketBuffer buffer) {
			buffer.writeCompoundTag((CompoundNBT) new PlayerVariablesStorage().writeNBT(null, message.data, null));
		}

		public static void handler(PlayerVariablesSyncMessage message, Supplier<NetworkEvent.Context> contextSupplier) {
			NetworkEvent.Context context = contextSupplier.get();
			context.enqueueWork(() -> {
				if (!context.getDirection().getReceptionSide().isServer()) {
					PlayerVariables variables = ((PlayerVariables) Minecraft.getInstance().player.getCapability(PLAYER_VARIABLES_CAPABILITY, null)
							.orElse(new PlayerVariables()));
					variables.BodyTemperature = message.data.BodyTemperature;
					variables.DisableClientSideBlocks = message.data.DisableClientSideBlocks;
					variables.Eaten = message.data.Eaten;
					variables.Fuel = message.data.Fuel;
					variables.GoalMPH = message.data.GoalMPH;
					variables.GooCooldown = message.data.GooCooldown;
					variables.Heat = message.data.Heat;
					variables.LastPlantTrapX = message.data.LastPlantTrapX;
					variables.LastPlantTrapY = message.data.LastPlantTrapY;
					variables.LastPlantTrapZ = message.data.LastPlantTrapZ;
					variables.LastPosX = message.data.LastPosX;
					variables.LastPosZ = message.data.LastPosZ;
					variables.MPH = message.data.MPH;
					variables.PilotingVehicle = message.data.PilotingVehicle;
					variables.PilotingVehicleID = message.data.PilotingVehicleID;
					variables.PSI = message.data.PSI;
					variables.Speed = message.data.Speed;
					variables.VehicleAccelerate = message.data.VehicleAccelerate;
					variables.VehicleBackward = message.data.VehicleBackward;
					variables.VehicleDecelerate = message.data.VehicleDecelerate;
					variables.VehicleForward = message.data.VehicleForward;
					variables.VehicleIncreasePressure = message.data.VehicleIncreasePressure;
					variables.VehicleOdometer = message.data.VehicleOdometer;
					variables.VehicleOdometerMetres = message.data.VehicleOdometerMetres;
					variables.VehicleReleasePressure = message.data.VehicleReleasePressure;
					variables.VehicleStrafeLeft = message.data.VehicleStrafeLeft;
					variables.VehicleStrafeRight = message.data.VehicleStrafeRight;
					variables.VehicleTurnLeft = message.data.VehicleTurnLeft;
					variables.VehicleTurnRight = message.data.VehicleTurnRight;
					variables.VehicleYaw = message.data.VehicleYaw;
					variables.Altimeter = message.data.Altimeter;
				}
			});
			context.setPacketHandled(true);
		}
	}
}
