package appeng.client.render.crafting;


import java.util.List;
import javax.annotation.Nullable;
import javax.vecmath.Matrix4f;

import com.google.common.collect.ImmutableMap;

import org.apache.commons.lang3.tuple.Pair;
import org.lwjgl.input.Keyboard;

import net.minecraft.block.state.IBlockState;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.block.model.IBakedModel;
import net.minecraft.client.renderer.block.model.ItemCameraTransforms;
import net.minecraft.client.renderer.block.model.ItemOverrideList;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.item.ItemStack;
import net.minecraft.util.EnumFacing;
import net.minecraft.world.World;
import net.minecraftforge.client.model.IPerspectiveAwareModel;
import net.minecraftforge.common.model.TRSRTransformation;

import appeng.items.misc.ItemEncodedPattern;


/**
 * This special model handles switching between rendering the crafting output of an encoded pattern (when shift is being held), and
 * showing the encoded pattern itself. Matters are further complicated by only wanting to show the crafting output when the pattern is being
 * rendered in the GUI, and not anywhere else.
 */
class ItemEncodedPatternBakedModel implements IPerspectiveAwareModel
{
	private final IBakedModel baseModel;

	private final ImmutableMap<ItemCameraTransforms.TransformType, TRSRTransformation> transforms;

	private final CustomOverrideList overrides;

	ItemEncodedPatternBakedModel( IBakedModel baseModel, ImmutableMap<ItemCameraTransforms.TransformType, TRSRTransformation> transforms )
	{
		this.baseModel = baseModel;
		this.transforms = transforms;
		this.overrides = new CustomOverrideList();
	}

	@Override
	public List<BakedQuad> getQuads( @Nullable IBlockState state, @Nullable EnumFacing side, long rand )
	{
		return baseModel.getQuads( state, side, rand );
	}

	@Override
	public boolean isAmbientOcclusion()
	{
		return baseModel.isAmbientOcclusion();
	}

	@Override
	public boolean isGui3d()
	{
		return baseModel.isGui3d();
	}

	@Override
	public boolean isBuiltInRenderer()
	{
		return baseModel.isBuiltInRenderer();
	}

	@Override
	public TextureAtlasSprite getParticleTexture()
	{
		return baseModel.getParticleTexture();
	}

	@Override
	@Deprecated
	public ItemCameraTransforms getItemCameraTransforms()
	{
		return baseModel.getItemCameraTransforms();
	}

	@Override
	public ItemOverrideList getOverrides()
	{
		return overrides;
	}

	@Override
	public Pair<? extends IBakedModel, Matrix4f> handlePerspective( ItemCameraTransforms.TransformType cameraTransformType )
	{
		if( baseModel instanceof IPerspectiveAwareModel )
		{
			return ( (IPerspectiveAwareModel) baseModel ).handlePerspective( cameraTransformType );
		}

		return IPerspectiveAwareModel.MapWrapper.handlePerspective( this, transforms, cameraTransformType );
	}

	/**
	 * Since the ItemOverrideList handling comes before handling the perspective awareness (which is the first place where we
	 * know how we are being rendered) we need to remember the model of the crafting output, and make the decision on which to render later on.
	 * Sadly, Forge is pretty inconsistent when it will call the handlePerspective method, so some methods are called even on this interim-model.
	 * Usually those methods only matter for rendering on the ground and other cases, where we wouldn't render the crafting output model anyway,
	 * so in those cases we delegate to the model of the encoded pattern.
	 */
	private class ShiftHoldingModelWrapper implements IPerspectiveAwareModel
	{

		private final IBakedModel outputModel;

		private ShiftHoldingModelWrapper( IBakedModel outputModel )
		{
			this.outputModel = outputModel;
		}

		@Override
		public Pair<? extends IBakedModel, Matrix4f> handlePerspective( ItemCameraTransforms.TransformType cameraTransformType )
		{
			final IBakedModel selectedModel;

			// No need to re-check for shift being held since this model is only handed out in that case
			if( cameraTransformType == ItemCameraTransforms.TransformType.GUI )
			{
				selectedModel = outputModel;
			}
			else
			{
				selectedModel = baseModel;
			}

			// Now retroactively handle the isGui3d call, for which we always return false below
			if( selectedModel.isGui3d() != baseModel.isGui3d() )
			{
				GlStateManager.enableLighting();
			}

			if( selectedModel instanceof IPerspectiveAwareModel )
			{
				return ( (IPerspectiveAwareModel) selectedModel ).handlePerspective( cameraTransformType );
			}

			return IPerspectiveAwareModel.MapWrapper.handlePerspective( this, transforms, cameraTransformType );
		}

		@Override
		public List<BakedQuad> getQuads( @Nullable IBlockState state, @Nullable EnumFacing side, long rand )
		{
			// This may be called for items on the ground, in which case we will always fall back to the pattern
			return baseModel.getQuads( state, side, rand );
		}

		@Override
		public boolean isAmbientOcclusion()
		{
			return baseModel.isAmbientOcclusion();
		}

		@Override
		public boolean isGui3d()
		{
			// NOTE: Sadly, Forge will let Minecraft call this method before handling the perspective awareness
			return baseModel.isGui3d();
		}

		@Override
		public boolean isBuiltInRenderer()
		{
			// This may be called for items on the ground, in which case we will always fall back to the pattern
			return baseModel.isBuiltInRenderer();
		}

		@Override
		public TextureAtlasSprite getParticleTexture()
		{
			// This may be called for items on the ground, in which case we will always fall back to the pattern
			return baseModel.getParticleTexture();
		}

		@Override
		public ItemCameraTransforms getItemCameraTransforms()
		{
			// This may be called for items on the ground, in which case we will always fall back to the pattern
			return baseModel.getItemCameraTransforms();
		}

		@Override
		public ItemOverrideList getOverrides()
		{
			// This may be called for items on the ground, in which case we will always fall back to the pattern
			return baseModel.getOverrides();
		}
	}


	/**
	 * Item Override Lists are the only point during item rendering where we can access the item stack that is being rendered.
	 * So this is the point where we actually check if shift is being held, and if so, determine the crafting output model.
	 */
	private class CustomOverrideList extends ItemOverrideList
	{

		CustomOverrideList()
		{
			super( baseModel.getOverrides().getOverrides() );
		}

		@Override
		public IBakedModel handleItemState( IBakedModel originalModel, ItemStack stack, World world, EntityLivingBase entity )
		{
			boolean shiftHeld = Keyboard.isKeyDown( Keyboard.KEY_LSHIFT ) || Keyboard.isKeyDown( Keyboard.KEY_RSHIFT );
			if( shiftHeld )
			{
				ItemEncodedPattern iep = (ItemEncodedPattern) stack.getItem();
				ItemStack output = iep.getOutput( stack );
				if( output != null )
				{
					IBakedModel realModel = Minecraft.getMinecraft().getRenderItem().getItemModelMesher().getItemModel( output );
					// Give the item model a chance to handle the overrides as well
					realModel = realModel.getOverrides().handleItemState( realModel, output, world, entity );
					return new ShiftHoldingModelWrapper( realModel );
				}
			}

			return baseModel.getOverrides().handleItemState( originalModel, stack, world, entity );
		}
	}
}
