package projectzombie.world.chunk;

import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Random;

import projectzombie.util.ClassBdf;
import projectzombie.util.TileState;
import bdf.types.BdfArray;
import bdf.types.BdfNamedList;
import bdf.types.BdfObject;
import gl_engine.MathHelpers;
import gl_engine.matrix.Matrix4;
import gl_engine.range.Range2i;
import gl_engine.texture.TextureRef3D;
import gl_engine.vec.Vec2d;
import gl_engine.vec.Vec2i;
import gl_engine.vec.Vec3d;
import projectzombie.Main;
import projectzombie.display.Camera;
import projectzombie.entity.Entity;
import projectzombie.entity.EntityAlive;
import projectzombie.entity.EntityHoldsEntities;
import projectzombie.entity.EntityKillWithParticles;
import projectzombie.entity.EntityParticle;
import projectzombie.entity.EntityParticlePart;
import projectzombie.entity.particle.ParticleBreak;
import projectzombie.entity.tileentity.TileEntity;
import projectzombie.init.Models;
import projectzombie.init.Tiles;
import projectzombie.model.IModel;
import projectzombie.model.Model;
import projectzombie.model.ModelChunk;
import projectzombie.model.ModelChunkBorder;
import projectzombie.tiles.Tile;
import projectzombie.util.math.random.RandomHelpers;
import projectzombie.world.layer.Layer;

public class Chunk implements ClassBdf
{
	public static final Range2i CHUNK_SIZE = new Range2i(16, 16);
	public static final Chunk CHUNK_EMPTY = new ChunkEmpty();
	public static final int CHUNK_INDEX = CHUNK_SIZE.mx * CHUNK_SIZE.my;
	public static final Random rand = new Random();
	
	public static int SIMULATION_DISTANCE = 10;
	public static int RENDER_DISTANCE = 2;
	
	public static boolean SHOW_CHUNKS = false;
	
	private Tile tiles_back[] = new Tile[CHUNK_INDEX];
	private Tile tiles_front[] = new Tile[CHUNK_INDEX];
	private byte tiles_front_meta[] = new byte[CHUNK_INDEX];
	private byte tiles_back_meta[] = new byte[CHUNK_INDEX];
	private byte tiles_lighting[] = new byte[CHUNK_INDEX];
	private byte tiles_temperature[] = new byte[CHUNK_INDEX];
	private byte tiles_humidity[] = new byte[CHUNK_INDEX];
	public ArrayList<Entity> entities = new ArrayList<Entity>();
	public Layer layer;
	public Vec2i c_pos;
	private boolean dirty;
	private boolean light_dirty;
	private boolean render_dirty;
	
	private ModelChunk model;
	
	public boolean isDirty()
	{
		if(entities.size() > 0) {
			return true;
		}
		
		return dirty;
	}
	
	public void clearDirty() {
		dirty = false;
	}
	
	private void setDirty() {
		this.light_dirty = true;
		this.render_dirty = true;
		this.dirty = true;
	}
	
	public boolean isLightDirty() {
		return light_dirty;
	}
	
	public void resetLightDirty() {
		this.light_dirty = false;
	}
	
	@Override
	public void BdfClassLoad(BdfObject bdf)
	{
		BdfNamedList nl = bdf.getNamedList();
		
		// Load all the tiles and meta
		short[] tb = nl.get("tilesBack").getShortArray();
		short[] tf = nl.get("tilesFront").getShortArray();
		byte[] mb = nl.get("metaBack").getByteArray();
		byte[] mf = nl.get("metaFront").getByteArray();
		byte[] a_t = nl.get("temperature").getByteArray();
		byte[] a_h = nl.get("humidity").getByteArray();
		
		if(
				tb.length != CHUNK_INDEX || tf.length != CHUNK_INDEX ||
				mb.length != CHUNK_INDEX || mf.length != CHUNK_INDEX ||
				a_t.length != CHUNK_INDEX || a_h.length != CHUNK_INDEX) {
			return;
		}
		
		for(int i=0;i<CHUNK_INDEX;i++)
		{
			tiles_back[i]  = Tiles.tiles.get(tb[i]);
			tiles_front[i] = Tiles.tiles.get(tf[i]);
			tiles_back_meta[i] = mb[i];
			tiles_front_meta[i] = mf[i];
			tiles_temperature[i] = a_t[i];
			tiles_humidity[i] = a_h[i];
			tiles_lighting[i] = 0;
		}
		
		// Load all the entities
		entities.clear();
		BdfArray bdf_entities = nl.get("entities").getArray();
		
		for(BdfObject bdf_entity : bdf_entities) {
			Entity entity = Entity.loadEntity(bdf_entity);
			if(entity != null) {
				entity.chunk = this;
				entities.add(entity);
			}
		}
	}
	
	@Override
	public void BdfClassSave(BdfObject bdf)
	{
		BdfNamedList nl = bdf.getNamedList();
		
		short[] tb = new short[CHUNK_INDEX];
		short[] tf = new short[CHUNK_INDEX];
		
		// Save all the tiles and tile meta
		for(int i=0;i<CHUNK_INDEX;i++) {
			tb[i] = tiles_back[i].id;
			tf[i] = tiles_front[i].id;
		}
		
		nl.set("tilesBack", bdf.newObject().setShortArray(tb));
		nl.set("tilesFront", bdf.newObject().setShortArray(tf));
		nl.set("metaBack", bdf.newObject().setByteArray(tiles_back_meta));
		nl.set("metaFront", bdf.newObject().setByteArray(tiles_front_meta));
		nl.set("temperature", bdf.newObject().setByteArray(tiles_temperature));
		nl.set("humidity", bdf.newObject().setByteArray(tiles_humidity));
		
		// Save all the saveable entity data
		BdfArray bdf_entities = bdf.newArray();
		nl.set("entities", bdf.newObject().setArray(bdf_entities));
		for(Entity e : entities) {
			if(e.getID() == -1)
				continue;
			BdfObject bdf_e = bdf.newObject();
			e.BdfClassSave(bdf_e);
			bdf_entities.add(bdf_e);
		}
	}
	
	public Chunk(Layer layer, Vec2i c_pos, BdfObject bdf)
	{
		this.layer = layer;
		this.c_pos = c_pos;
		
		setDirty();
		BdfClassLoad(bdf);
	}
	
	public Chunk(Layer layer, Vec2i c_pos, Random rand)
	{
		// Set some specified values
		this.layer = layer;
		this.c_pos = c_pos;
		this.dirty = false;
		this.light_dirty = true;
		
		// Loop over all the tiles in the chunk
		for(int i=0;i<CHUNK_INDEX;i++)
		{
			// Make all these tiles void
			tiles_back[i] = Tiles.VOID;
			tiles_front[i] = Tiles.VOID;
		}
	}
	
	public int getParticlePoolSize() {
		return getParticlePoolSize(entities.toArray());
	}
	
	public int getParticlePoolSize(Object[] entities)
	{
		int particle_pool_size = 0;
		
		for(Object o : entities)
		{
			Entity e = (Entity)o;
			
			if(e == null || e.isDead()) {
				continue;
			}
			
			if(e instanceof EntityHoldsEntities) {
				particle_pool_size += getParticlePoolSize(((EntityHoldsEntities) e).getEntities());
			}
			
			if(e instanceof EntityParticle)
			{
				EntityParticle ep = (EntityParticle) e;
				
				for(int i=0;i<ep.getParticleCount();i++)
				{
					EntityParticlePart p = ep.getParticleAt(i);
					
					if(p == null) {
						continue;
					}
					
					Vec3d pos = Matrix4.multiply(
							Camera.camera.projection,
							p.pos.subtract(new Vec3d(0.5 + Camera.camera.x, 0, 0.5 + Camera.camera.y)));
					
					if(Math.abs(pos.x) <= 1.125 && Math.abs(pos.y) <= 1.125 && Math.abs(pos.z) <= 1.125) {
						particle_pool_size += 1;
					}
				}
			}
		}
		
		return particle_pool_size;
	}
	
	private int renderEntities(Object[] entities, FloatBuffer particle_pool, int upto)
	{
		// Render each entity
		for(Object o : entities)
		{
			Entity e = (Entity)o;
			
			if(e == null || e.isDead()) {
				continue;
			}
			
			if(e instanceof EntityHoldsEntities) {
				upto = renderEntities(((EntityHoldsEntities) e).getEntities(), particle_pool, upto);
			}
			
			if(e instanceof EntityParticle && Main.world.isPoolDirty())
			{
				EntityParticle ep = (EntityParticle)e;
				
				for(int i=0;i<ep.getParticleCount();i++)
				{
					EntityParticlePart p = ep.getParticleAt(i);
					
					if(p == null) {
						continue;
					}
					
					Vec3d pos = Matrix4.multiply(
							Camera.camera.projection,
							p.pos.subtract(new Vec3d(0.5 + Camera.camera.x, 0, 0.5 + Camera.camera.y)));
					
					if(Math.abs(pos.x) > 1.125 || Math.abs(pos.y) > 1.125 || Math.abs(pos.z) > 1.125) {
						continue;
					}
					
					float px = (float)(p.pos.x - 0.5f - Camera.camera.x);
					float py = (float)(p.pos.y);
					float pz = (float)(p.pos.z - 0.5f - Camera.camera.y);
					
					float k = Model.OFFSET;
					float tsx = p.tex.sx+k;
					float tex = p.tex.ex-k;
					float tsy = p.tex.sy+k;
					float tey = p.tex.ey-k;
					float tz = p.tex.z;
					
					float g = (float)p.getFade();
					float s = (float)p.size / 2;
					float f = p.flags;
					
					float a_si = p.animationSize;
					float a_sp = p.animationSpeed;
					
					float a = -s, b = s, c = 0, d = 0;
					
					if(p.isFlat()) {
						c = a;
						d = b;
						a = 0;
						b = 0;
					}
					
					/*
						layout (location = 0) in vec3 aPos;
						layout (location = 1) in vec3 aTex;
						layout (location = 2) in vec2 aTexY;
						layout (location = 3) in vec3 aOffset;
						layout (location = 4) in vec2 aAnimate;
						layout (location = 5) in vec3 aFlags;
					 */
					
					float[] verticies = {
							-s, a, c,   tex, tsy, tz,   tsy, tey,   px, py, pz,   a_si, a_sp, g, 0, f,
							-s, b, d,   tex, tey, tz,   tsy, tey,   px, py, pz,   a_si, a_sp, g, 0, f,
							+s, b, d,   tsx, tey, tz,   tsy, tey,   px, py, pz,   a_si, a_sp, g, 0, f,
							+s, a, c,   tsx, tsy, tz,   tsy, tey,   px, py, pz,   a_si, a_sp, g, 0, f,
					};
					
					for(int j=0;j<verticies.length;j++) {
						particle_pool.put(upto+j, verticies[j]);
					}
					
					upto += verticies.length;
				}
			}
			
			IModel model = e.getModel();
			
			if(model == null) {
				continue;
			}
			
			Vec3d pos = e.getPos();
			
			// Don't try to render anything if the model is empty
			if(model.getSize() == 0) {
				continue;
			}
			
			// Render the model
			model.setModel(Matrix4.translate(
					pos.x - Camera.camera.x - 0.5, pos.y,
					pos.z - Camera.camera.y - 0.5));
			model.render();
		}
		
		return upto;
	}
	
	public int render(Camera camera, FloatBuffer particle_pool, int upto)
	{
		if(model == null || this.render_dirty)
		{
			this.render_dirty = false;
			int verticies_size = 0;
			int indicies_size = 0;
			int index_offset = 0;
			
			for(int i=0;i<CHUNK_INDEX;i++) {
				TileState bt = getBackTile(i);
				TileState ft = getFrontTile(i);
				verticies_size += bt.tile.getModel(bt.meta).getSize();
				verticies_size += ft.tile.getModel(ft.meta).getSize();
				indicies_size += bt.tile.getModel(bt.meta).getIndexSize();
				indicies_size += ft.tile.getModel(ft.meta).getIndexSize();
			}
			
			int[] indicies = new int[indicies_size];
			float[] verticies = new float[verticies_size * Model.SIZE];
			TextureRef3D[] textures = new TextureRef3D[verticies_size];
			int upto_v = 0;
			int upto_i = 0;
			
			for(int i=0;i<CHUNK_INDEX;i++)
			{
				Vec2i pos = Vec2i.fromId(CHUNK_SIZE, i);
				
				for(TileState state : new TileState[] {getFrontTile(pos), getBackTile(pos)})
				{
					Model model = state.tile.getModel(state.meta);
					
					TextureRef3D[] textures2 = model.getTextures();
					float[] verticies2 = model.getVerticies();
					int[] indicies2 = model.getIndicies();
					
					for(int j=0;j<model.getSize();j++)
					{
						textures[upto_v] = textures2[j];
						
						for(int k=0;k<Model.SIZE;k++)
						{
							switch(k)
							{
							case 8:
								verticies[upto_v * Model.SIZE + k ] = pos.x;
								continue;
								
							case 10:
								verticies[upto_v * Model.SIZE + k ] = pos.y;
								continue;
								
							default:
								verticies[upto_v * Model.SIZE + k ] = verticies2[j * Model.SIZE + k ];
								continue;
							}
						}
						
						upto_v += 1;
					}
					
					for(int j=0;j<indicies2.length;j++) {
						indicies[j + upto_i] = indicies2[j] + index_offset;
					}
					
					upto_i += indicies2.length;
					index_offset += model.getSize();
				}
			}
			
			if(model != null) {
				model.free();
			}
			
			model = new ModelChunk(verticies, indicies, textures, verticies_size);
		}
		
		// Render all the tiles in the chunk as a block
		model.setModel(Matrix4.translate(c_pos.x * 16 - Camera.camera.x, 0, c_pos.y * 16 - Camera.camera.y));
		model.render();
		
		if(SHOW_CHUNKS) {
			Models.CHUNK_BORDER.render();
		}
		
		return renderEntities(entities.toArray(), particle_pool, upto);
	}
	
	public void spawnEntity(Entity e) {
		e.chunk = this;
		entities.add(e);
	}
	
	public void tickEntities()
	{
		// Loop over every entitiy
		for(int i=0;i<entities.size();i++)
		{
			// Get the entity and tick it
			Entity e = entities.get(i);
			e.tick(this, layer);
		}
	}
	
	public void checkEntities()
	{
		// Loop over every entity
		for(int i=0;i<entities.size();i++)
		{
			// Get the entitiy
			Entity e = entities.get(i);
			
			// is this entity alive
			if(e instanceof EntityAlive)
			{
				// Get the alive entity
				EntityAlive ea = (EntityAlive)e;
				
				// Kill the entity if it should be dead and continue to the next loop iteration
				if(ea.getHealth() < 0) {
					ea.onDeath(layer);
					killEntity(e);
					continue;
				}
			}
			
			// Has the entity left the chunk
			int cx = c_pos.x * CHUNK_SIZE.mx;
			int cy = c_pos.y * CHUNK_SIZE.my;
			double px = e.getPos().x;
			double py = e.getPos().z;
			if(px > cx + CHUNK_SIZE.mx || px < cx || py > cy + CHUNK_SIZE.my || py < cy)
			{
				// Process the entity by layer and remove the entity from the array
				layer.spawnEntity(e);
				entities.remove(i);
				i -= 1;
			}
		}
	}
	
	public void createTileEntity(Vec2i pos, TileEntity te)
	{
		TileEntity te_old = getTileEntity(pos);
		
		// Kill the old tile entity if it exists
		if(te_old != null) {
			te_old.kill();
		}
		
		te.setPos(new Vec3d(pos.x, 0, pos.y));
		spawnEntity(te);
	}
	
	public void destroyTileEntity(Vec2i pos)
	{
		TileEntity te = getTileEntity(pos);
		
		if(te != null) {
			te.kill();
		}
	}
	
	public TileEntity getTileEntity(Vec2i pos)
	{
		for(Entity e : entities)
		{
			if(e instanceof TileEntity && e.getPos().xz().toInt().equal(pos)) {
				return (TileEntity)e;
			}
		}
		
		return null;
	}
	
	public void setTemperature(Vec2i pos, double v)
	{
		// Get the id
		Vec2i cpos = new Vec2i(0, 0);
		cpos.x = MathHelpers.mod(pos.x, CHUNK_SIZE.mx);
		cpos.y = MathHelpers.mod(pos.y, CHUNK_SIZE.my);
		int id = cpos.getId(CHUNK_SIZE);
		
		setTemperature(id, v);
	}
	
	public void setTemperature(int id, double v) {
		tiles_temperature[id] = (byte)(v * 255);
	}
	
	public double getTemperature(Vec2i pos)
	{
		// Get the id
		Vec2i cpos = new Vec2i(0, 0);
		cpos.x = MathHelpers.mod(pos.x, CHUNK_SIZE.mx);
		cpos.y = MathHelpers.mod(pos.y, CHUNK_SIZE.my);
		int id = cpos.getId(CHUNK_SIZE);
		
		return getTemperature(id);
	}
	
	public double getTemperature(int id) {
		return (0xff & tiles_temperature[id]) / 255.0;
	}
	
	public void setHumidity(Vec2i pos, double v)
	{
		// Get the id
		Vec2i cpos = new Vec2i(0, 0);
		cpos.x = MathHelpers.mod(pos.x, CHUNK_SIZE.mx);
		cpos.y = MathHelpers.mod(pos.y, CHUNK_SIZE.my);
		int id = cpos.getId(CHUNK_SIZE);
		
		setHumidity(id, v);
	}
	
	public void setHumidity(int id, double v) {
		tiles_humidity[id] = (byte)(v * 255);
	}
	
	public double getHumidity(Vec2i pos)
	{
		// Get the id
		Vec2i cpos = new Vec2i(0, 0);
		cpos.x = MathHelpers.mod(pos.x, CHUNK_SIZE.mx);
		cpos.y = MathHelpers.mod(pos.y, CHUNK_SIZE.my);
		int id = cpos.getId(CHUNK_SIZE);
		
		return getHumidity(id);
	}
	
	public double getHumidity(int id) {
		return (0xff & tiles_humidity[id]) / 255.0;
	}
	
	public void setBackTile(TileState tile, Vec2i pos)
	{
		// Get the id
		Vec2i cpos = new Vec2i(0, 0);
		cpos.x = MathHelpers.mod(pos.x, CHUNK_SIZE.mx);
		cpos.y = MathHelpers.mod(pos.y, CHUNK_SIZE.my);
		int id = cpos.getId(CHUNK_SIZE);
		
		setBackTile(tile, id);
	}
	
	public void setBackTile(TileState tile, int id)
	{
		Vec2i pos = Vec2i.fromId(CHUNK_SIZE, id).add(c_pos.multiply(16));
		tiles_back[id].onDestroy(layer, this, new TileState(tiles_back[id], tiles_back_meta[id]), tile, pos);
		
		// Set the back tile
		tiles_back[id] = tile.tile;
		tiles_back_meta[id] = tile.meta;
		
		setDirty();
		
		tile.tile.onGenerate(layer, this, tile, pos);
	}
	
	public void setFrontTile(TileState tile, Vec2i pos)
	{
		// Get the id
		Vec2i cpos = new Vec2i(0, 0);
		cpos.x = MathHelpers.mod(pos.x, CHUNK_SIZE.mx);
		cpos.y = MathHelpers.mod(pos.y, CHUNK_SIZE.my);
		int id = cpos.getId(CHUNK_SIZE);
		
		setFrontTile(tile, id);
	}
	
	public void setFrontTile(TileState tile, int id)
	{
		Vec2i pos = Vec2i.fromId(CHUNK_SIZE, id).add(c_pos.multiply(16));
		tiles_front[id].onDestroy(layer, this, new TileState(tiles_front[id], tiles_front_meta[id]), tile, pos);
		
		// Set the front tile
		this.tiles_front[id] = tile.tile;
		this.tiles_front_meta[id] = tile.meta;
		
		setDirty();
		
		tile.tile.onGenerate(layer, this, tile, pos);
	}
	
	public TileState getBackTile(Vec2i pos)
	{
		// Get the id
		Vec2i cpos = new Vec2i(0, 0);
		cpos.x = MathHelpers.mod(pos.x, CHUNK_SIZE.mx);
		cpos.y = MathHelpers.mod(pos.y, CHUNK_SIZE.my);
		int id = cpos.getId(CHUNK_SIZE);
		
		return getBackTile(id);
	}
	
	public TileState getBackTile(int id)
	{
		// Send back the back tile
		TileState ts = new TileState(this.tiles_back[id], this.tiles_back_meta[id]);
		ts.light = getLightLevel(id);
		return ts;
	}
	
	public TileState getFrontTile(Vec2i pos)
	{
		// Get the id
		Vec2i cpos = new Vec2i(0, 0);
		cpos.x = MathHelpers.mod(pos.x, CHUNK_SIZE.mx);
		cpos.y = MathHelpers.mod(pos.y, CHUNK_SIZE.my);
		int id = cpos.getId(CHUNK_SIZE);
		
		return getFrontTile(id);
	}
	
	public TileState getFrontTile(int id)
	{
		// Send back the front tile
		TileState ts = new TileState(this.tiles_front[id], this.tiles_front_meta[id]);
		ts.light = getLightLevel(id);
		return ts;
	}
	
	public void breakBackTile(Vec2i pos)
	{
		TileState ts = getBackTile(pos);
		setDirty();
		
		if(!ts.tile.unbreakable) {
			setBackTile(layer.layergen.getTileDestroyed(layer, pos), pos);
			spawnEntity(new ParticleBreak(new Vec3d(pos.x + 0.5, 0, pos.y + 0.5), new Vec3d(0, 0, 0), ts.tile.getModel(ts.meta)));
		}
	}
	
	public void breakFrontTile(Vec2i pos)
	{
		TileState ts = getFrontTile(pos);
		setDirty();
		
		if(!ts.tile.unbreakable) {
			setFrontTile(Tiles.VOID.getDefaultState(), pos);
			spawnEntity(new ParticleBreak(new Vec3d(pos.x + 0.5, 0, pos.y + 0.5),
					new Vec3d(0, 0, 0), ts.tile.getModel(ts.meta)));
		}
	}
	
	public double getLightLevel(int id) {
		return tiles_lighting[id] / (double)Byte.MAX_VALUE;
	}
	
	public double getLightLevel(Vec2i pos)
	{
		// Get the id
		Vec2i cpos = new Vec2i(0, 0);
		cpos.x = MathHelpers.mod(pos.x, CHUNK_SIZE.mx);
		cpos.y = MathHelpers.mod(pos.y, CHUNK_SIZE.my);
		int id = cpos.getId(CHUNK_SIZE);
		
		return getLightLevel(id);
	}
	
	public void setLightLevel(double light, int id) {
		this.tiles_lighting[id] = (byte)(light * Byte.MAX_VALUE);
	}
	
	public void setLightLevel(double light, Vec2i pos)
	{
		// Get the id
		Vec2i cpos = new Vec2i(0, 0);
		cpos.x = MathHelpers.mod(pos.x, CHUNK_SIZE.mx);
		cpos.y = MathHelpers.mod(pos.y, CHUNK_SIZE.my);
		int id = cpos.getId(CHUNK_SIZE);
		
		setLightLevel(light, id);
	}
	
	public void killEntity(Entity e)
	{
		if(e instanceof EntityKillWithParticles) {
			((EntityKillWithParticles)e).killWithParticles();
		}
		
		entities.remove(e);
	}
	
	public ArrayList<Entity> getNearbyEntities(Vec2d pos, double distance)
	{
		// Get the list of entities to send back
		ArrayList<Entity> nearby_entities = new ArrayList<Entity>();
		
		// Loop over the entities
		for(Entity e : entities)
		{
			Vec2d epos = e.getPos().xz();
			
			if(
				epos.x + distance > pos.x &&
				epos.x - distance < pos.x &&
				epos.y + distance > pos.y &&
				epos.y - distance < pos.y
			) {
				if(pos.squareDistance(epos) < distance) {
					nearby_entities.add(e);
				}
			}
		}
		
		// Send back the entities
		return nearby_entities;
	}
	
	public void tickRandomly()
	{
		// Get a random tile
		int id = RandomHelpers.randrange(rand, CHUNK_INDEX);
		Vec2i pos = Vec2i.fromId(CHUNK_SIZE, id);
		pos.x += c_pos.x * 16;
		pos.y += c_pos.y * 16;
		
		// Randomly select the front and back tiles
		TileState ts;
		if(rand.nextBoolean()) {
			ts = getBackTile(id);
		}
		else {
			ts = getFrontTile(id);
		}
		
		// Tick the tile
		ts.tile.tickRandomly(layer, this, ts, pos);
	}
	
	public void free()
	{
		if(this.model != null) {
			this.model.free();
			this.model = null;
		}
	}
	
}
