if(window.Asteroids == null)
	window.Asteroids= {};

Asteroids.Engine= function(scene)
{
	this.scene= scene;
	this.collisionClock= scene.getElement("collisionClock");
	this.collisionClock.addEventListener("Completed", $delegate(this, this.handleCollisionTimeout));
	this.collisionProgressBar= this.collisionClock.findName("collisionProgressBar");
	this.collisionProgressBar.x= 1 << 30;
	this.collisionProgressAnimation= this.collisionClock.children.getItem(0);
	this.explosionSounds= [
		scene.getElement("explosionSound1"),
		scene.getElement("explosionSound2"),
		scene.getElement("explosionSound3") ];
	this.explosionIndex= Math.floor(Math.random() * this.explosionSounds.length);
	this.extraShipLimit= EXTRA_SHIP_SCORE;
	this.lastShotTime= 0;
	this.cloudIndex= 0;
	this.bullets= [];
	this.visibleBullets= {};
	this.rocks= [];
	this.visibleRocks= {};
	this.collision= { timerId: -1 };
	this.alienSpawnClock= scene.getElement("alienSpawnClock");
	this.alienSpawnClock.addEventListener("Completed", $delegate(this, this.spawnAlien));
	this.kaboom= scene.getElement("kaboom");
	this.kaboomStoryboard= this.kaboom.findName("kaboomStoryboard");
	var fn= $delegate(this, this.handleKaboomCompleted);
	this.kaboomStoryboard.addEventListener("Completed", fn);
	this.respawnClock= scene.getElement("respawnClock");
	this.respawnClock.addEventListener("Completed", fn);
	this.roundClock= scene.getElement("roundClock");
	this.roundClock.addEventListener("Completed", $delegate(this, this.startNextRound));
	
	// Create the alien.
	var fn= $delegate(this, this.handleObjectWrapped);
	this.alien= new Asteroids.Alien(scene, fn,
			$delegate(this, this.handleShotClockCompleted));
	
	// Create all bullets.
	for(var i= 0; i < MAX_BULLETS; ++i)
	{
		this.bullets.push(new Asteroids.Bullet(scene, i, fn,
			$delegate(this, this.handleBulletCompleted)));
	}
	
	// Create all rocks.
	for(var i= 0; i < TOTAL_ROCKS; ++i)
		this.rocks.push(new Asteroids.Rock(scene, i, fn));
	
	// Create the player's ship.
	this.ship= new Asteroids.Ship(scene, fn,
		$delegate(this, this.handleShipMoved));
	
	// Create the collision handler delegates.
	this.collisionHandlers= {};
	this.collisionHandlers[Asteroids.Alien]= {};
	this.collisionHandlers[Asteroids.Alien][Asteroids.Bullet]= $delegate(this, this.handleCollidingAlienBullet);
	this.collisionHandlers[Asteroids.Alien][Asteroids.Rock]= $delegate(this, this.handleCollidingAlienRock);
	this.collisionHandlers[Asteroids.Alien][Asteroids.Ship]= $delegate(this, this.handleCollidingAlienShip);
	this.collisionHandlers[Asteroids.Bullet]= {};
	this.collisionHandlers[Asteroids.Bullet][Asteroids.Alien]= this.collisionHandlers[Asteroids.Alien][Asteroids.Bullet];
	this.collisionHandlers[Asteroids.Bullet][Asteroids.Rock]= $delegate(this, this.handleCollidingBulletRock);
	this.collisionHandlers[Asteroids.Bullet][Asteroids.Ship]= $delegate(this, this.handleCollidingBulletShip);
	this.collisionHandlers[Asteroids.Rock]= {};
	this.collisionHandlers[Asteroids.Rock][Asteroids.Alien]= this.collisionHandlers[Asteroids.Alien][Asteroids.Rock];
	this.collisionHandlers[Asteroids.Rock][Asteroids.Bullet]= this.collisionHandlers[Asteroids.Bullet][Asteroids.Rock];
	this.collisionHandlers[Asteroids.Rock][Asteroids.Ship]= $delegate(this, this.handleCollidingRockShip);
	this.collisionHandlers[Asteroids.Ship]= {};
	this.collisionHandlers[Asteroids.Ship][Asteroids.Alien]= this.collisionHandlers[Asteroids.Alien][Asteroids.Ship];
	this.collisionHandlers[Asteroids.Ship][Asteroids.Bullet]= this.collisionHandlers[Asteroids.Bullet][Asteroids.Ship];
	this.collisionHandlers[Asteroids.Ship][Asteroids.Rock]= this.collisionHandlers[Asteroids.Rock][Asteroids.Ship];
}

Asteroids.Engine.prototype.attract= function()
{
	// Launch some big rocks.
	for(var i= 0; i < MIN_START_ROCKS; ++i)
		this.launchRock(0);
}

Asteroids.Engine.prototype.centerIsClear= function()
{
	var t= this;
	var phantomShip= { x: (RIGHT_EDGE + LEFT_EDGE) / 2, y: (BOTTOM_EDGE + TOP_EDGE) / 2,
		radius: t.ship.radius, dx: 0, dy: 0 };
	var fn= Asteroids.SceneObject.prototype.getCollisionTime;
	var isClear= true;
	foreach(Asteroids.Bullet, t.visibleBullets, function(bullet)
	{
		var time= fn.call(phantomShip, bullet);
		isClear &= time >= t.respawnAllowanceTime;
	});
	foreach(Asteroids.Rock, t.visibleRocks, function(rock)
	{
		var time= fn.call(phantomShip, rock);
		isClear &= time >= t.respawnAllowanceTime;
	});
	return isClear;
}

Asteroids.Engine.prototype.explode= function(subject, object)
{
	var length= this.explosionSounds.length;
	this.explosionIndex= Math.floor(Math.random() * (length - 1) + 1 + this.explosionIndex) % length;
	var sound= this.explosionSounds[this.explosionIndex];
	sound.source= sound.source;
	sound.autoPlay= true;
	if(object != null)
	{
		var x= (subject.x + object.x) / 2, y= (subject.y + object.y) / 2;
		var cloud= this.scene.getElement("cloud" + this.cloudIndex);
		cloud["Canvas.Left"]= x;
		cloud["Canvas.Top"]= y;
		cloud.Visibility= "Visible";
		var cloudRotate= this.scene.getElement("cloudRotate" + this.cloudIndex);
		cloudRotate.angle= 360 * Math.random();
		var cloudStoryboard= this.scene.getElement("cloudStoryboard" + this.cloudIndex);
		cloudStoryboard.begin();
		this.cloudIndex= (this.cloudIndex + 1) % MAX_PARTICLE_CLOUDS;
	}
	else
		Debug.assert(subject == null);
}

Asteroids.Engine.prototype.getBulletCollision= function(sceneObject, first)
{
	var t= this;
	foreach(Asteroids.Bullet, t.visibleBullets, function(bullet)
	{
		t.getCollision(sceneObject, bullet, first);
	});
}

Asteroids.Engine.prototype.getCollision= function(subject, object, first)
{
	var time= subject.getCollisionTime(object);
	if(time < first.time)
	{
		first.time= time;
		first.subject= subject;
		first.object= object;
	}
}

Asteroids.Engine.prototype.getRockCollision= function(sceneObject, first)
{
	var t= this;
	foreach(Asteroids.Rock, t.visibleRocks, function(rock)
	{
		t.getCollision(sceneObject, rock, first);
	});
}

Asteroids.Engine.prototype.handleBulletCompleted= function(bullet)
{
	if(!bullet.isVisible)
		return;
	this.recoverBullet(bullet);
	this.planCollisions();
}

Asteroids.Engine.prototype.handleCollidingAlienBullet= function(subject, object)
{
	var bullet= subject === this.alien ? object : subject;
	this.alien.stop();
	if(bullet.shotByPlayer)
		this.updateScore(ALIEN_SCORE);
	this.recoverBullet(bullet);
}

Asteroids.Engine.prototype.handleCollidingAlienRock= function(subject, object)
{
	var rock= subject === this.alien ? object : subject;
	this.alien.stop();
	this.split(rock, false);
}

Asteroids.Engine.prototype.handleCollidingAlienShip= function(subject, object)
{
	this.alien.stop();
	this.updateScore(ALIEN_SCORE);
	this.startKaboom();
}

Asteroids.Engine.prototype.handleCollidingBulletRock= function(subject, object)
{
	var bullet= subject instanceof Asteroids.Bullet ? subject : object;
	var rock= subject === bullet ? object : subject;
	this.recoverBullet(bullet);
	this.split(rock, bullet.shotByPlayer);
}

Asteroids.Engine.prototype.handleCollidingBulletShip= function(subject, object)
{
	var bullet= subject === this.ship ? object : subject;
	this.recoverBullet(bullet);
	this.startKaboom();
}

Asteroids.Engine.prototype.handleCollidingRockShip= function(subject, object)
{
	var rock= subject === this.ship ? object : subject;
	this.split(rock, true);
	this.startKaboom();
}

Asteroids.Engine.prototype.handleCollisionTimeout= function()
{
	var subject= this.collision.subject, object= this.collision.object;
	if(subject.hasCollidedWith(object))
	{
		this.explode(subject, object);
		this.collisionHandlers[subject.constructor][object.constructor](subject, object);
	}
	this.planCollisions();
}

Asteroids.Engine.prototype.handleKaboomCompleted= function(sender)
{
	this.hideKaboom();
	if(this.shipCount >= 0)
	{
		if(this.centerIsClear())
		{
			this.scene.updateShipsAvailable(this.shipCount);
			this.ship.start((RIGHT_EDGE + LEFT_EDGE) / 2, (BOTTOM_EDGE + TOP_EDGE) / 2);
			this.planCollisions();
		}
		else
		{
			this.respawnAllowanceTime= Math.max(this.respawnAllowanceTime - RESPAWN_ALLOWANCE_DECREMENT,
				MIN_RESPAWN_ALLOWANCE_TIME);
			this.respawnClock.begin();
		}
	}
}

Asteroids.Engine.prototype.handleObjectWrapped= function(sceneObject)
{
	var first= { time: Number.POSITIVE_INFINITY };
	if(this.ship.isBallistic && sceneObject !== this.ship)
		this.getCollision(this.ship, sceneObject, first);
	if(this.alien.isVisible && sceneObject !== this.alien)
		this.getCollision(this.alien, sceneObject, first);
	if(!(sceneObject instanceof Asteroids.Bullet))
		this.getBulletCollision(sceneObject, first);
	if(!(sceneObject instanceof Asteroids.Rock))
		this.getRockCollision(sceneObject, first);
	if(first.time < this.collisionProgressBar.x)
	{
		this.collisionClock.stop();
		this.collision= first;
		this.collisionProgressAnimation.duration.seconds= first.time;
		this.collisionProgressAnimation.from= first.time;
		this.collisionClock.begin();
	}
}

Asteroids.Engine.prototype.handleShipMoved= function(ship)
{
	for(var e in this.visibleBullets)
	{
		var bullet= this.visibleBullets[e];
		if(bullet instanceof Asteroids.Bullet && ship.hasCollidedWith(bullet))
			return this.handleCollidingBulletShip(bullet, this.ship);
	}
	for(var e in this.visibleRocks)
	{
		var rock= this.visibleRocks[e];
		if(rock instanceof Asteroids.Rock && ship.hasCollidedWith(rock))
			return this.handleCollidingRockShip(rock, this.ship);
	}
}

Asteroids.Engine.prototype.handleShotClockCompleted= function(sender)
{
	if(this.alien.isVisible)
	{
		var bullet= this.bullets.pop();
		Debug.assert(bullet != null && !bullet.isVisible);
		bullet.shotByPlayer= false;
		var alien= this.alien, ux, uy;
		if(alien.isSmall)
		{
			ux= this.ship.x - alien.x;
			uy= this.ship.y - alien.y;
			var m= Math.sqrt(ux * ux + uy * uy);
			ux /= m;
			uy /= m;
		}
		else
		{
			var angle= 2 * Math.PI * Math.random();
			ux= Math.cos(angle);
			uy= Math.sin(angle);
		}
		var x= alien.x, y= alien.y, d= alien.radius + 1;
		x += ux * d;
		y += uy * d;
		bullet.start(x, y, ux, uy, ux, uy);
		Debug.assert(this.visibleBullets[bullet.index] == null);
		this.visibleBullets[bullet.index]= bullet;
		this.scene.playShotSound(true);
		this.planCollisions();
		sender.begin();
	}
}

Asteroids.Engine.prototype.hideKaboom= function()
{
	this.kaboom.visibility= "Collapsed";
	this.kaboomStoryboard.stop();
}

Asteroids.Engine.prototype.jumpShip= function()
{
	if(this.ship.isVisible)
		this.ship.jump();
}

Asteroids.Engine.prototype.launchRock= function(rockSizeIndex, x, y)
{
	var rock= this.rocks.pop();
	this.visibleRocks[rock.index]= rock;
	rock.start(rockSizeIndex, x, y);
}

Asteroids.Engine.prototype.pause= function()
{
	this.kaboomStoryboard.pause();
	this.collisionClock.pause();
	this.thrustShip(false);
	this.turnShip(0);
	this.ship.pause();
	foreach(Asteroids.Rock, this.visibleRocks, function(rock)
	{
		rock.pause();
	});
	foreach(Asteroids.Bullet, this.visibleBullets, function(bullet)
	{
		bullet.pause();
	});
	this.alien.pause();
	this.alienSpawnClock.pause();
}

Asteroids.Engine.prototype.planCollisions= function()
{
	var t= this;
	t.collisionClock.stop();
	var first= { time: Number.POSITIVE_INFINITY };
	if(t.ship.isBallistic)
	{
		if(t.alien.isVisible)
			t.getCollision(t.ship, t.alien, first);
		t.getBulletCollision(t.ship, first);
		t.getRockCollision(t.ship, first);
	}
	if(t.alien.isVisible)
	{
		t.getBulletCollision(t.alien, first);
		t.getRockCollision(t.alien, first);
	}
	foreach(Asteroids.Bullet, t.visibleBullets, function(bullet)
	{
		t.getRockCollision(bullet, first);
	});
	if(first.time < Number.POSITIVE_INFINITY)
	{
		t.collision= first;
		t.collisionProgressAnimation.duration.seconds= first.time;
		t.collisionProgressAnimation.from= first.time;
		t.collisionClock.begin();
	}
}

Asteroids.Engine.prototype.recoverBullet= function(bullet)
{
	if(bullet.shotByPlayer)
	{
		Debug.assert(this.bulletCount > 0);
		--this.bulletCount;
	}
	bullet.stop();
	Debug.assert(this.visibleBullets[bullet.index] != null && !bullet.isVisible);
	delete this.visibleBullets[bullet.index];
	this.bullets.push(bullet);
}

Asteroids.Engine.prototype.resume= function()
{
	this.alienSpawnClock.resume();
	this.alien.resume();
	this.kaboomStoryboard.resume();
	foreach(Asteroids.Bullet, this.visibleBullets, function(bullet)
	{
		bullet.resume();
	});
	foreach(Asteroids.Rock, this.visibleRocks, function(rock)
	{
		rock.resume();
	});
	this.ship.resume();
	this.collisionClock.resume();
}

Asteroids.Engine.prototype.shoot= function()
{
	if(this.ship.isVisible)
	{
		var now= new Date().valueOf();
		if(now - this.lastShotTime >= BULLET_DELAY && this.bulletCount < MAX_PLAYER_BULLETS)
		{
			++this.bulletCount;
			var bullet= this.bullets.pop();
			Debug.assert(bullet != null && !bullet.isVisible);
			bullet.shotByPlayer= true;
			this.lastShotTime= now;
			var ship= this.ship;
			var x= ship.x, y= ship.y, u= ship.getUnits(), d= ship.radius + 1;
			x += u.x * d;
			y += u.y * d;
			bullet.start(x, y, u.x, u.y, ship.dx, ship.dy);
			Debug.assert(this.visibleBullets[bullet.index] == null);
			this.visibleBullets[bullet.index]= bullet;
			this.scene.playShotSound(false);
			this.planCollisions();
		}
	}
}

Asteroids.Engine.prototype.showAlien= function(wantsSmall)
{
	this.alien.start(wantsSmall);
}

Asteroids.Engine.prototype.spawnAlien= function()
{
	this.showAlien(Math.random() / this.roundNumber < SMALL_ALIEN_PROBABILITY);
	this.alienSpawnClock.begin();
}

Asteroids.Engine.prototype.split= function(rock, isScored)
{
	// Update the score, remove the rock, and add smaller rocks if necessary.
	var parameters= Asteroids.Rock.PARAMETERS[rock.sizeIndex];
	var progress= ++this.rocksDestroyed / this.rocksToDestroy;
	this.scene.updateProgress(progress);
	if(isScored)
		this.updateScore(parameters.value);
	var count= parameters.fragments, rockSizeIndex= rock.sizeIndex + 1;
	var x= rock.translateTransform.x, y= rock.translateTransform.y;
	rock.stop();
	delete this.visibleRocks[rock.index];
	this.rocks.push(rock);
	for(var i= 0; i < count; ++i)
		this.launchRock(rockSizeIndex, x, y);
	if(progress == 1)
	{
		this.scene.stopHeartbeat();
		this.roundClock.begin();
	}
}

Asteroids.Engine.prototype.startGame= function()
{
	var t= this;
	
	// Clear the field.
	t.alien.stop();
	t.hideKaboom();
	foreach(Asteroids.Bullet, t.visibleBullets, function(bullet)
	{
		bullet.stop();
		t.bullets.push(bullet);
	});
	t.visibleBullets= {};
	t.bulletCount= 0;
	foreach(Asteroids.Rock, t.visibleRocks, function(rock)
	{
		rock.stop();
		t.rocks.push(rock);
	});
	t.visibleRocks= {};
	
	// Show all available ships.
	t.scene.updateShipsAvailable(t.shipCount= START_LIVES - 1);
	
	// Show a zero score.
	t.scene.updateProgress(0);
	t.scene.updateScore(this.score= 0);
	t.roundNumber= -1;
	
	// Show the ship in the center of the screen.
	t.ship.start((RIGHT_EDGE + LEFT_EDGE) / 2, (BOTTOM_EDGE + TOP_EDGE) / 2);
	
	t.startNextRound();
}

Asteroids.Engine.prototype.startKaboom= function()
{
	this.kaboom.findName("kaboomRotateTransform").angle= this.ship.rotateTransform.angle;
	var kaboomTranslateTransform= this.kaboom.findName("kaboomTranslateTransform");
	kaboomTranslateTransform.x= this.ship.x.valueOf();
	kaboomTranslateTransform.y= this.ship.y.valueOf();
	this.ship.stop();
	this.kaboom.visibility= "Visible";
	if(--this.shipCount < 0)
	{
		this.alienSpawnClock.stop();
		this.scene.endGame();
	}
	else
		this.respawnAllowanceTime= MAX_RESPAWN_ALLOWANCE_TIME;
	this.kaboomStoryboard.begin();
}

Asteroids.Engine.prototype.startNextRound= function()
{
	if(++this.roundNumber > 0)
	{
		// Set the alien appearance timer.
		this.alienSpawnClock.duration.seconds= MIN_ALIEN_SPAWN_TIME + MAX_ALIEN_SPAWN_TIME / this.roundNumber;
		this.alienSpawnClock.begin();
	}
	
	// Add new rocks.
	var startingRockCount= Math.min(this.roundNumber + MIN_START_ROCKS, MAX_START_ROCKS);
	this.rocksToDestroy= startingRockCount * (1 + MEDIUM_FRAGMENTS * (1 + SMALL_FRAGMENTS));
	this.rocksDestroyed= 0;
	this.scene.startHeartbeat();
	for(var i= 0; i < startingRockCount; ++i)
		this.launchRock(0);
	
	// Plan the initial collisions.
	this.planCollisions();
}

Asteroids.Engine.prototype.thrustShip= function(thrusting)
{
	this.ship.thrust(thrusting);
}

Asteroids.Engine.prototype.turnShip= function(direction)
{
	this.ship.turn(direction);
}

Asteroids.Engine.prototype.updateScore= function(increment)
{
	if((this.score += increment) >= this.extraShipLimit)
	{
		// Award another ship.
		this.extraShipLimit += EXTRA_SHIP_SCORE;
		var extraShipSound= this.scene.getElement("extraShipSound");
		extraShipSound.source= extraShipSound.source;
		extraShipSound.autoPlay= true;
		this.scene.updateShipsAvailable(++this.shipCount);
	}
	this.scene.updateScore(this.score);
}

