/*******************************************************************************
 * Copyright 2011 See AUTHORS file.
 * 
 * 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 com.badlogic.gdx.tests.bullet;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder;
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.physics.bullet.collision.btBroadphasePairArray;
import com.badlogic.gdx.physics.bullet.collision.btCollisionDispatcher;
import com.badlogic.gdx.physics.bullet.collision.btCollisionObject;
import com.badlogic.gdx.physics.bullet.collision.btCollisionShape;
import com.badlogic.gdx.physics.bullet.collision.btCollisionWorld;
import com.badlogic.gdx.physics.bullet.collision.btCompoundShape;
import com.badlogic.gdx.physics.bullet.collision.btConvexHullShape;
import com.badlogic.gdx.physics.bullet.collision.btDbvtBroadphase;
import com.badlogic.gdx.physics.bullet.collision.btDefaultCollisionConfiguration;
import com.badlogic.gdx.physics.bullet.collision.btPairCachingGhostObject;
import com.badlogic.gdx.physics.bullet.collision.btPersistentManifoldArray;
import com.badlogic.gdx.utils.Array;

/** @author Xoppa */
public class FrustumCullingTest extends BaseBulletTest {
	/** Only show entities inside the frustum */
	final static int CULL_FRUSTUM = 1;
	/** Transform the render cam with the frustum */
	final static int FRUSTUM_CAM = 2;

	final static boolean USE_BULLET_FRUSTUM_CULLING = true;

	int state = 0; // 0 = No culling, look from above

	final static int BOXCOUNT = 200;

	final static float BOX_X_MIN = -25;
	final static float BOX_Y_MIN = -25;
	final static float BOX_Z_MIN = -25;

	final static float BOX_X_MAX = 25;
	final static float BOX_Y_MAX = 25;
	final static float BOX_Z_MAX = 25;

	final static float SPEED_X = 360f / 7f;
	final static float SPEED_Y = 360f / 19f;
	final static float SPEED_Z = 360f / 13f;

	final static Vector3 tmpV = new Vector3();
	final static Matrix4 tmpM = new Matrix4();

	final static int ptrs[] = new int[512];
	final static Array<btCollisionObject> visibleObjects = new Array<btCollisionObject>();

	public static btPairCachingGhostObject createFrustumObject (final Vector3... points) {
		final btPairCachingGhostObject result = new TestPairCachingGhostObject();
		final boolean USE_COMPOUND = true;
		// Using a compound shape is not necessary, but it's good practice to create shapes around the center.
		if (USE_COMPOUND) {
			final Vector3 centerNear = new Vector3(points[2]).sub(points[0]).scl(0.5f).add(points[0]);
			final Vector3 centerFar = new Vector3(points[6]).sub(points[4]).scl(0.5f).add(points[4]);
			final Vector3 center = new Vector3(centerFar).sub(centerNear).scl(0.5f).add(centerNear);
			final btConvexHullShape hullShape = new btConvexHullShape();
			for (int i = 0; i < points.length; i++)
				hullShape.addPoint(tmpV.set(points[i]).sub(center));
			final btCompoundShape shape = new btCompoundShape();
			shape.addChildShape(tmpM.setToTranslation(center), hullShape);
			result.setCollisionShape(shape);
		} else {
			final btConvexHullShape shape = new btConvexHullShape();
			for (int i = 0; i < points.length; i++)
				shape.addPoint(points[i]);
			result.setCollisionShape(shape);
		}
		result.setCollisionFlags(btCollisionObject.CollisionFlags.CF_NO_CONTACT_RESPONSE);
		return result;
	}

	public static Array<BulletEntity> getEntitiesCollidingWithObject (final BulletWorld world, final btCollisionObject object,
		final Array<BulletEntity> out, final btPersistentManifoldArray tmpArr) {
		// Fetch the array of contacts
		btBroadphasePairArray arr = world.broadphase.getOverlappingPairCache().getOverlappingPairArray();
		// Get the user values (which are indices in the entities array) of all objects colliding with the object
		final int n = arr.getCollisionObjectsValue(ptrs, object);
		// Fill the array of entities
		out.clear();
		for (int i = 0; i < n; i++)
			out.add(world.entities.get(ptrs[i]));
		return out;
	}

	public static Model createFrustumModel (final Vector3... p) {
		ModelBuilder builder = new ModelBuilder();
		builder.begin();
		MeshPartBuilder mpb = builder.part("", GL20.GL_LINES, Usage.Position | Usage.Normal,
			new Material(new ColorAttribute(ColorAttribute.Diffuse, Color.WHITE)));
		mpb.vertex(p[0].x, p[0].y, p[0].z, 0, 0, 1, p[1].x, p[1].y, p[1].z, 0, 0, 1, p[2].x, p[2].y, p[2].z, 0, 0, 1, p[3].x,
			p[3].y, p[3].z, 0, 0, 1, // near
			p[4].x, p[4].y, p[4].z, 0, 0, -1, p[5].x, p[5].y, p[5].z, 0, 0, -1, p[6].x, p[6].y, p[6].z, 0, 0, -1, p[7].x, p[7].y,
			p[7].z, 0, 0, -1);
		mpb.index((short)0, (short)1, (short)1, (short)2, (short)2, (short)3, (short)3, (short)0);
		mpb.index((short)4, (short)5, (short)5, (short)6, (short)6, (short)7, (short)7, (short)4);
		mpb.index((short)0, (short)4, (short)1, (short)5, (short)2, (short)6, (short)3, (short)7);
		return builder.end();
	}

	private float angleX, angleY, angleZ;
	private btPairCachingGhostObject frustumObject;
	private BulletEntity frustumEntity;
	private final Array<BulletEntity> visibleEntities = new Array<BulletEntity>();
	private btPersistentManifoldArray tempManifoldArr;
	private PerspectiveCamera frustumCam;
	private PerspectiveCamera overviewCam;

	@Override
	public void create () {
		super.create();

		instructions = "Tap to toggle view\nLong press to toggle debug mode\nSwipe for next test\nCtrl+drag to rotate\nScroll to zoom";

		tempManifoldArr = new btPersistentManifoldArray();

		world.addConstructor("collisionBox", new BulletConstructor(world.getConstructor("box").model));

		// Create the entities
		final float dX = BOX_X_MAX - BOX_X_MIN;
		final float dY = BOX_Y_MAX - BOX_Y_MIN;
		final float dZ = BOX_Z_MAX - BOX_Z_MIN;
		for (int i = 0; i < BOXCOUNT; i++)
			world.add("collisionBox", BOX_X_MIN + dX * (float)Math.random(), BOX_Y_MIN + dY * (float)Math.random(),
				BOX_Z_MIN + dZ * (float)Math.random()).setColor(Color.GRAY);

		frustumCam = new PerspectiveCamera(camera.fieldOfView, camera.viewportWidth, camera.viewportHeight);
		frustumCam.far = Vector3.len(BOX_X_MAX, BOX_Y_MAX, BOX_Z_MAX);
		frustumCam.update();

		overviewCam = camera;
		overviewCam.position.set(BOX_X_MAX, BOX_Y_MAX, BOX_Z_MAX);
		overviewCam.lookAt(Vector3.Zero);
		overviewCam.far = 150f;
		overviewCam.update();

		final Model frustumModel = createFrustumModel(frustumCam.frustum.planePoints);
		disposables.add(frustumModel);
		frustumObject = createFrustumObject(frustumCam.frustum.planePoints);
		world.add(frustumEntity = new BulletEntity(frustumModel, frustumObject, 0, 0, 0));
		frustumEntity.setColor(Color.BLUE);
	}

	@Override
	public BulletWorld createWorld () {
		// No need to use dynamics for this test
		btDbvtBroadphase broadphase = new btDbvtBroadphase();
		btDefaultCollisionConfiguration collisionConfig = new btDefaultCollisionConfiguration();
		btCollisionDispatcher dispatcher = new btCollisionDispatcher(collisionConfig);
		btCollisionWorld collisionWorld = new btCollisionWorld(dispatcher, broadphase, collisionConfig);
		return new BulletWorld(collisionConfig, dispatcher, broadphase, null, collisionWorld);
	}

	@Override
	public void update () {
		super.update();
		// Not using dynamics, so update the collision world manually
		if (USE_BULLET_FRUSTUM_CULLING) {
			if (world.performanceCounter != null) world.performanceCounter.start();
			world.collisionWorld.performDiscreteCollisionDetection();
			if (world.performanceCounter != null) world.performanceCounter.stop();
		}
	}

	@Override
	public void render () {
		final float dt = Gdx.graphics.getDeltaTime();
		frustumEntity.transform.idt();
		frustumEntity.transform.rotate(Vector3.X, angleX = (angleX + dt * SPEED_X) % 360);
		frustumEntity.transform.rotate(Vector3.Y, angleY = (angleY + dt * SPEED_Y) % 360);
		frustumEntity.transform.rotate(Vector3.Z, angleZ = (angleZ + dt * SPEED_Z) % 360);

		// Transform the ghost object
		frustumEntity.body.setWorldTransform(frustumEntity.transform);
		// Transform the frustum cam
		frustumCam.direction.set(0, 0, -1);
		frustumCam.up.set(0, 1, 0);
		frustumCam.position.set(0, 0, 0);
		frustumCam.rotate(frustumEntity.transform);
		frustumCam.update();

		super.render();

		performance.append(" visible: ").append(visibleEntities.size);
	}

	@Override
	protected void renderWorld () {
		if (world.performanceCounter != null) world.performanceCounter.start();
		if (USE_BULLET_FRUSTUM_CULLING)
			getEntitiesCollidingWithObject(world, frustumObject, visibleEntities, tempManifoldArr);
		else {
			visibleEntities.clear();
			for (int i = 0; i < world.entities.size; i++) {
				final BulletEntity e = world.entities.get(i);
				if (e == frustumEntity) continue;
				e.modelInstance.transform.getTranslation(tmpV);
				if (frustumCam.frustum.sphereInFrustum(tmpV, 1)) visibleEntities.add(e);
			}
		}
		if (world.performanceCounter != null) world.performanceCounter.stop();

		for (int i = 0; i < visibleEntities.size; i++)
			visibleEntities.get(i).setColor(Color.RED);

		modelBatch.begin(camera);
		if ((state & CULL_FRUSTUM) == CULL_FRUSTUM) {
			world.render(modelBatch, environment, visibleEntities);
			world.render(modelBatch, environment, frustumEntity);
		} else
			world.render(modelBatch, environment);
		modelBatch.end();

		for (int i = 0; i < visibleEntities.size; i++)
			visibleEntities.get(i).setColor(Color.GRAY);
	}

	@Override
	protected void beginRender (boolean lighting) {
		super.beginRender(false);
	}

	@Override
	public void dispose () {
		frustumObject = null;

		super.dispose();

		if (tempManifoldArr != null) tempManifoldArr.dispose();
		tempManifoldArr = null;
	}

	@Override
	public boolean tap (float x, float y, int count, int button) {
		state = (state + 1) % 3;
		if ((state & FRUSTUM_CAM) == FRUSTUM_CAM)
			camera = frustumCam;
		else
			camera = overviewCam;
		return true;
	}

	// Simple helper class to keep a reference to the collision shape
	public static class TestPairCachingGhostObject extends btPairCachingGhostObject {
		public btCollisionShape shape;

		@Override
		public void setCollisionShape (btCollisionShape collisionShape) {
			shape = collisionShape;
			super.setCollisionShape(collisionShape);
		}

		@Override
		public void dispose () {
			super.dispose();
			if (shape != null) shape.dispose();
			shape = null;
		}
	}
}
