/*
 * 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.hv;

import blusunrize.immersiveengineering.api.ApiUtils;
import blusunrize.immersiveengineering.common.blocks.IEBlockInterfaces;
import blusunrize.immersiveengineering.common.blocks.IEBlockInterfaces.IDirectionalTile;
import blusunrize.immersiveengineering.common.blocks.TileEntityIEBase;
import com.elytradev.mirage.lighting.IColoredLight;
import com.elytradev.mirage.lighting.Light;
import ic2.api.energy.event.EnergyTileLoadEvent;
import ic2.api.energy.event.EnergyTileUnloadEvent;
import ic2.api.energy.tile.IEnergyEmitter;
import ic2.api.energy.tile.IEnergySink;
import malte0811.industrialWires.IWConfig;
import malte0811.industrialWires.IWDamageSources;
import malte0811.industrialWires.IndustrialWires;
import malte0811.industrialWires.blocks.IBlockBoundsIW;
import malte0811.industrialWires.blocks.IHasDummyBlocksIW;
import malte0811.industrialWires.blocks.ISyncReceiver;
import malte0811.industrialWires.network.MessageTileSyncIW;
import malte0811.industrialWires.util.Beziers;
import malte0811.industrialWires.util.ConversionUtil;
import malte0811.industrialWires.util.JouleEnergyStorage;
import net.minecraft.block.Block;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.Minecraft;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumHand;
import net.minecraft.util.IStringSerializable;
import net.minecraft.util.ITickable;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.energy.CapabilityEnergy;
import net.minecraftforge.fml.common.Optional;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import static malte0811.industrialWires.IndustrialWires.hasIC2;
import static malte0811.industrialWires.util.MiscUtils.interpolate;

@Optional.InterfaceList({
		@Optional.Interface(modid = "ic2", iface = "ic2.api.energy.tile.IEnergySink"),
		@Optional.Interface(modid = "mirage", iface = "com.elytradev.mirage.lighting.IColoredLight")
})
public class TileEntityJacobsLadder extends TileEntityIEBase implements ITickable, IHasDummyBlocksIW, ISyncReceiver,
		IEnergySink, IBlockBoundsIW, IDirectionalTile, IColoredLight, IEBlockInterfaces.IPlayerInteraction {
	public EnumFacing facing = EnumFacing.NORTH;
	private JouleEnergyStorage energy;
	public LadderSize size;

	public Vec3d[] controls;
	//first and last move along the "rails", only the middle points move in bezier curves
	private Vec3d[][] controlControls;
	// movement of the controls in blocks/tick
	public Vec3d[] controlMovement;
	private double t = 0;
	private int dummy = 0;
	public int timeTillActive = -1;
	private double tStep = 0;
	private double consumtionJoule;
	private boolean addedToIC2Net = false;
	private int soundPhase;
	private Vec3d soundPos;
	public double salt;

	TileEntityJacobsLadder(LadderSize s) {
		size = s;
		initControl();
	}

	public TileEntityJacobsLadder() {
		size = LadderSize.HUGE;
		initControl();
	}

	private void initControl() {
		controls = new Vec3d[size.arcPoints];
		controlControls = new Vec3d[size.arcPoints - 2][size.movementPoints];
		controlMovement = new Vec3d[size.arcPoints];
		int sizeId = size.ordinal();
		consumtionJoule = IWConfig.HVStuff.jacobsUsageWatt[sizeId];
		energy = new JouleEnergyStorage(20 * consumtionJoule,
				40 * consumtionJoule);
	}

	@Override
	public void update() {
		ApiUtils.checkForNeedlessTicking(this);
		if (isDummy()) {
			return;
		}
		if (!world.isRemote) {
			if (hasIC2&&!addedToIC2Net) {
				addToIC2Net();
			}
			if ((controlControls[0][0] == null || timeTillActive == -1 || t >= 1)
					&& energy.getEnergyStoredJ() >= 2 * consumtionJoule) {
				for (int j = 0; j < size.movementPoints; j++) {
					double y = j * (size.height + size.extraHeight) / (double) (size.movementPoints - 1) + size.innerPointOffset;
					double width = widthFromHeight(y);
					for (int i = 0; i < size.arcPoints - 2; i++) {
						double z = size.zMax * 2 * (world.rand.nextDouble() - .5);
						double xMin = width * i / (double) (size.arcPoints - 2) - width / 2 + size.bottomDistance / 2;
						double xDiff = width / (double) (size.arcPoints - 2);
						double x = world.rand.nextDouble() * xDiff + xMin;
						controlControls[i][j] = new Vec3d(x, y, z);
					}
				}
				t = 0;
				timeTillActive = size.delay;
				tStep = 1D / (int) (.875 * size.tickToTop + world.rand.nextInt(size.tickToTop / 4));
				IndustrialWires.packetHandler.sendToAll(new MessageTileSyncIW(this, writeArcStarter()));
			} else if (timeTillActive == 0 && t < 1) {
				double extracted = energy.extract(consumtionJoule, 1, true);
				if (extracted >= consumtionJoule) {
					energy.extract(consumtionJoule, 1, false);
				} else {
					timeTillActive = -1 - size.delay;
					NBTTagCompound nbt = new NBTTagCompound();
					nbt.setBoolean("cancel", true);
					IndustrialWires.packetHandler.sendToAll(new MessageTileSyncIW(this, nbt));
				}
			} else if (timeTillActive < -1) {
				//delay after energy was cut
				timeTillActive++;
			}
		} else {
			if (timeTillActive == 0 && t < 1) {
				for (int i = 0; i < size.arcPoints; i++) {
					controls[i] = controls[i].add(controlMovement[i]);
				}
				for (int i = 1; i < size.arcPoints - 1; i++) {
					controlMovement[i] = Beziers.getPoint(t, controlControls[i - 1]).subtract(controls[i]);
				}
				if (soundPhase < 0) {
					IndustrialWires.proxy.playJacobsLadderSound(this, 0, soundPos);
					soundPhase = 0;
				}
				if (t >= 7 * tStep && soundPhase == 0) {
					IndustrialWires.proxy.playJacobsLadderSound(this, 1, soundPos);
					soundPhase = 1;
				} else if (t >= 1 - (4 * tStep) && soundPhase == 1) {
					IndustrialWires.proxy.playJacobsLadderSound(this, 2, soundPos);
					soundPhase = 2;
				}
			} else if (t > 1) {
				timeTillActive = -1;
			}
		}
		if (timeTillActive > 0) {
			timeTillActive--;
		} else if (timeTillActive == 0 && t < 1) {
			t += tStep;
			if (salt > 0) {
				salt -= 1D / (20 * 20);//20 seconds per item of salt
			} else if (salt < 0) {
				salt = 0;
			}
		}
	}

	@Optional.Method(modid = "ic2")
	private void addToIC2Net() {
		MinecraftForge.EVENT_BUS.post(new EnergyTileLoadEvent(this));
		addedToIC2Net = true;
	}

	private void initArc(int delay) {
		if (controlMovement == null) {
			initControl();
		}
		controls[0] = new Vec3d(0, 0, 0);
		controls[size.arcPoints - 1] = new Vec3d(size.bottomDistance, 0, 0);
		controlMovement[0] = new Vec3d(-(size.topDistance - size.bottomDistance) / (2 * size.tickToTop), size.height / size.tickToTop, 0);
		controlMovement[size.arcPoints - 1] = new Vec3d((size.topDistance - size.bottomDistance) / (2 * size.tickToTop), size.height / size.tickToTop, 0);
		t = 0;
		for (int i = 1; i < size.arcPoints - 1; i++) {
			controls[i] = Beziers.getPoint(0, controlControls[i - 1]);
			controlMovement[i] = Beziers.getPoint(tStep, controlControls[i - 1]).subtract(controls[i]);
		}
		double soundX = pos.getX() + .5;
		double soundY = pos.getY() + .5 * size.dummyCount + size.heightOffset;
		double soundZ = pos.getZ() + .5;
		soundPos = new Vec3d(soundX, soundY, soundZ);
		soundPhase = -1;
		timeTillActive = delay;
	}

	private double widthFromHeight(double h) {
		return size.bottomDistance + h * (size.topDistance - size.bottomDistance) / size.height;
	}

	@Override
	public void readCustomNBT(@Nonnull NBTTagCompound nbt, boolean descPacket) {
		LadderSize oldSize = size;
		size = LadderSize.values()[nbt.getInteger("size")];
		if (size != oldSize) {
			initControl();
		}
		dummy = nbt.getInteger("dummy");
		energy.readFromNBT(nbt.getCompoundTag("energy"));
		facing = EnumFacing.HORIZONTALS[nbt.getInteger("facing")];
		salt = nbt.getDouble("salt");
	}

	@Override
	public void writeCustomNBT(@Nonnull NBTTagCompound nbt, boolean descPacket) {
		nbt.setInteger("size", size.ordinal());
		nbt.setInteger("dummy", dummy);
		energy.writeToNbt(nbt, "energy");
		nbt.setInteger("facing", facing.getHorizontalIndex());
		nbt.setDouble("salt", salt);
	}

	private NBTTagCompound writeArcStarter() {
		NBTTagCompound nbt = new NBTTagCompound();
		NBTTagList ctrlCtrl = write2DVecArray(controlControls);
		nbt.setTag("ctrlCtrl", ctrlCtrl);
		nbt.setInteger("timeTillActive", timeTillActive);
		nbt.setDouble("tStep", tStep);
		nbt.setBoolean("start", true);
		return nbt;
	}

	@SideOnly(Side.CLIENT)
	private void readArcStarter(NBTTagCompound nbt) {
		controlControls = read2DVecArray(nbt.getTagList("ctrlCtrl", 9));
		tStep = nbt.getDouble("tStep");
		Minecraft.getMinecraft().addScheduledTask(() -> initArc(nbt.getInteger("timeTillActive")));
	}

	private Vec3d[][] read2DVecArray(NBTTagList nbt) {
		Vec3d[][] ret = new Vec3d[nbt.tagCount()][];
		for (int i = 0; i < ret.length; i++) {
			ret[i] = readVecArray((NBTTagList) nbt.get(i));
		}
		return ret;
	}

	private Vec3d[] readVecArray(NBTTagList nbt) {
		Vec3d[] ret = new Vec3d[nbt.tagCount()];
		for (int i = 0; i < ret.length; i++) {
			NBTTagCompound vec = nbt.getCompoundTagAt(i);
			ret[i] = new Vec3d(vec.getDouble("x"), vec.getDouble("y"), vec.getDouble("z"));
		}
		return ret;
	}

	private NBTTagList write2DVecArray(Vec3d[][] array) {
		NBTTagList ret = new NBTTagList();
		for (Vec3d[] subArray : array) {
			ret.appendTag(writeVecArray(subArray));
		}
		return ret;
	}

	private NBTTagList writeVecArray(Vec3d[] array) {
		NBTTagList ret = new NBTTagList();
		for (Vec3d point : array) {
			NBTTagCompound vec = new NBTTagCompound();
			vec.setDouble("x", point.x);
			vec.setDouble("y", point.y);
			vec.setDouble("z", point.z);
			ret.appendTag(vec);
		}
		return ret;
	}

	@Override
	public boolean isDummy() {
		return dummy != 0;
	}

	@Override
	public void placeDummies(IBlockState state) {
		for (int i = 1; i <= size.dummyCount; i++) {
			BlockPos pos2 = pos.offset(EnumFacing.UP, i);
			world.setBlockState(pos2, state);
			TileEntity te = world.getTileEntity(pos2);
			if (te instanceof TileEntityJacobsLadder) {
				((TileEntityJacobsLadder) te).size = size;
				((TileEntityJacobsLadder) te).dummy = i;
				((TileEntityJacobsLadder) te).facing = facing;
			}
		}
	}

	@Override
	public void breakDummies() {
		for (int i = 0; i <= size.dummyCount; i++) {
			if (i != dummy && world.getTileEntity(pos.offset(EnumFacing.UP, i - dummy)) instanceof TileEntityJacobsLadder) {
				world.setBlockToAir(pos.offset(EnumFacing.UP, i - dummy));
			}
		}
	}

	@Override
	@SideOnly(Side.CLIENT)
	public void onSync(NBTTagCompound nbt) {
		if (nbt.hasKey("salt")) {
			salt = nbt.getDouble("salt");
		}
		if (nbt.getBoolean("cancel")) {
			timeTillActive = -1;
			IndustrialWires.proxy.playJacobsLadderSound(this, -1, soundPos);
		} else if (nbt.getBoolean("start")) {
			readArcStarter(nbt);
		}
	}

	private boolean isActive() {
		if (isDummy()) {
			TileEntity master = world.getTileEntity(pos.down(dummy));
			return master instanceof TileEntityJacobsLadder && ((TileEntityJacobsLadder) master).isActive();
		}
		return timeTillActive == 0 && t < 1;
	}

	public void onEntityTouch(Entity e) {
		if (isDummy() && !world.isRemote) {
			TileEntity master = world.getTileEntity(pos.down(dummy));
			if (master instanceof TileEntityJacobsLadder && ((TileEntityJacobsLadder) master).isActive()) {
				hurtEntity(e);
			}
		}
	}

	private void hurtEntity(Entity e) {
		e.attackEntityFrom(IWDamageSources.dmg_jacobs, IWConfig.HVStuff.jacobsBaseDmg * (size.ordinal() + 1));
	}

	@Override
	public boolean interact(@Nonnull EnumFacing side, @Nonnull EntityPlayer player,@Nonnull EnumHand hand,
							@Nonnull ItemStack heldItem, float hitX, float hitY, float hitZ) {
		TileEntity masterTE = dummy == 0 ? this : world.getTileEntity(pos.down(dummy));
		if (masterTE instanceof TileEntityJacobsLadder) {
			TileEntityJacobsLadder master = (TileEntityJacobsLadder) masterTE;
			if (master.isActive()) {
				if (!world.isRemote) {
					hurtEntity(player);
				}
				return true;
			} else if (!heldItem.isEmpty() && ApiUtils.compareToOreName(heldItem, "itemSalt")) {
				return master.salt(player, hand, heldItem);
			}
		}
		return false;
	}

	private boolean salt(EntityPlayer player, EnumHand hand, ItemStack held) {
		if (salt < 3) {
			if (!world.isRemote) {
				salt++;
				if (!player.isCreative()) {
					held.shrink(1);
					if (held.getCount() <= 0) {
						player.setHeldItem(hand, ItemStack.EMPTY);
					}
				}
				NBTTagCompound update = new NBTTagCompound();
				update.setDouble("salt", salt);
				markDirty();
				IndustrialWires.packetHandler.sendToAll(new MessageTileSyncIW(this, update));
			}
			return true;
		}
		return false;
	}

	public boolean rotate(World world, BlockPos pos) {
		if (isActive()) {
			return false;
		}
		if (!world.isRemote) {
			EnumFacing targetDir = facing.rotateAround(EnumFacing.Axis.Y);
			for (int i = -dummy; i < size.dummyCount - dummy + 1; i++) {
				BlockPos currPos = pos.up(i);
				TileEntity te = world.getTileEntity(currPos);
				if (te instanceof TileEntityJacobsLadder) {
					TileEntityJacobsLadder teJacobs = (TileEntityJacobsLadder) te;
					teJacobs.facing = targetDir;
					teJacobs.markDirty();
					IBlockState state = world.getBlockState(currPos).getActualState(world, currPos);
					world.notifyBlockUpdate(currPos, state, state, 3);
					world.addBlockEvent(currPos, state.getBlock(), 255, 0);
					world.notifyNeighborsOfStateChange(currPos, state.getBlock(), true);
				}
			}
		}
		return true;
	}


	//ENERGY
	@Override
	@Optional.Method(modid = "ic2")
	public double getDemandedEnergy() {
		return energy.getRequested(ConversionUtil.euPerJoule());
	}

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

	@Override
	@Optional.Method(modid = "ic2")
	public double injectEnergy(EnumFacing dir, double amount, double voltage) {
		return amount - energy.insert(amount, ConversionUtil.joulesPerEu(), true);
	}

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

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

	@Override
	@SuppressWarnings("unchecked")
	public <T> T getCapability(@Nonnull Capability<T> capability, @Nullable EnumFacing facing) {
		if (hasCapability(capability, facing)) {
			if (capability == CapabilityEnergy.ENERGY) {
				return CapabilityEnergy.ENERGY.cast(energy);
			}
		}
		return null;
	}

	@Override
	public void onChunkUnload() {
		if (hasIC2) {
			removeFromIC2Net();
		}
		addedToIC2Net = false;
		super.onChunkUnload();
	}

	@Override
	public void invalidate() {
		if (hasIC2)
			removeFromIC2Net();
		if (world.isRemote) {
			//stop sound
			IndustrialWires.proxy.playJacobsLadderSound(this, -1, soundPos);
		}
		addedToIC2Net = false;
		super.invalidate();
	}

	@Optional.Method(modid = "ic2")
	private void removeFromIC2Net() {
		if (!world.isRemote && addedToIC2Net) {
			MinecraftForge.EVENT_BUS.post(new EnergyTileUnloadEvent(this));
		}
	}


	@Nonnull
	@Override
	public AxisAlignedBB getRenderBoundingBox() {
		return new AxisAlignedBB(pos, pos.add(1, 2, 1));
	}

	@Override
	public AxisAlignedBB getBoundingBox() {
		if (!isDummy()) {
			//transformer
			return Block.FULL_BLOCK_AABB;
		} else {
			Vec3d min;
			Vec3d max;
			double distX = (1 - size.topDistance) / 2;
			double distZ = .5 - .0625 * (size.ordinal() + 1);
			double h = Math.min(1, 1 + size.height - dummy);
			if (facing.getAxis() == EnumFacing.Axis.Z) {
				min = new Vec3d(distX, 0, distZ);
				max = new Vec3d(1 - distX, h, 1 - distZ);
			} else {
				min = new Vec3d(distZ, 0, distX);
				max = new Vec3d(1 - distZ, h, 1 - distX);
			}
			return new AxisAlignedBB(min.x, min.y, min.z, max.x, max.y, max.z);
		}
	}

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

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

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

	@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 boolean canRotate(@Nonnull EnumFacing axis) {
		return false;
	}

	private static final float[] saltColor = {1, 190 / 255F, 50 / 255F};
	private static final float[] airColor = {1, .85F, 1};

	private static final int factor = 20;
	private static final double smallMin = Math.exp(-.5);
	private static final double normalMin = Math.exp(-.25 * factor);
	private static final double hugeMin = Math.exp(-.75 * factor);
	public static float[] getColor(double t, double salt, LadderSize size) {
		salt = Math.min(salt, 1);
		double saltyness = 0;
		double t2 = t - .5;
		switch (size) {
			case SMALL:
				saltyness = salt * (1 - .9 * (Math.exp(-Math.abs(t2)) - smallMin));
				break;
			case NORMAL:
				saltyness = salt * (1 - .9 * (Math.exp(-factor * t2 * t2) - normalMin));
				break;
			case HUGE:
				saltyness = salt * (1 - .9 * (Math.exp(-Math.abs(factor * t2 * t2 * t2)) - hugeMin));
				break;
		}
		return interpolate(saltyness, saltColor, 1 - saltyness, airColor);
	}
	@Nullable
	@Override
	@SideOnly(Side.CLIENT)
	@Optional.Method(modid = "mirage")
	public Light getColoredLight() {
		if (timeTillActive == 0) {
			float[] color = getColor(0, salt, size);
			return Light.builder().pos(pos.getX()+.5,
					Beziers.getPoint(.5, controls).y+pos.getY()+size.bottomDistance+1,
					pos.getZ()+.5)
					.color(color[0], color[1], color[2], 1)
					.radius(size.soundVolume/3)
					.build();
		}
		return null;
	}


	public enum LadderSize implements IStringSerializable {
		/*
		all on a block (HV transformer)
		small: height = .5 bottomDist = .15 topDist = .375
		normal: height = .95 bottomDist = .2 topDist = .75
		huge: height = 1.8 bottomDist = .25 topDist = 1

		 */
		SMALL(4, 4, .5, .375, .15, 20, .05, .2, .05, 1, 1, 5, 8, .03725, 1),
		NORMAL(4, 4, .95, .75, .2, 25, .15, .3, .15, 1, 1, 5, 9, .075, 2),
		HUGE(4, 5, 1.8, 1, .25, 30, .2, .5, .3, 1, 2, 5, 10, .125, 3);
		public final int arcPoints;
		public final int movementPoints;
		// height of the electrodes
		public final double height;
		// distance between the electrodes at top and bottom
		public final double topDistance;
		public final double bottomDistance;
		// ticks it takes for the arc to reach the top of the ladder on average
		public final int tickToTop;
		// maximum z offset for the inner bezier points
		public final double zMax;
		// additional height for inner bezier points, added to normal height and scaled
		public final double extraHeight;
		// fixed additional height for inner bezier points
		public final double innerPointOffset;
		// offset for rendering the arc, e.g. a value of 1 means the arc starts 1 block above the TE
		public final double heightOffset;
		// ticks between the end of one arc and the start of the next
		public final int delay;
		public final int renderPoints;
		public final int dummyCount;
		public final double renderDiameter;
		public final float soundVolume;

		LadderSize(int arcP, int movP, double height, double topD, double bottomD, int ttTop, double zMax, double extraH,
				   double iOff, double hOff, int dummies, int delay, int points, double renderDia, float volume) {
			arcPoints = arcP;
			movementPoints = movP;
			this.height = height;
			topDistance = topD;
			bottomDistance = bottomD;
			tickToTop = ttTop;
			this.zMax = zMax;
			extraHeight = extraH;
			innerPointOffset = iOff;
			heightOffset = hOff;
			dummyCount = dummies;
			this.delay = delay;
			renderPoints = points;
			renderDiameter = renderDia;
			soundVolume = volume;
		}

		@Override
		public String getName() {
			return name().toLowerCase();
		}
	}
}
