/*
 * This file is part of Industrial Wires.
 * Copyright (C) 2016-2018 malte0811
 * Industrial Wires is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * Industrial Wires is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with Industrial Wires.  If not, see <http://www.gnu.org/licenses/>.
 */
package malte0811.industrialWires.blocks.wire;

import blusunrize.immersiveengineering.api.ApiUtils;
import blusunrize.immersiveengineering.api.TargetingInfo;
import blusunrize.immersiveengineering.api.energy.wires.*;
import blusunrize.immersiveengineering.api.energy.wires.ImmersiveNetHandler.AbstractConnection;
import blusunrize.immersiveengineering.api.energy.wires.ImmersiveNetHandler.Connection;
import blusunrize.immersiveengineering.common.blocks.IEBlockInterfaces.IDirectionalTile;
import ic2.api.energy.event.EnergyTileUnloadEvent;
import ic2.api.energy.tile.IEnergyAcceptor;
import ic2.api.energy.tile.IEnergyEmitter;
import ic2.api.energy.tile.IEnergySink;
import ic2.api.energy.tile.IEnergySource;
import malte0811.industrialWires.IMixedConnector;
import malte0811.industrialWires.IndustrialWires;
import malte0811.industrialWires.blocks.IBlockBoundsIW;
import malte0811.industrialWires.compat.Compat;
import malte0811.industrialWires.util.ConversionUtil;
import malte0811.industrialWires.util.MiscUtils;
import malte0811.industrialWires.wires.EnergyType;
import malte0811.industrialWires.wires.MixedWireType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.ITickable;
import net.minecraft.util.math.*;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.energy.CapabilityEnergy;
import net.minecraftforge.energy.IEnergyStorage;
import net.minecraftforge.fml.common.Optional;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.function.Consumer;

import static malte0811.industrialWires.wires.EnergyType.*;
import static malte0811.industrialWires.wires.MixedWireType.TIN;

@Optional.InterfaceList({
		@Optional.Interface(iface = "ic2.api.energy.tile.IEnergySource", modid = "ic2"),
		@Optional.Interface(iface = "ic2.api.energy.tile.IEnergySink", modid = "ic2")
})
public class TileEntityIC2ConnectorTin extends TileEntityImmersiveConnectable implements IEnergySource, IEnergySink, IDirectionalTile,
		ITickable, IMixedConnector, IBlockBoundsIW {
	private static final double EPS = .1;
	private EnumFacing facing = EnumFacing.NORTH;
	private boolean relay;
	private boolean first = true;
	// external net to IE net buffer
	private double bufferToNet = 0;
	private double ieInputInTick = 0;
	private double maxToNet = 0;
	//IE net to external net buffer
	private double bufferToMachine = 0;
	private double externalInputInTick = 0;
	private double maxToMachine = 0;
	private EnergyType energyType = NONE;
	private boolean shouldBreak = false;
	private final double maxStored;
	private final MixedWireType wireType;
	private final int tier;
	private final double relayOffset;
	private final double connOffset;

	protected TileEntityIC2ConnectorTin(boolean relay, MixedWireType type, int tier, double relayLength, double connLength) {
		this.relay = relay;
		wireType = type;
		maxStored = type.getIORate();
		this.tier = tier;
		this.relayOffset = relayLength-.5;
		this.connOffset = connLength-.5;
	}

	public TileEntityIC2ConnectorTin(boolean relay) {
		this(relay, TIN, 1, .5, .5);
	}

	public TileEntityIC2ConnectorTin() {
		this(false);
	}

	@Override
	public void onLoad() {
		super.onLoad();
		if (!world.isRemote&& IndustrialWires.hasIC2)
			Compat.loadIC2Tile.accept(this);
		ImmersiveNetHandler.INSTANCE.onTEValidated(this);
	}

	@Override
	public void update() {
		if (!world.isRemote) {
			if (shouldBreak) {
				Deque<BlockPos> open = new ArrayDeque<>();
				open.push(pos);
				Set<BlockPos> closed = new HashSet<>();
				closed.add(pos);
				while (!open.isEmpty()) {
					BlockPos next = open.pop();
					Set<Connection> conns = ImmersiveNetHandler.INSTANCE.getConnections(world, next);
					if (conns!=null) {
						for (Connection c:conns) {
							ImmersiveNetHandler.INSTANCE.getTransferedRates(world.provider.getDimension())
									.put(c, 2*c.cableType.getTransferRate());
							if (closed.add(c.end)) {
								open.push(c.end);
							}
						}
					}
				}
				for (BlockPos p:closed) {
					TileEntity tile = world.getTileEntity(p);
					if (tile instanceof IImmersiveConnectable && ((IImmersiveConnectable) tile).isEnergyOutput()) {
						world.createExplosion(null, p.getX()+.5, p.getY()+.5, p.getZ()+.5,
								3, true);
					}
				}
				return;
			}
			if (externalInputInTick==0 && ieInputInTick == 0 && bufferToNet == 0 && bufferToMachine == 0) {
				energyType = NONE;
			}
			if (bufferToNet > EPS) {
				transferPowerToNet();
			}
			if (bufferToNet >EPS) {
				notifyAvailableEnergy(bufferToNet);
			}
			if (bufferToMachine > EPS && energyType==FE_AC) {
				transferPowerToFEMachine();
			}
			if (bufferToNet < maxToNet) {
				maxToNet = bufferToNet;
			}
			if (externalInputInTick > maxToNet) {
				maxToNet = externalInputInTick;
			}
			externalInputInTick = 0;

			if (bufferToMachine < maxToMachine) {
				maxToMachine = bufferToMachine;
			}
			if (ieInputInTick > maxToMachine) {
				maxToMachine = ieInputInTick;
			}
			ieInputInTick = 0;
		}
	}

	//TODO push FE

	private void transferPowerToNet() {
		Set<AbstractConnection> conns = ImmersiveNetHandler.INSTANCE.getIndirectEnergyConnections(pos, world, true);
		Map<AbstractConnection, Pair<IMixedConnector, Double>> maxOutputs = new HashMap<>();
		double outputMax = Math.min(bufferToNet, maxToNet);
		double sum = 0;
		for (AbstractConnection c : conns) {
			if (c.isEnergyOutput) {
				IImmersiveConnectable iic = ApiUtils.toIIC(c.end, world);
				if (iic instanceof IMixedConnector) {
					double extract =
							outputMax - ((IMixedConnector) iic).insertEnergy(outputMax, true, energyType);
					if (extract > EPS) {
						maxOutputs.put(c, new ImmutablePair<>((IMixedConnector) iic, extract));
						sum += extract;
					}
				}
			}
		}
		if (sum > EPS) {
			HashMap<Connection, Integer> transferedPerConn = ImmersiveNetHandler.INSTANCE.getTransferedRates(world.provider.getDimension());
			for (Map.Entry<AbstractConnection, Pair<IMixedConnector, Double>> entry : maxOutputs.entrySet()) {
				Pair<IMixedConnector, Double> p = entry.getValue();
				AbstractConnection c = entry.getKey();
				double out = outputMax * p.getRight() / sum;
				double loss = energyType.getLoss(getAverageLossRate(c), bufferToNet, out);
				out = Math.min(out, bufferToNet -loss);
				if (out<=0)
					continue;
				double inserted = out - p.getLeft().insertEnergy(out, false, energyType);
				double energyAtConn = inserted + loss;
				bufferToNet -= energyAtConn;
				float intermediaryLoss = 0;
				HashSet<IImmersiveConnectable> passedConnectors = new HashSet<>();
				for (Connection sub : c.subConnections) {
					int transferredPerCon = transferedPerConn.getOrDefault(sub, 0);
					energyAtConn -= sub.cableType.getLossRatio() * sub.length;
					ImmersiveNetHandler.INSTANCE.getTransferedRates(world.provider.getDimension()).put(sub, (int) (transferredPerCon + energyAtConn));
					IImmersiveConnectable subStart = ApiUtils.toIIC(sub.start, world);
					IImmersiveConnectable subEnd = ApiUtils.toIIC(sub.end, world);
					if (subStart != null && passedConnectors.add(subStart))
						subStart.onEnergyPassthrough((int) (inserted - inserted * intermediaryLoss));
					if (subEnd != null && passedConnectors.add(subEnd))
						subEnd.onEnergyPassthrough((int) (inserted - inserted * intermediaryLoss));
				}
			}
		}
	}

	private void transferPowerToFEMachine() {
		BlockPos outPos = pos.offset(facing);
		TileEntity te = MiscUtils.getLoadedTE(world, outPos, TileEntity.class);
		if (te!=null && te.hasCapability(CapabilityEnergy.ENERGY, facing.getOpposite())) {
			IEnergyStorage handler = te.getCapability(CapabilityEnergy.ENERGY, facing.getOpposite());
			assert handler!=null;
			double outJoules = Math.min(bufferToMachine, maxToMachine);
			int outFE = MathHelper.floor(outJoules*ConversionUtil.ifPerJoule());
			int received = handler.receiveEnergy(outFE, false);
			bufferToMachine -= received*ConversionUtil.joulesPerIf();
		}
	}

	private void notifyAvailableEnergy(double storedNew)
	{
		Set<AbstractConnection> outputs = ImmersiveNetHandler.INSTANCE.getIndirectEnergyConnections(pos, world, true);
		for(AbstractConnection con : outputs)
		{
			IImmersiveConnectable end = ApiUtils.toIIC(con.end, world);
			if(con.cableType!=null && end!=null && end.allowEnergyToPass(null))
			{
				Pair<Float, Consumer<Float>> e = getEnergyForConnection(con, storedNew);
				end.addAvailableEnergy(e.getKey(), e.getValue());
			}
		}
		addAvailableEnergy(-1, null);
	}

	private Pair<Float, Consumer<Float>> getEnergyForConnection(@Nullable AbstractConnection c, double storedNew)
	{
		float loss = c!=null?c.getAverageLossRate():0;
		float max = (float) (storedNew-loss);
		Consumer<Float> extract = (energy)-> bufferToNet -= energy+loss;
		return new ImmutablePair<>(max, extract);
	}

	private double getAverageLossRate(AbstractConnection conn) {
		double f = 0;
		for (Connection c : conn.subConnections) {
			f += c.length * c.cableType.getLossRatio();
		}
		return f;
	}

	//Input through the net
	@Override
	public double insertEnergy(double joules, boolean simulate, EnergyType type) {
		if (energyType==NONE) {
			energyType = type;
		} else if (energyType!=type) {
			shouldBreak = true;
			return 0;
		}
		final double insert = Math.min(maxStored - bufferToMachine, joules);
		if (!simulate) {
			bufferToMachine += insert;
		} else {
			//Yes, this is weird. But it works, otherwise the system can get stuck at a lower output rate with a full buffer
			ieInputInTick += insert;
		}
		return joules - insert;
	}

	@Override
	public void invalidate() {
		if (!world.isRemote)
			MinecraftForge.EVENT_BUS.post(new EnergyTileUnloadEvent(this));
		super.invalidate();
	}

	@Override
	public void onChunkUnload() {
		super.onChunkUnload();
		if (!world.isRemote)
			MinecraftForge.EVENT_BUS.post(new EnergyTileUnloadEvent(this));
	}

	@Override
	public Vec3d getConnectionOffset(Connection con) {
		EnumFacing side = facing.getOpposite();
		double conRadius = con.cableType.getRenderDiameter() / 2;
		double length = relay?relayOffset:connOffset;
		return new Vec3d(.5 + ( length - conRadius) * side.getFrontOffsetX(),
				.5 + (length - conRadius) * side.getFrontOffsetY(),
				.5 + (length - conRadius) * side.getFrontOffsetZ());
	}

	@Override
	public boolean canConnect() {
		return true;
	}

	@Override
	public boolean isEnergyOutput() {
		return !relay;
	}

	@Override
	public boolean canConnectCable(WireType cableType, TargetingInfo target, Vec3i offset) {
		return (limitType == null || this.isRelay()) && WireApi.canMix(cableType, wireType);
	}

	@Override
	protected boolean isRelay() {
		return relay;
	}

	@Override
	@Optional.Method(modid="ic2")
	public boolean emitsEnergyTo(IEnergyAcceptor receiver, EnumFacing side) {
		return !relay && side == facing;
	}

	@Override
	@Optional.Method(modid="ic2")
	public boolean acceptsEnergyFrom(IEnergyEmitter emitter, EnumFacing side) {
		return !relay && side == facing;
	}

	@Override
	@Optional.Method(modid="ic2")
	public double getDemandedEnergy() {
		double ret = (maxStored - bufferToNet) *ConversionUtil.euPerJoule() + .5;
		if (ret < .1)
			ret = 0;
		return ret;
	}

	@Override
	@Optional.Method(modid="ic2")
	public int getSinkTier() {
		return tier;
	}

	@Override
	@Optional.Method(modid="ic2")
	public double injectEnergy(EnumFacing directionFrom, double amount, double voltage) {
		return amount-ConversionUtil.euPerJoule()*addToIn(ConversionUtil.joulesPerEu()*amount, false, EU_DC);
	}

	@Override
	@Optional.Method(modid="ic2")
	public double getOfferedEnergy() {
		return Math.min(maxToMachine, bufferToMachine) * ConversionUtil.euPerJoule();
	}

	@Override
	@Optional.Method(modid="ic2")
	public void drawEnergy(double amount) {
		bufferToMachine -= amount*ConversionUtil.joulesPerEu();
		markDirty();
	}

	@Nullable
	protected Pair<Float,Consumer<Float>> getOwnEnergy()
	{
		if (isRelay())
			return null;
		return new ImmutablePair<>((float) bufferToNet, (d)-> bufferToNet -= d);
	}
	@Override
	protected float getBaseDamage(ImmersiveNetHandler.Connection c) {
		return 1/64F;
	}

	@Override
	@Optional.Method(modid="ic2")
	public int getSourceTier() {
		return tier;
	}

	// Returns amount of energy consumed
	private double addToIn(double joules, boolean simulate, EnergyType type) {
		if (energyType==NONE) {
			energyType = type;
		} else if (energyType!=type) {
			shouldBreak = true;
		}
		if (bufferToNet < maxStored) {
			if (!simulate) {
				bufferToNet += joules;
				externalInputInTick += joules;
				notifyAvailableEnergy(joules);
			}
			markDirty();
			return joules;
		}
		return 0;
	}

	@Override
	public void readCustomNBT(@Nonnull NBTTagCompound nbt, boolean descPacket) {
		super.readCustomNBT(nbt, descPacket);
		facing = EnumFacing.getFront(nbt.getInteger("facing"));
		relay = nbt.getBoolean("relay");
		int version = nbt.getInteger("version");
		bufferToNet = nbt.getDouble("inBuffer");
		bufferToMachine = nbt.getDouble("outBuffer");
		if (nbt.hasKey("maxToNet")) {
			maxToNet = nbt.getDouble("maxToNet");
		} else {
			maxToNet = bufferToNet;
		}
		if (nbt.hasKey("maxToMachine")) {
			maxToMachine = nbt.getDouble("maxToMachine");
		} else {
			maxToMachine = bufferToMachine;
		}
		energyType = EnergyType.values()[nbt.getInteger("energyType")];
		if (version==0) {
			bufferToNet *= ConversionUtil.joulesPerEu();
			bufferToMachine *= ConversionUtil.joulesPerEu();
			maxToNet *= ConversionUtil.joulesPerEu();
			maxToMachine *= ConversionUtil.joulesPerEu();
		}
	}

	@Override
	public void writeCustomNBT(@Nonnull NBTTagCompound nbt, boolean descPacket) {
		super.writeCustomNBT(nbt, descPacket);
		nbt.setInteger("facing", facing.getIndex());
		nbt.setBoolean("relay", relay);
		nbt.setDouble("inBuffer", bufferToNet);
		nbt.setDouble("outBuffer", bufferToMachine);
		nbt.setDouble("maxToNet", maxToNet);
		nbt.setDouble("maxToMachine", maxToMachine);
		nbt.setInteger("energyType", energyType.ordinal());
		nbt.setInteger("version", 1);
	}

	@Nonnull
	@Override
	public EnumFacing getFacing() {
		return facing;
	}

	@Override
	public void setFacing(@Nonnull EnumFacing facing) {
		this.facing = facing;
	}

	@Override
	public int getFacingLimitation() {
		return 0;
	}

	@Override
	public boolean mirrorFacingOnPlacement(@Nonnull EntityLivingBase placer) {
		return true;
	}

	@Override
	public boolean canHammerRotate(@Nonnull EnumFacing side, float hitX, float hitY, float hitZ, @Nonnull EntityLivingBase entity) {
		return false;
	}

	@Override
	public AxisAlignedBB getBoundingBox() {
		double length = .5+(relay?relayOffset:connOffset);
		double wMin = .3125;
		double wMax = .6875;
		switch (facing.getOpposite()) {
		case UP:
			return new AxisAlignedBB(wMin, 0, wMin, wMax, length, wMax);
		case DOWN:
			return new AxisAlignedBB(wMin, 1 - length, wMin, wMax, 1, wMax);
		case SOUTH:
			return new AxisAlignedBB(wMin, wMin, 0, wMax, wMax, length);
		case NORTH:
			return new AxisAlignedBB(wMin, wMin, 1 - length, wMax, wMax, 1);
		case EAST:
			return new AxisAlignedBB(0, wMin, wMin, length, wMax, wMax);
		case WEST:
			return new AxisAlignedBB(1 - length, wMin, wMin, 1, wMax, wMax);
		}
		return new AxisAlignedBB(0, 0, 0, 1, 1, 1);
	}

	@Override
	public boolean hasCapability(@Nonnull Capability<?> capability, @Nullable EnumFacing facing) {
		if (capability==CapabilityEnergy.ENERGY) {
			return !isRelay() && facing == this.facing;
		}
		return super.hasCapability(capability, facing);
	}

	@Override
	public <T> T getCapability(@Nonnull Capability<T> capability, @Nullable EnumFacing facing) {
		if (capability==CapabilityEnergy.ENERGY) {
			if (!isRelay() && facing == this.facing) {
				return CapabilityEnergy.ENERGY.cast(energyHandler);
			}
			return null;
		}
		return super.getCapability(capability, facing);
	}

	/*
	 * regarding equals+hashCode
	 * TE's are considered equal if they have the same pos+dimension id
	 * This is necessary to work around a weird bug causing a lot of log spam (100GB and above are well possible).
	 * For further information see #1 (https://github.com/malte0811/IndustrialWires/issues/1)
	 */
	@Override
	public int hashCode() {
		if (world==null)
			return 0;
		int ret = world.provider.getDimension();
		ret = 31 * ret + pos.hashCode();
		return ret;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == this) {
			return true;
		}
		if (!(obj instanceof TileEntityIC2ConnectorTin)) {
			return false;
		}
		if (obj.getClass() != getClass()) {
			return false;
		}
		TileEntityIC2ConnectorTin te = (TileEntityIC2ConnectorTin) obj;
		if (!te.pos.equals(pos)) {
			return false;
		}
		if (te.world.provider.getDimension() != world.provider.getDimension()) {
			return false;
		}
		return true;
	}

	@Override
	public boolean canRotate(@Nonnull EnumFacing axis) {
		return false;
	}

	private EnergyHandler energyHandler = new EnergyHandler();

	private class EnergyHandler implements IEnergyStorage {

		@Override
		public int receiveEnergy(int maxReceive, boolean simulate) {
			double joules = maxReceive*ConversionUtil.joulesPerIf();
			double accepted = addToIn(joules, simulate, FE_AC);
			return MathHelper.ceil(accepted*ConversionUtil.ifPerJoule());
		}

		@Override
		public int extractEnergy(int maxExtract, boolean simulate) {
			if (energyType!=FE_AC) {
				return 0;
			}
			double joules = maxExtract*ConversionUtil.joulesPerIf();
			if (joules>maxToMachine) {
				joules = maxToMachine;
			}
			if (joules>bufferToMachine) {
				joules = bufferToMachine;
			}
			if (!simulate) {
				bufferToMachine -= joules;
			}
			return MathHelper.floor(ConversionUtil.ifPerJoule()*joules);
		}

		@Override
		public int getEnergyStored() {
			return (int)((bufferToMachine+bufferToNet)*ConversionUtil.ifPerJoule());
		}

		@Override
		public int getMaxEnergyStored() {
			return (int) (2*maxStored*ConversionUtil.ifPerJoule());
		}

		@Override
		public boolean canExtract() {
			return true;
		}

		@Override
		public boolean canReceive() {
			return true;
		}
	}
}
