package erogenousbeef.bigreactors.common.multiblock.tileentity;

import net.minecraft.block.Block;
import net.minecraft.entity.player.InventoryPlayer;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.World;
import net.minecraftforge.common.util.ForgeDirection;
import powercrystals.minefactoryreloaded.api.rednet.IRedNetInputNode;
import powercrystals.minefactoryreloaded.api.rednet.IRedNetNetworkContainer;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import erogenousbeef.bigreactors.client.gui.GuiReactorRedNetPort;
import erogenousbeef.bigreactors.common.BRLog;
import erogenousbeef.bigreactors.common.BigReactors;
import erogenousbeef.bigreactors.common.multiblock.MultiblockReactor;
import erogenousbeef.bigreactors.common.multiblock.block.BlockReactorPart;
import erogenousbeef.bigreactors.common.multiblock.interfaces.INeighborUpdatableEntity;
import erogenousbeef.bigreactors.common.multiblock.interfaces.ITickableMultiblockPart;
import erogenousbeef.bigreactors.gui.container.ContainerBasic;
import erogenousbeef.bigreactors.net.helpers.RedNetChange;
import erogenousbeef.core.common.CoordTriplet;
import erogenousbeef.core.multiblock.MultiblockControllerBase;

public class TileEntityReactorRedNetPort extends TileEntityReactorPart implements ITickableMultiblockPart, INeighborUpdatableEntity {

	public enum CircuitType {
		DISABLED,
		inputActive, 				// Input: reactor on/off
		inputSetControlRod, 		// Input: control rod insertion (0-100)
		inputEjectWaste,			// Input: eject waste from the reactor

		outputFuelTemperature,		// Output: Temperature of the reactor fuel
		outputCasingTemperature,	// Output: Temperature of the reactor casing
		outputFuelMix, 		// Output: Fuel mix, % of contents that is fuel (0-100, 100 = 100% fuel)
		outputFuelAmount, 	// Output: Fuel amount in a control rod, raw value, (0-4*height)
		outputWasteAmount, 	// Output: Waste amount in a control rod, raw value, (0-4*height)
		outputEnergyAmount; // Output: Energy in the reactor's buffer, percentile (0-100, 100 = 100% full)
		
		public static final CircuitType[] s_Types = CircuitType.values();
		
		public static boolean hasCoordinate(TileEntityReactorRedNetPort.CircuitType circuitType) {
			return circuitType == CircuitType.inputSetControlRod;
		}

		public static boolean canBeToggledBetweenPulseAndNormal(CircuitType circuitType) {
			return circuitType == CircuitType.inputActive;
		}
	}

	protected final static int minInputEnumValue = CircuitType.inputActive.ordinal();
	protected final static int maxInputEnumValue = CircuitType.inputEjectWaste.ordinal();
	protected final static int minOutputEnumValue = CircuitType.outputFuelTemperature.ordinal();
	protected final static int maxOutputEnumValue = CircuitType.outputEnergyAmount.ordinal();

	protected CircuitType[] channelCircuitTypes;
	protected CoordTriplet[] coordMappings;
	protected boolean[] inputActivatesOnPulse;
	protected int[] oldValue;

	public final static int numChannels = 16;
	
	IRedNetNetworkContainer redNetwork;
	IRedNetInputNode redNetInput;

	int ticksSinceLastUpdate;
	
	public TileEntityReactorRedNetPort() {
		super();
		
		channelCircuitTypes = new CircuitType[numChannels];
		coordMappings = new CoordTriplet[numChannels];
		inputActivatesOnPulse = new boolean[numChannels];
		oldValue = new int[numChannels];

		for(int i = 0; i < numChannels; i++) {
			channelCircuitTypes[i] = CircuitType.DISABLED;
			coordMappings[i] = null;
			inputActivatesOnPulse[i] = false;
			oldValue[i] = 0;
		}
		
		redNetwork = null;
		redNetInput = null;

		ticksSinceLastUpdate = 0;
	}
	
	// IMultiblockPart
	@Override
	public void onAttached(MultiblockControllerBase newController) {
		super.onAttached(newController);

		if(this.worldObj.isRemote) { return; } 
		
		checkForConnections(this.worldObj, xCoord, yCoord, zCoord);
	}
	
	@Override
	public void onMachineAssembled(MultiblockControllerBase multiblockController) {
		super.onMachineAssembled(multiblockController);

		if(this.worldObj.isRemote) { return; } 
		
		checkForConnections(this.worldObj, xCoord, yCoord, zCoord);
	}

	@Override
	@SideOnly(Side.CLIENT)
	public Object getGuiElement(InventoryPlayer inventoryPlayer) {
		return new GuiReactorRedNetPort(new ContainerBasic(), this);
	}
	
	@Override
	public Object getContainer(InventoryPlayer inventoryPlayer) {
		return new ContainerBasic();
	}

	@Override
	public void writeToNBT(NBTTagCompound par1NBTTagCompound)
	{
		super.writeToNBT(par1NBTTagCompound);
		encodeSettings(par1NBTTagCompound);
	}
	
	@Override
	protected void encodeDescriptionPacket(NBTTagCompound packetData) {
		super.encodeDescriptionPacket(packetData);
		encodeSettings(packetData);
	}
	
	@Override
	public void readFromNBT(NBTTagCompound par1NBTTagCompound)
	{
		super.readFromNBT(par1NBTTagCompound);
		decodeSettings(par1NBTTagCompound);
	}
	
	@Override
	protected void decodeDescriptionPacket(NBTTagCompound packetData) {
		super.decodeDescriptionPacket(packetData);
		decodeSettings(packetData);
	}

	// RedNet API
	public int[] getOutputValues() {
		int[] outputs = new int[numChannels];
		for(int i = 0; i < numChannels; i++) {
			outputs[i] = getValueForChannel(i);
		}
		
		return outputs;
	}
	
	public int getValueForChannel(int channel) {
		if(channel < 0 || channel >= numChannels) { return 0; }
		
		if(!this.isConnected()) { return 0; }
		
		TileEntity te = null;
		
		switch(channelCircuitTypes[channel]) {
		case outputFuelTemperature:
			return (int)Math.floor(getReactorController().getFuelHeat());
		case outputCasingTemperature:
			return (int)Math.floor(getReactorController().getReactorHeat());
		case outputFuelMix:
			MultiblockReactor controller = getReactorController();
			return (int)Math.floor(((float)controller.getFuelAmount() / (float)controller.getCapacity())*100.0f);
		case outputFuelAmount:
			return getReactorController().getFuelAmount();
		case outputWasteAmount:
			return getReactorController().getWasteAmount();
		case outputEnergyAmount:
			int energyStored, energyTotal;
			MultiblockReactor reactor = this.getReactorController();
			if(reactor != null) {
				return reactor.getEnergyStoredPercentage();
			}
			return 0;
		default:
			return 0;
		}
	}
	
	public void onInputValuesChanged(int[] newValues) {
		for(int i = 0; i < newValues.length; i++) {
			onInputValueChanged(i, newValues[i]);
		}
	}
	
	public void onInputValueChanged(int channel, int newValue) {
		if(channel < 0 || channel >= numChannels) { return; }
		CircuitType type = channelCircuitTypes[channel];
		if(!isInput(type)) { return; }
		if(!this.isConnected()) { return; }
		
		if(newValue == oldValue[channel]) { return; }
		boolean isPulse = (oldValue[channel] == 0 && newValue != 0);
		
		MultiblockReactor reactor = null;
		switch(type) {
		case inputActive:
			reactor = getReactorController();
			if(inputActivatesOnPulse[channel]) {
				if(isPulse) {
					reactor.setActive(!reactor.getActive());
				}
			}
			else {
				boolean newActive = newValue != 0;
				if(newActive != reactor.getActive()) {
					reactor.setActive(newActive);
				}
			}
			break;
		case inputSetControlRod:
			// This doesn't make sense for pulsing
			newValue = Math.min(100, Math.max(0, newValue)); // Clamp to 0..100
			if(newValue == oldValue[channel]) { return; }

			if(coordMappings[channel] != null) {
				setControlRodInsertion(channel, coordMappings[channel], newValue);
			}
			else {
				reactor = getReactorController();
				reactor.setAllControlRodInsertionValues(newValue);
			}
			break;
		case inputEjectWaste:
			// This only makes sense for pulsing
			if(isPulse) {
				reactor = getReactorController();
				reactor.ejectWaste(false, null);
			}
		default:
			break;
		}
		
		oldValue[channel] = newValue;
	}
	
	// Public RedNet helpers for GUI & updates
	@Override
	public void onNeighborBlockChange(World world, int x, int y, int z, Block neighborBlock) {
		checkForConnections(world, x, y, z);
	}
	
	@Override
	public void onNeighborTileChange(IBlockAccess world, int x, int y, int z, int neighborX, int neighborY, int neighborZ) {
		checkForConnections(world, x, y, z);
	}
	
	/**
	 * Updates the connected RedNet network, if there is one.
	 * Will only send one update per N ticks, where N is a configurable setting.
	 */
	public void onMultiblockServerTick() {
		if(!this.isConnected()) { return; }

		ticksSinceLastUpdate++;
		if(ticksSinceLastUpdate < BigReactors.ticksPerRedstoneUpdate) { return; }

		ForgeDirection out = getOutwardsDir();
		
		if(redNetwork != null) {
				redNetwork.updateNetwork(worldObj, xCoord+out.offsetX, yCoord+out.offsetY, zCoord+out.offsetZ, out.getOpposite());
		}
		
		if(redNetInput != null) {
			redNetInput.onInputsChanged(worldObj, xCoord+out.offsetX, yCoord+out.offsetY, zCoord+out.offsetZ, out.getOpposite(), getOutputValues());
		}

		ticksSinceLastUpdate = 0;
	}

	public CircuitType getChannelCircuitType(int channel) {
		if(channel < 0 || channel >= numChannels) { return CircuitType.DISABLED; }
		return channelCircuitTypes[channel];
	}

	public CoordTriplet getMappedCoord(int channel) {
		return this.coordMappings[channel];
	}
	
	public boolean isInputActivatedOnPulse(int channel) {
		return this.inputActivatesOnPulse[channel];
	}

	// RedNet helper methods
	protected void clearChannel(int channel) {
		channelCircuitTypes[channel] = CircuitType.DISABLED;
		coordMappings[channel] = null;
		inputActivatesOnPulse[channel] = false;
		oldValue[channel] = 0;
	}

	protected TileEntity getMappedTileEntity(int channel) {
		if(channel < 0 || channel >= numChannels) { return null; }
		if(coordMappings[channel] == null) { return null; }
		
		CoordTriplet coord = coordMappings[channel];
		
		if(coord == null) { return null; }
		if(!this.worldObj.checkChunksExist(coord.x, coord.y, coord.z, coord.x, coord.y, coord.z)) {
			return null;
		}
		
		return this.worldObj.getTileEntity(coord.x, coord.y, coord.z);
	}
	
	protected void setControlRodInsertion(int channel, CoordTriplet coordTriplet, int newValue) {
		if(!this.isConnected()) { return; }
		if(!this.worldObj.checkChunksExist(coordTriplet.x, coordTriplet.y, coordTriplet.z,
											coordTriplet.x, coordTriplet.y, coordTriplet.z)) {
			return;
		}
		
		TileEntity te = this.worldObj.getTileEntity(coordTriplet.x, coordTriplet.y, coordTriplet.z);
		if(te instanceof TileEntityReactorControlRod) {
			((TileEntityReactorControlRod)te).setControlRodInsertion((short)newValue);
		}
		else {
			clearChannel(channel);
		}
	}
	
	protected NBTTagCompound encodeSetting(int channel) {
		NBTTagCompound entry = new NBTTagCompound();
		
		entry.setInteger("channel", channel);
		entry.setInteger("setting", this.channelCircuitTypes[channel].ordinal());
		if(isInput(this.channelCircuitTypes[channel]) && CircuitType.canBeToggledBetweenPulseAndNormal(this.channelCircuitTypes[channel])) {
			entry.setBoolean("pulse", this.inputActivatesOnPulse[channel]);
		}
		if( CircuitType.hasCoordinate(this.channelCircuitTypes[channel]) ) {
			CoordTriplet coord = this.coordMappings[channel];
			if(coord != null) {
				entry.setInteger("x", coord.x);
				entry.setInteger("y", coord.y);
				entry.setInteger("z", coord.z);
			}
		}
		
		return entry;
	}

	protected void decodeSetting(NBTTagCompound settingTag) {
		int channel = settingTag.getInteger("channel");
		int settingIdx = settingTag.getInteger("setting");
		
		clearChannel(channel);
		
		channelCircuitTypes[channel] = CircuitType.values()[settingIdx];
		
		if(isInput(this.channelCircuitTypes[channel]) && CircuitType.canBeToggledBetweenPulseAndNormal(this.channelCircuitTypes[channel])) {
			inputActivatesOnPulse[channel] = settingTag.getBoolean("pulse");
		}

		if( CircuitType.hasCoordinate(channelCircuitTypes[channel]) ) {
			if(settingTag.hasKey("x")) {
				int x, y, z;
				x = settingTag.getInteger("x");
				y = settingTag.getInteger("y");
				z = settingTag.getInteger("z");
				coordMappings[channel] = new CoordTriplet(x, y, z);
			}
		}
	}

	// Receives settings from a client via an update packet
	public void onCircuitUpdate(RedNetChange[] changes) {
		if(changes == null || changes.length < 1) { return; }
		
		for(int i = 0; i < changes.length; i++) {
			int channelID = changes[i].getChannel();
			CircuitType newType = changes[i].getType();
			
			channelCircuitTypes[channelID] = newType;
			
			if(CircuitType.canBeToggledBetweenPulseAndNormal(newType)) {
				inputActivatesOnPulse[channelID] = changes[i].getPulseOrToggle();
			}
			
			if(CircuitType.hasCoordinate(newType)) {
				CoordTriplet coord = changes[i].getCoord();
				
				// Validate that we're pointing at the right thing, just in case.
				if(coord != null) {
					TileEntity te = worldObj.getTileEntity(coord.x, coord.y, coord.z);
					if(!(te instanceof TileEntityReactorControlRod)) {
						BRLog.warning("Invalid tile entity reference at coordinate %s - rednet circuit expected a control rod", coord);
						coord = null;
					}
				}

				coordMappings[channelID] = coord;
			}
			else {
				coordMappings[channelID] = null;
			}
		}
		
		worldObj.markBlockForUpdate(xCoord, yCoord, zCoord);
		markDirty();
	}
	
	// Helpers
	protected void encodeSettings(NBTTagCompound destination) {
		NBTTagList tagArray = new NBTTagList();
		
		for(int i = 0; i < numChannels; i++) {
			tagArray.appendTag(encodeSetting(i));
		}
		
		destination.setTag("redNetConfig", tagArray);
	}
	
	protected void decodeSettings(NBTTagCompound source) {
		NBTTagList tagArray = source.getTagList("redNetConfig", 10);
		for(int i = 0; i < tagArray.tagCount(); i++) {
			decodeSetting( (NBTTagCompound)tagArray.getCompoundTagAt(i) );
		}
	}
	
	/**
	 * Check for a world connection, if we're assembled.
	 * @param world
	 * @param x
	 * @param y
	 * @param z
	 */
	protected void checkForConnections(IBlockAccess world, int x, int y, int z) {
		ForgeDirection out = getOutwardsDir();

		if(out == ForgeDirection.UNKNOWN) {
			redNetwork = null;
			redNetInput = null;
		}
		else {
			// Check for rednet connections nearby
			redNetwork = null;
			redNetInput = null;

			Block b = worldObj.getBlock(x + out.offsetX, y + out.offsetY, z + out.offsetZ);
			if(!(b instanceof BlockReactorPart)) {
				if(b instanceof IRedNetNetworkContainer) {
					redNetwork = (IRedNetNetworkContainer)b;
				}
				else if(b instanceof IRedNetInputNode) {
					redNetInput = (IRedNetInputNode)b;
				}
			}
		}
	}

	// Static Helpers
	public static boolean isInput(CircuitType type) { return type.ordinal() >= minInputEnumValue && type.ordinal() <= maxInputEnumValue; }
	public static boolean isOutput(CircuitType type) { return type.ordinal() >= minOutputEnumValue && type.ordinal() <= maxOutputEnumValue; }
}
