package projectzombie.world.layer;

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.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.tileentity.TileEntity;
import projectzombie.init.LayerGenerators;
import projectzombie.util.math.map.Map2D;
import projectzombie.util.math.map.Map2DElement;
import projectzombie.util.math.random.NoiseGenerator;
import projectzombie.worker.WorkerTasks;
import projectzombie.world.chunk.Chunk;
import projectzombie.world.layer.layergen.LayerGen;

public class Layer implements ClassBdf
{
	public Map2D<Chunk> chunks;
	public NoiseGenerator[] noise_gens;
	public ArrayList<Vec2i> chunks_saved;
	public LayerGen layergen;
	private Random rand;
	public long lseed;
	public long seed;
	public int id;
	
	public Layer(Random rand, LayerGen layergen)
	{
		// Create the array of tiles
		this.layergen = layergen;
		this.rand = new Random();
		this.seed = rand.nextLong();
		this.lseed = this.rand.nextLong();
		this.chunks = new Map2D<Chunk>(layergen);
		this.chunks_saved = new ArrayList<Vec2i>();
		
		this.layergen.init(this);
	}
	
	public Layer(BdfObject bdf) {
		BdfClassLoad(bdf);
	}
	
	public int getParticlePoolSize()
	{
		int size = 0;
		int r = Chunk.RENDER_DISTANCE;
		
		Vec2d ppos = Main.player.getPos().xz();
		
		for(int x=-r;x<=r;x++) {
			for(int y=-r;y<=r;y++)
			{
				// Get the chunk x and y
				int cx = MathHelpers.floor(ppos.x / 16) + x;
				int cy = MathHelpers.floor(ppos.y / 16) + y;
				
				// Get the chunk particle size
				size += chunks.get(new Vec2i(cx, cy)).getParticlePoolSize();
			}
		}
		
		return size;
	}
	
	public int render(Camera camera, FloatBuffer particle_pool, int upto)
	{
		Vec2d ppos = Main.player.getPos().xz();
		
		// Render every chunk in the players render distance
		int r = Chunk.RENDER_DISTANCE;
		for(int x=-r;x<=r;x++) {
			for(int y=-r;y<=r;y++)
			{
				// Get the chunk x and y
				int cx = MathHelpers.floor(ppos.x / 16) + x;
				int cy = MathHelpers.floor(ppos.y / 16) + y;
				
				// Render the chunk
				upto = chunks.get(new Vec2i(cx, cy)).render(camera, particle_pool, upto);
			}
		}
		
		return upto;
	}
	
	public void tickEntities()
	{
		// Tick every entity and every tile in every loaded chunk
		for(Map2DElement<Chunk> e : chunks) {
			layergen.tickLayer(this, e.o);
			e.o.tickEntities();
			e.o.tickRandomly();
		}
		
		// Move every entitiy in every loaded chunk
		for(Map2DElement<Chunk> e : chunks) {
			e.o.checkEntities();
		}
	}
	
	public void spawnRandomEntities() {
		this.layergen.spawnEntities(this, rand);
	}

	public Chunk getChunk(Vec2i pos) {
		return chunks.get(getChunkPosFromPos(pos));
	}

	public Chunk getChunk(Vec2d pos) {
		return chunks.get(getChunkPosFromPos(pos));
	}
	
	public void createTileEntity(Vec2i pos, TileEntity te)
	{
		// Get the chunk pos
		Vec2i c_pos = getChunkPosFromPos(pos);
		
		// Add the tile entity to that chunk
		chunks.get(c_pos).createTileEntity(pos, te);
	}
	
	public TileEntity getTileEntity(Vec2i pos)
	{
		// Get the chunk pos
		Vec2i c_pos = getChunkPosFromPos(pos);
		
		// Get the tile entity from that chunk
		return chunks.get(c_pos).getTileEntity(pos);
	}
	
	public void destroyTileEntity(Vec2i pos)
	{
		// Get the chunk pos
		Vec2i c_pos = getChunkPosFromPos(pos);
		
		// Destroy the tile entity from that chunk
		chunks.get(c_pos).destroyTileEntity(pos);
	}
	
	public double getTemperature(Vec2i pos)
	{
		// Get the chunk pos
		Vec2i c_pos = getChunkPosFromPos(pos);
		
		// Get the temperature from the chunk
		return chunks.get(c_pos).getTemperature(pos);
	}
	
	public double getHumidity(Vec2i pos)
	{
		// Get the chunk pos
		Vec2i c_pos = getChunkPosFromPos(pos);
		
		// Get the humidity from the chunk
		return chunks.get(c_pos).getHumidity(pos);
	}
	
	public void setTemperature(Vec2i pos, double v)
	{
		// Get the chunk pos
		Vec2i c_pos = getChunkPosFromPos(pos);
		
		// Get the temperature from the chunk
		chunks.get(c_pos).setTemperature(pos, v);
	}
	
	public void setHumidity(Vec2i pos, double v)
	{
		// Get the chunk pos
		Vec2i c_pos = getChunkPosFromPos(pos);
		
		// Get the humidity from the chunk
		chunks.get(c_pos).setHumidity(pos, v);
	}
	
	public void setBackTile(TileState tile, Vec2i pos)
	{
		// Get the chunk pos
		Vec2i c_pos = getChunkPosFromPos(pos);
		
		// Set the chunks back tile
		chunks.get(c_pos).setBackTile(tile, pos);
	}
	
	public void setFrontTile(TileState tile, Vec2i pos)
	{
		// Get the chunk pos
		Vec2i c_pos = getChunkPosFromPos(pos);
		
		// Set the chunks front tile
		chunks.get(c_pos).setFrontTile(tile, pos);
	}
	
	public TileState getBackTile(Vec2i pos)
	{
		// Get the chunk pos
		Vec2i c_pos = getChunkPosFromPos(pos);
		
		// Get the chunks back tile
		return chunks.get(c_pos).getBackTile(pos);
	}
	
	public TileState getFrontTile(Vec2i pos)
	{
		// Get the chunk pos
		Vec2i c_pos = getChunkPosFromPos(pos);
		
		// Get the chunks front tile
		return chunks.get(c_pos).getFrontTile(pos);
	}
	
	public void breakBackTile(Vec2i pos) {
		getChunk(pos).breakBackTile(pos);
	}
	
	public void breakFrontTile(Vec2i pos) {
		getChunk(pos).breakFrontTile(pos);
	}
	
	public static Vec2i getChunkPosFromPos(Vec2i pos) {
		return getChunkPosFromPos(new Vec2d(pos.x, pos.y));
	}
	
	public static Vec2i getChunkPosFromPos(Vec2d pos) {
		return new Vec2i(
				MathHelpers.floor(pos.x / (double)Chunk.CHUNK_SIZE.mx),
				MathHelpers.floor(pos.y / (double)Chunk.CHUNK_SIZE.my));
	}
	
	public double getLightLevel(Vec2i pos)
	{
		// Get the chunk pos
		Vec2i c_pos = getChunkPosFromPos(pos);
		
		// Get the chunks front tile
		return chunks.get(c_pos).getLightLevel(pos);
	}
	
	public void setLightLevel(double light, Vec2i pos)
	{
		// Get the chunk pos
		Vec2i c_pos = getChunkPosFromPos(pos);
		
		// Get the chunks front tile
		chunks.get(c_pos).setLightLevel(light, pos);
	}
	
	public void spawnEntity(Entity entity)
	{
		// Get the chunk pos
		Vec2i c_pos = getChunkPosFromPos(entity.getPos().xz().toInt());
		
		// Spawn the entity in the specified chunk
		chunks.get(c_pos).spawnEntity(entity);
	}
	
	public void loadChunks()
	{
		WorkerTasks.getChunks().forEach(chunk ->
		{
			if(chunk.layer != this) {
				return;
			}
			
			Vec2i pos = chunk.c_pos;
			chunks.set(pos, chunk);
			chunk.clearDirty();
		});
	}
	
	private boolean hasSavedChunk(Vec2i check)
	{
		for(Vec2i pos : chunks_saved) {
			if(pos.equal(check)) {
				return true;
			}
		}
		
		return false;
	}
	
	public void loadChunk(Vec2i pos)
	{
		// Has the chunk been saved for later
		if(hasSavedChunk(pos)) {
			WorkerTasks.loadChunk(Main.world.getSavePath(), this, pos);
		}
		
		else
		{
			// Create a unique seed specific to this chunk
			long cseed = new Random(pos.x).nextLong() + new Random(pos.y).nextLong() + seed;
			
			// Create and generate the chunk
			Chunk chunk = new Chunk(this, pos, rand);
			WorkerTasks.generateChunk(layergen, this, chunk, new Random(cseed), pos);
		}
	}
	
	public void unloadChunk(Vec2i pos)
	{
		// Store the chunk if its a dirty chunk
		Chunk chunk = chunks.get(pos);
		if(chunk.isDirty())
		{
			WorkerTasks.saveChunk(Main.world.getSavePath(), pos, chunk, id);
			
			if(!hasSavedChunk(pos)) {
				chunks_saved.add(pos);
			}
		}
		
		// Remove the chunk
		chunks.remove(pos);
	}
	
	public boolean chunkLoaded(Vec2i pos) {
		return chunks.contains(pos);
	}
	
	public ArrayList<Entity> getNearbyEntities(Vec2d pos, double distance)
	{
		// Create the list of nearby entities
		ArrayList<Entity> entities = new ArrayList<Entity>();
		Vec2i cpos = getChunkPosFromPos(pos);
		
		int scan_distance = (int)distance / Chunk.CHUNK_SIZE.mx + 1;
		
		for(int x=-scan_distance;x<=scan_distance;x++) {
			for(int y=-scan_distance;y<=scan_distance;y++)
			{
				for(Entity e : chunks.get(new Vec2i(x+cpos.x, y+cpos.y)).getNearbyEntities(pos, distance)) {
					entities.add(e);
				}
			}
		}
		
		// Check if the player is close enough
		if(pos.squareDistance(Main.player.getPos().xz()) < distance) {
			entities.add(Main.player);
		}
		
		// Send back the list of nearby entities
		return entities;
	}

	@Override
	public void BdfClassLoad(BdfObject bdf)
	{
		BdfNamedList nl = bdf.getNamedList();
		
		int[] bdf_chunks = nl.get("chunks").getIntegerArray();
		
		this.layergen = LayerGenerators.loadFromID((int)nl.get("lgen").getAutoInt());
		this.chunks_saved = new ArrayList<Vec2i>(bdf_chunks.length / 2);
		this.chunks = new Map2D<Chunk>(layergen);
		this.seed = nl.get("seed").getLong();
		this.lseed = nl.get("lseed").getLong();
		this.id = (int)nl.get("id").getAutoInt();
		this.rand = new Random();
		
		for(int i=0;i<bdf_chunks.length;i+=2) {
			this.chunks_saved.add(new Vec2i(bdf_chunks[i], bdf_chunks[i + 1]));
		}
		
		layergen.init(this);
	}

	@Override
	public void BdfClassSave(BdfObject bdf)
	{
		BdfNamedList nl = bdf.getNamedList();
		
		int[] bdf_chunks = new int[chunks_saved.size() * 2];
		
		for(int i=0;i<chunks_saved.size();i++) {
			Vec2i pos = chunks_saved.get(i);
			bdf_chunks[i*2] = pos.x;
			bdf_chunks[i*2+1] = pos.y;
		}
		
		nl.set("lseed", bdf.newObject().setLong(lseed));
		nl.set("seed", bdf.newObject().setLong(seed));
		nl.set("id", bdf.newObject().setAutoInt(id));
		nl.set("lgen", bdf.newObject().setAutoInt(LayerGenerators.getLGID(this.layergen)));
		nl.set("chunks", bdf.newObject().setIntegerArray(bdf_chunks));
	}
	
	public void save()
	{
		for(Map2DElement<Chunk> chunk : chunks)
		{
			if(chunk.o.isDirty())
			{
				WorkerTasks.saveChunk(Main.world.getSavePath(), chunk.pos, chunk.o, id);
				
				if(!hasSavedChunk(chunk.pos)) {
					chunks_saved.add(chunk.pos);
				}
			}
		}
	}
	
	public void free()
	{
		for(Map2DElement<Chunk> chunk : chunks) {
			chunk.o.free();
		}
	}
}
