/*******************************************************************************
 * Copyright 2015 Maximilian Stark | Dakror <mail@dakror.de>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/


package de.dakror.vloxlands.game;

import java.util.Random;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Buttons;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.g3d.Environment;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.attributes.BlendingAttribute;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalShadowLight;
import com.badlogic.gdx.graphics.g3d.utils.CameraInputController;
import com.badlogic.gdx.graphics.g3d.utils.DepthShaderProvider;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.math.Intersector;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.math.collision.BoundingBox;
import com.badlogic.gdx.math.collision.Ray;
import com.badlogic.gdx.utils.Array;

import de.dakror.vloxlands.Config;
import de.dakror.vloxlands.Vloxlands;
import de.dakror.vloxlands.ai.path.BFS;
import de.dakror.vloxlands.ai.path.node.BFSNode;
import de.dakror.vloxlands.game.entity.Entity;
import de.dakror.vloxlands.game.entity.creature.Creature;
import de.dakror.vloxlands.game.entity.creature.Human;
import de.dakror.vloxlands.game.entity.statics.StaticEntity;
import de.dakror.vloxlands.game.entity.structure.Structure;
import de.dakror.vloxlands.game.entity.structure.Towncenter;
import de.dakror.vloxlands.game.item.Item;
import de.dakror.vloxlands.game.item.ItemStack;
import de.dakror.vloxlands.game.query.VoxelPos;
import de.dakror.vloxlands.game.voxel.Voxel;
import de.dakror.vloxlands.game.world.Chunk;
import de.dakror.vloxlands.game.world.Island;
import de.dakror.vloxlands.game.world.World;
import de.dakror.vloxlands.layer.Layer;
import de.dakror.vloxlands.render.DDirectionalShadowLight;
import de.dakror.vloxlands.render.MeshingThread;
import de.dakror.vloxlands.util.Direction;
import de.dakror.vloxlands.util.event.SelectionListener;
import de.dakror.vloxlands.util.event.VoxelSelection;
import de.dakror.vloxlands.util.math.CustomizableFrustum;

/**
 * @author Dakror
 */
@SuppressWarnings("deprecation")
public class Game extends Layer {
	public static long seed = (long) (Math.random() * Long.MAX_VALUE);
	public static final float velocity = 10;
	public static final float rotateSpeed = 0.2f;
	public static float pickRayMaxDistance = 150f;
	
	public static final int dayInTicks = 72020; // 1 ingame day = 72020 ticks = 1200s = 20min
	
	public static Game instance;
	
	public static World world;
	public static Camera camera;
	public static float time = 0.99999999999f;
	
	public Environment env;
	
	public Array<SelectionListener> listeners = new Array<SelectionListener>();
	
	public Environment minimapEnv;
	public Camera minimapCamera;
	public ModelBatch minimapBatch;
	
	public StaticEntity cursorEntity;
	boolean cursorEntityPlacable;
	boolean cursorEntityContinousPlacing;
	Array<Material> defaultCursorEntityMaterials;
	
	public String activeAction = "";
	public Island activeIsland;
	public DirectionalShadowLight shadowLight;
	DirectionalLight directionalLight;
	public CameraInputController controller;
	
	ModelBatch modelBatch;
	ModelBatch shadowBatch;
	
	boolean middleDown;
	boolean doneLoading;
	
	ModelInstance sky;
	
	int tick;
	int ticksForTravel;
	int startTick;
	
	public boolean regionSelectionMode = false;
	boolean regionSelectionLMB;
	
	public Vector3 hoveredVoxel = new Vector3();
	public Vector3 selectedVoxel = new Vector3();
	public Vector3 selectionStartVoxel = new Vector3(-1, 0, 0);
	Vector3 controllerTarget = new Vector3();
	Vector3 cameraPos = new Vector3();
	Vector3 target = new Vector3();
	Vector3 targetDirection = new Vector3();
	Vector3 targetUp = new Vector3();
	
	Vector2 mouseDown = new Vector2();
	
	// -- temp -- //
	public final Vector3 tmp = new Vector3();
	public final Vector3 tmp1 = new Vector3();
	public final Vector3 tmp2 = new Vector3();
	public final Vector3 tmp3 = new Vector3();
	public final Vector3 tmp4 = new Vector3();
	public final Vector3 tmp5 = new Vector3();
	public final Vector3 tmp6 = new Vector3();
	public final Vector3 tmp7 = new Vector3();
	public final Vector3 tmp8 = new Vector3();
	public final Matrix4 m4 = new Matrix4();
	public final BoundingBox bb = new BoundingBox();
	public final BoundingBox bb2 = new BoundingBox();
	public final BoundingBox bb3 = new BoundingBox();
	
	@Override
	public void show() {
		modal = true;
		instance = this;
		
		Gdx.app.log("GameLayer.show", "Seed: " + seed + "");
		MathUtils.random = new Random(seed);
		
		modelBatch = new ModelBatch(Gdx.files.internal("shader/shader.vs"), Gdx.files.internal("shader/shader.fs"));
		minimapBatch = new ModelBatch(Gdx.files.internal("shader/shader.vs"), Gdx.files.internal("shader/shader.fs"));
		
		camera = new PerspectiveCamera(Config.fov, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		camera.near = 0.1f;
		camera.far = pickRayMaxDistance;
		controller = new CameraInputController(camera) {
			private final Vector3 tmpV1 = new Vector3();
			private final Vector3 tmpV2 = new Vector3();
			
			@Override
			protected boolean process(float deltaX, float deltaY, int button) {
				if (button == rotateButton && Gdx.input.isKeyPressed(Keys.CONTROL_LEFT)) return false;
				
				if (button == rotateButton) {
					tmpV1.set(camera.direction).crs(camera.up).y = 0f;
					camera.rotateAround(target, tmpV1.nor(), deltaY * rotateAngle);
					
					float dot = camera.direction.dot(Vector3.Y);
					if (dot < -0.95f) camera.rotateAround(target, tmpV1.nor(), -deltaY * rotateAngle);
					camera.rotateAround(target, Vector3.Y, deltaX * -rotateAngle);
				} else if (button == translateButton) {
					camera.translate(tmpV1.set(camera.direction).crs(camera.up).nor().scl(-deltaX * translateUnits));
					camera.translate(tmpV2.set(camera.up).scl(-deltaY * translateUnits));
					if (translateTarget) target.add(tmpV1).add(tmpV2);
				} else if (button == forwardButton) {
					camera.translate(tmpV1.set(camera.direction).scl(deltaY * translateUnits));
					if (forwardTarget) target.add(tmpV1);
				}
				if (autoUpdate) camera.update();
				return true;
			}
			
			@Override
			public boolean zoom(float amount) {
				if (!alwaysScroll && activateKey != 0 && !activatePressed) return false;
				
				tmpV1.set(camera.direction).scl(amount);
				tmpV2.set(camera.position).add(tmpV1);
				
				if (tmpV2.dst(target) > 5) {
					camera.translate(tmpV1);
					if (scrollTarget) target.add(tmpV1);
					if (autoUpdate) camera.update();
					return true;
				}
				
				return false;
			}
		};
		controller.translateUnits = 20;
		controller.rotateLeftKey = -1;
		controller.rotateRightKey = -1;
		controller.forwardKey = -1;
		controller.backwardKey = -1;
		controller.translateButton = -1;
		controller.rotateButton = Buttons.MIDDLE;
		Vloxlands.instance.getMultiplexer().addProcessor(controller);
		minimapCamera = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		minimapCamera.near = 0.1f;
		minimapCamera.far = pickRayMaxDistance;
		minimapEnv = new Environment();
		minimapEnv.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1.f));
		minimapEnv.add(new DirectionalLight().set(1f, 1f, 1f, -0.5f, -0.5f, -0.5f));
		minimapEnv.add(new DirectionalLight().set(0.5f, 0.5f, 0.5f, -0.5f, -0.5f, -0.5f));
		
		shadowBatch = new ModelBatch(new DepthShaderProvider());
		
		Vloxlands.shapeRenderer = new ShapeRenderer();
		
		new MeshingThread();
		
		env = new Environment();
		env.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1.f), new ColorAttribute(ColorAttribute.Fog, 0.5f, 0.8f, 0.85f, 1.f));
		env.add(directionalLight = new DirectionalLight().set(0.8f, 0.8f, 0.8f, -0.5f, -0.5f, -0.5f));
		
		env.add((shadowLight = new DDirectionalShadowLight(Config.shadowQuality, 128, 128, camera.near, camera.far)).set(0.6f, 0.6f, 0.6f, 0, -0.5f, time));
		env.shadowMap = shadowLight;
		
		// int w = MathUtils.random(1, 5);
		// int d = MathUtils.random(1, 5);
		
		world = new World(1, 1);
		// world = new World(w, d);
		// Gdx.app.log("GameLayer.show", "World size: " + w + "x" + d);
	}
	
	public void doneLoading() {
		for (Item item : Item.getAll())
			item.onLoaded();
		
		focusIsland(world.getIslands()[0], true);
		
		Human human = new Human(Island.SIZE / 2 - 5, Island.SIZE / 4 * 3, Island.SIZE / 2);
		activeIsland.addEntity(human, false, false);
		human = new Human(Island.SIZE / 2 - 4, Island.SIZE / 4 * 3, Island.SIZE / 2);
		activeIsland.addEntity(human, false, false);
		
		Towncenter tc = new Towncenter(Island.SIZE / 2 - 2, Island.SIZE / 4 * 3, Island.SIZE / 2 - 2);
		activeIsland.addEntity(tc, false, true);
		tc.setBuilt(true);
		tc.getInnerInventory().add(new ItemStack(Item.get("AXE"), 5));
		tc.getInnerInventory().add(new ItemStack(Item.get("PICKAXE"), 5));
		tc.getInnerInventory().add(new ItemStack(Item.get("SHOVEL"), 5));
		tc.getInnerInventory().add(new ItemStack(Item.get("HAMMER"), 5));
		tc.getInnerInventory().add(new ItemStack(Item.get("HOE"), 5));
		
		tc.getInnerInventory().add(new ItemStack(Item.get("WOODEN_LOG"), 40));
		tc.getInnerInventory().add(new ItemStack(Item.get("IRON_INGOT"), 5));
		
		doneLoading = true;
	}
	
	public void focusIsland(Island island, boolean initial) {
		Vector3 islandCenter = new Vector3(island.pos.x + Island.SIZE / 2, island.pos.y + Island.SIZE / 4 * 3, island.pos.z + Island.SIZE / 2);
		activeIsland = island;
		selectedVoxel.set(-1, 0, 0);
		if (!initial) {
			target.set(islandCenter).add(-Island.SIZE / 3, Island.SIZE / 3, -Island.SIZE / 3);
			if (target.equals(camera.position)) {
				camera.position.set(islandCenter).add(-Island.SIZE / 3, Island.SIZE / 3, -Island.SIZE / 3);
				controller.target.set(islandCenter);
				camera.lookAt(islandCenter);
				
				controller.update();
				camera.update();
				return;
			}
			
			ticksForTravel = (int) camera.position.dst(target) * Config.getGameSpeed();
			
			Vector3 pos = camera.position.cpy();
			Vector3 dir = camera.direction.cpy();
			Vector3 up = camera.up.cpy();
			
			camera.position.set(islandCenter).add(-Island.SIZE / 3, Island.SIZE / 3, -Island.SIZE / 3);
			controller.target.set(islandCenter);
			camera.lookAt(islandCenter);
			
			targetDirection.set(camera.direction);
			targetUp.set(camera.up);
			
			camera.position.set(pos);
			camera.direction.set(dir);
			camera.up.set(up);
			
			startTick = tick;
		} else {
			camera.position.set(islandCenter).add(-Island.SIZE / 3, Island.SIZE / 3, -Island.SIZE / 3);
			controller.target.set(islandCenter);
			camera.lookAt(islandCenter);
			
			controller.update();
			camera.update();
		}
	}
	
	@Override
	public void render(float delta) {
		if (!doneLoading) return;
		controller.update();
		((PerspectiveCamera) camera).fieldOfView = Config.fov;
		
		Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
		
		shadowLight.begin(controller.target, camera.direction);
		shadowBatch.begin(shadowLight.getCamera());
		world.render(shadowBatch, null);
		shadowBatch.end();
		shadowLight.end();
		
		Gdx.gl.glClearColor(0.5f, 0.8f, 0.85f, 1);
		
		modelBatch.begin(camera);
		world.render(modelBatch, env);
		if (!Config.paused) world.update(delta);
		// modelBatch.render(sky, env);
		if (cursorEntity != null) {
			cursorEntity.update(delta);
			cursorEntity.render(modelBatch, env, false);
		}
		modelBatch.end();
		
		if (selectionStartVoxel.x > -1 && selectedVoxel.x > -1) {
			Gdx.gl.glEnable(GL20.GL_DEPTH_TEST);
			Gdx.gl.glEnable(GL20.GL_BLEND);
			
			float minX = Math.min(selectionStartVoxel.x, selectedVoxel.x);
			float maxX = Math.max(selectionStartVoxel.x, selectedVoxel.x);
			
			float minY = Math.min(selectionStartVoxel.y, selectedVoxel.y);
			float maxY = Math.max(selectionStartVoxel.y, selectedVoxel.y);
			
			float minZ = Math.min(selectionStartVoxel.z, selectedVoxel.z);
			float maxZ = Math.max(selectionStartVoxel.z, selectedVoxel.z);
			
			Vloxlands.shapeRenderer.begin(ShapeType.Filled);
			Vloxlands.shapeRenderer.setProjectionMatrix(camera.combined);
			Vloxlands.shapeRenderer.identity();
			Vloxlands.shapeRenderer.translate(activeIsland.pos.x + minX, activeIsland.pos.y + minY, activeIsland.pos.z + maxZ + 1.01f);
			Vloxlands.shapeRenderer.setColor(0, 1, 0, 0.3f);
			Vloxlands.shapeRenderer.box(-0.005f, -0.005f, -0.005f, (maxX - minX) + 1.01f, (maxY - minY) + 1.01f, (maxZ - minZ) + 1.01f);
			Vloxlands.shapeRenderer.end();
		}
		
		if (Vloxlands.showPathDebug) {
			Gdx.gl.glEnable(GL20.GL_DEPTH_TEST);
			Gdx.gl.glEnable(GL20.GL_BLEND);
			Vloxlands.shapeRenderer.begin(ShapeType.Filled);
			Vloxlands.shapeRenderer.setProjectionMatrix(camera.combined);
			for (BFSNode node : BFS.visited) {
				Vloxlands.shapeRenderer.identity();
				Vloxlands.shapeRenderer.translate(activeIsland.pos.x + node.x, activeIsland.pos.y + node.y, activeIsland.pos.z + node.z + 1.01f);
				Vloxlands.shapeRenderer.setColor(1, 1, 1, 0.3f);
				Vloxlands.shapeRenderer.box(-0.005f, -0.005f, -0.005f, 1.01f, 1.01f, 1.01f);
			}
			Vloxlands.shapeRenderer.end();
		}
	}
	
	@Override
	public void tick(int tick) {
		this.tick = tick;
		
		if (!Config.paused) {
			time -= 0.00002777f;
			if (time <= -0.99999999999f) time = 0.99999999999f;
			
			float t = time * MathUtils.PI;
			
			float x = MathUtils.sin(t) * 0.5f;
			float z = MathUtils.cos(t);
			
			float light = MathUtils.cos(t - MathUtils.PI / 2) * 0.5f + 0.3f;
			
			shadowLight.set(light - 0.1f, light, light, x, -0.5f, z);
			directionalLight.set(light, light, light, x, -0.5f, z);
			world.tick(tick);
		}
		if (cursorEntity != null) cursorEntity.tick(tick);
		
		if (activeIsland != null && startTick > 0) {
			camera.position.interpolate(target, (tick - startTick) / (float) (ticksForTravel * Config.getGameSpeed()), Interpolation.linear);
			camera.direction.interpolate(targetDirection, (tick - startTick) / (float) (ticksForTravel * Config.getGameSpeed()), Interpolation.linear);
			camera.up.interpolate(new Vector3(0, 1, 0), (tick - startTick) / (float) (ticksForTravel * Config.getGameSpeed()), Interpolation.linear);
			
			if (tick >= startTick + ticksForTravel || camera.position.dst(target) < 0.1f) {
				Vector3 islandCenter = new Vector3(activeIsland.pos.x + Island.SIZE / 2, activeIsland.pos.y + Island.SIZE / 4 * 3, activeIsland.pos.z + Island.SIZE / 2);
				controller.target.set(islandCenter);
				camera.position.set(islandCenter).add(-Island.SIZE / 3, Island.SIZE / 3, -Island.SIZE / 3);
				camera.lookAt(islandCenter);
				startTick = 0;
			}
			
			controller.update();
			camera.update();
		}
	}
	
	@Override
	public void resize(int width, int height) {
		camera.viewportWidth = width;
		camera.viewportHeight = height;
		camera.update();
		
		minimapCamera.viewportWidth = width;
		minimapCamera.viewportHeight = height;
		minimapCamera.update();
	}
	
	public void pickRay(boolean hover, boolean lmb, int x, int y) {
		Ray ray = camera.getPickRay(x, y);
		
		if (hover) {
			Entity hovered = null;
			float distance = 0;
			
			for (Entity e : activeIsland.getEntities()) {
				e.hovered = false;
				if (!e.isVisible()) continue;
				if (!e.inFrustum) continue;
				
				e.getWorldBoundingBox(bb);
				
				if (Intersector.intersectRayBounds(ray, bb, tmp)) {
					float dst = ray.origin.dst(tmp);
					if (hovered == null || dst < distance) {
						hovered = e;
						distance = dst;
					}
				}
			}
			
			if (hovered != null) hovered.hovered = true;
		} else {
			Entity selectedEntity = null;
			Chunk selectedChunk = null;
			Vector3 selVoxel = new Vector3();
			
			float distance = 0;
			
			for (Entity e : activeIsland.getEntities()) {
				e.wasSelected = e.selected;
				if (lmb) e.selected = false;
				float dst = ray.origin.dst(e.posCache);
				if (e.isVisible() && e.inFrustum && e.hovered && (distance == 0 || dst < distance) && dst < pickRayMaxDistance) {
					distance = dst;
					selectedEntity = e;
				}
			}
			
			for (Chunk c : activeIsland.getChunks()) {
				if (c == null) continue;
				
				if (c.inFrustum && !c.isEmpty()) {
					tmp1.set(activeIsland.pos.x + c.pos.x, activeIsland.pos.y + c.pos.y, activeIsland.pos.z + c.pos.z);
					tmp2.set(tmp1.cpy().add(Chunk.SIZE, Chunk.SIZE, Chunk.SIZE));
					
					bb.set(tmp1, tmp2);
					if (Intersector.intersectRayBounds(ray, bb, null) && c.pickVoxel(ray, tmp5, tmp6)) {
						float dst = ray.origin.dst(tmp5);
						if ((distance == 0 || dst < distance) && dst <= pickRayMaxDistance) {
							distance = dst;
							selVoxel.set(tmp6);
							selectedChunk = c;
						}
					}
				}
			}
			
			if (selectedChunk != null) {
				// -- determine selectedVoxelFace -- //
				Direction dir = null;
				float distanc = 0;
				Vector3 is2 = new Vector3();
				byte air = Voxel.get("AIR").getId();
				
				for (Direction d : Direction.values()) {
					tmp7.set(activeIsland.pos.x + selectedChunk.pos.x + selVoxel.x + d.dir.x, activeIsland.pos.y + selectedChunk.pos.y + selVoxel.y + d.dir.y, activeIsland.pos.z + selectedChunk.pos.z + selVoxel.z + d.dir.z);
					tmp8.set(tmp7.cpy().add(1, 1, 1));
					bb3.set(tmp7, tmp8);
					
					if (activeIsland.get(selectedChunk.pos.x + selVoxel.x + d.dir.x, selectedChunk.pos.y + selVoxel.y + d.dir.y, selectedChunk.pos.z + selVoxel.z + d.dir.z) != air) continue;
					
					if (Intersector.intersectRayBounds(ray, bb3, is2)) {
						float dist = ray.origin.dst(is2);
						if (dir == null || dist < distanc) {
							distanc = dist;
							dir = d;
						}
					}
				}
				
				selectedVoxel.set(selVoxel).add(selectedChunk.pos);
				
				for (SelectionListener sl : listeners)
					sl.onVoxelSelection(new VoxelSelection(activeIsland, new VoxelPos(selVoxel.cpy().add(selectedChunk.pos), selectedChunk.get((int) selVoxel.x, (int) selVoxel.y, (int) selVoxel.z)), dir), lmb);
			} else if (selectedEntity != null) {
				selVoxel.set(-1, 0, 0);
				selectedEntity.selected = true;
				if (selectedEntity instanceof Structure) {
					for (SelectionListener sl : listeners)
						sl.onStructureSelection((Structure) selectedEntity, lmb);
				} else if (selectedEntity instanceof Creature) {
					for (SelectionListener sl : listeners)
						sl.onCreatureSelection((Creature) selectedEntity, lmb);
				}
			} else {
				for (SelectionListener sl : listeners)
					sl.onNoSelection(lmb);
			}
		}
	}
	
	public Chunk pickVoxelRay(Island island, Vector3 selVoxel, boolean lmb, int x, int y) {
		Chunk selectedChunk = null;
		Ray ray = camera.getPickRay(x, y);
		
		float distance = 0;
		
		for (Chunk c : island.getChunks()) {
			if (c == null) continue;
			if (c.inFrustum && !c.isEmpty()) {
				tmp1.set(island.pos.x + c.pos.x, island.pos.y + c.pos.y, island.pos.z + c.pos.z);
				tmp2.set(tmp1.cpy().add(Chunk.SIZE, Chunk.SIZE, Chunk.SIZE));
				
				bb.set(tmp1, tmp2);
				if (Intersector.intersectRayBounds(ray, bb, null) && c.pickVoxel(ray, tmp5, tmp6)) {
					float dst = ray.origin.dst(tmp5);
					if ((distance == 0 || dst < distance) && dst <= pickRayMaxDistance) {
						distance = dst;
						selVoxel.set(tmp6).add(c.pos);
						selectedChunk = c;
					}
				}
			}
		}
		
		return selectedChunk;
	}
	
	public void selectionBox(Rectangle rectangle) {
		CustomizableFrustum frustum = new CustomizableFrustum(rectangle);
		camera.update();
		frustum.update(camera.invProjectionView);
		Vector3 origin = camera.unproject(new Vector3(Gdx.graphics.getWidth() / 2, Gdx.graphics.getHeight() / 2, 0), 0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		
		boolean anyEntitySelected = false;
		boolean dispatched = false;
		
		for (Entity entity : activeIsland.getEntities()) {
			if (entity instanceof StaticEntity) continue;
			if (!entity.isVisible()) continue;
			entity.wasSelected = entity.selected;
			entity.selected = false;
			entity.getWorldBoundingBox(bb);
			
			float dst = origin.dst(entity.posCache);
			if (entity.inFrustum && frustum.boundsInFrustum(bb) && dst < pickRayMaxDistance) {
				entity.selected = true;
				anyEntitySelected = true;
				if (!dispatched && entity instanceof Creature) {
					for (SelectionListener sl : listeners)
						sl.onCreatureSelection((Creature) entity, true);
					dispatched = true;
				}
			}
		}
		
		if (!anyEntitySelected) {
			for (Island i : world.getIslands()) {
				if (i == null) continue;
				for (Entity e : i.getEntities()) {
					if (!(e instanceof StaticEntity)) continue;
					
					e.wasSelected = e.selected;
					e.selected = false;
					e.getWorldBoundingBox(bb);
					
					float dst = origin.dst(e.posCache);
					if (e.inFrustum && frustum.boundsInFrustum(bb) && dst < pickRayMaxDistance) e.selected = true;
				}
			}
		}
	}
	
	@Override
	public boolean touchDragged(int screenX, int screenY, int pointer) {
		if (middleDown && Gdx.input.isKeyPressed(Keys.CONTROL_LEFT)) {
			float f = 0.1f;
			
			controller.target.y = controllerTarget.y + (screenY - mouseDown.y) * f;
			camera.position.y = cameraPos.y + (screenY - mouseDown.y) * f;
			camera.update();
			controller.update();
		}
		return false;
	}
	
	@Override
	public boolean mouseMoved(int screenX, int screenY) {
		if (regionSelectionMode) pickVoxelRay(activeIsland, selectedVoxel, false, screenX, screenY);
		else if (cursorEntity != null) {
			pickVoxelRay(activeIsland, hoveredVoxel, false, screenX, screenY);
			cursorEntity.getModelInstance().transform.setToTranslation(activeIsland.pos);
			cursorEntity.getModelInstance().transform.translate(hoveredVoxel).translate(cursorEntity.getBoundingBox().getDimensions().x <= 1 ? cursorEntity.blockTrn.x : 0, cursorEntity.blockTrn.y, cursorEntity.getBoundingBox().getDimensions().z <= 1 ? cursorEntity.blockTrn.z : 0);
			cursorEntity.setIsland(activeIsland);
			cursorEntity.updateVoxelPos();
			cursorEntityPlacable = cursorEntity.canBePlaced();
			
			if (defaultCursorEntityMaterials == null) {
				defaultCursorEntityMaterials = new Array<Material>();
				
				for (Material m : cursorEntity.getModelInstance().materials)
					defaultCursorEntityMaterials.add(m.copy());
			}
			
			for (int i = 0; i < cursorEntity.getModelInstance().materials.size; i++) {
				Material m = cursorEntity.getModelInstance().materials.get(i);
				
				if (!cursorEntityPlacable) {
					m.set(new BlendingAttribute(0.8f), ColorAttribute.createDiffuse(Color.RED));
				} else {
					m.remove(ColorAttribute.Diffuse);
					BlendingAttribute ba = (BlendingAttribute) defaultCursorEntityMaterials.get(i).get(BlendingAttribute.Type);
					if (ba == null) m.remove(BlendingAttribute.Type);
					else m.set(ba);
				}
			}
		} else if (activeIsland != null) pickRay(true, false, screenX, screenY);
		return false;
	}
	
	@Override
	public boolean touchDown(int screenX, int screenY, int pointer, int button) {
		mouseDown.set(screenX, screenY);
		
		if (button == Buttons.MIDDLE) {
			controllerTarget.set(controller.target);
			cameraPos.set(camera.position);
			middleDown = true;
			Gdx.input.setCursorCatched(true);
		}
		return false;
	}
	
	@Override
	public boolean tap(float x, float y, int count, int button) {
		if (!doneLoading) return false;
		
		if (button != Buttons.MIDDLE) {
			if (!regionSelectionMode) {
				if (cursorEntity != null) {
					if (button == Buttons.LEFT) {
						if (cursorEntityPlacable) {
							for (int i = 0; i < defaultCursorEntityMaterials.size; i++) {
								cursorEntity.getModelInstance().materials.set(i, defaultCursorEntityMaterials.get(i));
							}
							
							if (cursorEntity instanceof Structure) ((Structure) cursorEntity).setBuilt(false);
							cursorEntity.getModelInstance().transform.translate(-activeIsland.pos.x, -activeIsland.pos.y, -activeIsland.pos.z);
							activeIsland.addEntity(cursorEntity, true, false);
							cursorEntity.updateVoxelPos();
							
							if (!cursorEntityContinousPlacing) {
								cursorEntity = null;
								defaultCursorEntityMaterials = null;
								cursorEntityPlacable = false;
							} else {
								cursorEntity = (StaticEntity) Entity.getForId(cursorEntity.getId(), cursorEntity.posCache.x, cursorEntity.posCache.y, cursorEntity.posCache.z);
								cursorEntity.setIsland(activeIsland);
								cursorEntity.getModelInstance().transform.translate(activeIsland.pos.x, activeIsland.pos.y, activeIsland.pos.z);
								cursorEntity.updateVoxelPos();
								if (cursorEntity instanceof Structure) ((Structure) cursorEntity).setBuilt(true);
								cursorEntity.setVisible(true);
							}
						}
					} else {
						cursorEntity = null;
						defaultCursorEntityMaterials = null;
						cursorEntityPlacable = false;
					}
				} else {
					selectionStartVoxel.set(-1, 0, 0);
					pickRay(false, button == Buttons.LEFT, (int) x, (int) y);
				}
			} else {
				if (selectionStartVoxel.x == -1) {
					selectedVoxel.set(-1, 0, 0);
					pickVoxelRay(activeIsland, selectionStartVoxel, regionSelectionLMB = button == Buttons.LEFT, (int) x, (int) y);
				} else if (regionSelectionLMB == (button == Buttons.LEFT)) {
					pickVoxelRay(activeIsland, selectedVoxel, button == Buttons.LEFT, (int) x, (int) y);
					
					for (SelectionListener sl : listeners)
						sl.onVoxelRangeSelection(activeIsland, selectionStartVoxel, selectedVoxel, regionSelectionLMB);
					
					regionSelectionMode = false;
				}
			}
		}
		return false;
	}
	
	@Override
	public boolean touchUp(int screenX, int screenY, int pointer, int button) {
		if (button == Buttons.MIDDLE) {
			middleDown = false;
			Gdx.input.setCursorCatched(false);
		}
		return false;
	}
	
	public void addListener(SelectionListener value) {
		listeners.insert(0, value);
	}
	
	public boolean removeListener(SelectionListener value) {
		return listeners.removeValue(value, true);
	}
	
	/*
	 * Only call when @param:action != null
	 */
	public void action(String action) {
		if (action.contains("|region")) {
			selectionStartVoxel.set(-1, 0, 0);
			selectedVoxel.set(-1, 0, 0);
			regionSelectionMode = true;
		}
		if (action.contains("entity")) {
			String[] a = action.split("\\|");
			String s = a[0].replace("entity:", "");
			Entity e = Entity.getForId((byte) Integer.parseInt(s), 0, 0, 0);
			if (e instanceof Structure) {
				((Structure) e).setBuilt(true);
				((Structure) e).tickRequestsEnabled = false;
			}
			e.setVisible(true);
			cursorEntity = (StaticEntity) e;
			cursorEntityContinousPlacing = a.length > 1 && a[1].equals("cont");
		}
		
		activeAction = action;
	}
}
