//----------------------------------------------------------------------------------
// GENERAL
//----------------------------------------------------------------------------------

//Constants
JSGL_KEY_LEFT = 37;
JSGL_KEY_RIGHT = 39;

//Cross-browser function to set inner text
function setTextContent(obj,text) {
	if (!obj.innerText && obj.innerText != '') obj.textContent = text;
	else obj.innerText = text;
}

//Prints to window title
function title(text) {
	document.title = text;
}

//JSGL_Vector Class
function JSGL_Vector(x,y,z) {

	//Default
	if (!x) x = 0;
	if (!y) y = 0;
	if (!z) z = 0;

	//Properties
	this.x = x;
	this.y = y;
	this.z = z;
	
	//Methods
	this.addVector = function (vector) {	//Adds another vetor to this vector
		this.x += vector.x;
		this.y += vector.y;
		this.z += vector.z;
	}
	this.subVector = function (vector) {	//Subtracts another vetor from this vector
		this.x -= vector.x;
		this.y -= vector.y;
		this.z -= vector.z;
	}
	this.mulScalar = function (value) {		//Multiplies by a scalar
		this.x *= value;
		this.y *= value;
		this.z *= value;
	}
	this.divScalar = function (value) {		//Divides by a scalar
		this.x /= value;
		this.y /= value;
		this.z /= value;
	}
	this.toOne = function () {				//Divides each element by its modulus
		this.x /= Math.abs(this.x);
		this.y /= Math.abs(this.y);
		this.z /= Math.abs(this.z);
	}
	this.norma = function () {				//Returns the norma of the vector
		return Math.sqrt(Math.pow(this.x,2)+Math.pow(this.y,2)+Math.pow(this.z,2));
	}
	this.orthonormal = function () {		//Transformes to orthonormal form
		this.divScalar(this.norma());
	}
}

//----------------------------------------------------------------------------------
// GRAPHICS
//----------------------------------------------------------------------------------

//JSGL_Tile Class
function JSGL_Tile(image,density,attrition) {

	//Default
	if (!image) image = '';
	if (!density) density = 0;
	if (!attrition) attrition = 0;

	//Properties
	this.image = new Image();	//Tile image object
	this.image.src = image;					//Tile image url
	this.density = density;					//Tile density (!passability) [0;1]
	this.attrition = attrition;				//Tile attrition [0;1]
	
	//Methods
	
	//Events
	this.onCollision = function(sender,obj,pos,intensity) {};	//Something has collided with the tile
	
}

//JSGL_TileSet Class
function JSGL_TileSet(tiles,width,height) {

	//Default
	if (!width) width = 16;
	if (!height) height = 16;
	if (!tiles) tiles = new Array();

	//Properties
	this.width = width;						//Default tile width
	this.height = height;					//Default tile height
	this.tiles = tiles;						//Tiles (JSGL_Tile) within the tileset
	
	//Methods
	this.pushTile = function (tile) {		//Adds a tile (JSGL_Tile) to the end of the tileset
		this.tiles.push(tile);
	}
	this.pullTile = function () {			//Removes and returns the last tile (JSGL_Tile)
		return this.tiles.pop();
	}
	this.merge = function (tileset) {		//Merges the tiles array with another tileset (JSGL_TileSet)
		this.tiles.concat(this.tiles,tileset.tiles);
	}
	this.length = function () {				//Returns how many tiles are within the tileset
		return this.tiles.length;
	}
	this.clear = function () {				//Clears the tileset
		this.tiles = new Array();
	}
	
	//Events
	
}

//JSGL_Layer Class
function JSGL_Layer(tileset,width,height,background,tiles,pos,color) {

	//Default
	if (!width) width = 16;
	if (!height) height = 16;
	if (!background) background = '';
	if (!color) color = '';
	if (!tileset) tileset = new JSGL_TileSet();
	if (!tiles) tiles = new Array();
	if (!pos) pos = new JSGL_Vector();

	//Properties
	this.tileset = tileset;					//Layer tileset (JSGL_TileSet)
	this.width = width;						//Layer width in tiles
	this.height = height;					//Layer height in tiles
	this.background = background;			//Layer background
	this.tiles = tiles;						//Tiles IDīs (from tileset) that forms the layer
	this.pos = pos;							//Layer posisiton
	this.color = color;						//Layer color
	this.div = null;						//HTML layer
	this.speed = new JSGL_Vector;			//Layer speed vector
	this.acceleration = new JSGL_Vector;	//Layer acceleration vector
	this.maxSpeed = new JSGL_Vector;		//Maximum modulus of speed
	this.pendulus = false;					//If true, inverts acceleration on maxSpeed
	this.angularSpeed = 0;					//Layer angular speed in radians
	this.maxAngularSpeed = 0;				//Maximum modulus of angular speed in radians
	this.angularAcceleration = 0;			//Layer angular acceleration in radians
	this.radius = 0;						//Radius for circular movement
	this.radian = 0;						//Current radian for circular movement
	this.minRadian = 0;						//Minimum radian for circular movement
	this.maxRadian = 2*Math.PI;				//Maximum radian for circular movement
	this.center = new JSGL_Vector();		//Center for circular movement
	
	//Methods
	this.getTileByPos = function(pos) {		//Returns tile at given position
		if (pos.z != this.pos.z) return null;
		var x = pos.x / this.tileset.width, y = pos.y / this.tileset.height;
		var id = y * this.width + x;
		return this.tiles[id];
	}
	this.draw = function () {				//Draws layer
		var i, j, img, div = document.createElement('DIV');
		var area = this.width * this.height;
		var pos = 0;
		for(i=0;i<this.height;i++){
		  	for(j=0;j<this.width;j++){
		  		if (this.tiles[pos] != null) {
		  			tile = this.tileset.tiles[this.tiles[pos]];
		  			img = new Image(this.tileset.width,this.tileset.height);
//					img = tile.image;
//					img.width = this.tileset.width;
//					img.height = this.tileset.height;
		  			img.style.position = 'absolute';
		  			img.style.top = i * this.tileset.width;
		  			img.style.left = j * this.tileset.height;
//		  			img.title = i+','+j+','+this.pos.z;
		  			img.src = tile.image.src;
		  			div.appendChild(img);
		  		}
		  		pos++;
		  	}
		}
		if (this.background) {
			bg = new Image();
			bg.src = this.background;
			bg.style.position = 'absolute';
			bg.style.width = this.width * this.tileset.width;
			bg.style.height = this.height * this.tileset.height;
			bg.style.zIndex = -1;
			div.appendChild(bg);
		}
		div.style.position = 'absolute';
		if (this.radius > 0) {
			div.style.left = this.center.x;
			div.style.top = this.center.y;
		}
		else {
			div.style.left = this.pos.x;
			div.style.top = this.pos.y;
		}
		div.style.zIndex = this.pos.z;
		div.style.backgroundColor = this.color; //Not supported
		this.div = div;
		document.getElementsByTagName('BODY')[0].appendChild(div);
	}
	this.moveTo = function(pos) {				//Moves layer to pos
		if (!pos) pos = new JSGL_Vector();
		this.pos = pos;
		this.div.style.left = this.pos.x + 'px';
		this.div.style.top = this.pos.y + 'px';
		this.div.style.zIndex = this.pos.z;
		this.onMove(this);
	}
	this.moveBy = function(dist) {				//Moves layer by dist
		if (!dist) dist = new JSGL_Vector();
		this.pos.addVector(dist);
		this.div.style.left = this.pos.x + 'px';
		this.div.style.top = this.pos.y + 'px';
		this.div.style.zIndex = this.pos.z;
		this.onMove(this);
	}
	this.loop = function() {				//Executes on each loop
		this.speed.addVector(this.acceleration);
		this.angularSpeed += this.angularAcceleration;
		if (Math.abs(this.speed.x) > this.maxSpeed.x) {
			var sign = this.speed.x / Math.abs(this.speed.x);
			this.speed.x = this.maxSpeed.x * sign;
			if (this.pendulus) this.acceleration.x *= -1;
		}
		if (Math.abs(this.speed.y) > this.maxSpeed.y) {
			var sign = this.speed.y / Math.abs(this.speed.y);
			this.speed.y = this.maxSpeed.y * sign;
			if (this.pendulus) this.acceleration.y *= -1;
		}
		if (Math.abs(this.speed.z) > this.maxSpeed.z) {
			var sign = this.speed.z / Math.abs(this.speed.z);
			this.speed.z = this.maxSpeed.z * sign;
			if (this.pendulus) this.acceleration.z *= -1;
		}
		if (Math.abs(this.angularSpeed) > this.maxAngularSpeed || this.radian < this.minRadian || this.radian > this.maxRadian) {
			var sign = this.angularSpeed / Math.abs(this.angularSpeed);
			this.angularSpeed = this.maxAngularSpeed * sign;
			if (this.pendulus) this.angularAcceleration *= -1;
		}
		if (this.radius > 0) {
			this.radian += this.angularSpeed;
			this.radian = Math.abs(this.radian % (2*Math.PI));
			if (this.radian < this.minRadian) this.radian = this.minRadian;
			if (this.radian > this.maxRadian) this.radian = this.maxRadian;
			this.moveTo(new JSGL_Vector(this.center.x+Math.cos(this.radian)*this.radius,this.center.y+Math.sin(this.radian)*this.radius,this.pos.z));
		}
		else this.moveBy(this.speed);

		if (this.speed.norma() > this.maxSpeed) this.speed = this.maxSpeed;
		if (this.angularSpeed > this.maxAngularSpeed) this.angularSpeed = this.maxAngularSpeed;

		this.onLoop(this);
	}
	
	//Events
	this.onCollision = function(sender,obj,pos,intensity) {};	//Has collided with something
	this.onMove = function (sender) {}
	this.onLoop = function (sender) {}
}

//JSGL_Action
function JSGL_Action(trigger,animation,sound,continuousSound,force) {

	//Default
	if (!trigger) trigger = 0;
	if (!animation) animation = '';
	if (!sound) sound = '';
	if (!continuousSound) continuousSound = '';
	if (!force) force = new JSGL_Vector();

	//Properties
	this.trigger = trigger;					//Activation key
	this.animation = animation;				//Animation for action
	this.sound = sound;						//Sound when action started
	this.continuousSound = continuousSound;	//Sound during action
	this.force = force;						//Force over actor during that action
	
	//Methods

	//Events

}

//JSGL_Actor
function JSGL_Actor(width,height,pos) {

	//Default
	if (!pos) pos = new JSGL_Vector();

	//Properties
	this.height = width;						//Actor height
	this.width = height;						//Actor width
	this.resistance = 0;						//Used for calculating damage
	this.life = 1;								//Dies when 0
	this.weight = 1;							//Used for calculating acceleration
	this.pos = pos;								//Actor position
	this.speed = new JSGL_Vector();				//Actor speed
	this.acceleration = new JSGL_Vector();		//Actor acceleration
	this.actions = new Array();					//Possible actions
	this.action = 0;							//Current action
	this.physics = true;						//Affected by external forces?
	this.controllable = false;					//Can be controlled by user?
	this.image = null;							//Actor image object
	
	//Methods
	this.collision = function (obj) {			//Tests collision with another object
		return (this.pos.z == obj.pos.z &&
				this.pos.x <= obj.pos.x + obj.width &&
				this.pos.x + this.width >= obj.pos.x &&
				this.pos.y <= obj.pos.y + obj.height &&
				this.pos.y + this.height >= obj.pos.y);
	}
	this.draw = function() {					//Draws actor
		var img = new Image(this.width,this.height);
		img.style.position = 'absolute';
		img.style.left = this.pos.x;
		img.style.top = this.pos.y;
		img.style.zIndex = this.pos.z;
		img.src = this.actions[this.action].animation;
		this.image = img;
		document.getElementsByTagName('BODY')[0].appendChild(img);
	}
	this.moveTo = function(pos) {				//Moves actor to pos
		if (!pos) pos = new JSGL_Vector();
		this.pos = pos;
		this.image.style.left = this.pos.x + 'px';
		this.image.style.top = this.pos.y + 'px';
		this.image.style.zIndex = this.pos.z;
		this.onMove(this);
	}
	this.moveBy = function(dist) {				//Moves actor by dist
		if (!dist) dist = new JSGL_Vector();
		this.pos.addVector(dist);
		this.image.style.left = this.pos.x + 'px';
		this.image.style.top = this.pos.y + 'px';
		this.image.style.zIndex = this.pos.z;
		this.onMove(this);
	}
	this.loop = function (game) {				//Actor main loop
		if (this.weight == 0) this.weight = 1;
		this.action = 0;
		for (i=0;i<this.actions.length;i++) {
			if (this.actions[i].trigger == game.keyboard.key) this.action = i;
		}
		this.acceleration = this.actions[this.action].force;
		for (i=0;i<game.map.forces.length;i++) {
			this.acceleration.addVector(game.map.forces[i]);
		}
		this.acceleration.divScalar(this.weight);
		this.speed.addVector(this.acceleration);
//		title(this.acceleration.x);
		//Layer collision
		for (i=0;i<game.map.layers.length;i++) {
			if (this.collision(game.map.layers[i])) {
				this.speed = new JSGL_Vector();
//				this.speed.divScalar(-1);
//				tile = game.map.layers[i].getTileByPos(this.pos);
//				title(tile.image.src);
			}
		}
		//Actor collision
		
		this.moveBy(this.speed);
		this.onLoop(this);
	}

	//Events
	this.onLoop = function (sender) {}							//Actor main loop
	this.onMove = function (sender) {}							//Actor moves
	this.onCollision = function (sender,obj,pos,intensity) {} 	//Actor has collided with something
	this.onBorn = function (sender) {}						  	//Actor has born
	this.onDie = function (sender) {}						  	//Actor has died (life <= 0)

	this.onBorn(this);

}

//JSGL_Map Class
function JSGL_Map(layers,actors,forces) {

	//Default
	if (!layers) layers = new Array();
	if (!actors) actors = new Array();
	if (!forces) forces = new Array(new JSGL_Vector(0,0.1,0));

	//Properties
	this.layers = layers;					//Map layers (JSGL_Layer)
	this.actors = actors;					//Map actors (JSGL_Actor)
	this.forces = forces;					//Map forces (JSGL_Vector)
	
	//Methods
	this.draw = function () {
		var i;
		for (i=0;i<this.layers.length;i++) {
			this.layers[i].draw();
		}
		for (i=0;i<this.actors.length;i++) {
			this.actors[i].draw();
		}
	}
	
	//Events

}

//----------------------------------------------------------------------------------
// INPUT
//----------------------------------------------------------------------------------

//JSGL_Key Class
function JSGL_Key(key,cmd) {

	//Properties
	this.key = key;
	this.cmd = cmd;

}

//JSGL_Keys Class
function JSGL_Keyboard() {

	//Properties
	this.keys = new Array();				//Keys array
	this.keys.push(new JSGL_Key(13,'game.pause()'));
	this.keys.push(new JSGL_Key(27,'game.close()'));
	this.key = 0;							//Last pressed key

	//Methods
	this.command = function (key) {			//Returns command activated by the key
		var i;
		for (i=0;i<this.keys.length;i++) {
			if (this.keys[i].key == key) return this.keys[i].cmd;
		}
		return null;
	}
	document.onkeydown = function (e) {	//Global onkeydown event
		var cmd, k = game.keyboard;
		if (!window.event) {
			cmd = k.command(e.which);
			k.key = e.which;
		}
		else {
			cmd = k.command(event.keyCode);
			k.key = event.keyCode;
		}

		eval(cmd);
	}

}

//----------------------------------------------------------------------------------
// GAME CORE
//----------------------------------------------------------------------------------

//JSGL_Game Class
function JSGL_Game(map,interval) {

	//Default
	if (!interval) interval = 30;
	if (!map) map = new JSGL_Map(this);

	//Properties
	this.map = map;							//Current map
	this.interval = interval;				//Game loop interval
	this.timer = null;						//Game clock
	this.milliseconds = 30;					//FPS clock
	this.showFPS = false;					//Show FPS
	this.FPS = 30;							//FPS
	this.minFPS = 10000;					//Minimum FPS
	this.maxFPS = -1;						//Maximum FPS
	this.keyboard = new JSGL_Keyboard();	//Key configuration
	
	//Methods
	this.run = function () {				//Starts / Unpauses JSGL Engine
		this.timer = setInterval(this.loop,this.interval);
		this.onRun(this);
	}
	this.stop = function () {				//Stops JSGL Engine
		clearInterval(this.timer);
		this.timer = null;
		this.onStop(this);
	}
	this.pause = function () {				//Pauses / unpauses JSGL Engine
		if (this.timer == null) this.run();
		else this.stop();
	}
	this.close = function () {				//Ends game
		var c = this.onCloseQuery(this);
		if (c || c == null) window.close();
		this.onClose(this);
	}
	this.loop = function () {				//Game main loop
		var i, fps, g = this.game;
		date = new Date();
		time = date.getTime();
		actualTime =  time - g.milliseconds;
		g.milliseconds = time;
		fps = Math.floor(1000 / actualTime);
		g.FPS = fps;
		if (fps > g.maxFPS) g.maxFPS = fps;
		if (fps < g.minFPS && fps > 0) g.minFPS = fps;
		if (g.showFPS) document.title = 'FPS: ' + g.FPS + '/' + Math.floor(1000 / g.interval) + ' MIN: ' + g.minFPS + ' MAX: ' + g.maxFPS;
		g.onLoop(g);
		for (i=0;i<g.map.layers.length;i++) {
			g.map.layers[i].loop(g);
		}
		for (i=0;i<g.map.actors.length;i++) {
			g.map.actors[i].loop(g);
		}
	}

	//Events
	this.onRun = function(sender) {}		//Game started or unpaused
	this.onStop = function(sender) {}		//Game paused or stoped
	this.onLoop = function(sender) {}		//Game iteration
	this.onClose = function(sender) {}		//Closing game
	this.onCloseQuery = function(sender) {}	//Query to close game
	
	this.map.draw();
}

//Test data (Actual Game)

//Tiles
brick = new JSGL_Tile('http://www.inf.ufrgs.br/~rcpinto/snake/tijolos.gif');
grass = new JSGL_Tile('http://www.inf.ufrgs.br/~rcpinto/snake/grama.gif');

//Tile Sets
tileset1 = new JSGL_TileSet(new Array(brick,grass),32,32);

//Platforms
brick2x1 = new Array(0,0);

//Layers
platform1 = new JSGL_Layer(tileset1,2,1,'',brick2x1);
platform1.center.x = 128;
platform1.center.y = 32;
platform1.pos.z = 1;
platform1.pendulus = true;
platform1.maxAngularSpeed = 0.5;
platform1.angularAcceleration = 0.1;
platform1.radius = 100;
platform2 = new JSGL_Layer(tileset1,2,1,'',brick2x1,new JSGL_Vector(64,128,1));
platform2.acceleration.y = 1;
platform2.maxSpeed.y = 8;
platform2.pendulus = true;
ground = new JSGL_Layer(tileset1,12,1,'',new Array (0,0,0,0,0,0,0,0,0,0,0,0),new JSGL_Vector(0,256,1));
bg = new JSGL_Layer(tileset1,12,9,'http://stephenbrooks.org/ss/clouds/clouds.jpg');

//Actions
standLeft = new JSGL_Action(0,'http://www.inf.ufrgs.br/~rcpinto/snake/tijolos.gif','','',new JSGL_Vector(0,0,0));
standRight = new JSGL_Action(0,'http://www.inf.ufrgs.br/~rcpinto/snake/tijolos.gif','','',new JSGL_Vector(0,0,0));
walkLeft = new JSGL_Action(JSGL_KEY_LEFT,'http://www.inf.ufrgs.br/~rcpinto/snake/grama.gif','','',new JSGL_Vector(-0.1,0,0));
walkRight = new JSGL_Action(JSGL_KEY_RIGHT,'http://www.inf.ufrgs.br/~rcpinto/snake/grama.gif','','',new JSGL_Vector(0.1,0,0));

//Actors
block = new JSGL_Actor(32,32);
block.actions.push(standLeft,standRight,walkLeft,walkRight);
block.pos.z = 1;

//Maps
map1 = new JSGL_Map(new Array(platform1,platform2,bg,ground),new Array(block));

//Game (instance name MUST be 'game')
game = new JSGL_Game(map1);

//Events
game.onRun = function (sender) {document.title = 'Game Running'}
game.onStop = function (sender) {document.title = 'Game Paused'}
game.onCloseQuery = function (sender) {return confirm('Close window?')}

//Game Properties
//game.showFPS = true;

//Run Game
game.run();

