/*******************************************************************************
 * 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.physics.box2d;

import java.util.Iterator;

import org.jbox2d.collision.AABB;
import org.jbox2d.common.Settings;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.joints.JointEdge;

import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.BodyDef.BodyType;
import com.badlogic.gdx.physics.box2d.JointDef.JointType;
import com.badlogic.gdx.physics.box2d.joints.DistanceJoint;
import com.badlogic.gdx.physics.box2d.joints.FrictionJoint;
import com.badlogic.gdx.physics.box2d.joints.MouseJoint;
import com.badlogic.gdx.physics.box2d.joints.PrismaticJoint;
import com.badlogic.gdx.physics.box2d.joints.PulleyJoint;
import com.badlogic.gdx.physics.box2d.joints.RevoluteJoint;
import com.badlogic.gdx.physics.box2d.joints.RopeJoint;
import com.badlogic.gdx.physics.box2d.joints.WeldJoint;
import com.badlogic.gdx.physics.box2d.joints.WheelJoint;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ObjectMap;

/** The world class manages all physics entities, dynamic simulation, and asynchronous queries. The world also contains efficient
 * memory management facilities.
 * @author mzechner */
public final class World implements Disposable {
	org.jbox2d.dynamics.World world;
	Vec2 tmp = new Vec2();
	Vector2 tmp2 = new Vector2();
	ObjectMap<org.jbox2d.dynamics.Body, Body> bodies = new ObjectMap<org.jbox2d.dynamics.Body, Body>();
	ObjectMap<org.jbox2d.dynamics.Fixture, Fixture> fixtures = new ObjectMap<org.jbox2d.dynamics.Fixture, Fixture>();
	ObjectMap<org.jbox2d.dynamics.joints.Joint, Joint> joints = new ObjectMap<org.jbox2d.dynamics.joints.Joint, Joint>();

	/** Construct a world object.
	 * @param gravity the world gravity vector.
	 * @param doSleep improve performance by not simulating inactive bodies. */
	public World (Vector2 gravity, boolean doSleep) {
		world = new org.jbox2d.dynamics.World(tmp.set(gravity.x, gravity.y));
		world.setAllowSleep(doSleep);
	}

	/** Register a destruction listener. The listener is owned by you and must remain in scope. */
	public void setDestructionListener (DestructionListener listener) {
	}

	/** Register a contact filter to provide specific control over collision. Otherwise the default filter is used
	 * (b2_defaultFilter). The listener is owned by you and must remain in scope. */
	public void setContactFilter (final ContactFilter filter) {
		if (filter != null) {
			world.setContactFilter(new org.jbox2d.callbacks.ContactFilter() {
				@Override
				public boolean shouldCollide (org.jbox2d.dynamics.Fixture fixtureA, org.jbox2d.dynamics.Fixture fixtureB) {
					return filter.shouldCollide(fixtures.get(fixtureA), fixtures.get(fixtureB));
				}
			});
		} else {
			world.setContactFilter(new org.jbox2d.callbacks.ContactFilter());
		}
	}

	/** Register a contact event listener. The listener is owned by you and must remain in scope. */
	Contact tmpContact = new Contact(this);
	Manifold tmpManifold = new Manifold();
	ContactImpulse tmpImpulse = new ContactImpulse();

	public void setContactListener (final ContactListener listener) {
		if (listener != null) {
			world.setContactListener(new org.jbox2d.callbacks.ContactListener() {
				@Override
				public void beginContact (org.jbox2d.dynamics.contacts.Contact contact) {
					tmpContact.contact = contact;
					listener.beginContact(tmpContact);
				}

				@Override
				public void endContact (org.jbox2d.dynamics.contacts.Contact contact) {
					tmpContact.contact = contact;
					listener.endContact(tmpContact);
				}

				@Override
				public void preSolve (org.jbox2d.dynamics.contacts.Contact contact, org.jbox2d.collision.Manifold oldManifold) {
					tmpContact.contact = contact;
					tmpManifold.manifold = oldManifold;
					listener.preSolve(tmpContact, tmpManifold);
				}

				@Override
				public void postSolve (org.jbox2d.dynamics.contacts.Contact contact, org.jbox2d.callbacks.ContactImpulse impulse) {
					tmpContact.contact = contact;
					tmpImpulse.impulse = impulse;
					listener.postSolve(tmpContact, tmpImpulse);
				}
			});
		} else {
			world.setContactListener(null);
		}
	}

	/** Create a rigid body given a definition. No reference to the definition is retained.
	 * @warning This function is locked during callbacks. */
	public Body createBody (BodyDef def) {
		org.jbox2d.dynamics.BodyDef bd = new org.jbox2d.dynamics.BodyDef();
		bd.active = def.active;
		bd.allowSleep = def.allowSleep;
		bd.angle = def.angle;
		bd.angularDamping = def.angularDamping;
		bd.angularVelocity = def.angularVelocity;
		bd.awake = def.awake;
		bd.bullet = def.bullet;
		bd.fixedRotation = def.fixedRotation;
		bd.gravityScale = def.gravityScale;
		bd.linearDamping = def.linearDamping;
		bd.linearVelocity.set(def.linearVelocity.x, def.linearVelocity.y);
		bd.position.set(def.position.x, def.position.y);
		if (def.type == BodyType.DynamicBody) bd.type = org.jbox2d.dynamics.BodyType.DYNAMIC;
		if (def.type == BodyType.StaticBody) bd.type = org.jbox2d.dynamics.BodyType.STATIC;
		if (def.type == BodyType.KinematicBody) bd.type = org.jbox2d.dynamics.BodyType.KINEMATIC;

		org.jbox2d.dynamics.Body b = world.createBody(bd);
		Body body = new Body(this, b);
		bodies.put(b, body);
		return body;
	}

	/** Destroy a rigid body given a definition. No reference to the definition is retained. This function is locked during
	 * callbacks.
	 * @warning This automatically deletes all associated shapes and joints.
	 * @warning This function is locked during callbacks. */
	public void destroyBody (Body body) {
		world.destroyBody(body.body);
		bodies.remove(body.body);
		for (Fixture fixture : body.fixtures) {
			fixtures.remove(fixture.fixture);
		}
		JointEdge jointEdge = body.body.getJointList();
		while (jointEdge != null) {
			joints.remove(jointEdge.joint);
		}
	}

	/** Create a joint to constrain bodies together. No reference to the definition is retained. This may cause the connected bodies
	 * to cease colliding.
	 * @warning This function is locked during callbacks. */
	public Joint createJoint (JointDef def) {
		org.jbox2d.dynamics.joints.JointDef jd = def.toJBox2d();
		org.jbox2d.dynamics.joints.Joint j = world.createJoint(jd);
		Joint joint = null;
		if (def.type == JointType.DistanceJoint) joint = new DistanceJoint(this, (org.jbox2d.dynamics.joints.DistanceJoint)j);
		if (def.type == JointType.FrictionJoint) joint = new FrictionJoint(this, (org.jbox2d.dynamics.joints.FrictionJoint)j);
		if (def.type == JointType.GearJoint) joint = new DistanceJoint(this, (org.jbox2d.dynamics.joints.DistanceJoint)j);
		if (def.type == JointType.MouseJoint) joint = new MouseJoint(this, (org.jbox2d.dynamics.joints.MouseJoint)j);
		if (def.type == JointType.PrismaticJoint) joint = new PrismaticJoint(this, (org.jbox2d.dynamics.joints.PrismaticJoint)j);
		if (def.type == JointType.PulleyJoint) joint = new PulleyJoint(this, (org.jbox2d.dynamics.joints.PulleyJoint)j);
		if (def.type == JointType.RevoluteJoint) joint = new RevoluteJoint(this, (org.jbox2d.dynamics.joints.RevoluteJoint)j);
		if (def.type == JointType.RopeJoint) joint = new RopeJoint(this, (org.jbox2d.dynamics.joints.RopeJoint)j);
		if (def.type == JointType.WeldJoint) joint = new WeldJoint(this, (org.jbox2d.dynamics.joints.WeldJoint)j);
		if (def.type == JointType.WheelJoint) joint = new WheelJoint(this, (org.jbox2d.dynamics.joints.WheelJoint)j);
		if (joint == null) throw new GdxRuntimeException("Joint type '" + def.type + "' not yet supported by GWT backend");
		joints.put(j, joint);
		return joint;
	}

	/** Destroy a joint. This may cause the connected bodies to begin colliding.
	 * @warning This function is locked during callbacks. */
	public void destroyJoint (Joint joint) {
		joint.setUserData(null);
		world.destroyJoint(joint.joint);
		joints.remove(joint.joint);
	}

	/** Take a time step. This performs collision detection, integration, and constraint solution.
	 * @param timeStep the amount of time to simulate, this should not vary.
	 * @param velocityIterations for the velocity constraint solver.
	 * @param positionIterations for the position constraint solver. */
	public void step (float timeStep, int velocityIterations, int positionIterations) {
		world.step(timeStep, velocityIterations, positionIterations);
	}

	/** Manually clear the force buffer on all bodies. By default, forces are cleared automatically after each call to Step. The
	 * default behavior is modified by calling SetAutoClearForces. The purpose of this function is to support sub-stepping.
	 * Sub-stepping is often used to maintain a fixed sized time step under a variable frame-rate. When you perform sub-stepping
	 * you will disable auto clearing of forces and instead call ClearForces after all sub-steps are complete in one pass of your
	 * game loop. {@link #setAutoClearForces(boolean)} */
	public void clearForces () {
		world.clearForces();
	}

	/** Enable/disable warm starting. For testing. */
	public void setWarmStarting (boolean flag) {
		world.setWarmStarting(flag);
	}

	/** Enable/disable continuous physics. For testing. */
	public void setContinuousPhysics (boolean flag) {
		world.setContinuousPhysics(flag);
	}

	/** Get the number of broad-phase proxies. */
	public int getProxyCount () {
		return world.getProxyCount();
	}

	/** Get the number of bodies. */
	public int getBodyCount () {
		return world.getBodyCount();
	}

	/** Get the number of joints. */
	public int getJointCount () {
		return world.getJointCount();
	}

	/** Get the number of contacts (each may have 0 or more contact points). */
	public int getContactCount () {
		return world.getContactCount();
	}

	/** Change the global gravity vector. */
	public void setGravity (Vector2 gravity) {
		world.setGravity(tmp.set(gravity.x, gravity.y));
	}

	public Vector2 getGravity () {
		Vec2 gravity = world.getGravity();
		return tmp2.set(gravity.x, gravity.y);
	}

	/** Is the world locked (in the middle of a time step). */
	public boolean isLocked () {
		return world.isLocked();
	}

	/** Set flag to control automatic clearing of forces after each time step. */
	public void setAutoClearForces (boolean flag) {
		world.setAutoClearForces(flag);
	}

	/** Get the flag that controls automatic clearing of forces after each time step. */
	public boolean getAutoClearForces () {
		return world.getAutoClearForces();
	}

	/** Query the world for all fixtures that potentially overlap the provided AABB.
	 * @param callback a user implemented callback class.
	 * @param lowerX the x coordinate of the lower left corner
	 * @param lowerY the y coordinate of the lower left corner
	 * @param upperX the x coordinate of the upper right corner
	 * @param upperY the y coordinate of the upper right corner */
	AABB aabb = new AABB();

	public void QueryAABB (final QueryCallback callback, float lowerX, float lowerY, float upperX, float upperY) {
		// FIXME pool QueryCallback?
		aabb.lowerBound.set(lowerX, lowerY);
		aabb.upperBound.set(upperX, upperY);
		world.queryAABB(new org.jbox2d.callbacks.QueryCallback() {
			@Override
			public boolean reportFixture (org.jbox2d.dynamics.Fixture f) {
				Fixture fixture = fixtures.get(f);
				return callback.reportFixture(fixture);
			}
		}, aabb);
	}

	/** Returns the list of {@link Contact} instances produced by the last call to {@link #step(float, int, int)}. Note that the
	 * returned list will have O(1) access times when using indexing. contacts are created and destroyed in the middle of a time
	 * step. Use {@link ContactListener} to avoid missing contacts
	 * @return the contact list */
	Array<Contact> contacts = new Array<Contact>();

	public Array<Contact> getContactList () {
		// FIXME pool contacts
		org.jbox2d.dynamics.contacts.Contact contactList = world.getContactList();
		contacts.clear();
		while (contactList != null) {
			Contact contact = new Contact(this, contactList);
			contacts.add(contact);
			contactList = contactList.m_next;
		}
		return contacts;
	}

	/** @return all bodies currently in the simulation */
	public void getBodies (Array<Body> bodies) {
		bodies.clear();
		bodies.ensureCapacity(this.bodies.size);
		for (Iterator<Body> iter = this.bodies.values(); iter.hasNext();) {
			bodies.add(iter.next());
		}
	}

	/** @return all joints currently in the simulation */
	public void getJoints (Array<Joint> joints) {
		joints.clear();
		joints.ensureCapacity(this.joints.size);
		for (Iterator<Joint> iter = this.joints.values(); iter.hasNext();) {
			joints.add(iter.next());
		}
	}

	public void dispose () {
	}

	/** Sets the box2d velocity threshold globally, for all World instances.
	 * @param threshold the threshold, default 1.0f */
	public static void setVelocityThreshold (float threshold) {
		Settings.velocityThreshold = threshold;
	}

	/** @return the global box2d velocity threshold. */
	public static float getVelocityThreshold () {
		return Settings.velocityThreshold;
	}

	/** Ray-cast the world for all fixtures in the path of the ray. The ray-cast ignores shapes that contain the starting point.
	 * @param callback a user implemented callback class.
	 * @param point1 the ray starting point
	 * @param point2 the ray ending point */
	Vec2 point1 = new Vec2();
	Vec2 point2 = new Vec2();
	Vector2 point = new Vector2();
	Vector2 normal = new Vector2();

	public void rayCast (final RayCastCallback callback, Vector2 point1, Vector2 point2) {
		// FIXME pool RayCastCallback?
		world.raycast(new org.jbox2d.callbacks.RayCastCallback() {
			@Override
			public float reportFixture (org.jbox2d.dynamics.Fixture f, Vec2 p, Vec2 n, float fraction) {
				return callback.reportRayFixture(fixtures.get(f), point.set(p.x, p.y), normal.set(n.x, n.y), fraction);
			}
		}, this.point1.set(point1.x, point1.y), this.point2.set(point2.x, point2.y));
	}
}
