/*
 * This file is part of Applied Energistics 2.
 * Copyright (c) 2013 - 2014, AlgorithmX2, All rights reserved.
 *
 * Applied Energistics 2 is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Applied Energistics 2 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Applied Energistics 2.  If not, see <http://www.gnu.org/licenses/lgpl>.
 */

package appeng.client.render.cablebus;


import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

import net.minecraft.block.state.IBlockState;
import net.minecraft.client.Minecraft;
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.client.renderer.texture.TextureMap;
import net.minecraft.util.BlockRenderLayer;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.client.MinecraftForgeClient;
import net.minecraftforge.common.property.IExtendedBlockState;

import appeng.api.util.AECableType;
import appeng.api.util.AEColor;
import appeng.block.networking.BlockCableBus;


public class CableBusBakedModel implements IBakedModel
{

	private final CableBuilder cableBuilder;

	private final FacadeBuilder facadeBuilder;

	private final Map<ResourceLocation, IBakedModel> partModels;

	private final TextureAtlasSprite particleTexture;

	private final TextureMap textureMap = Minecraft.getMinecraft().getTextureMapBlocks();

	CableBusBakedModel( CableBuilder cableBuilder, FacadeBuilder facadeBuilder, Map<ResourceLocation, IBakedModel> partModels, TextureAtlasSprite particleTexture )
	{
		this.cableBuilder = cableBuilder;
		this.facadeBuilder = facadeBuilder;
		this.partModels = partModels;
		this.particleTexture = particleTexture;
	}

	@Override
	public List<BakedQuad> getQuads( @Nullable IBlockState state, @Nullable EnumFacing side, long rand )
	{
		CableBusRenderState renderState = getRenderingState( state );

		if( renderState == null || side != null )
		{
			return Collections.emptyList();
		}

		BlockRenderLayer layer = MinecraftForgeClient.getRenderLayer();

		List<BakedQuad> quads = new ArrayList<>();

		// The core parts of the cable will only be rendered in the CUTOUT layer. TRANSLUCENT is used only for translucent facades further down below.
		if ( layer == BlockRenderLayer.CUTOUT )
		{
			// First, handle the cable at the center of the cable bus
			addCableQuads( renderState, quads );

			// Then handle attachments
			for( EnumFacing facing : EnumFacing.values() )
			{
				List<ResourceLocation> models = renderState.getAttachments().get( facing );
				if( models == null )
				{
					continue;
				}

				for( ResourceLocation model : models )
				{
					IBakedModel bakedModel = partModels.get( model );

					if( bakedModel == null )
					{
						throw new IllegalStateException( "Trying to use an unregistered part model: " + model );
					}

					List<BakedQuad> partQuads = bakedModel.getQuads( state, null, rand );

					// Rotate quads accordingly
					QuadRotator rotator = new QuadRotator();
					partQuads = rotator.rotateQuads( partQuads, facing, EnumFacing.UP );

					quads.addAll( partQuads );
				}
			}
		}

		facadeBuilder.addFacades(
				layer,
				renderState.getFacades(),
				renderState.getBoundingBoxes(),
				renderState.getAttachments().keySet(),
				rand,
				quads
		);

		return quads;
	}

	// Determines whether a cable is connected to exactly two sides that are opposite each other
	private static boolean isStraightLine( AECableType cableType, EnumMap<EnumFacing, AECableType> sides )
	{
		Iterator<EnumFacing> it = sides.keySet().iterator();
		if( !it.hasNext() )
		{
			return false; // No connections
		}
		EnumFacing firstSide = it.next();
		AECableType firstType = sides.get( firstSide );
		if( !it.hasNext() )
		{
			return false; // Only a single connection
		}
		if( firstSide.getOpposite() != it.next() )
		{
			return false; // Connected to two sides that are not opposite each other
		}
		if (it.hasNext()) {
			return false; // Must not have any other connection points
		}
		AECableType secondType = sides.get( firstSide.getOpposite() );

		// Certain cable types have restrictions on when they're rendered as a straight connection
		switch( cableType )
		{
			case GLASS:
				return firstType == AECableType.GLASS && secondType == AECableType.GLASS;
			case DENSE:
				return firstType == AECableType.DENSE && secondType == AECableType.DENSE;
		}

		return true;
	}

	private void addCableQuads( CableBusRenderState renderState, List<BakedQuad> quadsOut )
	{
		AECableType cableType = renderState.getCableType();
		if( cableType == AECableType.NONE )
		{
			return;
		}

		AEColor cableColor = renderState.getCableColor();
		EnumMap<EnumFacing, AECableType> connectionTypes = renderState.getConnectionTypes();

		// If the connection is straight, no busses are attached, and no covered core has been forced (in case of glass
		// cables), then render the cable as a simplified straight line.
		boolean noAttachments = renderState.getAttachments().isEmpty();
		if( isStraightLine( cableType, connectionTypes ) && noAttachments )
		{
			EnumFacing facing = connectionTypes.keySet().iterator().next();

			switch( cableType )
			{
				case GLASS:
					cableBuilder.addStraightGlassConnection( facing, cableColor, quadsOut );
					break;
				case COVERED:
					cableBuilder.addStraightCoveredConnection( facing, cableColor, quadsOut );
					break;
				case SMART:
					cableBuilder.addStraightSmartConnection( facing, cableColor, renderState.getChannelsOnSide().get( facing ), quadsOut );
					break;
				case DENSE:
					cableBuilder.addStraightDenseConnection( facing, cableColor, renderState.getChannelsOnSide().get( facing ), quadsOut );
					break;
			}

			return; // Don't render the other form of connection
		}

		cableBuilder.addCableCore( renderState.getCoreType(), cableColor, quadsOut );

		// Render all internal connections to attachments
		EnumMap<EnumFacing, Integer> attachmentConnections = renderState.getAttachmentConnections();
		for( EnumFacing facing : attachmentConnections.keySet() )
		{
			int distance = attachmentConnections.get( facing );
			int channels = renderState.getChannelsOnSide().get( facing );

			switch( cableType )
			{
				case GLASS:
					cableBuilder.addConstrainedGlassConnection( facing, cableColor, distance, quadsOut );
					break;
				case COVERED:
					cableBuilder.addConstrainedCoveredConnection( facing, cableColor, distance, quadsOut );
					break;
				case SMART:
					cableBuilder.addConstrainedSmartConnection( facing, cableColor, distance, channels, quadsOut );
					break;
				case DENSE:
					// Dense cables do not render connections to parts since none can be attached
					break;
			}
		}

		// Render all outgoing connections using the appropriate type
		for( EnumFacing facing : connectionTypes.keySet() )
		{
			AECableType connectionType = connectionTypes.get( facing );
			boolean cableBusAdjacent = renderState.getCableBusAdjacent().contains( facing );
			int channels = renderState.getChannelsOnSide().get( facing );

			switch( cableType )
			{
				case GLASS:
					cableBuilder.addGlassConnection( facing, cableColor, connectionType, cableBusAdjacent, quadsOut );
					break;
				case COVERED:
					cableBuilder.addCoveredConnection( facing, cableColor, connectionType, cableBusAdjacent, quadsOut );
					break;
				case SMART:
					cableBuilder.addSmartConnection( facing, cableColor, connectionType, cableBusAdjacent, channels, quadsOut );
					break;
				case DENSE:
					cableBuilder.addDenseConnection( facing, cableColor, connectionType, cableBusAdjacent, channels, quadsOut );
					break;
			}
		}
	}

	/**
	 * Gets a list of texture sprites appropriate for particles (digging, etc.) given the render state for a cable bus.
	 */
	public List<TextureAtlasSprite> getParticleTextures( CableBusRenderState renderState )
	{
		CableCoreType coreType = CableCoreType.fromCableType( renderState.getCableType() );
		AEColor cableColor = renderState.getCableColor();

		List<TextureAtlasSprite> result = new ArrayList<>();

		if( coreType != null )
		{
			result.add( cableBuilder.getCoreTexture( coreType, cableColor ) );
		}

		// If no core is present, just use the first part that comes into play
		for( EnumFacing side : renderState.getAttachments().keySet() )
		{
			List<ResourceLocation> models = renderState.getAttachments().get( side );

			for( ResourceLocation model : models )
			{
				IBakedModel bakedModel = partModels.get( model );

				if( bakedModel == null )
				{
					throw new IllegalStateException( "Trying to use an unregistered part model: " + model );
				}

				TextureAtlasSprite particleTexture = bakedModel.getParticleTexture();

				// If a part sub-model has no particle texture (indicated by it being the missing texture), don't add it,
				// so we don't get ugly missing texture break particles.
				if ( textureMap.getMissingSprite() != particleTexture )
				{
					result.add( particleTexture );
				}
			}
		}

		return result;
	}

	private static CableBusRenderState getRenderingState( IBlockState state )
	{
		if( state == null || !( state instanceof IExtendedBlockState ) )
		{
			return null;
		}

		IExtendedBlockState extendedBlockState = (IExtendedBlockState) state;
		return extendedBlockState.getValue( BlockCableBus.RENDER_STATE_PROPERTY );
	}

	@Override
	public boolean isAmbientOcclusion()
	{
		return false;
	}

	@Override
	public boolean isGui3d()
	{
		return false;
	}

	@Override
	public boolean isBuiltInRenderer()
	{
		return false;
	}

	@Override
	public TextureAtlasSprite getParticleTexture()
	{
		return particleTexture;
	}

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

	@Override
	public ItemOverrideList getOverrides()
	{
		return ItemOverrideList.NONE;
	}
}
