'use strict';

// This file is very similar, but not identical to its client counterpart
// so most changes done to it should be mirrored there to keep consistent
// physics between client and server.

// Note:
// b2Body.GetWorldPoint is broken or something and to get it to work, you
// must pass an object as the second argument. I don't think it matters what
// object but numbers and strings don't work, so do something like
// b2body.GetWorldPoint(new b2Vec2(x, y), {});

const SCALE = 32;

const Box2D = require('box2d-html5');

const b2Vec2 = Box2D.b2Vec2;

class Physics {
	constructor() {
		this.world = new Box2D.b2World(new b2Vec2(0, 0), false);
		this.toRemove = [];
		this.toWeld = [];
		this.contacts = [];
		this.idk = false;

		let onContact = contact => {
			let bodya = contact.GetFixtureA().GetBody().GetUserData();
			let bodyb = contact.GetFixtureB().GetBody().GetUserData();

			this.contacts.push([bodya, bodyb, contact]);
		}

		let listener = new Box2D.b2ContactListener();
		listener.BeginContact = onContact;
		//listener.EndContact = onContact;

		this.world.SetContactListener(listener);
	}

	createBody(body) {
		//console.log(Object.keys(Box2D.b2Settings).sort());
		//console.log(Box2D.b2Settings.b2_linearSleepTolerance = 0.002);
		let s = SCALE;
		let bodyDef = new Box2D.b2BodyDef();
		bodyDef.userData = body;
		bodyDef.position = new b2Vec2(body.x || 0, body.y || 0);
		bodyDef.angle = body.r || 0;
		bodyDef.fixedRotation = false;
		bodyDef.active = true;
		bodyDef.linearVelocity = new b2Vec2(body.xvel || 0, body.yvel || 0);
		bodyDef.angularVelocity = body.rvel || 0;
		bodyDef.bullet = body.type == 'missile';
		bodyDef.linearDamping = body.type == 'asteroid' ? 0.003 : 0.01;
		bodyDef.angularDamping = body.type == 'asteroid' ? 0.003 : 0.06;
		bodyDef.type = body.type == 'structure' ?
			Box2D.b2BodyType.b2_staticBody : Box2D.b2BodyType.b2_dynamicBody;
		if (body.player) bodyDef.allowSleep = false;
		let b2body = this.world.CreateBody(bodyDef);

		let fixtureDef = new Box2D.b2FixtureDef();
		fixtureDef.density = 10.1;
		fixtureDef.friction = 1;
		fixtureDef.restitution = 1;

		for (var poly of body.frame) {
			poly = poly.map(vertex => new b2Vec2(vertex[0] / s, vertex[1] / s));
			fixtureDef.shape = new Box2D.b2PolygonShape();
			fixtureDef.shape.SetAsArray(poly, poly.length);
			b2body.CreateFixture(fixtureDef);
		}

		body.b2body = b2body;

		//if (body.type == 'ship') console.log(b2body.GetLocalCenter());
	}

	// TODO: Make this shorter somehow.
	createCopula(copula) {
		let s = SCALE;

		if (copula.type == 'rope') {
			let b1 = copula.bodyA.b2body;
			let b2 = copula.bodyB.b2body;
			let p1 = new b2Vec2(copula.pointA.x, copula.pointA.y);
			let p2 = new b2Vec2(copula.pointB.x, copula.pointB.y);
			// See top of file.
			let start = b1.GetWorldPoint(p1, {});
			let end = b2.GetWorldPoint(p2, {});
			copula.bodyA.debug = p1;
			//copula.bodyB.debug = end;
			let dx = start.x - end.x
			let dy = start.y - end.y;
			let len = Math.sqrt(dx * dx + dy * dy);

			let ropeDef = new Box2D.b2RopeJointDef();
			ropeDef.bodyA = b1;
			ropeDef.bodyB = b2;
			ropeDef.maxLength = copula.initLength || len;
			ropeDef.collideConnected = true;
			ropeDef.localAnchorA = p1;
			ropeDef.localAnchorB = p2;
			let b2joint = this.world.CreateJoint(ropeDef);

			copula.b2joint = b2joint;
		}

	}

	weld(bodyA, bodyB, point) {
		let b1 = bodyA.b2body;
		let b2 = bodyB.b2body;
		let jointDef = new Box2D.b2WeldJointDef();
		let anchor = b1.GetWorldPoint(new b2Vec2(point.x, point.y), {});
		jointDef.bodyA = b1;
		jointDef.bodyB = b2;
		jointDef.collideConnected = true;
		jointDef.localAnchorA = b1.GetLocalPoint(anchor, {});
		jointDef.localAnchorB = b2.GetLocalPoint(anchor, {});
		jointDef.localAnchorA = new b2Vec2(jointDef.localAnchorA.x, jointDef.localAnchorA.y);
		jointDef.localAnchorB = new b2Vec2(jointDef.localAnchorB.x, jointDef.localAnchorB.y);
		jointDef.referenceAngle = b2.GetAngleRadians() - b1.GetAngleRadians();
		this.world.CreateJoint(jointDef);
	}

	contactData(contact) {
		let worldManifold = new Box2D.b2WorldManifold();
		contact.GetWorldManifold(worldManifold);
		let localManifold = contact.GetManifold();
		let worldNormal = worldManifold.normal;
		let normal = localManifold.localNormal;

		return {
			normal: normal,
			worldNormal: worldNormal
		};
	}

	raycast(start, end) {
		let p1 = new b2Vec2(start.x, start.y);
		let p2 = new b2Vec2(end.x, end.y);
		let dx = p1.x - p2.x;
		let dy = p1.y - p2.y;
		let dis = Math.sqrt(dx * dx + dy * dy);
		let closest = {
			fraction: 1
		};
		let i = 0;
		this.world.RayCast((fixture, point, normal, fraction) => {
			let body = fixture.GetBody().GetUserData();
			if (fraction <= closest.fraction) {
				closest = {
					body: body,
					fraction: fraction,
					point: point,
					dis: dis * fraction
				}
			}
			return fraction;
		}, p1, p2);
		return closest.body ? closest : false;
	}

	remove(body) {
		this.toRemove.push(body);
	}

	removeCopula(copula) {
		this.toRemove.push.apply(this.toRemove, copula.bodies);
	}

	step() {
		this.world.Step(1, 5, 1 / 60);

		for (var i = 0; i < this.toRemove.length; i++) {
			this.world.DestroyBody(this.toRemove[i].b2body);
		}

		this.toRemove = [];

		for (var i = 0; i < this.contacts.length; i++) {
			let contact = this.contacts[i][2];
			let bodya = this.contacts[i][0];
			let bodyb = this.contacts[i][1];

			if (bodya) {
				bodya.applyDelta();
				bodya.contact(bodyb, contact);
			}

			if (bodyb) {
				bodyb.applyDelta();
				bodyb.contact(bodya, contact);
			}
		}

		this.contacts = [];
	}
}

module.exports = Physics;
