/*
 * Copyright (C) 2017 - 2019 | Wurst-Imperium | All rights reserved.
 *
 * This source code is subject to the terms of the GNU General Public
 * License, version 3. If a copy of the GPL was not distributed with this
 * file, You can obtain one at: https://www.gnu.org/licenses/gpl-3.0.txt
 */
package net.wurstclient.forge.hacks;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.lwjgl.opengl.GL11;

import net.minecraft.block.*;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.entity.EntityPlayerSP;
import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import net.minecraftforge.client.event.RenderWorldLastEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.wurstclient.fmlevents.WUpdateEvent;
import net.wurstclient.forge.Category;
import net.wurstclient.forge.Hack;
import net.wurstclient.forge.compatibility.WItem;
import net.wurstclient.forge.compatibility.WMinecraft;
import net.wurstclient.forge.settings.SliderSetting;
import net.wurstclient.forge.settings.SliderSetting.ValueDisplay;
import net.wurstclient.forge.utils.BlockUtils;
import net.wurstclient.forge.utils.PlayerControllerUtils;
import net.wurstclient.forge.utils.RenderUtils;
import net.wurstclient.forge.utils.RotationUtils;

public final class AutoFarmHack extends Hack
{
	private final SliderSetting range =
		new SliderSetting("Range", 5, 1, 6, 0.05, ValueDisplay.DECIMAL);
	
	private final HashMap<BlockPos, Item> plants = new HashMap<>();
	
	private final ArrayDeque<Set<BlockPos>> prevBlocks = new ArrayDeque<>();
	private BlockPos currentBlock;
	private float progress;
	private float prevProgress;
	
	private int displayList;
	private int box;
	private int node;
	
	public AutoFarmHack()
	{
		super("AutoFarm", "Harvests and re-plants crops automatically.\n"
			+ "Works with wheat, carrots, potatoes, beetroots,\n"
			+ "pumpkins, melons, cacti, sugar canes and\n" + "nether warts.");
		setCategory(Category.BLOCKS);
		addSetting(range);
	}
	
	@Override
	protected void onEnable()
	{
		plants.clear();
		displayList = GL11.glGenLists(1);
		box = GL11.glGenLists(1);
		node = GL11.glGenLists(1);
		
		GL11.glNewList(box, GL11.GL_COMPILE);
		AxisAlignedBB box = new AxisAlignedBB(1 / 16.0, 1 / 16.0, 1 / 16.0,
			15 / 16.0, 15 / 16.0, 15 / 16.0);
		GL11.glBegin(GL11.GL_LINES);
		RenderUtils.drawOutlinedBox(box);
		GL11.glEnd();
		GL11.glEndList();
		
		GL11.glNewList(node, GL11.GL_COMPILE);
		AxisAlignedBB node =
			new AxisAlignedBB(0.25, 0.25, 0.25, 0.75, 0.75, 0.75);
		GL11.glBegin(GL11.GL_LINES);
		RenderUtils.drawNode(node);
		GL11.glEnd();
		GL11.glEndList();
		
		MinecraftForge.EVENT_BUS.register(this);
	}
	
	@Override
	protected void onDisable()
	{
		MinecraftForge.EVENT_BUS.unregister(this);
		
		if(currentBlock != null)
			try
			{
				PlayerControllerUtils.setIsHittingBlock(true);
				mc.playerController.resetBlockRemoving();
				currentBlock = null;
				
			}catch(ReflectiveOperationException e)
			{
				throw new RuntimeException(e);
			}
		
		prevBlocks.clear();
		GL11.glDeleteLists(displayList, 1);
		GL11.glDeleteLists(box, 1);
		GL11.glDeleteLists(node, 1);
	}
	
	@SubscribeEvent
	public void onUpdate(WUpdateEvent event)
	{
		currentBlock = null;
		Vec3d eyesVec = RotationUtils.getEyesPos().subtract(0.5, 0.5, 0.5);
		BlockPos eyesBlock = new BlockPos(RotationUtils.getEyesPos());
		double rangeSq = Math.pow(range.getValue(), 2);
		int blockRange = (int)Math.ceil(range.getValue());
		
		List<BlockPos> blocks = getBlockStream(eyesBlock, blockRange)
			.filter(pos -> eyesVec.squareDistanceTo(new Vec3d(pos)) <= rangeSq)
			.filter(pos -> BlockUtils.canBeClicked(pos))
			.collect(Collectors.toList());
		
		registerPlants(blocks);
		
		List<BlockPos> blocksToHarvest = new ArrayList<>();
		List<BlockPos> blocksToReplant = new ArrayList<>();
		
		if(!wurst.getHax().freecamHack.isEnabled())
		{
			blocksToHarvest =
				blocks.parallelStream().filter(this::shouldBeHarvested)
					.sorted(Comparator.comparingDouble(
						pos -> eyesVec.squareDistanceTo(new Vec3d(pos))))
					.collect(Collectors.toList());
			blocksToReplant = getBlockStream(eyesBlock, blockRange)
				.filter(
					pos -> eyesVec.squareDistanceTo(new Vec3d(pos)) <= rangeSq)
				.filter(pos -> BlockUtils.getMaterial(pos).isReplaceable())
				.filter(pos -> plants.containsKey(pos))
				.filter(this::canBeReplanted)
				.sorted(Comparator.comparingDouble(
					pos -> eyesVec.squareDistanceTo(new Vec3d(pos))))
				.collect(Collectors.toList());
		}
		
		while(!blocksToReplant.isEmpty())
		{
			BlockPos pos = blocksToReplant.get(0);
			Item neededItem = plants.get(pos);
			if(tryToReplant(pos, neededItem))
				break;
			
			blocksToReplant.removeIf(p -> plants.get(p) == neededItem);
		}
		
		if(blocksToReplant.isEmpty())
			harvest(blocksToHarvest);
		
		updateDisplayList(blocksToHarvest, blocksToReplant);
	}
	
	@SubscribeEvent
	public void onRenderWorldLast(RenderWorldLastEvent event)
	{
		// GL settings
		GL11.glEnable(GL11.GL_BLEND);
		GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
		GL11.glEnable(GL11.GL_LINE_SMOOTH);
		GL11.glLineWidth(2);
		GL11.glDisable(GL11.GL_TEXTURE_2D);
		GL11.glEnable(GL11.GL_CULL_FACE);
		GL11.glDisable(GL11.GL_DEPTH_TEST);
		
		GL11.glPushMatrix();
		GL11.glTranslated(-TileEntityRendererDispatcher.staticPlayerX,
			-TileEntityRendererDispatcher.staticPlayerY,
			-TileEntityRendererDispatcher.staticPlayerZ);
		
		GL11.glCallList(displayList);
		
		if(currentBlock != null)
		{
			GL11.glPushMatrix();
			
			AxisAlignedBB box = new AxisAlignedBB(BlockPos.ORIGIN);
			float p = prevProgress
				+ (progress - prevProgress) * event.getPartialTicks();
			float red = p * 2F;
			float green = 2 - red;
			
			GL11.glTranslated(currentBlock.getX(), currentBlock.getY(),
				currentBlock.getZ());
			if(p < 1)
			{
				GL11.glTranslated(0.5, 0.5, 0.5);
				GL11.glScaled(p, p, p);
				GL11.glTranslated(-0.5, -0.5, -0.5);
			}
			
			GL11.glColor4f(red, green, 0, 0.25F);
			GL11.glBegin(GL11.GL_QUADS);
			RenderUtils.drawSolidBox(box);
			GL11.glEnd();
			
			GL11.glColor4f(red, green, 0, 0.5F);
			GL11.glBegin(GL11.GL_LINES);
			RenderUtils.drawOutlinedBox(box);
			GL11.glEnd();
			
			GL11.glPopMatrix();
		}
		
		GL11.glPopMatrix();
		
		// GL resets
		GL11.glColor4f(1, 1, 1, 1);
		GL11.glEnable(GL11.GL_DEPTH_TEST);
		GL11.glEnable(GL11.GL_TEXTURE_2D);
		GL11.glDisable(GL11.GL_BLEND);
		GL11.glDisable(GL11.GL_LINE_SMOOTH);
	}
	
	private Stream<BlockPos> getBlockStream(BlockPos center, int range)
	{
		return StreamSupport
			.stream(BlockPos.getAllInBox(center.add(range, range, range),
				center.add(-range, -range, -range)).spliterator(), true);
	}
	
	private boolean shouldBeHarvested(BlockPos pos)
	{
		Block block = BlockUtils.getBlock(pos);
		IBlockState state = BlockUtils.getState(pos);
		
		if(block instanceof BlockCrops)
			return ((BlockCrops)block).isMaxAge(state);
		else if(block instanceof BlockPumpkin || block instanceof BlockMelon)
			return true;
		else if(block instanceof BlockReed)
			return BlockUtils.getBlock(pos.down()) instanceof BlockReed
				&& !(BlockUtils.getBlock(pos.down(2)) instanceof BlockReed);
		else if(block instanceof BlockCactus)
			return BlockUtils.getBlock(pos.down()) instanceof BlockCactus
				&& !(BlockUtils.getBlock(pos.down(2)) instanceof BlockCactus);
		else if(block instanceof BlockNetherWart)
			return state.getValue(BlockNetherWart.AGE).intValue() >= 3;
		
		return false;
	}
	
	private void registerPlants(List<BlockPos> blocks)
	{
		HashMap<Block, Item> seeds = new HashMap<>();
		seeds.put(Blocks.WHEAT, Items.WHEAT_SEEDS);
		seeds.put(Blocks.CARROTS, Items.CARROT);
		seeds.put(Blocks.POTATOES, Items.POTATO);
		seeds.put(Blocks.BEETROOTS, Items.BEETROOT_SEEDS);
		seeds.put(Blocks.PUMPKIN_STEM, Items.PUMPKIN_SEEDS);
		seeds.put(Blocks.MELON_STEM, Items.MELON_SEEDS);
		seeds.put(Blocks.NETHER_WART, Items.NETHER_WART);
		
		plants.putAll(blocks.parallelStream()
			.filter(pos -> seeds.containsKey(BlockUtils.getBlock(pos)))
			.collect(Collectors.toMap(pos -> pos,
				pos -> seeds.get(BlockUtils.getBlock(pos)))));
	}
	
	private boolean canBeReplanted(BlockPos pos)
	{
		Item item = plants.get(pos);
		
		if(item == Items.WHEAT_SEEDS || item == Items.CARROT
			|| item == Items.POTATO || item == Items.BEETROOT_SEEDS
			|| item == Items.PUMPKIN_SEEDS || item == Items.MELON_SEEDS)
			return BlockUtils.getBlock(pos.down()) instanceof BlockFarmland;
		
		if(item == Items.NETHER_WART)
			return BlockUtils.getBlock(pos.down()) instanceof BlockSoulSand;
		
		return false;
	}
	
	private boolean tryToReplant(BlockPos pos, Item neededItem)
	{
		EntityPlayerSP player = WMinecraft.getPlayer();
		ItemStack heldItem = player.getHeldItemMainhand();
		
		if(!WItem.isNullOrEmpty(heldItem) && heldItem.getItem() == neededItem)
		{
			BlockUtils.placeBlockSimple(pos);
			return true;
		}
		
		for(int slot = 0; slot < 36; slot++)
		{
			if(slot == player.inventory.currentItem)
				continue;
			
			ItemStack stack = player.inventory.getStackInSlot(slot);
			if(WItem.isNullOrEmpty(stack) || stack.getItem() != neededItem)
				continue;
			
			if(slot < 9)
				player.inventory.currentItem = slot;
			else if(player.inventory.getFirstEmptyStack() < 9)
				PlayerControllerUtils.windowClick_QUICK_MOVE(slot);
			else if(player.inventory.getFirstEmptyStack() != -1)
			{
				PlayerControllerUtils
					.windowClick_QUICK_MOVE(player.inventory.currentItem + 36);
				PlayerControllerUtils.windowClick_QUICK_MOVE(slot);
			}else
			{
				PlayerControllerUtils
					.windowClick_PICKUP(player.inventory.currentItem + 36);
				PlayerControllerUtils.windowClick_PICKUP(slot);
				PlayerControllerUtils
					.windowClick_PICKUP(player.inventory.currentItem + 36);
			}
			
			return true;
		}
		
		return false;
	}
	
	private void harvest(List<BlockPos> blocksToHarvest)
	{
		if(WMinecraft.getPlayer().capabilities.isCreativeMode)
		{
			Stream<BlockPos> stream3 = blocksToHarvest.parallelStream();
			for(Set<BlockPos> set : prevBlocks)
				stream3 = stream3.filter(pos -> !set.contains(pos));
			List<BlockPos> blocksToHarvest2 =
				stream3.collect(Collectors.toList());
			
			prevBlocks.addLast(new HashSet<>(blocksToHarvest2));
			while(prevBlocks.size() > 5)
				prevBlocks.removeFirst();
			
			if(!blocksToHarvest2.isEmpty())
				currentBlock = blocksToHarvest2.get(0);
			
			mc.playerController.resetBlockRemoving();
			progress = 1;
			prevProgress = 1;
			BlockUtils.breakBlocksPacketSpam(blocksToHarvest2);
			return;
		}
		
		for(BlockPos pos : blocksToHarvest)
			if(BlockUtils.breakBlockSimple(pos))
			{
				currentBlock = pos;
				break;
			}
		
		if(currentBlock == null)
			mc.playerController.resetBlockRemoving();
		
		if(currentBlock != null && BlockUtils.getHardness(currentBlock) < 1)
			try
			{
				prevProgress = progress;
				progress = PlayerControllerUtils.getCurBlockDamageMP();
				
				if(progress < prevProgress)
					prevProgress = progress;
				
			}catch(ReflectiveOperationException e)
			{
				setEnabled(false);
				throw new RuntimeException(e);
			}
		else
		{
			progress = 1;
			prevProgress = 1;
		}
	}
	
	private void updateDisplayList(List<BlockPos> blocksToHarvest,
		List<BlockPos> blocksToReplant)
	{
		GL11.glNewList(displayList, GL11.GL_COMPILE);
		GL11.glColor4f(0, 1, 0, 0.5F);
		for(BlockPos pos : blocksToHarvest)
		{
			GL11.glPushMatrix();
			GL11.glTranslated(pos.getX(), pos.getY(), pos.getZ());
			GL11.glCallList(box);
			GL11.glPopMatrix();
		}
		GL11.glColor4f(0, 1, 1, 0.5F);
		for(BlockPos pos : plants.keySet())
		{
			GL11.glPushMatrix();
			GL11.glTranslated(pos.getX(), pos.getY(), pos.getZ());
			GL11.glCallList(node);
			GL11.glPopMatrix();
		}
		GL11.glColor4f(1, 0, 0, 0.5F);
		for(BlockPos pos : blocksToReplant)
		{
			GL11.glPushMatrix();
			GL11.glTranslated(pos.getX(), pos.getY(), pos.getZ());
			GL11.glCallList(box);
			GL11.glPopMatrix();
		}
		GL11.glEndList();
	}
}