package mekanism.api.transmitters;

import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map.Entry;
import java.util.Set;

import mekanism.api.Coord4D;
import mekanism.api.IClientTicker;
import mekanism.api.Range4D;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.EnumFacing;
import net.minecraft.world.World;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.Event;

import org.apache.commons.lang3.tuple.Pair;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

public abstract class DynamicNetwork<A, N extends DynamicNetwork<A, N>> implements IClientTicker, INetworkDataHandler
{
	public LinkedHashSet<IGridTransmitter<A, N>> transmitters = Sets.newLinkedHashSet();
	public LinkedHashSet<IGridTransmitter<A, N>> transmittersToAdd = Sets.newLinkedHashSet();
	public LinkedHashSet<IGridTransmitter<A, N>> transmittersAdded = Sets.newLinkedHashSet();

	public HashMap<Coord4D, A> possibleAcceptors = new HashMap<Coord4D, A>();
	public HashMap<Coord4D, EnumSet<EnumFacing>> acceptorDirections = new HashMap<Coord4D, EnumSet<EnumFacing>>();
	public HashMap<IGridTransmitter<A, N>, EnumSet<EnumFacing>> changedAcceptors = Maps.newHashMap();

	private Set<DelayQueue> updateQueue = new LinkedHashSet<DelayQueue>();

	protected Range4D packetRange = null;

	protected int capacity = 0;
	protected double meanCapacity = 0;

	protected boolean needsUpdate = false;
	protected int updateDelay = 0;

	protected boolean firstUpdate = true;
	protected World worldObj = null;

	public void addNewTransmitters(Collection<IGridTransmitter<A, N>> newTransmitters)
	{
		transmittersToAdd.addAll(newTransmitters);
	}

	public void commit()
	{
		if(!transmittersToAdd.isEmpty())
		{
			for(IGridTransmitter<A, N> transmitter : transmittersToAdd)
			{
				if(transmitter.isValid())
				{
					if(worldObj == null)
					{
						worldObj = transmitter.world();
					}

					for(EnumFacing side : EnumFacing.VALUES)
					{
						updateTransmitterOnSide(transmitter, side);
					}
					
					transmitter.setTransmitterNetwork((N)this);
					absorbBuffer(transmitter);
					transmitters.add(transmitter);
				}
			}
			
			updateCapacity();
			clampBuffer();
			queueClientUpdate(Lists.newArrayList(transmittersToAdd));
			transmittersToAdd.clear();
		}

		if(!changedAcceptors.isEmpty())
		{
			for(Entry<IGridTransmitter<A, N>, EnumSet<EnumFacing>> entry : changedAcceptors.entrySet())
			{
				IGridTransmitter<A, N> transmitter = entry.getKey();
				
				if(transmitter.isValid())
				{
					EnumSet<EnumFacing> directionsChanged = entry.getValue();

					for(EnumFacing side : directionsChanged)
					{
						updateTransmitterOnSide(transmitter, side);
					}
				}
			}
			
			changedAcceptors.clear();
		}
	}

	public void updateTransmitterOnSide(IGridTransmitter<A, N> transmitter, EnumFacing side)
	{
		A acceptor = transmitter.getAcceptor(side);
		Coord4D acceptorCoord = transmitter.coord().offset(side);
		EnumSet<EnumFacing> directions = acceptorDirections.get(acceptorCoord);

		if(acceptor != null)
		{
			possibleAcceptors.put(acceptorCoord, acceptor);

			if(directions != null)
			{
				directions.add(side.getOpposite());
			}
			else {
				acceptorDirections.put(acceptorCoord, EnumSet.of(side.getOpposite()));
			}
		}
		else {
			if(directions != null)
			{
				directions.remove(side.getOpposite());

				if(directions.isEmpty())
				{
					possibleAcceptors.remove(acceptorCoord);
					acceptorDirections.remove(acceptorCoord);
				}
			}
			else {
				possibleAcceptors.remove(acceptorCoord);
				acceptorDirections.remove(acceptorCoord);
			}
		}

	}

	public abstract void absorbBuffer(IGridTransmitter<A, N> transmitter);

	public abstract void clampBuffer();

	public void invalidate()
	{
        //Remove invalid transmitters first for share calculations
        for(Iterator<IGridTransmitter<A, N>> iter = transmitters.iterator(); iter.hasNext();)
        {
            IGridTransmitter<A, N> transmitter = iter.next();

            if(!transmitter.isValid())
            {
                iter.remove();
                continue;
            }
        }

        //Clamp the new buffer
        clampBuffer();

        //Update all shares
        for(IGridTransmitter<A, N> transmitter : transmitters)
        {
            transmitter.updateShare();
        }

        //Now invalidate the transmitters
		for(IGridTransmitter<A, N> transmitter : transmitters)
		{
			invalidateTransmitter(transmitter);
		}
		
		transmitters.clear();
		deregister();
	}

	public void invalidateTransmitter(IGridTransmitter<A, N> transmitter)
	{
		if(!worldObj.isRemote && transmitter.isValid())
		{
			transmitter.takeShare();
			transmitter.setTransmitterNetwork(null);
			TransmitterNetworkRegistry.registerOrphanTransmitter(transmitter);
		}
	}

	public void acceptorChanged(IGridTransmitter<A, N> transmitter, EnumFacing side)
	{
		EnumSet<EnumFacing> directions = changedAcceptors.get(transmitter);
		
		if(directions != null)
		{
			directions.add(side);
		} 
		else {
			changedAcceptors.put(transmitter, EnumSet.of(side));
		}
		
		TransmitterNetworkRegistry.registerChangedNetwork(this);
	}

	public void adoptTransmittersAndAcceptorsFrom(N net)
	{
		for(IGridTransmitter<A, N> transmitter : net.transmitters)
		{
			transmitter.setTransmitterNetwork((N)this);
			transmitters.add(transmitter);
			transmittersAdded.add(transmitter);
		}
		
		possibleAcceptors.putAll(net.possibleAcceptors);
		
		for(Entry<Coord4D, EnumSet<EnumFacing>> entry : net.acceptorDirections.entrySet())
		{
			Coord4D coord = entry.getKey();
			
			if(acceptorDirections.containsKey(coord))
			{
				acceptorDirections.get(coord).addAll(entry.getValue());
			}
			else {
				acceptorDirections.put(coord, entry.getValue());
			}
		}

	}

	public Range4D getPacketRange()
	{
		if(packetRange == null)
		{
			return genPacketRange();
		}
		
		return packetRange;
	}
	
	protected Range4D genPacketRange()
	{
		if(getSize() == 0)
		{
			deregister();
			return null;
		}

		IGridTransmitter<A, N> initTransmitter = transmitters.iterator().next();
		Coord4D initCoord = initTransmitter.coord();
		
		int minX = initCoord.xCoord;
		int minY = initCoord.yCoord;
		int minZ = initCoord.zCoord;
		int maxX = initCoord.xCoord;
		int maxY = initCoord.yCoord;
		int maxZ = initCoord.zCoord;
		
		for(IGridTransmitter transmitter : transmitters)
		{
			Coord4D coord = transmitter.coord();
			
			if(coord.xCoord < minX) minX = coord.xCoord;
			if(coord.yCoord < minY) minY = coord.yCoord;
			if(coord.zCoord < minZ) minZ = coord.zCoord;
			if(coord.xCoord > maxX) maxX = coord.xCoord;
			if(coord.yCoord > maxY) maxY = coord.yCoord;
			if(coord.xCoord > maxZ) maxZ = coord.zCoord;
		}
		
		return new Range4D(minX, minY, minZ, maxX, maxY, maxZ, initTransmitter.world().provider.getDimension());
	}

	public void register()
	{
		if(FMLCommonHandler.instance().getEffectiveSide().isServer())
		{
			TransmitterNetworkRegistry.getInstance().registerNetwork(this);
		}
		else {
			MinecraftForge.EVENT_BUS.post(new ClientTickUpdate(this, (byte)1));
		}
	}

	public void deregister()
	{
		transmitters.clear();

		if(FMLCommonHandler.instance().getEffectiveSide().isServer())
		{
			TransmitterNetworkRegistry.getInstance().removeNetwork(this);
		}
		else {
			MinecraftForge.EVENT_BUS.post(new ClientTickUpdate(this, (byte)0));
		}
	}

	public int getSize()
	{
		return transmitters.size();
	}

	public int getAcceptorSize()
	{
		return possibleAcceptors.size();
	}

	public synchronized void updateCapacity() 
	{
		updateMeanCapacity();
		capacity = (int)meanCapacity * transmitters.size();
	}

    /**
     * Override this if things can have variable capacity along the network.
     * @return An 'average' value of capacity. Calculate it how you will.
     */
	protected synchronized void updateMeanCapacity() 
	{
		if(transmitters.size() > 0) 
		{
			meanCapacity = transmitters.iterator().next().getCapacity();
		} 
		else {
			meanCapacity = 0;
		}
	}
	
    public int getCapacity()
    {
    	return capacity;
    }

	public World getWorld()
	{
		return worldObj;
	}

	public abstract Set<Pair<Coord4D, A>> getAcceptors(Object data);

	public void tick()
	{
		onUpdate();
	}

	public void onUpdate()
	{
		if(FMLCommonHandler.instance().getEffectiveSide().isServer())
		{
			Iterator<DelayQueue> i = updateQueue.iterator();

			try {
				while(i.hasNext())
				{
					DelayQueue q = i.next();

					if(q.delay > 0)
					{
						q.delay--;
					} 
					else {
						transmittersAdded.addAll(transmitters);
						updateDelay = 1;
						i.remove();
					}
				}
			} catch(Exception e) {}

			if(updateDelay > 0)
			{
				updateDelay--;

				if(updateDelay == 0)
				{
					MinecraftForge.EVENT_BUS.post(new TransmittersAddedEvent(this, firstUpdate, (Collection)transmittersAdded));
					firstUpdate = false;
					transmittersAdded.clear();
					needsUpdate = true;
				}
			}
		}
	}

	@Override
	public boolean needsTicks()
	{
		return getSize() > 0;
	}

	@Override
	public void clientTick() {}

	public void queueClientUpdate(Collection<IGridTransmitter<A, N>> newTransmitters)
	{
		transmittersAdded.addAll(newTransmitters);
		updateDelay = 3;
	}

	public static class TransmittersAddedEvent extends Event
	{
		public DynamicNetwork<?, ?> network;
		public boolean newNetwork;
		public Collection<IGridTransmitter> newTransmitters;

		public TransmittersAddedEvent(DynamicNetwork net, boolean newNet, Collection<IGridTransmitter> added)
		{
			network = net;
			newNetwork = newNet;
			newTransmitters = added;
		}
	}

	public static class ClientTickUpdate extends Event
	{
		public DynamicNetwork network;
		public byte operation; /*0 remove, 1 add*/

		public ClientTickUpdate(DynamicNetwork net, byte b)
		{
			network = net;
			operation = b;
		}
	}

	public static class NetworkClientRequest extends Event
	{
		public TileEntity tileEntity;

		public NetworkClientRequest(TileEntity tile)
		{
			tileEntity = tile;
		}
	}

	public void addUpdate(EntityPlayer player)
	{
		updateQueue.add(new DelayQueue(player));
	}

	public static class DelayQueue
	{
		public EntityPlayer player;
		public int delay;

		public DelayQueue(EntityPlayer p)
		{
			player = p;
			delay = 5;
		}

		@Override
		public int hashCode()
		{
			return player.hashCode();
		}

		@Override
		public boolean equals(Object o)
		{
			return o instanceof DelayQueue && ((DelayQueue)o).player.equals(this.player);
		}
	}
}
