package projectzombie.display;

import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL33;

import bdf.types.BdfNamedList;
import bdf.types.BdfObject;
import gl_engine.MathHelpers;
import gl_engine.vec.Vec2d;
import gl_engine.vec.Vec2i;
import projectzombie.Main;
import projectzombie.entity.Entity;
import projectzombie.entity.player.EntityPlayer;
import projectzombie.util.TileState;
import projectzombie.world.chunk.Chunk;
import projectzombie.world.chunk.ChunkEventHandler;
import projectzombie.world.layer.Layer;

public class DisplayLighting
{
	private static class Lighting {
		float[] p;
		int w, h;
		int x, y;
		
		private int getID(int x, int y) {
			return (x + y * w) * 3;
		}
	}
	
	private static int lighting_last_x = 0;
	private static int lighting_last_y = 0;
	private static boolean lighting_dirty = false;
	public static int lightmap;
	
	private static AtomicBoolean lighting_new = new AtomicBoolean(false);
	private static AtomicReference<Lighting> lighting = new AtomicReference<DisplayLighting.Lighting>();
	
	public static void setDirty() {
		lighting_dirty = true;
	}
	
	private static Lighting getLighting() {
		return lighting.get();
	}
	
	private static void setLighting(Lighting lighting) {
		DisplayLighting.lighting.set(lighting);
		lighting_new.set(true);
	}
	
	public DisplayLighting() {
		lightmap = GL33.glGenTextures();
	}
	
	public static void update()
	{
		if(Camera.camera == null) return;
		if(!ChunkEventHandler.loaded) return;
		Layer layer = Main.world.getLayer();
		EntityPlayer player = Main.player;
		Vec2d ppos = player.getPos().xz();
		
		boolean dirty = lighting_dirty;
		
		if(!dirty)
		{
			for(int cx=-Chunk.RENDER_DISTANCE;cx<=Chunk.RENDER_DISTANCE;cx++) {
				for(int cy=-Chunk.RENDER_DISTANCE;cy<=Chunk.RENDER_DISTANCE;cy++) {
					Vec2i cpos = new Vec2i(
							cx + MathHelpers.floor(ppos.x / 16),
							cy + MathHelpers.floor(ppos.y / 16));
					Chunk chunk = layer.chunks.get(cpos);
					if(chunk.isLightDirty()) {
						chunk.resetLightDirty();
						dirty = true;
					}
				}
			}
		}
		
		if(!dirty) {
			return;
		}
		
		lighting_dirty = false;
		int size = (Chunk.RENDER_DISTANCE * 2 + 1) * 16;
		float[] lights = new float[size * size * 2];
		
		for(int cx=-Chunk.RENDER_DISTANCE;cx<=Chunk.RENDER_DISTANCE;cx++) {
			for(int cy=-Chunk.RENDER_DISTANCE;cy<=Chunk.RENDER_DISTANCE;cy++)
			{
				Vec2i cpos = new Vec2i(
						cx + MathHelpers.floor(ppos.x / 16),
						cy + MathHelpers.floor(ppos.y / 16));
				
				Chunk chunk = layer.chunks.get(cpos);
				
				for(int x=0;x<16;x++) {
					for(int y=0;y<16;y++)
					{
						Vec2i tpos = new Vec2i(x, y);
						int tid = tpos.getId(Chunk.CHUNK_SIZE);
						
						TileState ft = chunk.getFrontTile(tid);
						TileState bt = chunk.getBackTile(tid);
					
						float transparency = (float)MathHelpers.biggest(
								ft.tile.getLightDissipation(ft),
								bt.tile.getLightDissipation(bt));
						
						float lightSrc = (float)MathHelpers.biggest(
								ft.tile.getLightLevel(ft, tpos),
								bt.tile.getLightLevel(bt, tpos));
						
						int id = ((cx * 16 + x + Chunk.RENDER_DISTANCE * 16) +
								  (cy * 16 + y + Chunk.RENDER_DISTANCE * 16) * size) * 2;
						
						lights[id+0] = lightSrc;
						lights[id+1] = transparency;
					}
				}
			}
		}
		
		Main.workerLighting.processLighting(lights, size, size,
				MathHelpers.floor(ppos.x / 16.0) - Chunk.RENDER_DISTANCE,
				MathHelpers.floor(ppos.y / 16.0) - Chunk.RENDER_DISTANCE);
	}
	
	private static void calculateLight(Layer layer, Lighting lighting, float[] pixels, int x, int y, float level)
	{
		if(x < 0 || y < 0 || x >= lighting.w || y >= lighting.h || level <= 0) {
			return;
		}
		
		int id = lighting.getID(x, y);
		float level_current = pixels[id];
		
		Vec2i tpos = new Vec2i(x + lighting.x * 16, y + lighting.y * 16);
		int tid = tpos.getId(Chunk.CHUNK_SIZE);
		
		if(level_current >= level) {
			return;
		}
		
		pixels[id] = level;
		
		Chunk chunk = layer.getChunk(tpos);
		TileState ft = chunk.getFrontTile(tid);
		TileState bt = chunk.getBackTile(tid);
		
		float dissipation = (float)Math.max(ft.tile.getLightDissipation(ft), bt.tile.getLightDissipation(bt));
		float level_next = level - dissipation;
		
		int[] adjacent = new int[] {
				x+1, y+0,
				x+0, y+1,
				x-1, y-0,
				x-0, y-1,
		};
		
		for(int i=0;i<8;i+=2) {
			calculateLight(layer, lighting, pixels, adjacent[i], adjacent[i+1], level_next);
		}
	}
	
	private static void calculateEntityLighting(Layer layer, Lighting lighting, Entity entity, float[] pixels)
	{
		if(!entity.emitsLight) {
			return;
		}
		
		double level = entity.getLightLevel();
		
		if(level <= 0) {
			return;
		}
		
		if(level > 1) {
			level = 1;
		}
		
		int[] positions = {
				0, 0,
				1, 0,
				1, 1,
				0, 1
		};
		
		Vec2d epos = entity.getPos().xz();
		
		for(int i=0;i<positions.length;i+=2)
		{
			int x = (int)Math.round(epos.x) - positions[i+0];
			int y = (int)Math.round(epos.y) - positions[i+1];
			
			Vec2i lpos = new Vec2i(x, y).subtract(new Vec2i(lighting.x * 16, lighting.y * 16));
			
			if(lpos.x < 0 || lpos.y < 0 || lpos.x >= lighting.w || lpos.y >= lighting.h) {
				continue;
			}
			
			int id = lighting.getID(lpos.x, lpos.y);
			float level_current = pixels[id];
			
			Vec2i tpos = new Vec2i(x, y);
			int tid = tpos.getId(Chunk.CHUNK_SIZE);
			
			if(level_current >= level) {
				continue;
			}
			
			Chunk chunk = layer.getChunk(tpos);
			TileState ft = chunk.getFrontTile(tid);
			TileState bt = chunk.getBackTile(tid);
			
			double dissipation = Math.max(ft.tile.getLightDissipation(ft), bt.tile.getLightDissipation(bt));
			double level2 = level - dissipation * (Math.abs(x + 0.5 - epos.x) + Math.abs(y + 0.5 - epos.y));
			
			calculateLight(layer, lighting, pixels, lpos.x, lpos.y, (float)level2);
		}
	}
	
	public static void updateLighting()
	{
		Lighting lighting = getLighting();
		Layer layer = Main.world.getLayer();
		
		if(lighting_new.getAndSet(false))
		{
			for(int i=0;i<lighting.p.length/3;i++)
			{
				int i3 = i*3;
				int x = i % lighting.w;
				int y = i / lighting.w;
				
				Vec2i tpos = new Vec2i(x + lighting.x * 16, y + lighting.y * 16);
				
				// Store light level data from the image
				layer.setLightLevel(lighting.p[i3+0], tpos);
				
				// Store temperature and humidity data
				lighting.p[i3+1] = (float)layer.getTemperature(new Vec2i(x + lighting.x * 16, y + lighting.y * 16));
				lighting.p[i3+2] = (float)layer.getHumidity(new Vec2i(x + lighting.x * 16, y + lighting.y * 16));
			}
		}
		
		// Copy the pixels
		float[] pixels = new float[lighting.p.length];
		for(int i=0;i<pixels.length;i++) {
			pixels[i] = lighting.p[i];
		}
		
		Vec2d ppos = Main.player.getPos().xz();
		
		calculateEntityLighting(layer, lighting, Main.player, pixels);
		
		for(int cx=-Chunk.RENDER_DISTANCE;cx<=Chunk.RENDER_DISTANCE;cx++) {
			for(int cy=-Chunk.RENDER_DISTANCE;cy<=Chunk.RENDER_DISTANCE;cy++)
			{
				Vec2i cpos = new Vec2i(
						cx + MathHelpers.floor(ppos.x / 16),
						cy + MathHelpers.floor(ppos.y / 16));
				
				Chunk chunk = layer.chunks.get(cpos);
				
				for(Entity entity : chunk.entities) {
					calculateEntityLighting(layer, lighting, entity, pixels);
				}
			}
		}
		
		ByteBuffer pixels_b = BufferUtils.createByteBuffer(pixels.length);
		
		for(int i=0;i<pixels.length;i++) {
			pixels_b.put(i, (byte)(pixels[i]*255));
		}
		
		// Update the texture
		GL33.glBindTexture(GL33.GL_TEXTURE_2D, lightmap);
		
		if(lighting_last_x != lighting.w || lighting_last_y != lighting.h)
		{
			GL33.glTexImage2D(GL33.GL_TEXTURE_2D, 0, GL33.GL_RGB,
					lighting.w, lighting.h, 0, GL33.GL_RGB, GL33.GL_UNSIGNED_BYTE, pixels_b);
			
			lighting_last_x = lighting.w;
			lighting_last_y = lighting.h;
		}
		
		else {
			GL33.glTexSubImage2D(GL33.GL_TEXTURE_2D, 0, 0, 0,
					lighting.w, lighting.h, GL33.GL_RGB, GL33.GL_UNSIGNED_BYTE, pixels_b);
		}
		
		GL33.glGenerateMipmap(GL33.GL_TEXTURE_2D);
		
		// Set the texture location data
		GL33.glUniform2f(Main.window.glsl_lightmap_size, lighting.w, lighting.h);
		
		if(Camera.camera != null) {
			GL33.glUniform2f(Main.window.glsl_lightmap_offset,
					(float)(lighting.x * 16 - Camera.camera.x - 0.5),
					(float)(lighting.y * 16 - Camera.camera.y - 0.5));
		}
		
		
		
	}
	
	public static void clearLighting()
	{
		Lighting lighting = new Lighting();
		lighting.p = new float[3];
		lighting.w = 1;
		lighting.h = 1;
		lighting.x = 0;
		lighting.y = 0;
		
		setLighting(lighting);
	}
	
	public static void updateLighting(float[] light, int x, int y, int width, int height)
	{
		float[] pixels = new float[width*height*3];
		
		for(int i=0;i<width*height;i++) {
			pixels[i*3] = light[i*2];
		}
		
		Lighting lighting = new Lighting();
		lighting.p = pixels;
		lighting.w = width;
		lighting.h = height;
		lighting.x = x;
		lighting.y = y;
		
		setLighting(lighting);
	}
}
