public class LevelBuilder {
	public static LevelBuilder inst;
	private static Array<Model> models;
	public static Array<LevelStatic> staticPieces; // server only: stores level geometry for sending to clients on connect

	private static ModelBuilder mb;

	public static void init() {
		staticPieces = new Array<>();
		groundPieces = new Array<>();
		mb = new ModelBuilder();
		models = new Array<>();
	}

	public LevelBuilder() {
		inst = this;
	}

	public static Array<ModelInstance> staticGeometry;

	public static Array<ModelInstance> groundPieces;
	public static final float groundPieceSize = 100f;

	public static void createLevel() {
		// graphical representation of the ground
		Log.debug("createLevel - create ground");
		if (Main.isClient()) {
			ModelBuilder mb = new ModelBuilder();
			mb.begin();
			Vector3 bl = new Vector3();
			Vector3 tl = new Vector3();
			Vector3 tr = new Vector3();
			Vector3 br = new Vector3();
			Vector3 norm = new Vector3(0f, 1f, 0f);
			// the size of each rect that makes up the ground
			Texture groundTex = Assets.manager.get("textures/ground1.jpg", Texture.class);
			Material groundMat = new Material(TextureAttribute.createDiffuse(groundTex));
			MeshPartBuilder mpb = mb.part("ground", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal | Usage.TextureCoordinates, groundMat);
			float u1 = 0f;
			float v1 = 0f;
			float u2 = groundPieceSize / 5f;
			float v2 = groundPieceSize / 5f;
			mpb.setUVRange(u1, v1, u2, v2);
			bl.set(0, 0, 0);
			tl.set(0, 0, groundPieceSize);
			tr.set(groundPieceSize, 0, groundPieceSize);
			br.set(groundPieceSize, 0, 0);
			//mpb.rect(bl, tl, tr, br, norm);
			int divisions = ((int) groundPieceSize) / 4;
			mpb.patch(bl, tl, tr, br, norm, divisions, divisions);
			Model groundModel = mb.end();
			models.add(groundModel);
			groundPieces.clear();
			int count = 0;
			for (int x = 0; x < GameWorld.WORLD_WIDTH; x += groundPieceSize) {
				for (int z = 0; z < GameWorld.WORLD_DEPTH; z += groundPieceSize) {
					count++;
					ModelInstance groundPiece = new ModelInstance(groundModel);
					groundPiece.transform.setToTranslation(x, 0f, z);
					groundPieces.add(groundPiece);
				}
			}
			Log.debug("createLevel - created " + count + " groundPieces");
		}

		// physical representation of the ground
		btCollisionObject groundObj = new btCollisionObject();
		btCollisionShape groundShape = new btStaticPlaneShape(Vector3.Y, 0f);
		groundObj.setCollisionShape(groundShape);
		Physics.applyStaticGeometryCollisionFlags(groundObj);
		Physics.inst.addStaticGeometryToWorld(groundObj);

		if (Main.isServer()) {
			Log.debug("createLevel - create static models");
			// server creates static models here, client will create the models when received from server upon connection
			createStaticModels(25);
		}

		Log.debug("createLevel - create boxes");
		Box.createBoxes(10);
	}

	/** client builds statics, probably based on info from server */
	public static void buildStatics(LevelStatic[] statics) {
		if (staticGeometry == null) {
			staticGeometry = new Array<>();
		}
		Log.debug("client building statics received from server: " + statics.length);
		ModelBuilder mb = new ModelBuilder();
		mb.begin();
		for (LevelStatic stat : statics) {
			Model model = Assets.manager.get(stat.modelName, Model.class);
			setupStaticModel(model.meshParts, stat.mtx, true);
			Node node = mb.node("piece", model);
			stat.mtx.getTranslation(tmp);
			node.translation.set(tmp);
			node.rotation.set(stat.mtx.getRotation(q));
		}
		Model finalModel = mb.end();
		ModelInstance instance = new ModelInstance(finalModel);
		staticGeometry.add(instance);
	}

	private static Matrix4 mtx = new Matrix4();
	private static Vector3 tmp = new Vector3();
	private static Quaternion q = new Quaternion();

	/** bullet bodies are offset from model instances by a 90 degree rotation on X axis, boolean set to true handles this */
	private static void setupStaticModel(Array<MeshPart> meshParts, Matrix4 matrix, boolean performVisualToPhysicalRotation) {
		//Log.debug("create static model at: " + matrix.getTranslation(tmp));
		btCollisionObject obj = new btCollisionObject();
		btCollisionShape shape = new btBvhTriangleMeshShape(meshParts);
		obj.setCollisionShape(shape);
		Physics.applyStaticGeometryCollisionFlags(obj);
		mtx.set(matrix);
		if (performVisualToPhysicalRotation) {
			mtx.rotate(Vector3.X, -90);
		}
		obj.setWorldTransform(mtx);
		Physics.inst.addStaticGeometryToWorld(obj);
	}

	private static void createStaticModels(int number) {
		staticGeometry = new Array<>();
		if (Main.isClient()) {
			mb = new ModelBuilder();
			mb.begin();
		}
		Array<String> modelChoices = new Array<>();
		modelChoices.addAll("models/gate.g3db", "models/strange-ramp2.g3db");
		//Array<Model> modelChoices = new Array<>();
		//modelChoices.add(Assets.manager.get("gate.g3db", Model.class));
		//modelChoices.add(Assets.manager.get("strange-ramp1.g3db", Model.class));
		//modelChoices.add(Assets.manager.get("strange-ramp2.g3db", Model.class));
		Quaternion quat = new Quaternion();
		Matrix4 mtx = new Matrix4();
		Vector3 translation = new Vector3();
		//float lo = 10f;
		//float hi = GameWorld.WORLD_WIDTH;
		float x = 20f;
		float z = 0f;
		for (int i = 0; i < number; i++) {
			x = MathUtils.random(GameWorld.WORLD_WIDTH);
			z = MathUtils.random(GameWorld.WORLD_DEPTH);
			quat.setEulerAngles(MathUtils.random(360f), MathUtils.random(360f), MathUtils.random(360f));
			String modelName = modelChoices.random();
			// bullet builds its physics shape using meshparts
			Array<MeshPart> meshParts = null;
			Node node = null;
			if (Main.isClient()) {
				Model model = Assets.manager.get(modelName, Model.class);
				meshParts = model.meshParts;
				node = mb.node("thing", model);
			} else { // the regular Model class requires a gl context, doesn't work on headless
				HeadlessModel model = Assets.manager.get(modelName, HeadlessModel.class);
				meshParts = model.meshParts;
			}
			translation.set(x, 0f, z);
			mtx.set(quat);
			mtx.setTranslation(translation);
			setupStaticModel(meshParts, mtx, true);
			if (Main.isServer()) {
				LevelStatic levelStatic = new LevelStatic();
				levelStatic.modelName = modelName;
				levelStatic.mtx.set(mtx);
				staticPieces.add(levelStatic);
			}
			if (Main.isClient()) {
				node.translation.set(translation);
				node.rotation.set(quat);
			}
		}
		if (Main.isClient()) {
			Model model = mb.end();
			ModelInstance modelInst = new ModelInstance(model);
			staticGeometry.add(modelInst);
		}
	}

	public static void dispose() {
		for (Model model : models) {
			Tools.dispose(model);
		}
		models.clear();
	}
} public class LevelStatic {
	public String modelName;
	public Matrix4 mtx = new Matrix4();
} public class Log {
	private static final String TAG = "LOG";
	private static FileHandle logFile;
	private static final String DEBUG_S = "DEBUG";
	private static final String INFO_S = "INFO";
	private static final String ERROR_S = "ERROR";
	private static boolean serverLogToFile = false;
	private static final String serverLogFileName = "fps.log";

	public static void init() {
		if (Main.isServer() && serverLogToFile) {
			logFile = Gdx.files.local(serverLogFileName);
		}
	}

	public static void debug(String msg) {
		Gdx.app.debug(TAG, msg);
		if (Gdx.app.getLogLevel() <= DEBUG)
			writeToFile(DEBUG_S, TAG, msg);
	}

	public static void info(String msg) {
		Gdx.app.log(TAG, msg);
		if (Gdx.app.getLogLevel() <= INFO)
			writeToFile(INFO_S, TAG, msg);
	}

	public static void error(String msg) {
		Gdx.app.log(TAG, msg);
		if (Gdx.app.getLogLevel() <= ERROR)
			writeToFile(ERROR_S, TAG, msg);
	}

	public static void setLevel(int level) {
		Gdx.app.setLogLevel(level);
	}

	private static StringBuilder sb = new StringBuilder();

	private static void writeToFile(String level, String tag, String msg) {
		if (logFile != null) {
			sb.delete(0, sb.length());
			sb.append("[").append(level).append("] ").append(tag).append(": ").append(msg).append("\n");
			try {
				logFile.writeString(sb.toString(), true);
			} catch (Exception e) {
				logFile = null;
				//Log.error(e.toString());
				throw new GdxRuntimeException(e);
			}
		}
	}

	public static final int DEBUG = Application.LOG_DEBUG;
	public static final int INFO = Application.LOG_INFO;
	public static final int ERROR = Application.LOG_ERROR;
} public class DynamicEntity extends Entity {
	public static Array<DynamicEntity> list;
	public Shadow shadow;

	public static DynamicEntity createEntityNoId(EntityGraphicsType graphicsType) {
		if (!Main.isClient()) {
			// server does not care about graphics
			graphicsType = EntityGraphicsType.None;
		}
		DynamicEntity dynEnt = new DynamicEntity(graphicsType);
		dynEnt.id = -1;
		return dynEnt;
	}

	public static DynamicEntity createEntity(int id, boolean isPlayer, EntityGraphicsType graphicsType) {
		DynamicEntity dynEnt = createEntityNoId(graphicsType);
		assignEntityID(dynEnt, id);
		if (Main.isClient() && isPlayer) {
			dynEnt.setPlayer(new Player(dynEnt));
		}
		return dynEnt;
	}

	public DynamicEntity(EntityGraphicsType graphicsType) {
		super(graphicsType);
		DynamicEntity.list.add(this);
		// TODO move dimensions to constructor parameters
		dimen.set(1f, 2f, 1f);
		tmp.set(dimen).scl(0.5f);
		float radius = 0.45f * (float) Math.sqrt((dimen.x * dimen.x) + (dimen.z * dimen.z));
		body = Physics.inst.createCapsuleObject(radius, dimen.y * 0.35f);
		bodyOffset.set(0f, 0.05f, 0f);
		Physics.applyDynamicEntityCollisionFlags(body);
		body.userData = this;

		movement = new GroundMovement(id);
		movement.setAffectedByGravity(true);
		movement.setAccelRate(0.03f);
		movement.setMaxHorizontalSpeed(0.12f);
		movement.setMaxVerticalSpeed(0.6f);
		movement.setBraking(0.9f);
		Physics.inst.addDynamicEntityToWorld(body);

		float shadowRadius = (float) Math.sqrt(dimen.x * dimen.x + dimen.z * dimen.z);
		if (Main.isClient()) {
			shadow = new Shadow(shadowRadius);
		}
	}

	private long lastSetDestTime;
	private int nextDestInterval = 10000;

	@Override
	void update(float timeStep) {
		boolean testSimpleAI = true;
		super.update(timeStep);
		if (testSimpleAI && this.player == null) {
			if (Main.isClient() && Main.inst.client.player != null) {
				// look at the player entity (billboard behaviour, always face the camera)
				lookAt(Main.inst.client.player.entity.getPosition());
			}
			long now = TimeUtils.millis();
			movement.cancelDestinationAtThreshold(3f);
			if ((now - lastSetDestTime) >= nextDestInterval) {
				float x = MathUtils.random(0f, 100f);
				float y = 0f;
				float z = MathUtils.random(0f, 100f);
				setDestination(tmp.set(x, y, z));
				lastSetDestTime = now;
				nextDestInterval = MathUtils.random(5000, 15000);
			}
		}
		if (shooting) {
			if (tickCountdown <= 0) {
				shoot();
				tickCountdown = ticksPerShot;
			}
		}
		if (shadow != null) {
			shadow.update(getPosition());
		}
		tickCountdown--;
	}

	public Vector3 getForwardFacing(Vector3 storage) {
		storage.set(Vector3.Z);
		relativize(storage);
		return storage;
	}

	private static long minJumpInterval = 250;
	private long lastJumpTime = 0;

	public void jump() {
		if (onGround) {
			long now = System.currentTimeMillis();
			if ((now - lastJumpTime) < minJumpInterval) {
				return;
			}
			lastJumpTime = now;
			float jumpStrength = 0.35f;
			adjustVelocity(tmp.set(0f, jumpStrength, 0f));
		}
	}

	private int ticksPerShot = 6;
	private int tickCountdown;
	private boolean shooting;
	private float aimError = 0.025f;

	public void startShoot() {
		shooting = true;
	}

	public void stopShoot() {
		shooting = false;
	}

	public void shoot() {
		// self pos = ray start
		tmp.set(getPosition());
		// store forward vector in tmp2 and keep for processing hit result
		// also add aim error
		float xErr = MathUtils.random(-aimError, aimError);
		float yErr = MathUtils.random(-aimError, aimError);
		tmp2.set(Vector3.Z).add(xErr, yErr, 0f);
		relativize(tmp2);
		float rayLen = 1000f;
		// get forward facing and apply aim error
		tmp3.set(tmp2).scl(rayLen).add(tmp);
		Physics.inst.castRay(tmp, tmp3);
		Physics.RaycastReport ray = Physics.inst.raycastReport;
		if (ray.hit) {
			// position + (forward vector * hitDistance) = hit location
			tmp2.scl(ray.hitDistance).add(tmp);
			new BulletHit(tmp2);
			Main.inst.server.queueBulletHit(tmp2);
			//Log.debug(Tools.fmt(tmp, "hit"));
		} else {
			//Log.debug("no hit");
		}
	}

	@Override
	protected void removeFromGame() {
		super.removeFromGame();
		if (shadow != null) {
			shadow.removeFromGame();
		}
	}

	public boolean isPlayer() {
		return player != null;
	}
} public abstract class Entity {
	public int id = -1;
	public static Array<Entity> list;
	public static Array<Integer> destroyQueue;
	public static IntMap<Entity> idMap;
	public static Array<Integer> usedIDs;
	protected static Quaternion q = new Quaternion();
	protected static Matrix4 mtx = new Matrix4();
	public EntityInterpolator interpolator = new EntityInterpolator(this);
	public float health = 100f;


	protected btCollisionObject body;
	protected Vector3 bodyOffset = new Vector3();
	public Movement movement;
	public boolean onGround;
	public float distFromGround;
	/** height, width, depth dimensions of the entity */
	protected Vector3 dimen = new Vector3();
	protected Quaternion rotation = new Quaternion();
	/** the Physics class collision callbacks will set collision position change, which is processed during update */
	protected Vector3 collisionPositionChanges = new Vector3();
	protected int collisionPositionChangeCount = 0;

	/** Entities have three choices for graphical representation: a 3d model, a billboard (decal), and none */
	private EntityModel entityModel;

	/** null for non-player entity */
	protected Player player;

	public static enum EntityGraphicsType {
		Decal,
		Model,
		None
	}

	public Entity(EntityGraphicsType graphicsType) {
		synchronized (Entity.list) {
			list.add(this);
		}
		switch(graphicsType) {
			case Decal:
				//entityDecal = new EntityDecal(this);
				entityModel = new EntityBillboard(this);
				break;
			case Model:
				entityModel = new EntityModel(this);
				break;
			case None:
				// do nothing!
				break;
			default:
				throw new GdxRuntimeException("unhandled enum type");
		}
	}

	public static Entity getEntityById(int id) {
		Entity ent = idMap.get(id);
		if (ent == null) {
			if (!Main.isClient()) {
				throw new GdxRuntimeException("Fatal Error: Server could not find entity by id: " + id);
				//Log.error("Server could not find entity by id: " + id);
			} else {
				// this is where the client sees an entity it hasn't heard of before, and asks the server
				// for info about it in order to create it
				Log.debug("Client couldn't find entity by id, requesting entity info from server");
				Main.getNetClient().requestEntityInfo(id);
			}
		}
		return ent;
	}

	public static boolean entityWithIdExists(int id) {
		synchronized (list) {
			for (Entity ent : list) {
				if (ent.id == id) {
					return true;
				}
			}
		}
		return false;
	}

	private static int nextEntityId;

	public static void assignEntityID(Entity ent) {
		if (usedIDs.contains(nextEntityId, false)) {
			nextEntityId++;
		}
		int id = nextEntityId;
		nextEntityId++;
		assignEntityID(ent, id);
	}

	public static void assignEntityID(Entity ent, int id) {
		for (Entity e : list) {
			if (e.id == id) {
				throw new GdxRuntimeException("Fatal Error: Cannot assign id to entity, other entity with id already exists: " + id);
			}
		}
		if (ent.id != -1) {
			throw new GdxRuntimeException("Fatal Error: Entity has already been assigned id: " + ent.id);
		}
		ent.id = id;
		usedIDs.add(id);
		idMap.put(id, ent);
	}

	public static void init() {
		list = new Array<>();
		destroyQueue = new Array<>();
		idMap = new IntMap<>();
		usedIDs = new Array<>();
		DynamicEntity.list = new Array<>();
		EntityModel.list = new Array<>();
	}

	public static void updateAll(float timeStep) {
		for (int i = 0; i < Entity.list.size; i++) {
			Entity ent = Entity.list.get(i);
			ent.update(timeStep);
		}
		processDestroyQueue();
	}

	void update(float timeStep) {
		// the server sets entity data directly, no need to handle updates from server or interpolate
		if (!Main.isServer()) {
			interpolator.update();
			if (player == null) {
				updateTransforms();
				return; // don't simulate movement/physics of non-player entities for clients
						// they are simulated completely server-side
			}
		}
		handleCollisions();
		updateDistFromGround();
		int groundRayHits = Physics.lastDistanceFromGroundRayHitCount;
		float avgDist = Physics.lastDistanceFromGroundAvgDist;
		// when the ray went the full length and did not hit the ground, NaN is the return value
		onGround = false;
		float embedThreshold = 0f;
		if (!Float.isNaN(distFromGround)) {
			Vector3 vel = getVelocity();
			if (distFromGround < 0.1f) {
				adjustPosition(tmp.set(0f, -distFromGround, 0f));
				if (vel.y < 0f) vel.y = 0f;
				onGround = true;
			}
			if (distFromGround < embedThreshold && groundRayHits == 4 && avgDist <= 0f) {
				// penetrating into the ground
				if (player != null) {
					Log.debug("ground embed adjust");
				}
				getPosition().y += -distFromGround;
			} else if (distFromGround > 0f) {
				Vector3 velocity = getVelocity();
				// cap velocity to distance from ground
				if (velocity.y < 0 && distFromGround - velocity.y <= 0f) {
					velocity.y = -distFromGround;
					System.out.println("cap velocity: " + velocity.y);
				}
			}
		}
		movement.update(timeStep, onGround);
		updateTransforms();
	}

	/** The way bullet works, there might be multiple collision points when two object collide with each other.
	 *  Therefore, we take the average of the position change caused by each collision and apply it as the final
	 *  position change.
	 */
	public void handleCollisions() {
		if (collisionPositionChangeCount > 0) {
			collisionPositionChanges.scl(1f / collisionPositionChangeCount);
			collisionPositionChanges.scl(-1f); // subtraction
			adjustPosition(collisionPositionChanges);
			collisionPositionChangeCount = 0;
			collisionPositionChanges.setZero();
		}
	}

	// TODO check this works properly
	public void faceTowards(Vector3 targ) {
		float desired = Tools.getAngleFromAtoB(movement.getPosition(), targ, Vector3.Y);
		rotation.setEulerAngles(-desired, rotation.getPitch(), rotation.getRoll());
	}

	public void updateTransforms() {
		// physics body transform
		q.setEulerAngles(rotation.getYaw(), 0f, 0f); // physics bodies only care about yaw
		mtx.set(q);
		mtx.setTranslation(tmp.set(getPosition()).add(bodyOffset));
		body.setWorldTransform(mtx);
	}

	public void setDestination(Vector3 d) {
		movement.setDestination(d);
	}

	public void setRelativeDestination(Vector3 delta) {
		tmp.set(movement.getPosition()).add(delta);
		setDestination(tmp);
	}

	/** Stops ground units from moving up when looking up.
	 * for example, if an entity is rotated to be facing almost straight up,
	 * this method relativizes the destination to be "in front of" the entity
	 * on the xz (ground) plane */
	public void setRelativeDestinationByYaw(Vector3 delta) {
		// TODO what happens if one of the added vectors == Vector3.Y? i.e. straight up or straight down
		relativizeByYaw(delta);
		tmp.set(movement.getPosition()).add(delta);
		setDestination(tmp);
	}

	/** transform vector based on the current rotation, but set pitch to zero, useful for relative directions like "forward" and "back" */
	public Vector3 relativizeByYaw(Vector3 v) {
		q.setEulerAngles(rotation.getYaw(), 0f, 0f);
		q.transform(v);
		return v;
	}

	/** transform vector by current rotation, makes vector relative to current facing */
	public Vector3 relativize(Vector3 v) {
		rotation.transform(v);
		return v;
	}


	public void setPosition(Vector3 pos) {
		if (pos == null) throw new NullPointerException();
		if (movement == null) throw new NullPointerException();
		movement.setPosition(pos);
	}

	public void setPosition(float x, float y, float z) {
		setPosition(tmp.set(x, y, z));
	}

	public void adjustPosition(Vector3 delta) {
		movement.getPosition().add(delta);
	}

	public void setVelocity(Vector3 vel) {
		movement.getVelocity().set(vel);
	}

	// TODO is there a more correct way to handle collision position changes?
	/** combines all collision position changes, which are averaged when processed during update */
	public void addCollisionPositionChange(Vector3 posDelta) {
		collisionPositionChangeCount++;
		collisionPositionChanges.add(posDelta);
	}

	public Movement getMovement() {
		return movement;
	}

	static Vector3 tmp = new Vector3();
	static Vector3 tmp2 = new Vector3();
	static Vector3 tmp3 = new Vector3();

	public Vector3 getPosition() {
		return movement.getPosition();
	}

	public Vector3 getVelocity() {
		return movement.getVelocity();
	}

	public void setRotation(Quaternion newRot) {
		rotation.set(newRot);
	}

	public void adjustRotation(Direction.Rotation rot) {
		System.out.println("adjust rotation: " + rot.vector);
		float rotSpeed = 4f;
		adjustYaw(-rot.vector.x * rotSpeed);
		adjustPitch(-rot.vector.y * rotSpeed);
		adjustRoll(-rot.vector.z * rotSpeed);
	}

	public Quaternion getRotation() {
		return rotation;
	}

	public void adjustVelocity(Vector3 delta) {
		movement.getVelocity().add(delta);
	}

	/** adjust velocity based on relative directions. i.e. Vector3.Z == forward, (0, 1, 1) == forward-up */
	public void adjustVelocityRelativeByYaw(Vector3 delta) {
		adjustVelocity(relativizeByYaw(delta));
	}

	public void setYawPitchRoll(float y, float p, float r) {
		getRotation().setEulerAngles(y, p, r);
	}

	public void setYawPitchRoll(Vector3 rot) {
		getRotation().setEulerAngles(rot.x, rot.y, rot.z);
	}

	public void lookAt(Vector3 pos) {
		tmp.set(pos).sub(getPosition());
		q.setFromCross(Vector3.Z, tmp.nor());
		setYawPitchRoll(q.getYaw(), getPitch(), getRoll());
	}

	public float getYaw() {
		return rotation.getYaw();
	}


	public void setYaw(float amt) {
		rotation.setEulerAngles(amt, rotation.getPitch(), rotation.getRoll());
	}

	public void adjustYaw(float amt) {
		float yaw = getYaw();
		yaw += amt;
		setYaw(yaw);
	}

	public float getPitch() {
		return rotation.getPitch();
	}

	public void setPitch(float amt) {
		rotation.setEulerAngles(rotation.getYaw(), amt, rotation.getRoll());
	}

	public void adjustPitch(float amt) {
		float pitch = getPitch();
		// avoid gimbal lock
		// technically could use Quaternions for free rotation, but not necessary for FPS
		pitch = MathUtils.clamp(pitch + amt, -89f, 89f);
		setPitch(pitch);
	}

	public float getRoll() {
		return rotation.getRoll();
	}

	public void setRoll(float amt) {
		rotation.setEulerAngles(rotation.getYaw(), rotation.getPitch(), amt);
	}

	public void adjustRoll(float amt) {
		float roll = getRoll();
		roll += amt;
		setRoll(roll);
	}

	public Vector3 getDimensions() {
		return dimen;
	}

	public btCollisionObject getBody() {
		return body;
	}

	public boolean isFlyingEntity() {
		return movement instanceof FlyingMovement;
	}

	public Player getPlayer() {
		return player;
	}

	public void setPlayer(Player player) {
		if (player == null) throw new GdxRuntimeException("cannot set to null (yet!)");
		this.player = player;
	}

	public static void destroy(int id) {
		Entity ent = getEntityById(id);
		ent.destroy();
	}

	private boolean destroyed;

	public void destroy() {
		if (destroyed) return;
		destroyed = true;
		synchronized (destroyQueue) {
			Log.debug("destroy entity, id: " + id);
			destroyQueue.add(id);
			if (Main.isServer()) {
				Main.inst.server.queueDestroyedEntity(id);
			}
			if (Main.isClient()) {
				new BlueExplosion(getPosition());
			}
		}
	}

	protected void removeFromGame() {
		synchronized (Entity.list) {
			list.removeValue(this, true);
		}
		if (entityModel != null) {
			EntityModel.list.removeValue(entityModel, true);
		}
		Physics.inst.removeBody(body);
	}

	public static void processDestroyQueue() {
		synchronized (destroyQueue) {
			for (int id : destroyQueue) {
				synchronized (Entity.list) {
					for (Entity ent : list) {
						if (ent.id == id) {
							ent.removeFromGame();
							// id should be unique, unless something is broken
							break;
						}
					}
				}
			}
			destroyQueue.clear();
		}
	}

	public void applyDamage(float dmg) {
		//Log.debug("Entity[" + id + "] took damage: " + dmg);
		health -= dmg;
		if (health <= 0f) {
			destroy();
		}
	}

	public void updateDistFromGround() {
		if (Main.isMobile()) {
			distFromGround = Physics.inst.distanceFromGroundFast(movement.getPosition(), dimen);
		} else {
			distFromGround = Physics.inst.distanceFromGround(movement.getPosition(), dimen);
		}
	}

	public EntityModel getEntityModel() {
		return entityModel;
	}

	public float getHeight() {
		return dimen.y;
	}

	public float getWidth() {
		return dimen.x;
	}

	public float getDepth() {
		return dimen.z;
	}

	// TODO make this more accurate
	public float getRadius() {
		return Math.max(dimen.z, (Math.max(dimen.x, dimen.y)));
	}

} public class ClientUpdate implements Pool.Poolable {
	private static Pool<ClientUpdate> clientUpdatePool = new Pool<ClientUpdate>() {
		@Override
		protected ClientUpdate newObject() {
			return new ClientUpdate();
		}
	};

	/** tick number this input was sent at, used for client-side prediction, not related to main server timing tick */
	public int inputTick;
	public int entityId;
	public CommandPackage cmdPack = new CommandPackage();

	public void reset() {
		inputTick = -1;
		entityId = -1;
	}

	public void set(ClientUpdate other) {
		entityId = other.entityId;
		cmdPack.commandBits = other.cmdPack.commandBits;
	}

	public static ClientUpdate obtain() {
		return clientUpdatePool.obtain();
	}

	public static void free(ClientUpdate update) {
		clientUpdatePool.free(update);
	}
} public class BulletPackage {
	public Vector3[] locations;
} public class ChatMessage {
	public int playerId = -1;
	public String text;
	public long createTime;

	public ChatMessage() {}

	public ChatMessage(String txt) {
		this.text = txt;
	}

	@Override
	public String toString() {
		return String.format("playerId: %d, createTime: %d, text: %s",
				playerId, createTime, text);
	}
} public enum ClientRequest {
	CreateNewPlayerAssignID, // create a new entity for this player, and assign an entity ID
	RequestServerInfo,
	RequestLevelGeometry,
	RequestResetPlayerPosition,
} public class EntityInfoRequest {
	public int id;

	public static class Response {
		public int id;
		public Entity.EntityGraphicsType graphicsType;
		public boolean isPlayer;
		// add more things like health, speed, graphics type, etc
	}
} public class ServerMessage {

	public static class AssignPlayerEntityId {
		public int id;
	}

	public static class DestroyEntity {
		public int id;
	}

	public static class ServerInfo {
		public int tickNum;
		public float tickInterval;
		public String serverName;
		public String serverMsg;
	}

	public static class LevelGeometry {
		public LevelStatic[] staticPieces;
	}

} public class ServerUpdate implements Pool.Poolable {
	public int tickNum;
	public int playerInputTick; // the last input processed from the player who will receive this update
	public EntityUpdate[] entityUpdates;
	private boolean processed;

	public static ServerUpdate createServerUpdate() {
		ServerUpdate serverUpdate = Pooler.serverUpdate();
		synchronized (Entity.list) {
			serverUpdate.tickNum = Main.inst.server.tickNum;
			serverUpdate.entityUpdates = new EntityUpdate[Entity.list.size];
			for (int i = 0; i < Entity.list.size; i++) {
				Entity ent = Entity.list.get(i);
				EntityUpdate entUpd = Pooler.entityUpdate();
				entUpd.setEntityId(ent.id);
				entUpd.setPosition(ent.getPosition());
				entUpd.setRotation(ent.getYaw(), ent.getPitch(), ent.getRoll());
				serverUpdate.entityUpdates[i] = entUpd;
			}
		}
		return serverUpdate;
	}

	public void free() {
		for (EntityUpdate entUpd : entityUpdates) {
			Pooler.free(entUpd);
		}
		Pooler.free(this);
	}

	public void applyUpdates() {
		if (processed) throw new GdxRuntimeException("serverUpdate already processed");
		for (EntityUpdate entUpdate : entityUpdates) {
			Entity ent = Entity.getEntityById(entUpdate.getEntityId());
			if (ent != null) {
				if (Main.inst.client.player == null && ent.id == Main.inst.client.playerId) {
					Main.inst.client.assignClientPlayerToId(ent.id);
					Log.debug("Entity matched player id, entity was assigned to player: " + ent.id);
				}
				ent.interpolator.handleUpdateFromServer(entUpdate, playerInputTick);
			}
		}
		processed = true;
		free();
	}

	@Override
	public void reset() {
		entityUpdates = null;
		processed = false;
	}

} public class Pooler {
	private static Vector3Pool v3Pool;
	private static Matrix4Pool mtxPool;
	private static ServerUpdatePool serverUpdatePool;
	private static EntityUpdatePool entityUpdatePool;
	private static MovementPackagePool movementPackagePool;

	public static void init() {
		v3Pool = new Vector3Pool();
		mtxPool = new Matrix4Pool();
		serverUpdatePool = new ServerUpdatePool();
		entityUpdatePool = new EntityUpdatePool();
		movementPackagePool = new MovementPackagePool();
	}



	public static class Vector3Pool extends CountingPool<Vector3> {
		@Override
		protected Vector3 newObject() {
			return new Vector3();
		}
	}
	public static Vector3 v3() {
		return v3Pool.obtain();
	}

	public static void free(Vector3 v3) {
		v3Pool.free(v3);
	}

	public static void free(Vector3 ...v3) {
		for (int i = 0; i < v3.length; i++) {
			v3Pool.free(v3[i]);
		}
	}



	public static class Matrix4Pool extends CountingPool<Matrix4> {
		@Override
		protected Matrix4 newObject() {
			return new Matrix4();
		}
	}

	public static Matrix4 mtx() {
		return mtxPool.obtain();
	}

	public static void free(Matrix4 mtx) {
		mtxPool.free(mtx);
	}

	public static void free(Matrix4 ...mtx) {
		for (int i = 0; i < mtx.length; i++) {
			mtxPool.free(mtx[i]);
		}
	}



	public static class ServerUpdatePool extends CountingPool<ServerUpdate> {
		@Override
		protected ServerUpdate newObject() {
			return new ServerUpdate();
		}
	}
	
	public static ServerUpdate serverUpdate() {
		return serverUpdatePool.obtain();
	}

	public static void free(ServerUpdate upd) {
		serverUpdatePool.free(upd);
	}

	public static void free(ServerUpdate ...upd) {
		for (int i = 0; i < upd.length; i++) {
			serverUpdatePool.free(upd[i]);
		}
	}



	public static class EntityUpdatePool extends CountingPool<EntityUpdate> {
		@Override
		protected EntityUpdate newObject() {
			return new EntityUpdate();
		}
	}

	public static EntityUpdate entityUpdate() {
		return entityUpdatePool.obtain();
	}

	public static void free(EntityUpdate upd) {
		entityUpdatePool.free(upd);
	}

	public static void free(EntityUpdate ...upd) {
		for (int i = 0; i < upd.length; i++) {
			entityUpdatePool.free(upd[i]);
		}
	}



	public static class MovementPackagePool extends CountingPool<CommandPackage> {
		@Override
		protected CommandPackage newObject() {
			return new CommandPackage();
		}
	}

	public static CommandPackage movementPackage() {
		return movementPackagePool.obtain();
	}

	public static void free(CommandPackage upd) {
		movementPackagePool.free(upd);
	}

	public static void free(CommandPackage...upd) {
		for (int i = 0; i < upd.length; i++) {
			movementPackagePool.free(upd[i]);
		}
	}


}
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ObjectMap;
import com.esotericsoftware.kryonet.Connection;
import com.esotericsoftware.kryonet.Listener;
import com.esotericsoftware.kryonet.Server;
import org.jrenner.fps.LevelBuilder;
import org.jrenner.fps.LevelStatic;
import org.jrenner.fps.Log;
import java.io.IOException;
import org.jrenner.fps.entity.DynamicEntity;
import org.jrenner.fps.entity.Entity;
import org.jrenner.fps.net.client.ClientUpdate;
import org.jrenner.fps.net.packages.BulletPackage;
import org.jrenner.fps.net.packages.ChatMessage;
import org.jrenner.fps.net.packages.ClientRequest;
import org.jrenner.fps.net.packages.EntityInfoRequest;
import org.jrenner.fps.net.packages.ServerMessage;
import org.jrenner.fps.net.packages.ServerUpdate;
import org.jrenner.fps.utils.Pooler;
package org.jrenner.fps.net;




public class NetServer extends AbstractServer {
	private Server server;
	public Array<Connection> connectedClients = new Array<>();
	private ObjectMap<Connection, ClientInfo> clientMap;
	public Array<Integer> playerEntityIds = new Array<>();
	public static int lagMin;
	public static int lagMax;
	public static boolean simulateLag = false;


	public NetServer() {
		clientMap = new ObjectMap<>();
		server = new Server(NetManager.writeBufferSize, NetManager.objectBufferSize);
		NetManager.registerKryoClasses(server.getKryo());
		try {
			server.bind(NetManager.tcpPort, NetManager.udpPort);
			server.start();
			server.addListener(createListener(simulateLag));
			Log.debug("server is listening at: " + NetManager.host + ":" + NetManager.tcpPort);
		} catch (IOException e) {
			Log.error(e.toString());
			throw new GdxRuntimeException(e);
		}
	}

	private void handleConnect(Connection conn) {
		Log.debug("connected to server: " + conn);
		ClientInfo clientInfo = new ClientInfo();
		clientMap.put(conn, clientInfo);
		connectedClients.add(conn);
	}

	private void handleDisconnect(Connection conn) {
		Log.debug("disconnected from server: " + conn);
		ClientInfo clientInfo = clientMap.get(conn);
		Entity.destroy(clientInfo.playerEntityId);
		connectedClients.removeValue(conn, true);
	}

	private void handleReceived(Connection conn, Object obj) {
		//AsLog.debug("received from client (" + conn + "): " + obj);
		if (obj instanceof ClientRequest) {