package projectzombie.world;

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

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

import projectzombie.util.ClassBdf;
import projectzombie.util.FileHelpers;
import bdf.types.BdfArray;
import bdf.types.BdfNamedList;
import bdf.types.BdfObject;
import bdf.types.BdfReader;
import gl_engine.matrix.Matrix4;
import gl_engine.vec.Vec3d;
import projectzombie.Main;
import projectzombie.display.Camera;
import projectzombie.display.DisplayLighting;
import projectzombie.display.bossbar.BossBars;
import projectzombie.entity.player.EntityPlayer;
import projectzombie.init.LayerGenerators;
import projectzombie.init.Layers;
import projectzombie.menu.MenuLoadingWorld;
import projectzombie.model.Model;
import projectzombie.settings.Environment;
import projectzombie.time.GameTimer;
import projectzombie.worker.WorkerTasks;
import projectzombie.world.chunk.ChunkEventHandler;
import projectzombie.world.layer.Layer;

public class World implements ClassBdf
{
	public static final double BIOME_SIZE = 1024;
	
	private static long demoWorldSeed = new Random().nextLong();
	private static EntityPlayer demoWorldPlayer;
	private static World demoWorld;
	
	private Layer loaded;
	private ArrayList<Layer> layers = new ArrayList<Layer>();
	
	private String path;
	private int pool_vao, pool_vbo, pool_ibo;
	private boolean pool_dirty = true;
	private int pool_particle_count = 0;
	private int pool_size = 1;
	
	public World(String path) {
		this.path = path;
		
		if(path != null)
		{
			path = "./saves/" + path + "/";
			File save_dir = new File(Environment.gdir + path);
			File file_dir = new File(Environment.gdir + path + "world.bdf.gz");
			
			if(!save_dir.exists()) {
				save_dir.mkdirs();
			}
			
			if(file_dir.exists()) {
				BdfReader reader = FileHelpers.readBDF(path + "world.bdf.gz");
				BdfClassLoad(reader.getObject());
			}
		}
	}
	
	public static void createDemoWorld()
	{
		long seed = demoWorldSeed;
		
		if(demoWorld == null)
		{
			// Create all the layers
			Layers.EARTH = new Layer(new Random(seed), LayerGenerators.EARTH);
			Layers.CAVES = new Layer(new Random(seed), LayerGenerators.CAVES);
			Layers.LAVA_CAVES = new Layer(new Random(seed), LayerGenerators.LAVA_CAVES);
			
			// Create the world and set the earth as the default layer
			Main.world = new World(null);
			Main.world.addLayer(Layers.EARTH);
			Main.world.addLayer(Layers.CAVES);
			Main.world.addLayer(Layers.LAVA_CAVES);
			Main.world.setLayer(0);
			
			Main.player = new EntityPlayer();
			
			demoWorldPlayer = Main.player;
			demoWorld = Main.world;
		}
		
		else {
			Main.world = demoWorld;
			Main.player = demoWorldPlayer;
		}
		
		// Initialize some other objects
		GameTimer.resetTime();
		BossBars.clear();
	}
	
	public static void createWorld(String path, long seed)
	{
		// Create all the layers
		Layers.EARTH = new Layer(new Random(seed), LayerGenerators.EARTH);
		Layers.CAVES = new Layer(new Random(seed), LayerGenerators.CAVES);
		Layers.LAVA_CAVES = new Layer(new Random(seed), LayerGenerators.LAVA_CAVES);
		
		// Create the world and set the earth as the default layer
		Main.world = new World(path);
		Main.world.addLayer(Layers.EARTH);
		Main.world.addLayer(Layers.CAVES);
		Main.world.addLayer(Layers.LAVA_CAVES);
		Main.world.setLayer(0);
		
		// Initialize some other objects
		Main.player = new EntityPlayer();
		GameTimer.resetTime();
		BossBars.clear();
		
		Main.world.save();
	}
	
	public String getSavePath() {
		return path;
	}
	
	public boolean isPoolDirty() {
		return pool_dirty;
	}
	
	public void markPoolDirty() {
		this.pool_dirty = true;
	}
	
	public void render(Camera camera)
	{
		// Render the world and the player
		if(!Main.player.dead) {
			Main.player.render();
		}
		
		// Re-generate the particle pool if the pool has changed
		if(pool_dirty)
		{
			pool_particle_count = loaded.getParticlePoolSize();
			boolean changed = false;
			
			if(pool_particle_count >= pool_size)
			{
				changed = true;
				
				while(pool_particle_count >= pool_size) {
					pool_size *= 2;
				}
			}
			
			if(changed)
			{
				FloatBuffer pool = BufferUtils.createFloatBuffer(pool_size * Model.SIZE * 16);
				
				loaded.render(camera, pool, 0);
				
				if(pool_vao != 0) {
					GL33.glDeleteVertexArrays(pool_vao);
					GL33.glDeleteBuffers(pool_vbo);
					GL33.glDeleteBuffers(pool_ibo);
				}
				
				pool_vao = GL33.glGenVertexArrays();
				pool_vbo = GL33.glGenBuffers();
				pool_ibo = GL33.glGenBuffers();
				
				GL33.glBindVertexArray(pool_vao);
				
				int[] indicies = new int[pool_size * 6];
				
				for(int i=0;i<pool_size;i++) {
					indicies[i*6+0] = i*4+0;
					indicies[i*6+1] = i*4+1;
					indicies[i*6+2] = i*4+2;
					indicies[i*6+3] = i*4+2;
					indicies[i*6+4] = i*4+3;
					indicies[i*6+5] = i*4+0;
				}
				
				GL33.glBindBuffer(GL33.GL_ELEMENT_ARRAY_BUFFER, pool_ibo);
				GL33.glBufferData(GL33.GL_ELEMENT_ARRAY_BUFFER, indicies, GL33.GL_STATIC_DRAW);
				
				GL33.glBindBuffer(GL33.GL_ARRAY_BUFFER, pool_vbo);
				GL33.glBufferData(GL33.GL_ARRAY_BUFFER, pool, GL33.GL_DYNAMIC_DRAW);
				
				Model.setGLArrayAttributes();
				pool.clear();
			}
			
			else
			{
				FloatBuffer pool = BufferUtils.createFloatBuffer(pool_particle_count * Model.SIZE * 16);
				
				loaded.render(camera, pool, 0);
				
				GL33.glBindVertexArray(pool_vao);
				
				GL33.glBindBuffer(GL33.GL_ARRAY_BUFFER, pool_vbo);
				GL33.glBufferSubData(GL33.GL_ARRAY_BUFFER, 0, pool);
				pool.clear();
			}
			
			pool_dirty = false;
		}
		
		else
		{
			loaded.render(camera, null, 0);
			
			GL33.glBindVertexArray(pool_vao);
		}
		
		GL33.glUniformMatrix4fv(Main.window.glsl_model, true, Matrix4.identity().getArray());
		GL33.glDrawElements(GL33.GL_TRIANGLES, pool_particle_count*6, GL33.GL_UNSIGNED_INT, 0);
	}
	
	public void tickEntities() {
		loaded.tickEntities();
	}
	
	public void spawnRandomEntities() {
		loaded.spawnRandomEntities();
	}
	
	public void setLayer(int id)
	{
		Main.menu = new MenuLoadingWorld();
		ChunkEventHandler.loaded = false;
		DisplayLighting.clearLighting();
		DisplayLighting.setDirty();
		
		if(this.loaded != null) {
			this.loaded.free();
		}
		
		this.loaded = layers.get(id);
	}
	
	public void removeLayer(int id) {
		layers.remove(id).free();
	}
	
	public int addLayer(Layer layer) {
		int id = layers.size();
		layers.add(layer);
		layer.id = id;
		return id;
	}
	
	public Layer getLayer() {
		return loaded;
	}
	
	public void save()
	{
		if(path != null)
		{
			for(Layer layer : layers) {
				layer.save();
			}
			
			BdfReader reader = new BdfReader();
			BdfObject bdf = reader.getObject();
			BdfClassSave(bdf);
			
			WorkerTasks.saveToFile("./saves/" + path + "/world.bdf.gz", reader);
		}
	}
	
	public void free()
	{
		for(Layer layer : layers) {
			layer.free();
		}
	}

	@Override
	public void BdfClassLoad(BdfObject bdf)
	{
		BdfNamedList nl = bdf.getNamedList();
		
		// Get the layers and the loaded layer
		BdfArray layers_bdf = nl.get("layers").getArray();
		
		for(BdfObject o : layers_bdf) {
			layers.add(new Layer(o));
		}
		
		int loaded_id = nl.get("loaded").getInteger();
		setLayer(loaded_id);
		
		// Load the player data
		BdfArray players = nl.get("players").getArray();
		Main.player = new EntityPlayer(players.get(0));
		
		// Set the games time
		GameTimer.setTime(nl.get("time").getLong());
	}

	@Override
	public void BdfClassSave(BdfObject bdf)
	{
		BdfNamedList nl = bdf.getNamedList();
		
		// Get the current loaded layer
		int loaded_id = -1;
		for(int i=0;i<layers.size();i++) {
			if(layers.get(i) == loaded) {
				loaded_id = i;
			}
		}
		
		// Save layer data
		BdfArray layers_bdf = bdf.newArray();
		nl.set("loaded", bdf.newObject().setInteger(loaded_id));
		nl.set("layers", bdf.newObject().setArray(layers_bdf));
		
		for(Layer l : layers) {
			BdfObject o = bdf.newObject();
			l.BdfClassSave(o);
			layers_bdf.add(o);
		}
		
		// Save player data
		BdfArray players = bdf.newArray();
		nl.set("players", bdf.newObject().setArray(players));
		
		BdfObject player = bdf.newObject();
		Main.player.BdfClassSave(player);
		players.add(player);
		
		// Save the game timer
		nl.set("time", bdf.newObject().setLong(GameTimer.getTime()));
	}
}
