package com.infinityraider.infinitylib.block.tile;

import com.google.common.collect.Maps;
import com.infinityraider.infinitylib.InfinityLib;
import com.infinityraider.infinitylib.network.MessageAutoSyncTileField;
import com.infinityraider.infinitylib.network.MessageSyncTile;
import net.minecraft.block.BlockState;
import net.minecraft.nbt.CompoundNBT;
import net.minecraft.network.NetworkManager;
import net.minecraft.network.play.server.SUpdateTileEntityPacket;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.tileentity.TileEntityType;
import net.minecraft.world.World;
import net.minecraftforge.fml.LogicalSide;

import javax.annotation.Nonnull;
import java.util.Map;
import java.util.Random;
import java.util.function.BiConsumer;
import java.util.function.Function;

@SuppressWarnings("unused")
public abstract class TileEntityBase extends TileEntity {
    private static final Random RANDOM = new Random();

    private final Map<Integer, AutoSyncedField<?>> syncedFields;

    public TileEntityBase(TileEntityType<?> tileEntityTypeIn) {
        super(tileEntityTypeIn);
        this.syncedFields = Maps.newHashMap();
    }

    public final int xCoord() {
        return this.getPos().getX();
    }

    public final int yCoord() {
        return this.getPos().getY();
    }

    public final int zCoord() {
        return this.getPos().getZ();
    }

    public Random getRandom() {
        return this.getWorld() == null ? RANDOM : this.getWorld().rand;
    }

    public boolean isRemote() {
        return this.getWorld() != null && this.getWorld().isRemote;
    }

    @Override
    public SUpdateTileEntityPacket getUpdatePacket() {
        return new SUpdateTileEntityPacket(this.getPos(), -1, this.getUpdateTag()); //TODO: figure out the int argument
    }

    @Nonnull
    @Override
    public CompoundNBT getUpdateTag() {
        CompoundNBT tag = new CompoundNBT();
        this.write(tag);
        return tag;
    }

    //read data from packet
    @Override
    public void onDataPacket(NetworkManager networkManager, SUpdateTileEntityPacket pkt){
        if(this.getWorld() == null) {
            return;
        }
        BlockState before = this.getBlockState();
        this.read(before, pkt.getNbtCompound());
        BlockState after = this.getWorld().getBlockState(pkt.getPos());
        if(!after.equals(before)) {
            this.getWorld().markBlockRangeForRenderUpdate(pkt.getPos(), before, after);
        }
    }

    @Nonnull
    @Override
    public final CompoundNBT write(@Nonnull CompoundNBT tag) {
        super.write(tag);
        this.syncedFields.values().forEach(field -> field.serialize(tag));
        this.writeTileNBT(tag);
        return tag;
    }

    @Override
    public final void read(@Nonnull BlockState state, @Nonnull CompoundNBT tag) {
        super.read(state, tag);
        this.syncedFields.values().forEach(field -> field.deserialize(tag));
        this.readTileNBT(state, tag);
    }

    protected abstract void writeTileNBT(@Nonnull CompoundNBT tag);

    protected abstract void readTileNBT(@Nonnull BlockState state, @Nonnull CompoundNBT tag);

    public void markForUpdateAndNotify() {
        if(this.getWorld() != null) {
            BlockState state = this.getBlockState();
            this.getWorld().notifyBlockUpdate(getPos(), state, state, 3);
            this.markDirty();
        }
    }

    public void syncToClient() {
        this.syncToClient(false);
    }

    public void syncToClient(boolean renderUpdate) {
        World world = this.getWorld();
        if(world != null && !this.getWorld().isRemote) {
            new MessageSyncTile(this, renderUpdate).sendToAllAround(this.getWorld(), this.xCoord(), this.yCoord(), this.zCoord(), 128);
        }
    }

    protected <F> AutoSyncedField<F> createField(
            F value, BiConsumer<F, CompoundNBT> serializer, Function<CompoundNBT, F> deserializer) {

        AutoSyncedField<F> field = new AutoSyncedField<>(value, this.syncedFields.size(), this, serializer, deserializer);
        this.syncedFields.put(field.getId(), field);
        return field;
    }

    @SuppressWarnings("unchecked")
    public <F> AutoSyncedField<F> getField(int id) {
        return (AutoSyncedField<F>) this.syncedFields.get(id);
    }

    public static class AutoSyncedField<F> {
        private F value;

        private final int id;
        private final TileEntityBase tile;
        private final LogicalSide side;

        private final BiConsumer<F, CompoundNBT> serializer;
        private final Function<CompoundNBT, F> deserializer;

        private AutoSyncedField(F value, final int id, TileEntityBase tile, BiConsumer<F, CompoundNBT> serializer, Function<CompoundNBT, F> deserializer) {
            this.value = value;
            this.id = id;
            this.tile = tile;
            this.side = InfinityLib.instance.proxy().getLogicalSide();
            this.serializer = serializer;
            this.deserializer = deserializer;
        }

        public void set(F value) {
            if(this.getSide().isServer() && !this.get().equals(value)) {
                this.setInternal(value);
                this.sync();
                this.getTile().markDirty();
            }
        }

        public void setClient(F value) {
            if(this.getSide().isClient()) {
                this.setInternal(value);
            }
        }

        private void setInternal(F value) {
            this.value = value;
        }

        public F get() {
            return this.value;
        }

        public int getId() {
            return this.id;
        }

        public TileEntityBase getTile() {
            return this.tile;
        }

        public LogicalSide getSide() {
            return this.side;
        }

        public CompoundNBT serialize(CompoundNBT tag) {
            this.serializer.accept(this.get(), tag);
            return tag;
        }

        public void deserialize(CompoundNBT tag) {
            this.setInternal(this.deserializer.apply(tag));
        }

        protected void sync() {
            new MessageAutoSyncTileField<>(this).sendToAll();
        }

    }
}

@@ -2568,32 +2568,71 @@
 per.write(tag);%0A
+        // Order shouldn't matter here%0A
         this.syn
@@ -2682,32 +2682,32 @@
 erialize(tag));%0A
-
         this.wri
@@ -2869,32 +2869,71 @@
 ad(state, tag);%0A
+        // Again, order doesn't matter%0A
         this.syn
@@ -4274,24 +4274,68 @@
 d(int id) %7B%0A
+        // Cast should not be an issue here%0A
         retu
@@ -5353,32 +5353,131 @@
    %7D%0A        %7D%0A%0A
+        // Do not call this directly, called by the message handler to set the field on the client%0A
         public v

Add some comments