package com.hbm.tileentity.machine;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import com.google.gson.JsonObject;
import com.google.gson.stream.JsonWriter;
import com.hbm.blocks.BlockDummyable;
import com.hbm.config.ServerConfig;
import com.hbm.handler.pollution.PollutionHandler;
import com.hbm.handler.pollution.PollutionHandler.PollutionType;
import com.hbm.handler.threading.PacketThreading;
import com.hbm.inventory.container.ContainerCrucible;
import com.hbm.inventory.gui.GUICrucible;
import com.hbm.inventory.material.MaterialShapes;
import com.hbm.inventory.material.Mats;
import com.hbm.inventory.material.Mats.MaterialStack;
import com.hbm.inventory.material.NTMMaterial;
import com.hbm.inventory.recipes.CrucibleRecipes;
import com.hbm.inventory.recipes.CrucibleRecipes.CrucibleRecipe;
import com.hbm.items.ModItems;
import com.hbm.main.MainRegistry;
import com.hbm.packet.toclient.AuxParticlePacketNT;
import com.hbm.tileentity.IConfigurableMachine;
import com.hbm.tileentity.IGUIProvider;
import com.hbm.tileentity.IMetalCopiable;
import com.hbm.tileentity.TileEntityMachineBase;
import com.hbm.util.BobMathUtil;
import com.hbm.util.CrucibleUtil;

import api.hbm.block.ICrucibleAcceptor;
import api.hbm.tile.IHeatSource;
import cpw.mods.fml.common.network.NetworkRegistry.TargetPoint;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.Container;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import net.minecraftforge.common.util.ForgeDirection;

public class TileEntityCrucible extends TileEntityMachineBase implements IGUIProvider, ICrucibleAcceptor, IConfigurableMachine, IMetalCopiable {

	public int heat;
	public int progress;

	public List<MaterialStack> recipeStack = new ArrayList();
	public List<MaterialStack> wasteStack = new ArrayList();

	/* CONFIGURABLE CONSTANTS */
	//because eclipse's auto complete is dumb as a fucking rock, it's now called "ZCapacity" so it's listed AFTER the actual stacks in the auto complete list.
	//also martin i know you read these: no i will not switch to intellij after using eclipse for 8 years.
	public static int recipeZCapacity = MaterialShapes.BLOCK.q(16);
	public static int wasteZCapacity = MaterialShapes.BLOCK.q(16);
	public static int processTime = 20_000;
	public static double diffusion = 0.25D;
	public static int maxHeat = 100_000;

	@Override
	public String getConfigName() {
		return "crucible";
	}

	@Override
	public void readIfPresent(JsonObject obj) {
		recipeZCapacity = IConfigurableMachine.grab(obj, "I:recipeCapacity", recipeZCapacity);
		wasteZCapacity = IConfigurableMachine.grab(obj, "I:wasteCapacity", wasteZCapacity);
		processTime = IConfigurableMachine.grab(obj, "I:processHeat", processTime);
		diffusion = IConfigurableMachine.grab(obj, "D:diffusion", diffusion);
		maxHeat = IConfigurableMachine.grab(obj, "I:heatCap", maxHeat);
	}

	@Override
	public void writeConfig(JsonWriter writer) throws IOException {
		writer.name("I:recipeCapacity").value(recipeZCapacity);
		writer.name("I:wasteCapacity").value(wasteZCapacity);
		writer.name("I:processHeat").value(processTime);
		writer.name("D:diffusion").value(diffusion);
		writer.name("I:heatCap").value(maxHeat);
	}

	public TileEntityCrucible() {
		super(10);
	}

	@Override
	public String getName() {
		return "container.machineCrucible";
	}

	@Override
	public int getInventoryStackLimit() {
		return 1; //prevents clogging
	}

	@Override
	public void updateEntity() {

		if(!worldObj.isRemote) {
			tryPullHeat();

			/* collect items */
			if(worldObj.getTotalWorldTime() % 5 == 0) {
				List<EntityItem> list = worldObj.getEntitiesWithinAABB(EntityItem.class, AxisAlignedBB.getBoundingBox(xCoord - 0.5, yCoord + 0.5, zCoord - 0.5, xCoord + 1.5, yCoord + 1, zCoord + 1.5));

				for(EntityItem item : list) {
					if(item.isDead) continue;
					ItemStack stack = item.getEntityItem();
					if(this.isItemSmeltable(stack)) {

						for(int i = 1; i < 10; i++) {
							if(slots[i] == null) {

								if(stack.stackSize == 1) {
									slots[i] = stack.copy();
									item.setDead();
									item.delayBeforeCanPickup = 60;
									break;
								} else {
									slots[i] = stack.copy();
									slots[i].stackSize = 1;
									stack.stackSize--;
								}

								this.markChanged();
							}
						}
					}
				}
			}

			int totalCap = recipeZCapacity + wasteZCapacity;
			int totalMass = 0;

			for(MaterialStack stack : recipeStack) totalMass += stack.amount;
			for(MaterialStack stack : wasteStack) totalMass += stack.amount;

			double level = ((double) totalMass / (double) totalCap) * 0.875D;

			List<EntityLivingBase> living = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, AxisAlignedBB.getBoundingBox(xCoord + 0.5, yCoord + 0.5, zCoord + 0.5, xCoord + 0.5, yCoord + 0.5 + level, zCoord + 0.5).expand(1, 0, 1));
			for(EntityLivingBase entity : living) {
				entity.attackEntityFrom(DamageSource.lava, 5F);
				entity.setFire(5);
			}

			/* smelt items from buffer */
			if(!trySmelt()) {
				this.progress = 0;
			}

			tryRecipe();

			/* pour waste stack */
			if(!this.wasteStack.isEmpty()) {

				ForgeDirection dir = ForgeDirection.getOrientation(this.getBlockMetadata() - BlockDummyable.offset).getOpposite();
				Vec3 impact = Vec3.createVectorHelper(0, 0, 0);
				MaterialStack didPour = CrucibleUtil.pourFullStack(worldObj, xCoord + 0.5D + dir.offsetX * 1.875D, yCoord + 0.25D, zCoord + 0.5D + dir.offsetZ * 1.875D, 6, true, this.wasteStack, MaterialShapes.NUGGET.q(3), impact);

				if(didPour != null) {
					NBTTagCompound data = new NBTTagCompound();
					data.setString("type", "foundry");
					data.setInteger("color", didPour.material.moltenColor);
					data.setByte("dir", (byte) dir.ordinal());
					data.setFloat("off", 0.625F);
					data.setFloat("base", 0.625F);
					data.setFloat("len", Math.max(1F, yCoord - (float) (Math.ceil(impact.yCoord) - 0.875)));
					PacketThreading.createAllAroundThreadedPacket(new AuxParticlePacketNT(data, xCoord + 0.5D + dir.offsetX * 1.875D, yCoord, zCoord + 0.5D + dir.offsetZ * 1.875D), new TargetPoint(worldObj.provider.dimensionId, xCoord + 0.5, yCoord + 1, zCoord + 0.5, 50));

				}

				PollutionHandler.incrementPollution(worldObj, xCoord, yCoord, zCoord, PollutionType.SOOT, PollutionHandler.SOOT_PER_SECOND / 20F);
			}

			/* pour recipe stack */
			if(!this.recipeStack.isEmpty()) {

				ForgeDirection dir = ForgeDirection.getOrientation(this.getBlockMetadata() - BlockDummyable.offset);
				List<MaterialStack> toCast = new ArrayList();

				CrucibleRecipe recipe = this.getLoadedRecipe();
				//if no recipe is loaded, everything from the recipe stack will be drainable
				if(recipe == null) {
					toCast.addAll(this.recipeStack);
				} else {

					for(MaterialStack stack : this.recipeStack) {
						for(MaterialStack output : recipe.output) {
							if(stack.material == output.material) {
								toCast.add(stack);
								break;
							}
						}
					}
				}

				Vec3 impact = Vec3.createVectorHelper(0, 0, 0);
				MaterialStack didPour = CrucibleUtil.pourFullStack(worldObj, xCoord + 0.5D + dir.offsetX * 1.875D, yCoord + 0.25D, zCoord + 0.5D + dir.offsetZ * 1.875D, 6, true, toCast, MaterialShapes.NUGGET.q(3), impact);

				if(didPour != null) {
					NBTTagCompound data = new NBTTagCompound();
					data.setString("type", "foundry");
					data.setInteger("color", didPour.material.moltenColor);
					data.setByte("dir", (byte) dir.ordinal());
					data.setFloat("off", 0.625F);
					data.setFloat("base", 0.625F);
					data.setFloat("len", Math.max(1F, yCoord - (float) (Math.ceil(impact.yCoord) - 0.875)));
					PacketThreading.createAllAroundThreadedPacket(new AuxParticlePacketNT(data, xCoord + 0.5D + dir.offsetX * 1.875D, yCoord, zCoord + 0.5D + dir.offsetZ * 1.875D), new TargetPoint(worldObj.provider.dimensionId, xCoord + 0.5, yCoord + 1, zCoord + 0.5, 50));

				}

				PollutionHandler.incrementPollution(worldObj, xCoord, yCoord, zCoord, PollutionType.SOOT, PollutionHandler.SOOT_PER_SECOND / 20F);
			}

			/* clean up stacks */
			this.recipeStack.removeIf(o -> o.amount <= 0);
			this.wasteStack.removeIf(x -> x.amount <= 0);

			/* sync */
			this.networkPackNT(25);
		} else {
			
			if(!this.recipeStack.isEmpty() || !this.wasteStack.isEmpty()) {

				if(worldObj.getTotalWorldTime() % 10 == 0) {
					NBTTagCompound fx = new NBTTagCompound();
					fx.setString("type", "tower");
					fx.setFloat("lift", 10F);
					fx.setFloat("base", 0.75F);
					fx.setFloat("max", 3.5F);
					fx.setInteger("life", 100 + worldObj.rand.nextInt(20));
					fx.setInteger("color",0x202020);
					fx.setDouble("posX", xCoord + 0.5);
					fx.setDouble("posY", yCoord + 1);
					fx.setDouble("posZ", zCoord + 0.5);
					MainRegistry.proxy.effectNT(fx);
				}
			}
		}
	}

	@Override
	public void serialize(ByteBuf buf) {
		super.serialize(buf);
		buf.writeInt(progress);
		buf.writeInt(heat);

		buf.writeShort(recipeStack.size());
		for(MaterialStack sta : recipeStack) {
			if (sta.material == null)
				buf.writeInt(-1);
			else
				buf.writeInt(sta.material.id);
			buf.writeInt(sta.amount);
		}

		buf.writeShort(wasteStack.size());
		for(MaterialStack sta : wasteStack) {
			if (sta.material == null)
				buf.writeInt(-1);
			else
				buf.writeInt(sta.material.id);
			buf.writeInt(sta.amount);
		}
	}

	@Override
	public void deserialize(ByteBuf buf) {
		super.deserialize(buf);
		progress = buf.readInt();
		heat = buf.readInt();

		recipeStack.clear();
		wasteStack.clear();

		int mats = buf.readShort();
		for(int i = 0; i < mats; i++) {
			int id = buf.readInt();
			if (id == -1)
				continue;
			recipeStack.add(new MaterialStack(Mats.matById.get(id), buf.readInt()));
		}

		mats = buf.readShort();
		for(int i = 0; i < mats; i++) {
			int id = buf.readInt();
			if (id == -1)
				continue;
			wasteStack.add(new MaterialStack(Mats.matById.get(id), buf.readInt()));
		}
	}

	@Override
	public void readFromNBT(NBTTagCompound nbt) {
		super.readFromNBT(nbt);

		int[] rec = nbt.getIntArray("rec");
		for(int i = 0; i < rec.length / 2; i++) {
			recipeStack.add(new MaterialStack(Mats.matById.get(rec[i * 2]), rec[i * 2 + 1]));
		}

		int[] was = nbt.getIntArray("was");
		for(int i = 0; i < was.length / 2; i++) {
			wasteStack.add(new MaterialStack(Mats.matById.get(was[i * 2]), was[i * 2 + 1]));
		}

		this.progress = nbt.getInteger("progress");
		this.heat = nbt.getInteger("heat");
	}

	@Override
	public void writeToNBT(NBTTagCompound nbt) {
		super.writeToNBT(nbt);

		int[] rec = new int[recipeStack.size() * 2];
		int[] was = new int[wasteStack.size() * 2];
		for(int i = 0; i < recipeStack.size(); i++) { MaterialStack sta = recipeStack.get(i); rec[i * 2] = sta.material.id; rec[i * 2 + 1] = sta.amount; }
		for(int i = 0; i < wasteStack.size(); i++) { MaterialStack sta = wasteStack.get(i); was[i * 2] = sta.material.id; was[i * 2 + 1] = sta.amount; }
		nbt.setIntArray("rec", rec);
		nbt.setIntArray("was", was);
		nbt.setInteger("progress", progress);
		nbt.setInteger("heat", heat);
	}

	protected void tryPullHeat() {

		if(this.heat >= this.maxHeat) return;

		TileEntity con = worldObj.getTileEntity(xCoord, yCoord - 1, zCoord);

		if(con instanceof IHeatSource) {
			IHeatSource source = (IHeatSource) con;
			int diff = source.getHeatStored() - this.heat;

			if(diff == 0) {
				return;
			}
			
			diff = Math.min(diff, this.maxHeat - this.heat);

			if(diff > 0) {
				diff = (int) Math.ceil(diff * diffusion);
				source.useUpHeat(diff);
				this.heat += diff;
				if(this.heat > this.maxHeat)
					this.heat = this.maxHeat;
				return;
			}
		}

		this.heat = Math.max(this.heat - Math.max(this.heat / 1000, 1), 0);
	}

	protected boolean trySmelt() {

		if(this.heat < maxHeat / 2) return false;

		int slot = this.getFirstSmeltableSlot();
		if(slot == -1) return false;

		int delta = this.heat - (maxHeat / 2);
		delta *= 0.05;

		this.progress += delta;
		this.heat -= delta;

		if(this.progress >= processTime) {
			this.progress = 0;

			List<MaterialStack> materials = Mats.getSmeltingMaterialsFromItem(slots[slot]);
			CrucibleRecipe recipe = getLoadedRecipe();

			for(MaterialStack material : materials) {
				boolean recipeMaterial = recipe != null && (getQuantaFromType(recipe.input, material.material) > 0 || getQuantaFromType(recipe.output, material.material) > 0);

				if((recipe == null && !ServerConfig.LEGACY_CRUCIBLE_RULES.get()) || recipeMaterial) {
					this.addToStack(this.recipeStack, material);
				} else {
					this.addToStack(this.wasteStack, material);
				}
			}

			this.decrStackSize(slot, 1);
		}

		return true;
	}

	protected void tryRecipe() {
		CrucibleRecipe recipe = this.getLoadedRecipe();

		if(recipe == null) return;
		if(worldObj.getTotalWorldTime() % recipe.frequency > 0) return;

		for(MaterialStack stack : recipe.input) {
			if(getQuantaFromType(this.recipeStack, stack.material) < stack.amount) return;
		}

		for(MaterialStack stack : this.recipeStack) {
			stack.amount -= getQuantaFromType(recipe.input, stack.material);
		}

		outer:
		for(MaterialStack out : recipe.output) {

			for(MaterialStack stack : this.recipeStack) {
				if(stack.material == out.material) {
					stack.amount += out.amount;
					continue outer;
				}
			}

			this.recipeStack.add(out.copy());
		}
	}

	protected int getFirstSmeltableSlot() {

		for(int i = 1; i < 10; i++) {

			ItemStack stack = slots[i];

			if(stack != null && isItemSmeltable(stack)) {
				return i;
			}
		}

		return -1;
	}

	@Override
	public boolean isItemValidForSlot(int i, ItemStack stack) {

		if(i == 0) {
			return stack.getItem() == ModItems.crucible_template;
		}

		return isItemSmeltable(stack);
	}

	public boolean isItemSmeltable(ItemStack stack) {

		List<MaterialStack> materials = Mats.getSmeltingMaterialsFromItem(stack);

		//if there's no materials in there at all, don't smelt
		if(materials.isEmpty())
			return false;
		CrucibleRecipe recipe = getLoadedRecipe();

		//needs to be true, will always be true if there's no recipe loaded
		boolean matchesRecipe = recipe == null;

		//the amount of material in the entire recipe input
		int recipeContent = recipe != null ? recipe.getInputAmount() : 0;
		//the total amount of the current waste stack, used for simulation
		int recipeAmount = getQuantaFromType(this.recipeStack, null);
		int wasteAmount = getQuantaFromType(this.wasteStack, null);

		for(MaterialStack mat : materials) {
			//if no recipe is loaded, everything will land in the waste stack
			int recipeInputRequired = recipe != null ? getQuantaFromType(recipe.input, mat.material) : 0;

			//this allows pouring the ouput material back into the crucible
			if(recipe != null && getQuantaFromType(recipe.output, mat.material) > 0) {
				recipeAmount += mat.amount;
				matchesRecipe = true;
				continue;
			}

			if(recipeInputRequired == 0) {
				//if this type isn't required by the recipe, add it to the waste stack
				wasteAmount += mat.amount;
			} else {

				//the maximum is the recipe's ratio scaled up to the recipe stack's capacity
				int matMaximum = recipeInputRequired * this.recipeZCapacity / recipeContent;
				int amountStored = getQuantaFromType(recipeStack, mat.material);

				matchesRecipe = true;

				recipeAmount += mat.amount;

				//if the amount of that input would exceed the amount dictated by the recipe, return false
				if(recipe != null && amountStored + mat.amount > matMaximum)
					return false;
			}
		}

		//if the amount doesn't exceed the capacity and the recipe matches (or isn't null), return true
		return recipeAmount <= this.recipeZCapacity && wasteAmount <= this.wasteZCapacity && matchesRecipe;
	}

	public void addToStack(List<MaterialStack> stack, MaterialStack matStack) {

		for(MaterialStack mat : stack) {
			if(mat.material == matStack.material) {
				mat.amount += matStack.amount;
				return;
			}
		}

		stack.add(matStack.copy());
	}

	public CrucibleRecipe getLoadedRecipe() {

		if(slots[0] != null && slots[0].getItem() == ModItems.crucible_template) {
			return CrucibleRecipes.indexMapping.get(slots[0].getItemDamage());
		}

		return null;
	}

	/* "Arrays and Lists don't have a common ancestor" my fucking ass */
	public int getQuantaFromType(MaterialStack[] stacks, NTMMaterial mat) {
		for(MaterialStack stack : stacks) {
			if(mat == null || stack.material == mat) {
				return stack.amount;
			}
		}
		return 0;
	}

	public int getQuantaFromType(List<MaterialStack> stacks, NTMMaterial mat) {
		int sum = 0;
		for(MaterialStack stack : stacks) {
			if(stack.material == mat) {
				return stack.amount;
			}
			if(mat == null) {
				sum += stack.amount;
			}
		}
		return sum;
	}

	@Override
	public int[] getAccessibleSlotsFromSide(int meta) {
		return new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	}

	@Override
	public Container provideContainer(int ID, EntityPlayer player, World world, int x, int y, int z) {
		return new ContainerCrucible(player.inventory, this);
	}

	@Override
	@SideOnly(Side.CLIENT)
	public Object provideGUI(int ID, EntityPlayer player, World world, int x, int y, int z) {
		return new GUICrucible(player.inventory, this);
	}

	AxisAlignedBB bb = null;

	@Override
	public AxisAlignedBB getRenderBoundingBox() {

		if(bb == null) {
			bb = AxisAlignedBB.getBoundingBox(
					xCoord - 1,
					yCoord,
					zCoord - 1,
					xCoord + 2,
					yCoord + 2,
					zCoord + 2
					);
		}

		return bb;
	}

	@Override
	@SideOnly(Side.CLIENT)
	public double getMaxRenderDistanceSquared() {
		return 65536.0D;
	}

	@Override
	public boolean canAcceptPartialPour(World world, int x, int y, int z, double dX, double dY, double dZ, ForgeDirection side, MaterialStack stack) {

		CrucibleRecipe recipe = getLoadedRecipe();

		if(recipe == null) {
			return getQuantaFromType(this.wasteStack, null) < this.wasteZCapacity;
		}

		int recipeContent = recipe.getInputAmount();
		int recipeInputRequired = getQuantaFromType(recipe.input, stack.material);
		int matMaximum = recipeInputRequired * this.recipeZCapacity / recipeContent;
		int amountStored = getQuantaFromType(recipeStack, stack.material);

		return amountStored < matMaximum && getQuantaFromType(this.recipeStack, null) < this.recipeZCapacity;
	}

	@Override
	public MaterialStack pour(World world, int x, int y, int z, double dX, double dY, double dZ, ForgeDirection side, MaterialStack stack) {

		CrucibleRecipe recipe = getLoadedRecipe();

		if(recipe == null) {

			int amount = getQuantaFromType(this.wasteStack, null);

			if(amount + stack.amount <= this.wasteZCapacity) {
				this.addToStack(this.wasteStack, stack.copy());
				return null;
			} else {
				int toAdd = this.wasteZCapacity - amount;
				this.addToStack(this.wasteStack, new MaterialStack(stack.material, toAdd));
				return new MaterialStack(stack.material, stack.amount - toAdd);
			}
		}

		int recipeContent = recipe.getInputAmount();
		int recipeInputRequired = getQuantaFromType(recipe.input, stack.material);
		int matMaximum = recipeInputRequired * this.recipeZCapacity / recipeContent;

		if(recipeInputRequired + stack.amount <= matMaximum) {
			this.addToStack(this.recipeStack, stack.copy());
			return null;
		}

		int toAdd = matMaximum - stack.amount;
		toAdd = Math.min(toAdd, this.recipeZCapacity - getQuantaFromType(this.recipeStack, null));
		this.addToStack(this.recipeStack, new MaterialStack(stack.material, toAdd));
		return new MaterialStack(stack.material, stack.amount - toAdd);
	}

	@Override public boolean canAcceptPartialFlow(World world, int x, int y, int z, ForgeDirection side, MaterialStack stack) { return false; }
	@Override public MaterialStack flow(World world, int x, int y, int z, ForgeDirection side, MaterialStack stack) { return null; }

	@Override
	public int[] getMatsToCopy() {
		ArrayList<Integer> types = new ArrayList<>();

		for (MaterialStack stack : recipeStack) {
			types.add(stack.material.id);
		}
		for (MaterialStack stack : wasteStack) {
			types.add(stack.material.id);
		}
		return BobMathUtil.intCollectionToArray(types);
	}

}
