
function pixels(x) { return Math.floor(x) + "px"; }
function write(x) { console.firstChild.nodeValue += x; }
function writeLn(x) { write(x); write("\n"); }
function sign(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; }

cWindowWidth = 512;
cWindowHeight = 384;
cFrameWidth = 170;
cFrameHeight = 128;
cScaleSize = 3;
cLevelSize = 64;
cFovFactor = Math.tan(115 * Math.PI / 360);

cTexFiles = [ "grey1_1.gif", "grey1_2.gif", "blue1_1.gif", "blue1_2.gif", "wood1_1.gif", "wood1_2.gif", "door_s_1.gif", "door_s_2.gif", "door_1.gif", "door_2.gif" ];
cTexImages = [];
cNumTextures = cTexFiles.length;
cTexSize = 64;
cTexDoor = 8;
cTexDoorSide = 6;

cPlayerSize = 0.5;
cMoveSpeed = 5;
cTurnSpeed = 90 * Math.PI / 180;

imagesLoaded = false;
numImagesLoaded = 0;
bodyLoaded = false;

function onImageLoad()
{
	if (++numImagesLoaded == cNumTextures)
	{
		imagesLoaded = true;
		if (bodyLoaded)
			init();
	}
}

function onBodyLoad()
{
	bodyLoaded = true;
	if (imagesLoaded)
		init();
}

myImage = new Image();
myImage.src = cTexFiles[0];
myImage.style.height = pixels(cScaleSize * cFrameHeight);
myImage.style.width = pixels(cScaleSize * cTexSize /** cNumTextures*/);
myImage.style.position = "relative";

for (var i in cTexFiles)
{
	cTexImages[i] = new Image();
	cTexImages[i].onload = onImageLoad;
	cTexImages[i].src = cTexFiles[i];
}

scanLines = [];

var console;

function ScanLine(x)
{
	this.x = x;
	this.height = cFrameHeight;
	this.texPos = x % cTexSize;
	this.id = 0;
	this.idChanged = true;
}

ScanLine.prototype.createDOM = function()
{
	if (this.img)
		return;

	this.img = myImage.cloneNode(true);
	this.img.scanLine = this;
	this.img.onload = null;
	this.img.src = cTexImages[this.id].src;
	this.idChanged = false;
	
	this.div = document.createElement("div");
	this.div.style.overflow = "hidden";
	this.div.style.position = "absolute";
	this.div.style.backgroundColor = "#707070";
	this.div.appendChild(this.img);
	
	frameDiv.appendChild(this.div);
	
	this.readjustDOM();
}

ScanLine.prototype.destroyDOM = function()
{
	if (!this.img)
		return;

	delete this.img.scanLine;
	this.div.removeChild(this.img);
	frameDiv.removeChild(this.div);
	
	delete this.div;
	delete this.img;
}

ScanLine.prototype.readjustDOM = function()
{
	this.div.style.left = pixels(this.x * cScaleSize);
	this.div.style.width = pixels(cScaleSize);
	this.div.style.height = pixels(cScaleSize * cFrameHeight);
	this.img.style.width = pixels(cScaleSize * cTexSize);
	
	this.renderDOM();
}

ScanLine.prototype.renderDOM = function()
{
	var h = this.height;
	var pos = this.texPos;
	this.img.style.left = pixels(-this.texPos * cScaleSize);
	this.img.style.top = pixels(cScaleSize * (cFrameHeight - h) / 2);
	this.img.style.height = pixels(cScaleSize * h);
	
	if (this.idChanged)
	{
		this.img.src = cTexImages[this.id].src;
		this.idChanged = false;
	}
}

ScanLine.prototype.setHeight = function(h)
{
	this.height = h;
}
	
ScanLine.prototype.setTexture = function(id, pos)
{
	id %= cNumTextures;
	if (this.id != id)
	{
		this.id = id;
		this.idChanged = true;
	}
	
	pos %= cTexSize;
	if (pos < 0)
		pos += cTexSize;
	pos = Math.floor(pos);
	this.texPos = pos;
}


frame = 0;

startTime = 0;
avgStartTime = 0;
avgStartWindow = 0;
avgFps = 0;
lastTime = 0;
frameTime = 0;

function render()
{
	var forwardX = Math.cos(playerAngle);
	var forwardY = Math.sin(playerAngle);
	var eyeX = playerX;
	var eyeY = playerY;
	var rightX = -forwardY;
	var rightY = forwardX;
	
	var halfFovFactor = cFovFactor / 2;
	var posX = eyeX + forwardX - halfFovFactor * rightX;
	var posY = eyeY + forwardY - halfFovFactor * rightY;
	var step = cFovFactor / cFrameWidth;
	
	var gridX = Math.floor(eyeX);
	var gridY = Math.floor(eyeY);
	
	for (i = 0; i < cFrameWidth; i++)
	{
		var rayX = posX - eyeX;
		var rayY = posY - eyeY;
		var rayPosX = eyeX;
		var rayPosY = eyeY;
		var rayGridX = gridX;
		var rayGridY = gridY;
		var texPos = 0;
		var texId = 0;
		var hit = false;
		var hitDoor = false;
		var leftX, leftY;
		var signRayX, signRayY;
		
		if (rayX < 0)
		{
			leftX = (rayPosX - rayGridX);
			signRayX = -1;
		}
		else if (rayX > 0)
		{
			leftX = (rayGridX + 1 - rayPosX);
			signRayX = 1;
		}
		else
		{
			leftX = Infinity;
			signRayX = 0;
		}

		if (rayY < 0)
		{
			leftY = (rayPosY - rayGridY);
			signRayY = -1;
		}
		else if (rayY > 0)
		{
			leftY = (rayGridY + 1 - rayPosY);
			signRayY = 1;
		}
		else
		{
			leftY = Infinity;
			signRayY = 0;
		}
			
		if (level[rayGridX + rayGridY * cLevelSize] == -1)
			hitDoor = true;
	

		while (!hit)
		{
			if (leftY / Math.abs(rayY) < leftX / Math.abs(rayX))
			{
				var d = signRayY * leftY;
				rayGridY += signRayY;
				rayPosX += rayX * (d / rayY);
				rayPosY += d;
				texPos = signRayY * (Math.floor(rayPosX) - rayPosX);
				texId = 0;
				leftX -= Math.abs(rayX * (d / rayY));
				leftY = 1;
			}
			else
			{
				d = signRayX * leftX;
				rayGridX += signRayX;
				rayPosX += d;
				rayPosY += rayY * (d / rayX);
				texPos = signRayX * (rayPosY - Math.floor(rayPosY));
				texId = 1;
				leftY -= Math.abs(rayY * (d / rayX));
				leftX = 1;
			}
			
			var levelPos = rayGridY * cLevelSize + rayGridX;
			var wallId = level[levelPos];
			
			if (wallId == 0)
			{
				hitDoor = false;
			}
			
			else if (wallId > 0)			// wall
			{
				hit = true;

				if (hitDoor)
					texId += cTexDoorSide;
				else
					texId += 2*(wallId - 1);
			}
			
			else if (wallId < 0)	// door
			{
				var door = doors[levelPos];
				var secret = door instanceof SecretDoor;
				hitDoor = !secret;
				zPos = secret ? door.pushPos : 0.5;
				
				if (texId == 0)
				{
					if (leftX / Math.abs(rayX) < zPos / Math.abs(rayY))
						continue;
				
					var dy = zPos * signRayY;
					var dx = rayX * (dy / rayY);
					rayPosY += dy;
					rayPosX += dx;
					if (secret)
					{
						texPos = signRayY * (Math.floor(rayPosX) - rayPosX);
						texId += 2 * (door.wallId - 1);
						hit = true;
					}
					else if ((texPos = Math.abs((Math.floor(rayPosX) - rayPosX)) - door.openPos) >= 0)
					{
						texId = cTexDoor;
						hit = true;
					}
					else
					{
						hitDoor = false;
						leftX -= Math.abs(dx);
						leftY -= 0.5;
						if (leftX / Math.abs(rayX) < leftY / Math.abs(rayY))
						{
							var d = signRayX * leftX;
							rayPosX += d;
							rayPosY += rayY * (d / rayX);
							texPos = signRayX * (rayPosY - Math.floor(rayPosY));
							texId = cTexDoorSide + 1;
							hit = true;
						}
					}
				}
				else
				{
					if (leftY / Math.abs(rayY) < zPos / Math.abs(rayX))
						continue;
				
					var dx = zPos * signRayX;
					var dy = rayY * (dx / rayX);
					rayPosX += dx;
					rayPosY += dy;
					if (secret)
					{
						texPos = signRayX * (rayPosY - Math.floor(rayPosY));
						texId += 2 * (door.wallId - 1);
						hit = true;
					}
					else if ((texPos = Math.abs((Math.floor(rayPosY) - rayPosY)) - door.openPos) >= 0)
					{
						texId = cTexDoor + 1;
						hit = true;
					}
					else
					{
						hitDoor = false;
						leftX -= 0.5;
						leftY -= Math.abs(dy);
						if (leftY / Math.abs(rayY) < leftX / Math.abs(rayX))
						{
							var d = signRayY * leftY;
							rayPosY += d;
							rayPosX += rayX * (d / rayY);
							texPos = signRayY * (Math.floor(rayPosX) - rayPosX);
							texId = cTexDoorSide;
							hit = true;
						}
					}
				}
			}
		}
	
		var z = (rayPosX - eyeX) * forwardX + (rayPosY - eyeY) * forwardY;
		scanLines[i].setHeight(cFrameWidth / cFovFactor / z);
		scanLines[i].setTexture(texId, texPos * cTexSize);
		
		posX += step * rightX;
		posY += step * rightY;
	}
}

function getLevel(x, y)
{
	return level[Math.floor(x) + Math.floor(y) * cLevelSize];
}

function walkable(levelPos)
{
	var l = level[levelPos];
	return l == 0 || (l < 0 && doors[levelPos].walkable());
}

function traceLine(rayPosX, rayPosY, rayToX, rayToY, boxSize)
{
	var rayX = rayToX - rayPosX;
	var rayY = rayToY - rayPosY;
	
	if (!rayX && !rayY)
		return false;
	
	var totalX = Math.abs(rayX);
	var totalY = Math.abs(rayY);
	var len = Math.sqrt(rayX * rayX + rayY * rayY);
	rayX /= len;
	rayY /= len;

	boxSize *= 0.5;

	var rayGridX = Math.floor(rayPosX);
	var rayGridY = Math.floor(rayPosY);
	
	var leftX, leftY;
	var signRayX, signRayY;
	var stateX, stateY;

	if (rayX < 0)
	{
		leftX = (rayPosX - rayGridX);
		signRayX = -1;
	}
	else if (rayX > 0)
	{
		leftX = (rayGridX + 1 - rayPosX);
		signRayX = 1;
	}
	else
	{
		leftX = Infinity;
		signRayX = 0;
	}

	if (rayY < 0)
	{
		leftY = (rayPosY - rayGridY);
		signRayY = -1;
	}
	else if (rayY > 0)
	{
		leftY = (rayGridY + 1 - rayPosY);
		signRayY = 1;
	}
	else
	{
		leftY = Infinity;
		signRayY = 0;
	}
	
	if (rayX)
	{
		if (leftX <= boxSize)
			stateX = 0;
		else
		{
			leftX -= boxSize;
			stateX = 1;
		}
	}
	if (rayY)
	{
		if (leftY <= boxSize)
			stateY = 0;
		else
		{
			leftY -= boxSize;
			stateY = 1;
		}
	}
	
	while (totalX > 0 || totalY > 0)
	{
		if (leftY / Math.abs(rayY) < leftX / Math.abs(rayX))
		{
			totalY -= leftY;
			if (totalY <= 0)
				return false;
				
			var d = signRayY * leftY;
			rayPosX += rayX * (d / rayY);
			rayPosY += d;
			leftX -= Math.abs(rayX * (d / rayY));
			if (stateY == 0)
			{
				rayGridY += signRayY;
				leftY = 1 - boxSize;
				stateY = 1;
			}
			else
			{
				leftY = boxSize;
				stateY = 0;
				var levelPos = rayGridX + (rayGridY + signRayY) * cLevelSize;
				var texPos = rayPosX - Math.floor(rayPosX);

				if (!walkable(levelPos) ||
					(texPos < boxSize && !walkable(levelPos - 1)) ||
					(texPos > 1-boxSize && !walkable(levelPos + 1)))
					return { x:rayPosX, y:rayPosY, axis:1 };
			}
		}
		else
		{
			totalX -= leftX;
			if (totalX <= 0)
				return false;
			
			var d = signRayX * leftX;
			rayPosX += d;
			rayPosY += rayY * (d / rayX);
			leftY -= Math.abs(rayY * (d / rayX));
			if (stateX == 0)
			{
				rayGridX += signRayX;
				leftX = 1 - boxSize;
				stateX = 1;
			}
			else
			{
				leftX = boxSize;
				stateX = 0;
				var levelPos = (rayGridX + signRayX) + rayGridY * cLevelSize;
				var texPos = rayPosY - Math.floor(rayPosY);

				if (!walkable(levelPos) ||
					(texPos < boxSize && !walkable(levelPos - cLevelSize)) ||
					(texPos > 1-boxSize && !walkable(levelPos + cLevelSize)))
					return { x:rayPosX, y:rayPosY, axis:0 };
			}
		}
	}
	
	return false;
}

function processInput()
{
	var forwardX = Math.cos(playerAngle);
	var forwardY = Math.sin(playerAngle);
	
	if (keyMoveForward.down || keyMoveBackward.down)
	{
		var dir = keyMoveForward.down ? 1 : -1;
		var speed = dir * frameTime * cMoveSpeed;
		
		var rayX = speed * forwardX;
		var rayY = speed * forwardY;
		var toX = playerX + rayX;
		var toY = playerY + rayY;
		var fromX = playerX - 0.01 * dir * forwardX;
		var fromY = playerY - 0.01 * dir * forwardY;
		var hit = traceLine(fromX, fromY, toX, toY, cPlayerSize);
		if (hit)
		{
			playerX = hit.x;
			playerY = hit.y;
			if (hit.axis == 0)
			{
				fromY = playerY - 0.01 * sign(rayY);
				hit = traceLine(playerX, fromY, playerX, toY, cPlayerSize);
				playerY = hit ? hit.y : toY;
			}
			else
			{
				fromX = playerX - 0.01 * sign(rayX);
				hit = traceLine(fromX, playerY, toX, playerY, cPlayerSize);
				playerX = hit ? hit.x : toX;
			}
		}
		else
		{
			playerX = toX;
			playerY = toY;
		}
	}
	
	if (keyTurnLeft.down || keyTurnRight.down)
	{
		var dir = keyTurnLeft.down ? -1 : 1;
		playerAngle += dir * frameTime * cTurnSpeed;
	}
	
	if (keyUse.pressed)
	{
		keyUse.pressed = false;
		var levelPos = Math.floor(playerX) + Math.floor(playerY) * cLevelSize;
		var sx = sign(forwardX);
		var sy = sign(forwardY) * cLevelSize;
		if (sx && doors[levelPos+sx])
			doors[levelPos+sx].toggle();
		if (sy && doors[levelPos+sy])
			doors[levelPos+sy].toggle();
	}
}

requestOffsetWidth = 1;
function gameLoop()
{
	newTime = new Date().getTime();

	if (frame == 0)
	{
		avgStartTime = startTime = lastTime = new Date().getTime();
		frameTime = 0;
	}
	else
	{
		frameTime = (newTime - lastTime) / 1000;
	}
	
	frame++;
	
	for (var i in doors)
		doors[i].think();
	processInput();
	render();
	
	if (useCanvas)
	{
		canvasContext.fillStyle = "#707070";
		canvasContext.fillRect(0, 0, cWindowWidth, cWindowHeight);
		canvasContext.save();
		canvasContext.scale(cScaleSize, cScaleSize);

		for (var i = 0; i < cFrameWidth; i++)
		{
			var s = scanLines[i];
			var img = cTexImages[s.id];
			var pos = s.texPos;
			var height = s.height;
			
			canvasContext.drawImage(img, pos, 0, 0, cTexSize, i, (cFrameHeight - height) / 2, 1.01, height);
		}

		canvasContext.restore();
	}
	else
	{
		for (var i = 0; i < cFrameWidth; i++)
		{
			var s = scanLines[i];
			s.renderDOM();
//			var w = s.img.offsetWidth;
		}
		var w = scanLines[0].img.offsetWidth;
	}
	
	var fps = 1 / frameTime;
	document.getElementById("_fps").firstChild.nodeValue = Math.round(100 * fps) / 100;
	document.getElementById("_avgfps").firstChild.nodeValue = Math.round(100 * avgFps) / 100;
//	window.status = "fps: " + Math.round(100 * fps) / 100 + " (average: " + Math.round(100 * avgFps) / 100 + ")";

	lastTime = newTime;
	if (newTime - avgStartTime > 1000)
	{
		avgFps = (frame - avgStartWindow) * 1000 / (newTime - avgStartTime);
		avgStartTime = newTime;
		avgStartWindow = frame;
	}
	
	
	if (editorData.length)
		editorUpdate();
}


doHandleInput = true;
function Key() { }
Key.prototype = { down: false, pressed: false, released: false };
Key.prototype.push = function() { this.pressed = !this.down; this.released = false; this.down = true; }
Key.prototype.release = function() { this.pressed = false; this.released = true; this.down = false; }
keyMoveForward = new Key;
keyMoveBackward = new Key;
keyTurnLeft = new Key;
keyTurnRight = new Key;
keyUse = new Key;


function handleInput(evt)
{
	if (!doHandleInput)
		return true;

	var e = evt || window.event;
	var c = e.keyCode;
	
	var fn = e.type == "keydown" ? Key.prototype.push : Key.prototype.release;
	
	switch(c)
	{
	case 37:
		fn.apply(keyTurnLeft);
		return false;
		
	case 38:
		fn.apply(keyMoveForward);
		return false;
		
	case 39:
		fn.apply(keyTurnRight);
		return false;
		
	case 40:
		fn.apply(keyMoveBackward);
		return false;
	
	case 32:
		fn.apply(keyUse);
		return false;
	}
	
	return true;
}


var frameDiv;
var canvas;
var canvasContext;
var canvasSupported = false;
var useCanvas = false;
function init()
{
	console = document.getElementById("_console");

	canvas = document.getElementById("_canvas");
	frameDiv = document.getElementById("_frame");
	
	canvasSupported = "getContext" in canvas;
	//document.getElementById("_usecanvas").checked = canvasSupported;
	if (canvasSupported)
		canvasContext = canvas.getContext("2d");
	//useCanvas = !canvasSupported;
	//setUseCanvas(canvasSupported);

    useCanvas = true;
    setUseCanvas(false);
	
	changeFrameSize(cFrameWidth, cFrameHeight);
	
	document.onkeydown = handleInput;
	document.onkeyup = handleInput;
	
	window.setInterval("gameLoop()", 1);
}

function changeFrameSize(newWidth, newHeight)
{
	cFrameWidth = newWidth;
	cFrameHeight = newHeight;

	// remove extra scanlines
	for (var i = newWidth; i < scanLines.length; i++)
		scanLines[i].destroyDOM();
	if (newWidth < scanLines.length)
		scanLines.length = newWidth;
	
	// add scanlines
	for (var i = scanLines.length; i < newWidth; i++)
	{
		scanLines[i] = new ScanLine(i);
		if (!useCanvas)
			scanLines[i].createDOM();
	}
	
	if (!useCanvas)
		for (var i = 0; i < newWidth; i++)
			scanLines[i].readjustDOM();
}

function setWindowSize(newWidth, newHeight)
{
	if (useCanvas)
	{
		canvas.width = newWidth;
		canvas.height = newHeight;		
	}
	else
	{
		frameDiv.style.width = pixels(newWidth);
		frameDiv.style.height = pixels(newHeight);
	}
	
	cWindowWidth = newWidth;
	cWindowHeight = newHeight;
	
	changeFrameSize(Math.floor(cWindowWidth / cScaleSize), Math.floor(cWindowHeight / cScaleSize));
	render();
}

function setWindowScale(newScale)
{
	cScaleSize = newScale;

	changeFrameSize(Math.floor(cWindowWidth / cScaleSize), Math.floor(cWindowHeight / cScaleSize));
	render();
}

function setUseCanvas(newUseCanvas)
{
	if (newUseCanvas && !canvasSupported)
		return false;

	if (newUseCanvas == useCanvas)
		return true;
		
	useCanvas = newUseCanvas;
	if (useCanvas)
	{
		for (i in scanLines)
			scanLines[i].destroyDOM();
		frameDiv.style.display = "none";
		
		canvas.style.display = "block";
		canvas.width = cWindowWidth;
		canvas.height = cWindowHeight;		
	}
	else
	{
		canvas.style.display = "none";

		for (i in scanLines)
			scanLines[i].createDOM();
		frameDiv.style.display = "block";
		frameDiv.style.width = pixels(cWindowWidth);
		frameDiv.style.height = pixels(cWindowHeight);
	}
	
	return true;
}
