package com.boydti.fawe.sponge.v1_11;

import com.boydti.fawe.FaweCache;
import com.boydti.fawe.config.Settings;
import com.boydti.fawe.example.CharFaweChunk;
import com.boydti.fawe.example.NMSMappedFaweQueue;
import com.boydti.fawe.object.FaweChunk;
import com.boydti.fawe.object.RunnableVal;
import com.boydti.fawe.util.MainUtil;
import com.boydti.fawe.util.TaskManager;
import com.sk89q.jnbt.CompoundTag;
import com.sk89q.worldedit.world.biome.BaseBiome;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import net.minecraft.block.Block;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityTracker;
import net.minecraft.entity.EntityTrackerEntry;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.play.server.S13PacketDestroyEntities;
import net.minecraft.network.play.server.S21PacketChunkData;
import net.minecraft.server.management.PlayerManager;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.BlockPos;
import net.minecraft.util.ClassInheritanceMultiMap;
import net.minecraft.util.IntHashMap;
import net.minecraft.util.LongHashMap;
import net.minecraft.world.ChunkCoordIntPair;
import net.minecraft.world.WorldServer;
import net.minecraft.world.chunk.IChunkProvider;
import net.minecraft.world.chunk.storage.ExtendedBlockStorage;
import net.minecraft.world.gen.ChunkProviderServer;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.block.BlockState;
import org.spongepowered.api.block.BlockTypes;
import org.spongepowered.api.world.Chunk;
import org.spongepowered.api.world.World;
import org.spongepowered.api.world.extent.UnmodifiableBlockVolume;
import org.spongepowered.api.world.extent.worker.MutableBlockVolumeWorker;
import org.spongepowered.api.world.extent.worker.procedure.BlockVolumeMapper;

public class SpongeQueue_ALL extends NMSMappedFaweQueue<World, net.minecraft.world.chunk.Chunk, ExtendedBlockStorage[], ExtendedBlockStorage> {
    private Method methodToNative;

    public SpongeQueue_ALL(String world) {
        super(world);
        try {
            Class<?> converter = Class.forName("com.sk89q.worldedit.forge.NBTConverter");
            this.methodToNative = converter.getDeclaredMethod("fromNative", NBTBase.class);
            methodToNative.setAccessible(true);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void refreshChunk(FaweChunk fs) {

    }

    @Override
    public void refreshChunk(World world, net.minecraft.world.chunk.Chunk nmsChunk) {
        if (!nmsChunk.isLoaded()) {
            return;
        }
        try {
            ChunkCoordIntPair pos = nmsChunk.getChunkCoordIntPair();
            WorldServer w = (WorldServer) nmsChunk.getWorld();
            PlayerManager chunkMap = w.getPlayerManager();
            int x = pos.chunkXPos;
            int z = pos.chunkZPos;
            if (!chunkMap.hasPlayerInstance(x, z)) {
                return;
            }
            EntityTracker tracker = w.getEntityTracker();
            HashSet<EntityPlayerMP> players = new HashSet<>();
            for (EntityPlayer player : w.playerEntities) {
                if (player instanceof EntityPlayerMP) {
                    if (chunkMap.isPlayerWatchingChunk((EntityPlayerMP) player, x, z)) {
                        players.add((EntityPlayerMP) player);
                    }
                }
            }
            if (players.size() == 0) {
                return;
            }
            HashSet<EntityTrackerEntry> entities = new HashSet<>();
            ClassInheritanceMultiMap<Entity>[] entitieSlices = nmsChunk.getEntityLists();
            IntHashMap<EntityTrackerEntry> entries = null;
            for (Field field : tracker.getClass().getDeclaredFields()) {
                if (field.getType() == IntHashMap.class) {
                    field.setAccessible(true);
                    entries = (IntHashMap<EntityTrackerEntry>) field.get(tracker);
                }
            }
            for (ClassInheritanceMultiMap<Entity> slice : entitieSlices) {
                if (slice == null) {
                    continue;
                }
                for (Entity ent : slice) {
                    EntityTrackerEntry entry = entries != null ? entries.lookup(ent.getEntityId()) : null;
                    if (entry == null) {
                        continue;
                    }
                    entities.add(entry);
                    S13PacketDestroyEntities packet = new S13PacketDestroyEntities(ent.getEntityId());
                    for (EntityPlayerMP player : players) {
                        player.playerNetServerHandler.sendPacket(packet);
                    }
                }
            }
            // Send chunks
            S21PacketChunkData packet = new S21PacketChunkData(nmsChunk, false, 65535);
            for (EntityPlayerMP player : players) {
                player.playerNetServerHandler.sendPacket(packet);
            }
            // send ents
            for (EntityTrackerEntry entry : entities) {
                try {
                    TaskManager.IMP.later(new Runnable() {
                        @Override
                        public void run() {
                            for (EntityPlayerMP player : players) {
                                boolean result = entry.trackingPlayers.remove(player);
                                if (result && entry.trackedEntity != player) {
                                    entry.updatePlayerEntity(player);
                                }
                            }
                        }
                    }, 2);
                } catch (Throwable e) {
                    MainUtil.handleError(e);
                }
            }
        } catch (Throwable e) {
            MainUtil.handleError(e);
        }
    }

    @Override
    public CompoundTag getTileEntity(net.minecraft.world.chunk.Chunk chunk, int x, int y, int z) {
        Map<BlockPos, TileEntity> tiles = chunk.getTileEntityMap();
        TileEntity tile = tiles.get(new BlockPos(x, y, z));
        return tile != null ? getTag(tile) : null;
    }

    public CompoundTag getTag(TileEntity tile) {
        try {
            NBTTagCompound tag = new NBTTagCompound();
            tile.readFromNBT(tag); // readTagIntoEntity
            return (CompoundTag) methodToNative.invoke(null, tag);
        } catch (Exception e) {
            MainUtil.handleError(e);
            return null;
        }
    }

    @Override
    public net.minecraft.world.chunk.Chunk getChunk(World world, int x, int z) {
        net.minecraft.world.chunk.Chunk chunk = ((net.minecraft.world.World) world).getChunkProvider().provideChunk(x, z);
        if (chunk != null && !chunk.isLoaded()) {
            chunk.onChunkLoad();
        }
        return chunk;
    }

    @Override
    public char[] getCachedSection(ExtendedBlockStorage[] chunk, int cy) {
        ExtendedBlockStorage value = chunk[cy];
        return value == null ? null : value.getData();
    }

    @Override
    public World getWorld(String world) {
        return Sponge.getServer().getWorld(super.getWorldName()).get();
    }

    @Override
    public boolean isChunkLoaded(World world, int x, int z) {
        net.minecraft.world.World nmsWorld = (net.minecraft.world.World) world;
        IChunkProvider provider = nmsWorld.getChunkProvider();
        return provider.chunkExists(x, z);
    }

    @Override
    public boolean regenerateChunk(World world, int x, int z, BaseBiome biome, Long seed) {
        try {
            net.minecraft.world.World nmsWorld = (net.minecraft.world.World) world;
            IChunkProvider provider = nmsWorld.getChunkProvider();
            if (!(provider instanceof ChunkProviderServer)) {
                return false;
            }
            ChunkProviderServer chunkServer = (ChunkProviderServer) provider;
            Field chunkProviderField = chunkServer.getClass().getDeclaredField("field_73246_d");
            chunkProviderField.setAccessible(true);
            IChunkProvider chunkProvider = (IChunkProvider) chunkProviderField.get(chunkServer);
            long pos = ChunkCoordIntPair.chunkXZ2Int(x, z);
            net.minecraft.world.chunk.Chunk mcChunk;
            if (chunkServer.chunkExists(x, z)) {
                mcChunk = chunkServer.loadChunk(x, z);
                mcChunk.onChunkUnload();
            }
            Field droppedChunksSetField = chunkServer.getClass().getDeclaredField("field_73248_b");
            droppedChunksSetField.setAccessible(true);
            Set droppedChunksSet = (Set) droppedChunksSetField.get(chunkServer);
            droppedChunksSet.remove(pos);
            Field id2ChunkMapField = chunkServer.getClass().getDeclaredField("field_73244_f");
            id2ChunkMapField.setAccessible(true);
            LongHashMap<net.minecraft.world.chunk.Chunk> id2ChunkMap = (LongHashMap<net.minecraft.world.chunk.Chunk>) id2ChunkMapField.get(chunkServer);
            id2ChunkMap.remove(pos);
            mcChunk = chunkProvider.provideChunk(x, z);
            id2ChunkMap.add(pos, mcChunk);
            List<net.minecraft.world.chunk.Chunk> loadedChunks = chunkServer.func_152380_a();
            loadedChunks.add(mcChunk);
            if (mcChunk != null) {
                mcChunk.onChunkLoad();
                mcChunk.populateChunk(chunkProvider, chunkProvider, x, z);
            }
            return true;
        } catch (Throwable e) {
            MainUtil.handleError(e);
        }
        return false;
    }

    private BlockState AIR = BlockTypes.AIR.getDefaultState();

    @Override
    public boolean setComponents(FaweChunk fc, RunnableVal<FaweChunk> changeTask) {
        if (changeTask != null) {
            Settings.IMP.HISTORY.COMBINE_STAGES = false;
            throw new UnsupportedOperationException("Combine stages not supported");
        }
        SpongeChunk_1_8 fs = (SpongeChunk_1_8) fc;
        net.minecraft.world.chunk.Chunk nmsChunk = fs.getChunk();
        Chunk spongeChunk = (Chunk) nmsChunk;

        char[][] ids = ((SpongeChunk_1_8) fc).getCombinedIdArrays();
        MutableBlockVolumeWorker<? extends Chunk> blockWorker = spongeChunk.getBlockWorker();
        blockWorker.map(new BlockVolumeMapper() {
            @Override
            public BlockState map(UnmodifiableBlockVolume volume, int xx, int y, int zz) {
                int x = xx & 15;
                int z = zz & 15;
                int i = FaweCache.CACHE_I[y][z][x];
                char[] array = ids[i];
                if (array == null) {
                    return null;
                }
                int combinedId = array[FaweCache.CACHE_J[y][z][x]];
                switch (combinedId) {
                    case 0:
                        return null;
                    case 1:
                        return AIR;
                    default:
                        int id = combinedId >> 4;
                        Block block = Block.getBlockById(id);
                        int data = combinedId & 0xf;
                        IBlockState ibd;
                        if (data != 0) {
                            ibd = block.getStateFromMeta(data);
                        } else {
                            ibd = block.getDefaultState();
                        }
                        return (BlockState) ibd;
                }
            }
        });
        sendChunk(fs, null);
        return true;
    }

    public void setCount(int tickingBlockCount, int nonEmptyBlockCount, ExtendedBlockStorage section) throws NoSuchFieldException, IllegalAccessException {
        Class<? extends ExtendedBlockStorage> clazz = section.getClass();
        Field fieldTickingBlockCount = clazz.getDeclaredField("field_76683_c");
        Field fieldNonEmptyBlockCount = clazz.getDeclaredField("field_76682_b");
        fieldTickingBlockCount.setAccessible(true);
        fieldNonEmptyBlockCount.setAccessible(true);
        fieldTickingBlockCount.set(section, tickingBlockCount);
        fieldNonEmptyBlockCount.set(section, nonEmptyBlockCount);
    }

    @Override
    public FaweChunk<net.minecraft.world.chunk.Chunk> getFaweChunk(int x, int z) {
        return new SpongeChunk_1_8(this, x, z);
    }

    @Override
    public CharFaweChunk getPrevious(CharFaweChunk fs, ExtendedBlockStorage[] sections, Map<?, ?> tilesGeneric, Collection<?>[] entitiesGeneric, Set<UUID> createdEntities, boolean all) throws Exception {
        Settings.IMP.HISTORY.COMBINE_STAGES = false;
        throw new UnsupportedOperationException("Combine stages not supported");
    }

    @Override
    public boolean loadChunk(World world, int x, int z, boolean generate) {
        return getCachedSections(world, x, z) != null;
    }

    @Override
    public ExtendedBlockStorage[] getCachedSections(World world, int cx, int cz) {
        net.minecraft.world.World nmsWorld = (net.minecraft.world.World) world;
        IChunkProvider provider = nmsWorld.getChunkProvider();
        net.minecraft.world.chunk.Chunk chunk = provider.provideChunk(cx, cz);
        if (chunk == null) {
            return null;
        }
        if (!chunk.isLoaded()) {
            chunk.onChunkLoad();
        }
        return chunk.getBlockStorageArray();
    }


    @Override
    public int getCombinedId4Data(char[] chars, int x, int y, int z) {
        return chars[FaweCache.CACHE_J[y][z & 15][x & 15]];
    }
}
