package pneumaticCraft.common.semiblock;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import net.minecraft.block.Block;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.world.ChunkPosition;
import net.minecraft.world.World;
import net.minecraft.world.chunk.Chunk;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.world.ChunkDataEvent;
import net.minecraftforge.event.world.ChunkEvent;
import pneumaticCraft.PneumaticCraft;
import pneumaticCraft.common.network.NetworkHandler;
import pneumaticCraft.common.network.PacketDescription;
import pneumaticCraft.common.network.PacketSetSemiBlock;
import pneumaticCraft.common.util.PneumaticCraftUtils;
import pneumaticCraft.lib.Log;

import com.google.common.collect.HashBiMap;

import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.TickEvent;
import cpw.mods.fml.common.registry.GameRegistry;

public class SemiBlockManager{
    private final Map<Chunk, Map<ChunkPosition, ISemiBlock>> semiBlocks = new HashMap<Chunk, Map<ChunkPosition, ISemiBlock>>();
    private final List<ISemiBlock> addingBlocks = new ArrayList<ISemiBlock>();
    private final Map<Chunk, Set<EntityPlayerMP>> syncList = new HashMap<Chunk, Set<EntityPlayerMP>>();
    private final Set<Chunk> chunksMarkedForRemoval = new HashSet<Chunk>();
    public static final int SYNC_DISTANCE = 64;
    private static final HashBiMap<String, Class<? extends ISemiBlock>> registeredTypes = HashBiMap.create();
    private static final HashBiMap<Class<? extends ISemiBlock>, Item> semiBlockToItems = HashBiMap.create();
    private static final SemiBlockManager INSTANCE = new SemiBlockManager();
    private static final SemiBlockManager CLIENT_INSTANCE = new SemiBlockManager();

    public static SemiBlockManager getServerInstance(){
        return INSTANCE;
    }

    public static SemiBlockManager getClientOldInstance(){
        return CLIENT_INSTANCE;
    }

    public static SemiBlockManager getInstance(World world){
        return world.isRemote ? CLIENT_INSTANCE : INSTANCE;
    }

    public static Item registerSemiBlock(String key, Class<? extends ISemiBlock> semiBlock, boolean addItem){
        if(registeredTypes.containsKey(key)) throw new IllegalArgumentException("Duplicate registration key: " + key);
        registeredTypes.put(key, semiBlock);

        if(addItem) {
            ItemSemiBlockBase item = new ItemSemiBlockBase(key);
            GameRegistry.registerItem(item, key);
            PneumaticCraft.proxy.registerSemiBlockRenderer(item);
            registerSemiBlockToItemMapping(semiBlock, item);
            return item;
        } else {
            return null;
        }
    }

    public static void registerSemiBlockToItemMapping(Class<? extends ISemiBlock> semiBlock, Item item){
        semiBlockToItems.put(semiBlock, item);
    }

    public static Item getItemForSemiBlock(ISemiBlock semiBlock){
        return getItemForSemiBlock(semiBlock.getClass());
    }

    public static Item getItemForSemiBlock(Class<? extends ISemiBlock> semiBlock){
        return semiBlockToItems.get(semiBlock);
    }

    public static Class<? extends ISemiBlock> getSemiBlockForItem(Item item){
        return semiBlockToItems.inverse().get(item);
    }

    public static String getKeyForSemiBlock(ISemiBlock semiBlock){
        return getKeyForSemiBlock(semiBlock.getClass());
    }

    public static String getKeyForSemiBlock(Class<? extends ISemiBlock> semiBlock){
        return registeredTypes.inverse().get(semiBlock);
    }

    public static ISemiBlock getSemiBlockForKey(String key){
        try {
            Class<? extends ISemiBlock> clazz = registeredTypes.get(key);
            if(clazz != null) {
                return clazz.newInstance();
            } else {
                Log.warning("Semi Block with id \"" + key + "\" isn't registered!");
                return null;
            }
        } catch(Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    @SubscribeEvent
    public void onChunkUnLoad(ChunkEvent.Unload event){
        if(!event.world.isRemote) {
            chunksMarkedForRemoval.add(event.getChunk());
        }
    }

    @SubscribeEvent
    public void onChunkSave(ChunkDataEvent.Save event){
        Map<ChunkPosition, ISemiBlock> map = semiBlocks.get(event.getChunk());
        if(map != null && map.size() > 0) {
            NBTTagList tagList = new NBTTagList();
            for(Map.Entry<ChunkPosition, ISemiBlock> entry : map.entrySet()) {
                NBTTagCompound t = new NBTTagCompound();
                entry.getValue().writeToNBT(t);
                t.setInteger("x", entry.getKey().chunkPosX);
                t.setInteger("y", entry.getKey().chunkPosY);
                t.setInteger("z", entry.getKey().chunkPosZ);
                t.setString("type", getKeyForSemiBlock(entry.getValue()));
                tagList.appendTag(t);
            }
            event.getData().setTag("SemiBlocks", tagList);
        }
    }

    @SubscribeEvent
    public void onChunkLoad(ChunkDataEvent.Load event){
        try {
            if(!event.world.isRemote) {
                if(event.getData().hasKey("SemiBlocks")) {
                    Map<ChunkPosition, ISemiBlock> map = getOrCreateMap(event.getChunk());
                    map.clear();
                    NBTTagList tagList = event.getData().getTagList("SemiBlocks", 10);
                    for(int i = 0; i < tagList.tagCount(); i++) {
                        NBTTagCompound t = tagList.getCompoundTagAt(i);
                        ISemiBlock semiBlock = getSemiBlockForKey(t.getString("type"));
                        if(semiBlock != null) {
                            semiBlock.readFromNBT(t);
                            setSemiBlock(event.world, t.getInteger("x"), t.getInteger("y"), t.getInteger("z"), semiBlock, event.getChunk());
                        }
                    }
                }
            }
        } catch(Throwable e) {
            e.printStackTrace();
        }
    }

    @SubscribeEvent
    public void onServerTick(TickEvent.ServerTickEvent event){
        for(ISemiBlock semiBlock : addingBlocks) {
            Chunk chunk = semiBlock.getWorld().getChunkFromBlockCoords(semiBlock.getPos().chunkPosX, semiBlock.getPos().chunkPosZ);
            getOrCreateMap(chunk).put(semiBlock.getPos(), semiBlock);
            chunk.setChunkModified();

            for(EntityPlayerMP player : syncList.get(chunk)) {
                NetworkHandler.sendTo(new PacketSetSemiBlock(semiBlock), player);
                PacketDescription descPacket = semiBlock.getDescriptionPacket();
                if(descPacket != null) NetworkHandler.sendTo(descPacket, player);
            }
        }
        addingBlocks.clear();

        for(Chunk removingChunk : chunksMarkedForRemoval) {
            if(!removingChunk.isChunkLoaded) {
                semiBlocks.remove(removingChunk);
                syncList.remove(removingChunk);
            }
        }
        chunksMarkedForRemoval.clear();

        for(Map<ChunkPosition, ISemiBlock> map : semiBlocks.values()) {
            for(ISemiBlock semiBlock : map.values()) {
                if(!semiBlock.isInvalid()) semiBlock.update();
            }
            Iterator<ISemiBlock> iterator = map.values().iterator();
            while(iterator.hasNext()) {
                if(iterator.next().isInvalid()) {
                    iterator.remove();
                }
            }
        }
    }

    @SubscribeEvent
    public void onClientTick(TickEvent.ClientTickEvent event){
        if(this == getServerInstance()) getClientOldInstance().onClientTick(event);
        else {
            EntityPlayer player = PneumaticCraft.proxy.getPlayer();
            if(player != null) {
                for(ISemiBlock semiBlock : addingBlocks) {
                    Chunk chunk = semiBlock.getWorld().getChunkFromBlockCoords(semiBlock.getPos().chunkPosX, semiBlock.getPos().chunkPosZ);
                    getOrCreateMap(chunk).put(semiBlock.getPos(), semiBlock);
                }
                addingBlocks.clear();

                Iterator<Map.Entry<Chunk, Map<ChunkPosition, ISemiBlock>>> iterator = semiBlocks.entrySet().iterator();
                while(iterator.hasNext()) {
                    Map.Entry<Chunk, Map<ChunkPosition, ISemiBlock>> entry = iterator.next();
                    if(PneumaticCraftUtils.distBetween(player.posX, 0, player.posZ, entry.getKey().xPosition * 16 - 8, 0, entry.getKey().zPosition * 16 - 8) > SYNC_DISTANCE + 10) {
                        iterator.remove();
                    } else {
                        for(ISemiBlock semiBlock : entry.getValue().values()) {
                            if(!semiBlock.isInvalid()) semiBlock.update();
                        }
                        Iterator<ISemiBlock> it = entry.getValue().values().iterator();
                        while(it.hasNext()) {
                            if(it.next().isInvalid()) {
                                it.remove();
                            }
                        }
                    }
                }
            } else {
                semiBlocks.clear();
            }
        }
    }

    @SubscribeEvent
    public void onWorldTick(TickEvent.WorldTickEvent event){
        if(!event.world.isRemote) {
            syncWithPlayers(event.world);
        }
    }

    private void syncWithPlayers(World world){
        List<EntityPlayerMP> players = world.playerEntities;
        for(Map.Entry<Chunk, Set<EntityPlayerMP>> entry : syncList.entrySet()) {
            Chunk chunk = entry.getKey();
            Set<EntityPlayerMP> syncedPlayers = entry.getValue();
            int chunkX = chunk.xPosition * 16 - 8;
            int chunkZ = chunk.zPosition * 16 - 8;
            for(EntityPlayerMP player : players) {
                if(chunk.worldObj == world) {
                    double dist = PneumaticCraftUtils.distBetween(player.posX, 0, player.posZ, chunkX, 0, chunkZ);
                    if(dist < SYNC_DISTANCE) {
                        if(syncedPlayers.add(player)) {
                            for(ISemiBlock semiBlock : semiBlocks.get(chunk).values()) {
                                if(!semiBlock.isInvalid()) {
                                    NetworkHandler.sendTo(new PacketSetSemiBlock(semiBlock), player);
                                    PacketDescription descPacket = semiBlock.getDescriptionPacket();
                                    if(descPacket != null) NetworkHandler.sendTo(descPacket, player);
                                }
                            }
                        }
                    } else if(dist > SYNC_DISTANCE + 5) {
                        syncedPlayers.remove(player);
                    }
                } else {
                    syncedPlayers.remove(player);
                }
            }
        }
    }

    @SubscribeEvent
    public void onInteraction(PlayerInteractEvent event){
        if(!event.world.isRemote && event.action == PlayerInteractEvent.Action.RIGHT_CLICK_BLOCK) {
            ItemStack curItem = event.entityPlayer.getCurrentEquippedItem();
            if(curItem != null && curItem.getItem() instanceof ISemiBlockItem) {
                if(getSemiBlock(event.world, event.x, event.y, event.z) != null) {
                    if(event.entityPlayer.capabilities.isCreativeMode) {
                        setSemiBlock(event.world, event.x, event.y, event.z, null);
                    } else {
                        breakSemiBlock(event.world, event.x, event.y, event.z, event.entityPlayer);
                    }
                    event.setCanceled(true);
                } else {
                    ISemiBlock newBlock = ((ISemiBlockItem)curItem.getItem()).getSemiBlock(event.world, event.x, event.y, event.z, curItem);
                    newBlock.initialize(event.world, new ChunkPosition(event.x, event.y, event.z));
                    if(newBlock.canPlace()) {
                        setSemiBlock(event.world, event.x, event.y, event.z, newBlock);
                        newBlock.onPlaced(event.entityPlayer, curItem);
                        event.world.playSoundEffect(event.x + 0.5, event.y + 0.5, event.z + 0.5, Block.soundTypeGlass.func_150496_b(), (Block.soundTypeGlass.getVolume() + 1.0F) / 2.0F, Block.soundTypeGlass.getPitch() * 0.8F);
                        if(!event.entityPlayer.capabilities.isCreativeMode) {
                            curItem.stackSize--;
                            if(curItem.stackSize <= 0) event.entityPlayer.setCurrentItemOrArmor(0, null);
                        }
                        event.setCanceled(true);
                    }
                }
            }
        }
    }

    private Map<ChunkPosition, ISemiBlock> getOrCreateMap(Chunk chunk){
        Map<ChunkPosition, ISemiBlock> map = semiBlocks.get(chunk);
        if(map == null) {
            map = new HashMap<ChunkPosition, ISemiBlock>();
            semiBlocks.put(chunk, map);
            syncList.put(chunk, new HashSet<EntityPlayerMP>());
        }
        return map;
    }

    public void breakSemiBlock(World world, int x, int y, int z){
        breakSemiBlock(world, x, y, z, null);
    }

    public void breakSemiBlock(World world, int x, int y, int z, EntityPlayer player){
        ISemiBlock semiBlock = getSemiBlock(world, x, y, z);
        if(semiBlock != null) {
            List<ItemStack> drops = new ArrayList<ItemStack>();
            semiBlock.addDrops(drops);
            for(ItemStack stack : drops) {
                EntityItem item = new EntityItem(world, x + 0.5, y + 0.5, z + 0.5, stack);
                world.spawnEntityInWorld(item);
                if(player != null) item.onCollideWithPlayer(player);
            }
            setSemiBlock(world, x, y, z, null);
        }
    }

    public void setSemiBlock(World world, int x, int y, int z, ISemiBlock semiBlock){
        setSemiBlock(world, x, y, z, semiBlock, world.getChunkFromBlockCoords(x, z));
    }

    private void setSemiBlock(World world, int x, int y, int z, ISemiBlock semiBlock, Chunk chunk){
        if(semiBlock != null && !registeredTypes.containsValue(semiBlock.getClass())) throw new IllegalStateException("ISemiBlock \"" + semiBlock + "\" was not registered!");
        ChunkPosition pos = new ChunkPosition(x, y, z);
        if(semiBlock != null) {
            semiBlock.initialize(world, pos);
            addingBlocks.add(semiBlock);
        } else {
            ISemiBlock removedBlock = getOrCreateMap(chunk).get(pos);
            if(removedBlock != null) {
                removedBlock.invalidate();
                for(EntityPlayerMP player : syncList.get(chunk)) {
                    NetworkHandler.sendTo(new PacketSetSemiBlock(pos, null), player);
                }
            }
        }
        chunk.setChunkModified();
    }

    public ISemiBlock getSemiBlock(World world, int x, int y, int z){
        for(ISemiBlock semiBlock : addingBlocks) {
            if(semiBlock.getWorld() == world && semiBlock.getPos().equals(new ChunkPosition(x, y, z))) return semiBlock;
        }

        Chunk chunk = world.getChunkFromBlockCoords(x, z);
        Map<ChunkPosition, ISemiBlock> map = semiBlocks.get(chunk);
        if(map != null) {
            return map.get(new ChunkPosition(x, y, z));
        } else {
            return null;
        }
    }

    public Map<Chunk, Map<ChunkPosition, ISemiBlock>> getSemiBlocks(){
        return semiBlocks;
    }
}