package appeng.tile;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.IInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.NetworkManager;
import net.minecraft.network.Packet;
import net.minecraft.network.play.server.S35PacketUpdateTileEntity;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.world.World;
import net.minecraftforge.common.util.ForgeDirection;
import appeng.api.implementations.tiles.ISegmentedInventory;
import appeng.api.util.ICommonTile;
import appeng.api.util.IConfigManager;
import appeng.api.util.IConfigurableObject;
import appeng.api.util.IOrientable;
import appeng.core.AELog;
import appeng.core.features.ItemStackSrc;
import appeng.helpers.ICustomNameObject;
import appeng.helpers.IPriorityHost;
import appeng.tile.events.AETileEventHandler;
import appeng.tile.events.TileEventType;
import appeng.tile.inventory.AppEngInternalAEInventory;
import appeng.util.Platform;
import appeng.util.SettingsFrom;

public class AEBaseTile extends TileEntity implements IOrientable, ICommonTile, ICustomNameObject
{

	static private final HashMap<Class, EnumMap<TileEventType, List<AETileEventHandler>>> handlers = new HashMap<Class, EnumMap<TileEventType, List<AETileEventHandler>>>();
	static private final HashMap<Class, ItemStackSrc> myItem = new HashMap();

	private ForgeDirection forward = ForgeDirection.UNKNOWN;
	private ForgeDirection up = ForgeDirection.UNKNOWN;

	public static ThreadLocal<WeakReference<AEBaseTile>> dropNoItems = new ThreadLocal();

	public void disableDrops()
	{
		dropNoItems.set( new WeakReference<AEBaseTile>( this ) );
	}

	public boolean dropItems()
	{
		WeakReference<AEBaseTile> what = dropNoItems.get();
		return what == null || what.get() != this;
	}

	public int renderFragment = 0;
	public String customName;

	public boolean notLoaded()
	{
		return !worldObj.blockExists( xCoord, yCoord, zCoord );
	}

	public TileEntity getTile()
	{
		return this;
	}

	static public void registerTileItem(Class c, ItemStackSrc wat)
	{
		myItem.put( c, wat );
	}

	protected ItemStack getItemFromTile(Object obj)
	{
		ItemStackSrc src = myItem.get( obj.getClass() );
		if ( src == null )
			return null;
		return src.stack( 1 );
	}

	protected boolean hasHandlerFor(TileEventType type)
	{
		List<AETileEventHandler> list = getHandlerListFor( type );
		return list != null && !list.isEmpty();
	}

	protected List<AETileEventHandler> getHandlerListFor(TileEventType type)
	{
		Class clz = getClass();
		EnumMap<TileEventType, List<AETileEventHandler>> handlerSet = handlers.get( clz );

		if ( handlerSet == null )
		{
			handlers.put( clz, handlerSet = new EnumMap<TileEventType, List<AETileEventHandler>>( TileEventType.class ) );

			for (Method m : clz.getMethods())
			{
				TileEvent te = m.getAnnotation( TileEvent.class );
				if ( te != null )
				{
					addHandler( handlerSet, te.value(), m );
				}
			}
		}

		List<AETileEventHandler> list = handlerSet.get( type );

		if ( list == null )
			handlerSet.put( type, list = new LinkedList<AETileEventHandler>() );

		return list;
	}

	private void addHandler(EnumMap<TileEventType, List<AETileEventHandler>> handlerSet, TileEventType value, Method m)
	{
		List<AETileEventHandler> list = handlerSet.get( value );

		if ( list == null )
			handlerSet.put( value, list = new ArrayList() );

		list.add( new AETileEventHandler( m, value ) );
	}

	@Override
	final public boolean canUpdate()
	{
		return hasHandlerFor( TileEventType.TICK );
	}

	final public void Tick()
	{

	}

	@Override
	final public void updateEntity()
	{
		for (AETileEventHandler h : getHandlerListFor( TileEventType.TICK ))
			h.Tick( this );
	}

	@Override
	public void onChunkUnload()
	{
		if ( !isInvalid() )
			invalidate();
	}

	/**
	 * for dormant chunk cache.
	 */
	public void onChunkLoad()
	{
		if ( isInvalid() )
			validate();
	}

	@Override
	// NOTE: WAS FINAL, changed for Immibis
	final public void writeToNBT(NBTTagCompound data)
	{
		super.writeToNBT( data );

		if ( canBeRotated() )
		{
			data.setString( "orientation_forward", forward.name() );
			data.setString( "orientation_up", up.name() );
		}

		if ( customName != null )
			data.setString( "customName", customName );

		for (AETileEventHandler h : getHandlerListFor( TileEventType.WORLD_NBT_WRITE ))
			h.writeToNBT( this, data );
	}

	@Override
	// NOTE: WAS FINAL, changed for Immibis
	final public void readFromNBT(NBTTagCompound data)
	{
		super.readFromNBT( data );

		if ( data.hasKey( "customName" ) )
			customName = data.getString( "customName" );
		else
			customName = null;

		try
		{
			if ( canBeRotated() )
			{
				forward = ForgeDirection.valueOf( data.getString( "orientation_forward" ) );
				up = ForgeDirection.valueOf( data.getString( "orientation_up" ) );
			}
		}
		catch (IllegalArgumentException iae)
		{
		}

		for (AETileEventHandler h : getHandlerListFor( TileEventType.WORLD_NBT_READ ))
		{
			h.readFromNBT( this, data );
		}
	}

	final public void writeToStream(ByteBuf data)
	{
		try
		{
			if ( canBeRotated() )
			{
				byte orientation = (byte) ((up.ordinal() << 3) | forward.ordinal());
				data.writeByte( orientation );
			}

			for (AETileEventHandler h : getHandlerListFor( TileEventType.NETWORK_WRITE ))
				h.writeToStream( this, data );
		}
		catch (Throwable t)
		{
			AELog.error( t );
		}
	}

	final public boolean readFromStream(ByteBuf data)
	{
		boolean output = false;

		try
		{

			if ( canBeRotated() )
			{
				ForgeDirection old_Forward = forward;
				ForgeDirection old_Up = up;

				byte orientation = data.readByte();
				forward = ForgeDirection.getOrientation( orientation & 0x7 );
				up = ForgeDirection.getOrientation( orientation >> 3 );

				output = !forward.equals( old_Forward ) || !up.equals( old_Up );
			}

			renderFragment = 100;
			for (AETileEventHandler h : getHandlerListFor( TileEventType.NETWORK_READ ))
				if ( h.readFromStream( this, data ) )
					output = true;

			if ( (renderFragment & 1) == 1 )
				output = true;
			renderFragment = 0;
		}
		catch (Throwable t)
		{
			AELog.error( t );
		}

		return output;
	}

	/**
	 * By default all blocks can have orientation, this handles saving, and loading, as well as synchronization.
	 * 
	 * @return
	 */
	@Override
	public boolean canBeRotated()
	{
		return true;
	}

	@Override
	public ForgeDirection getForward()
	{
		return forward;
	}

	@Override
	public ForgeDirection getUp()
	{
		return up;
	}

	@Override
	public void setOrientation(ForgeDirection inForward, ForgeDirection inUp)
	{
		forward = inForward;
		up = inUp;
		markForUpdate();
		Platform.notifyBlocksOfNeighbors( worldObj, xCoord, yCoord, zCoord );
	}

	public void onPlacement(ItemStack stack, EntityPlayer player, int side)
	{
		if ( stack.hasTagCompound() )
		{
			uploadSettings( SettingsFrom.DISMANTLE_ITEM, stack.getTagCompound() );
		}
	}

	@Override
	public Packet getDescriptionPacket()
	{
		NBTTagCompound data = new NBTTagCompound();

		ByteBuf stream = Unpooled.buffer();

		try
		{
			writeToStream( stream );
			if ( stream.readableBytes() == 0 )
				return null;
		}
		catch (Throwable t)
		{
			AELog.error( t );
		}

		stream.capacity( stream.readableBytes() );
		data.setByteArray( "X", stream.array() );
		return new S35PacketUpdateTileEntity( xCoord, yCoord, zCoord, 64, data );
	}

	@Override
	public void onDataPacket(NetworkManager net, S35PacketUpdateTileEntity pkt)
	{
		// / pkt.actionType
		if ( pkt.func_148853_f() == 64 )
		{
			ByteBuf stream = Unpooled.copiedBuffer( pkt.func_148857_g().getByteArray( "X" ) );
			if ( readFromStream( stream ) )
				markForUpdate();
		}
	}

	public void markForUpdate()
	{
		if ( renderFragment > 0 )
			renderFragment = renderFragment | 1;
		else
		{
			// TODO: Optimize Network Load
			if ( worldObj != null )
			{
				AELog.blockUpdate( xCoord, yCoord, zCoord, this );
				worldObj.markBlockForUpdate( xCoord, yCoord, zCoord );
			}
		}
	}

	/**
	 * returns the contents of the tile entity, into the world, defaults to dropping everything in the inventory.
	 * 
	 * @param w
	 * @param x
	 * @param y
	 * @param z
	 * @param drops
	 */
	@Override
	public void getDrops(World w, int x, int y, int z, ArrayList<ItemStack> drops)
	{
		if ( this instanceof IInventory )
		{
			IInventory inv = (IInventory) this;

			for (int l = 0; l < inv.getSizeInventory(); l++)
			{
				ItemStack is = inv.getStackInSlot( l );
				if ( is != null )
					drops.add( is );
			}
		}

	}

	public void getNoDrops(World w, int x, int y, int z, ArrayList<ItemStack> drops)
	{

	}

	public void onReady()
	{

	}

	/**
	 * depending on the from, different settings will be accepted, don't call this with null
	 * 
	 * @param from
	 * @param compound
	 */
	public void uploadSettings(SettingsFrom from, NBTTagCompound compound)
	{
		if ( compound != null && this instanceof IConfigurableObject )
		{
			IConfigManager cm = ((IConfigurableObject) this).getConfigManager();
			if ( cm != null )
				cm.readFromNBT( compound );
		}

		if ( this instanceof IPriorityHost )
		{
			IPriorityHost pHost = (IPriorityHost) this;
			pHost.setPriority( compound.getInteger( "priority" ) );
		}

		if ( this instanceof ISegmentedInventory )
		{
			IInventory inv = ((ISegmentedInventory) this).getInventoryByName( "config" );
			if ( inv != null && inv instanceof AppEngInternalAEInventory )
			{
				AppEngInternalAEInventory target = (AppEngInternalAEInventory) inv;
				AppEngInternalAEInventory tmp = new AppEngInternalAEInventory( null, target.getSizeInventory() );
				tmp.readFromNBT( compound, "config" );
				for (int x = 0; x < tmp.getSizeInventory(); x++)
					target.setInventorySlotContents( x, tmp.getStackInSlot( x ) );
			}
		}
	}

	/**
	 * null means nothing to store...
	 * 
	 * @param from
	 * @return
	 */
	public NBTTagCompound downloadSettings(SettingsFrom from)
	{
		NBTTagCompound output = new NBTTagCompound();

		if ( hasCustomName() )
		{
			NBTTagCompound dsp = new NBTTagCompound();
			dsp.setString( "Name", getCustomName() );
			output.setTag( "display", dsp );
		}

		if ( this instanceof IConfigurableObject )
		{
			IConfigManager cm = ((IConfigurableObject) this).getConfigManager();
			if ( cm != null )
				cm.writeToNBT( output );
		}

		if ( this instanceof IPriorityHost )
		{
			IPriorityHost pHost = (IPriorityHost) this;
			output.setInteger( "priority", pHost.getPriority() );
		}

		if ( this instanceof ISegmentedInventory )
		{
			IInventory inv = ((ISegmentedInventory) this).getInventoryByName( "config" );
			if ( inv != null && inv instanceof AppEngInternalAEInventory )
			{
				((AppEngInternalAEInventory) inv).writeToNBT( output, "config" );
			}
		}

		return output.hasNoTags() ? null : output;
	}

	public void securityBreak()
	{
		worldObj.func_147480_a( xCoord, yCoord, zCoord, true );
		disableDrops();
	}

	public void saveChanges()
	{
		super.markDirty();
	}

	public boolean requiresTESR()
	{
		return false;
	}

	public void setName(String name)
	{
		this.customName = name;
	}

	@Override
	public String getCustomName()
	{
		return hasCustomName() ? customName : getClass().getSimpleName();
	}

	@Override
	public boolean hasCustomName()
	{
		return customName != null && customName.length() > 0;
	}

}