/*
 * Copyright (c) 2012-2017, John Campbell and other contributors.  All rights reserved.
 *
 * This file is part of Tectonicus. It is subject to the license terms in the LICENSE file found in
 * the top-level directory of this distribution.  The full list of project contributors is contained
 * in the AUTHORS file found in the same location.
 *
 */

package tectonicus;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;

import javax.imageio.ImageIO;

import org.lwjgl.util.vector.Vector3f;
import org.lwjgl.util.vector.Vector4f;

import tectonicus.blockTypes.BlockRegistry;
import tectonicus.cache.PlayerSkinCache;
import tectonicus.configuration.ImageFormat;
import tectonicus.configuration.LightFace;
import tectonicus.configuration.LightStyle;
import tectonicus.configuration.Map;
import tectonicus.configuration.NorthDirection;
import tectonicus.rasteriser.Rasteriser;
import tectonicus.rasteriser.SubMesh;
import tectonicus.rasteriser.SubMesh.Rotation;
import tectonicus.raw.BedEntity;
import tectonicus.raw.RawChunk;
import tectonicus.raw.SignEntity;
import tectonicus.renderer.Geometry;
import tectonicus.renderer.OrthoCamera;
import tectonicus.texture.SubTexture;
import tectonicus.texture.TexturePack;
import tectonicus.util.BoundingBox;
import tectonicus.util.Colour4f;
import tectonicus.util.Vector2f;

public class ItemRenderer
{	
	private final Rasteriser rasteriser;
	
	public ItemRenderer(Rasteriser rasteriser) throws Exception
	{	
		this.rasteriser = rasteriser;
	}
	
	public void renderCompass(Map map, File outFile) throws Exception
	{
		System.out.println("Generating compass image...");
		
		BufferedImage compassImage = null;
		try
		{
			File compassFile = map.getCustomCompassRose();
			if (compassFile != null)
			{
				compassImage = ImageIO.read(compassFile);
			}
		}
		catch (Exception e)
		{
			System.err.println("Error while trying to read custom compass rose image: "+e);
			System.err.println("Tried to read:"+map.getCustomCompassRose().getAbsolutePath());
			e.printStackTrace();
		}
		
		if (compassImage == null)
			compassImage = ImageIO.read( getClass().getClassLoader().getResourceAsStream("Images/Compass.png") );
		
		ItemGeometry item = createCompassGeometry(rasteriser, map.getNorthDirection(), compassImage);
		
		renderItem(item, outFile, 2, map.getCameraAngleRad(), map.getCameraElevationRad());
	}
	
	public void renderPortal(File outFile, BlockTypeRegistry registry, TexturePack texturePack) throws Exception
	{
		System.out.println("Generating portal image...");
		
		ItemContext context = new ItemContext(texturePack, registry);
		
		Geometry geometry = new Geometry(rasteriser, texturePack.getTexture());
		
		RawChunk rawChunk = new RawChunk();
		
		// Bottom row
		rawChunk.setBlockId(0, 0, 0, (byte)BlockIds.OBSIDIAN);
		rawChunk.setBlockId(1, 0, 0, (byte)BlockIds.OBSIDIAN);
		rawChunk.setBlockId(2, 0, 0, (byte)BlockIds.OBSIDIAN);
		rawChunk.setBlockId(3, 0, 0, (byte)BlockIds.OBSIDIAN);
		
		// First collumn
		rawChunk.setBlockId(0, 1, 0, (byte)BlockIds.OBSIDIAN);
		rawChunk.setBlockId(0, 2, 0, (byte)BlockIds.OBSIDIAN);
		rawChunk.setBlockId(0, 3, 0, (byte)BlockIds.OBSIDIAN);
		
		// Second collumn
		rawChunk.setBlockId(3, 1, 0, (byte)BlockIds.OBSIDIAN);
		rawChunk.setBlockId(3, 2, 0, (byte)BlockIds.OBSIDIAN);
		rawChunk.setBlockId(3, 3, 0, (byte)BlockIds.OBSIDIAN);
		
		// Top row
		rawChunk.setBlockId(0, 4, 0, (byte)BlockIds.OBSIDIAN);
		rawChunk.setBlockId(1, 4, 0, (byte)BlockIds.OBSIDIAN);
		rawChunk.setBlockId(2, 4, 0, (byte)BlockIds.OBSIDIAN);
		rawChunk.setBlockId(3, 4, 0, (byte)BlockIds.OBSIDIAN);
		
		for (int y=0; y<RawChunk.HEIGHT; y++)
		{
			for (int x=0; x<RawChunk.WIDTH; x++)
			{
				for (int z=0; z<RawChunk.DEPTH; z++)
				{
					final int blockId = rawChunk.getBlockId(x, y, z);
					final int blockData = rawChunk.getBlockData(x, y, z);
					
					BlockType type = registry.find(blockId, blockData);
					if (type != null)
					{
						if (x == 0 || y == 0 || z == 0 || x == RawChunk.WIDTH-1 || y == RawChunk.HEIGHT-1 || z == RawChunk.DEPTH-1)
						{
							type.addEdgeGeometry(x, y, z, context, registry, rawChunk, geometry);
						}
						else
						{
							type.addInteriorGeometry(x, y, z, context, registry, rawChunk, geometry);
						}
					}
				}
			}
		}
		
		BoundingBox bounds = new BoundingBox(new Vector3f(0, 0, 0), 4, 5, 1);

		ItemGeometry item = new ItemGeometry(geometry, bounds);
		renderItem(item, outFile, 4, getAngleRad(55), getAngleRad(35));
	}
	
	public void renderBlock(File outFile, BlockTypeRegistry registry, TexturePack texturePack, int blockId, int blockData) throws Exception
	{		
		ItemContext context = new ItemContext(texturePack, registry);
		
		Geometry geometry = new Geometry(rasteriser, texturePack.getTexture());
		
		RawChunk rawChunk = new RawChunk();
		
		rawChunk.setBlockId(0, 0, 0, (byte)blockId);
		rawChunk.setBlockData(0, 0, 0, (byte)blockData);
		rawChunk.setBlockLight(0, 0, 0, (byte)16);
		rawChunk.setSkyLight(0, 0, 0, (byte) 16);
		
		BlockType type = registry.find(blockId, blockData);
		if (type != null)
		{
			type.addInteriorGeometry(0, 0, 0, context, registry, rawChunk, geometry);
		}
		
		BoundingBox bounds = new BoundingBox(new Vector3f(0, 0.1f, 0), 1, 1, 1);

		ItemGeometry item = new ItemGeometry(geometry, bounds);
		renderItem(item, outFile, 4, getAngleRad(45), getAngleRad(25));
	}
	
	public void renderSign(File outFile, BlockTypeRegistry registry, TexturePack texturePack, int blockId, int blockData) throws Exception
	{		
		System.out.println("Generating sign icon...");
		
		ItemContext context = new ItemContext(texturePack, registry);
		
		Geometry geometry = new Geometry(rasteriser, texturePack.getTexture());
		
		RawChunk rawChunk = new RawChunk();
		
		rawChunk.setBlockId(0, 0, 0, (byte)blockId);
		rawChunk.setBlockData(0, 0, 0, (byte)blockData);
		rawChunk.setBlockLight(0, 0, 0, (byte)16);
		rawChunk.setSkyLight(0, 0, 0, (byte) 16);
		HashMap<String, SignEntity> signs = new HashMap<>();
		signs.put("x0y0z0", new SignEntity(0, 0, 0, 0, 0, 0, "", "Tectonicus", "", "", blockData));
		rawChunk.setSigns(signs);
		
		BlockType type = registry.find(blockId, blockData);
		if (type != null)
		{
			type.addInteriorGeometry(0, 0, 0, context, registry, rawChunk, geometry);
		}
		
		BoundingBox bounds = new BoundingBox(new Vector3f(0, 0.4f, 0), 1, 1, 0);

		ItemGeometry item = new ItemGeometry(geometry, bounds);
		renderItem(item, outFile, 4, getAngleRad(45), getAngleRad(25));
	}
	
	public void renderBed(File outFile, BlockTypeRegistry registry, TexturePack texturePack) throws Exception
	{
		System.out.println("Generating bed icon...");
		
		ItemContext context = new ItemContext(texturePack, registry);
		
		Geometry geometry = new Geometry(rasteriser, texturePack.getTexture());
		
		RawChunk rawChunk = new RawChunk();
		
		rawChunk.setBlockId(0, 0, 0, (byte)BlockIds.BED);
		rawChunk.setBlockData(0, 0, 0, (byte)10);
		rawChunk.setBlockLight(0, 0, 0, (byte)16);
		rawChunk.setSkyLight(0, 0, 0, (byte) 16);
		rawChunk.setBlockId(0, 0, 1, (byte)BlockIds.BED);
		rawChunk.setBlockData(0, 0, 1, (byte)6);
		rawChunk.setBlockLight(0, 0, 1, (byte)16);
		rawChunk.setSkyLight(0, 0, 1, (byte) 16);
		HashMap<String, BedEntity> beds = new HashMap<>();
		beds.put("x0y0z0", new BedEntity(0, 0, 0, 0, 0, 0, 14));
		beds.put("x0y0z1", new BedEntity(0, 0, 1, 0, 0, 1, 14));
		rawChunk.setBeds(beds);
		
		BlockType type = registry.find(BlockIds.BED, 10);
		if (type != null)
		{
			type.addInteriorGeometry(0, 0, 0, context, registry, rawChunk, geometry);
			type.addInteriorGeometry(0, 0, 1, context, registry, rawChunk, geometry);
		}
		
		BoundingBox bounds = new BoundingBox(new Vector3f(-1, -0.5f, 0), 2, 1, 0);
				
		ItemGeometry item = new ItemGeometry(geometry, bounds);
		renderItem(item, outFile, 4, getAngleRad(65), getAngleRad(35));
	}
	
	private void renderItem(ItemGeometry item, File outFile, final int numDownsamples, final float cameraAngle, final float cameraElevationAngle)
	{	
		Geometry geometry = item.geometry;
		BoundingBox bounds = item.bounds;
		
		Color colourKey = new Color(255, 0, 128);
		
		rasteriser.beginFrame();
		rasteriser.resetState();
		rasteriser.clear(colourKey);
		rasteriser.clearDepthBuffer();
		
		OrthoCamera camera = new OrthoCamera(rasteriser, 512, 512);
		final float lookX = bounds.getCenterX();
		final float lookY = bounds.getCenterY();
		final float lookZ = bounds.getCenterZ();
		final float size = (float)Math.sqrt(bounds.getWidth()*bounds.getWidth() + bounds.getHeight()*bounds.getHeight());
		
		camera.lookAt(lookX, lookY, lookZ, size, cameraAngle, cameraElevationAngle);
		camera.apply();
		
		ArrayList<Vector2f> corners = new ArrayList<Vector2f>();
		corners.add( camera.projectf(new Vector3f(bounds.getOrigin().x,						bounds.getOrigin().y,	bounds.getOrigin().z) ) );
		corners.add( camera.projectf(new Vector3f(bounds.getOrigin().x+bounds.getWidth(),	bounds.getOrigin().y,	bounds.getOrigin().z) ) );
		corners.add( camera.projectf(new Vector3f(bounds.getOrigin().x, 					bounds.getOrigin().y,	bounds.getOrigin().z+bounds.getDepth()) ) );
		corners.add( camera.projectf(new Vector3f(bounds.getOrigin().x+bounds.getWidth(),	bounds.getOrigin().y,	bounds.getOrigin().z+bounds.getDepth()) ) );
		
		corners.add( camera.projectf(new Vector3f(bounds.getOrigin().x,						bounds.getOrigin().y+bounds.getHeight(),	bounds.getOrigin().z) ) );
		corners.add( camera.projectf(new Vector3f(bounds.getOrigin().x+bounds.getWidth(),	bounds.getOrigin().y+bounds.getHeight(),	bounds.getOrigin().z) ) );
		corners.add( camera.projectf(new Vector3f(bounds.getOrigin().x, 					bounds.getOrigin().y+bounds.getHeight(),	bounds.getOrigin().z+bounds.getDepth()) ) );
		corners.add( camera.projectf(new Vector3f(bounds.getOrigin().x+bounds.getWidth(),	bounds.getOrigin().y+bounds.getHeight(),	bounds.getOrigin().z+bounds.getDepth()) ) );
		
		float minX = Integer.MAX_VALUE;
		float minY = Integer.MAX_VALUE;
		float maxX = Integer.MIN_VALUE;
		float maxY = Integer.MIN_VALUE;
		for (Vector2f p : corners)
		{
			minX = Math.min(minX, p.x);
			maxX = Math.max(maxX, p.x);
			
			minY = Math.min(minY, p.y);
			maxY = Math.max(maxY, p.y);
		}
		
		Vector3f topLeftWorld = camera.unproject(new Vector2f(minX, minY));
		Vector3f topRightWorld = camera.unproject(new Vector2f(maxX, minY));
		Vector3f bottomLeftWorld = camera.unproject(new Vector2f(minX, maxY));
		
		final float xSize = Vector3f.sub(topLeftWorld, topRightWorld, null).length();
		final float ySize = Vector3f.sub(topLeftWorld, bottomLeftWorld, null).length();
		float longest = Math.max(xSize, ySize);
		
		camera.lookAt(lookX, lookY, lookZ, longest, cameraAngle, cameraElevationAngle);
		camera.apply();
		
		geometry.finalise();
		
		{
			rasteriser.enableAlphaTest(false);
			rasteriser.enableBlending(false);
			rasteriser.enableDepthWriting(true);
			
			geometry.drawSolidSurfaces(0, 0, 0);
		}
		
		{
			rasteriser.enableAlphaTest(true);
			rasteriser.enableBlending(false);
			rasteriser.enableDepthWriting(true);
			
			geometry.drawAlphaTestedSurfaces(0, 0, 0);
		}
		
		{
			rasteriser.enableAlphaTest(false);
			rasteriser.enableBlending(true);
			rasteriser.enableDepthWriting(false);
			
			geometry.drawTransparentSurfaces(0, 0, 0);
		}
		
		// Reset to default
		rasteriser.enableAlphaTest(false);
		rasteriser.enableBlending(false);
		rasteriser.enableDepthWriting(true);
		
		BufferedImage outImg = rasteriser.takeScreenshot(0, 0, 512, 512, ImageFormat.Png);
		
		filterColourKey(outImg, colourKey);
		
		for (int i=0; i<numDownsamples; i++)
		{
			outImg = downsample(outImg);
		}
		
		Screenshot.write(outFile, outImg, ImageFormat.Png, 1.0f);
	}
	
	private float getAngleRad(int angle)
	{
		final float normalised = (float)angle / 360.0f;
		final float rad = normalised * (float)Math.PI * 2.0f;
		return rad;
	}
	
	private static ItemGeometry createCompassGeometry(Rasteriser rasteriser, NorthDirection dir, BufferedImage img)
	{
		Geometry geometry = new Geometry(rasteriser, null);
		
		SubMesh mesh = new SubMesh();
		
		for (int x=0; x<img.getWidth(); x++)
		{
			for (int y=0; y<img.getHeight(); y++)
			{
				final int rgb = img.getRGB(x, y);
				final int alpha = (rgb>>24) & 0xFF;
				
				if (alpha > 128)
				{
					Color c = new Color(img.getRGB(x, y));
					Vector4f colour = new Vector4f(c.getRed()/255.0f, c.getGreen()/255.0f, c.getBlue()/255.0f, 1.0f);
					
					Vector4f lighter = Vector4f.add(colour, new Vector4f(0.1f, 0.1f, 0.1f, 0), null);
					clamp(lighter, 0, 1);
					
					Vector4f darker = Vector4f.add(colour, new Vector4f(-0.1f, -0.1f, -0.1f, 0), null);
					clamp(darker, 0, 1);
					
					final float xx = y - (img.getHeight()/2);
					final float yy = 0;
					final float zz = img.getWidth()-x - (img.getWidth()/2);
					
					Vector3f p0 = new Vector3f(xx,   yy, zz);
					Vector3f p1 = new Vector3f(xx+1, yy, zz);
					Vector3f p2 = new Vector3f(xx+1, yy, zz+1);
					Vector3f p3 = new Vector3f(xx,   yy, zz+1);
					
					Vector3f p4 = new Vector3f(xx,   yy+1, zz);
					Vector3f p5 = new Vector3f(xx+1, yy+1, zz);
					Vector3f p6 = new Vector3f(xx+1, yy+1, zz+1);
					Vector3f p7 = new Vector3f(xx,   yy+1, zz+1);
					
					SubTexture tex = new SubTexture(null, 0, 0, 0, 0);
					
					// Bottom, top
					mesh.addQuad(p1, p0, p3, p2, lighter, tex);
					mesh.addQuad(p4, p5, p6, p7, lighter, tex);
					
					// Sides
					mesh.addQuad(p7, p6, p2, p3, darker, tex);
					mesh.addQuad(p6, p5, p1, p2, colour, tex);
					
					mesh.addQuad(p5, p4, p0, p1, darker, tex);
					mesh.addQuad(p4, p7, p3, p0, colour, tex);
				}
			}
		}
		
		float compassRotation = 0;
		if (dir == NorthDirection.PlusX)
		{
			compassRotation = 180;
		}
		else if (dir == NorthDirection.MinusX)
		{
			compassRotation = 0;
		}
		else if (dir == NorthDirection.PlusZ)
		{
			compassRotation = 90;
		}
		else if (dir == NorthDirection.MinusZ)
		{
			compassRotation = 270;
		}
		
		mesh.pushTo(geometry.getBaseMesh(), 0, 0, 0, Rotation.Clockwise, compassRotation);
		
		BoundingBox bounds = new BoundingBox(new Vector3f(-img.getHeight()/2, 0, -img.getWidth()/2), img.getHeight()+1, 1, img.getWidth()+1);
		
		return new ItemGeometry(geometry, bounds);
	}
	
	
	public static void filterColourKey(BufferedImage image, Color colourKey)
	{
		for (int x=0; x<image.getWidth(); x++)
		{
			for (int y=0; y<image.getHeight(); y++)
			{
				final int rgb = image.getRGB(x, y);
				
				final int dRed = Math.abs(getRed(rgb) - colourKey.getRed());
				final int dGreen = Math.abs(getGreen(rgb) - colourKey.getGreen());
				final int dBlue = Math.abs(getBlue(rgb) - colourKey.getBlue());
				
				final int delta = dRed + dGreen + dBlue;
				
				// If the total colour delta is lower than the threshold then mark
				// the pixel as transparent
				if (delta < 3)
				{
					image.setRGB(x, y, 0x0);
				}
			}
		}
	}
	
	private static int getRed(final int rgba)
	{
		return (rgba >> 16) & 0xFF;
	}
	
	private static int getGreen(final int rgba)
	{
		return (rgba >> 8) & 0xFF;
	}
	
	private static int getBlue(final int rgba)
	{
		return (rgba) & 0xFF;
	}
	
	public static void clamp(Vector4f vec, final float min, final float max)
	{
		vec.x = (float)Math.min(Math.max(vec.x, min), max);
		vec.y = (float)Math.min(Math.max(vec.y, min), max);
		vec.z = (float)Math.min(Math.max(vec.z, min), max);
		vec.w = (float)Math.min(Math.max(vec.w, min), max);
	}
	
	public static BufferedImage downsample(BufferedImage src)
	{
		BufferedImage out = new BufferedImage(src.getWidth()/2, src.getHeight()/2, BufferedImage.TYPE_4BYTE_ABGR);
		
		Graphics2D g = (Graphics2D)out.getGraphics();
		g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
		g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		
		g.drawImage(src, 0, 0, out.getWidth(), out.getHeight(), null);
		
		return out;
	}
	
	private static class ItemGeometry
	{
		private Geometry geometry;
		private BoundingBox bounds;
		
		public ItemGeometry(Geometry g, BoundingBox b)
		{
			this.geometry = g;
			this.bounds = b;
		}
	}
	
	private class ItemContext implements BlockContext
	{
		private TexturePack texturePack;
		private BlockTypeRegistry registry;
		
		public ItemContext(TexturePack texturePack, BlockTypeRegistry registry)
		{
			this.texturePack = texturePack;
			this.registry = registry;
		}
		
		@Override
		public int getBlockId(ChunkCoord chunkCoord, int x, int y, int z)
		{
			return 0;
		}

		@Override
		public BlockType getBlockType(ChunkCoord chunkCoord, int x, int y, int z)
		{
			return registry.find(0, 0);
		}
		
		@Override
		public float getLight(ChunkCoord chunkCoord, int x, int y, int z, LightFace face)
		{
			return 1;
		}

		@Override
		public LightStyle getLightStyle()
		{
			return LightStyle.Day;
		}

		@Override
		public TexturePack getTexturePack()
		{
			return texturePack;
		}

		@Override
		public int getBiomeId(ChunkCoord chunkCoord, int x, int y, int z)
		{
			return 0;
		}

		@Override
		public Colour4f getGrassColour(ChunkCoord chunkCoord, int x, int y, int z)
		{
			return new Colour4f(1, 1, 1, 1);
		}
		
		@Override
		public PlayerSkinCache getPlayerSkinCache()
		{
			return null;
		}
		
		@Override
		public BlockRegistry getModelRegistry()
		{
			return null;
		}
	}
}