(function () {
  var timer = null;
  var time = lastupdatetime = lastrendertime = lastfpstime = new Date().getTime();

  var player = {
	x : 1.1,
	y : 1.5,
	dir : 0,
	rot : 0,
	speed : 0,
	moveSpeed : 0.10,
	rotSpeed : 3 * Math.PI / 180
  }

  var map = [];
  var mapWidth = 0;
  var mapHeight = 0;
  var screenWidth = 560;
  var screenHeight = 420;
  var stripWidth = 3;
  var fov = 60 * Math.PI / 180;
  var numRays = Math.ceil(screenWidth / stripWidth);
  var fovHalf = fov / 2;
  var viewDist = (screenWidth/2) / Math.tan((fov / 2));
  var temp_viewDist2 = viewDist*viewDist; //slight optimization
  var twoPI = Math.PI * 2;
  var screenStrips = [];
  var lightdistance = 1;
  var lightentities = [];
  var frame = 0;
  var radiansnap = 0.10;

  function initialize() {
    timer = setInterval(gameCycle,10);
    mapWidth = map[0].length;
    mapHeight = map.length;
    bindKeys();
    initScreen();
  }

  function initScreen() {
    var screen = document.getElementById('screen');
    for (var i=0;i<screenWidth;i+=stripWidth) {
      var element = new Image();
      element.style.position = 'absolute';
      element.style.left = i + 'px';
      element.style.width = stripWidth+'px';
      element.style.height = '0px';
      element.src = 'walls.png';
      var strip = new ScreenStrips(element);
      screenStrips[screenStrips.length] = strip;
      screen.appendChild(element);
    }
  }
  
  function ScreenStrips(element) {
    this.element = element;
    this.left = -1;
    this.top = -1;
    this.height = -1;
    this.width = -1;
    this.clipping = 0;
  }
  
  
  

  function gameCycle() {
    time = new Date().getTime();
    move(); //this needs to be regulated too
    if(lastrendertime + 10 < time) {
      render();
    }
  }

  function render() {
    castRays();
    frame++;
    if (lastfpstime +1000 < time) {
      document.title = frame;
      frame = 0;
      lastfpstime = time;
    }
    lastrendertime = time;
  }





// bind keyboard events to game functions (movement, etc)
  function bindKeys() {
    document.onkeydown = function(e) {
      e = e || window.event;
      switch (e.keyCode) { // which key was pressed?
      case 38: // up, move player forward, ie. increase speed
        player.speed = 1;
      break;
      case 40: // down, move player backward, set negative speed
        player.speed = -1;
      break;
      case 37: // left, rotate player left
        player.dir = -1;
      break;
      case 39: // right, rotate player right
        player.dir = 1;
      break;
      }
    }
    document.onkeyup = function(e) {
      e = e || window.event;
      switch (e.keyCode) {
      case 38:
      case 40:
        player.speed = 0;	// stop the player movement when up/down key is released
      break;
      case 37:
      case 39:
        player.dir = 0;
      break;
      }
    }
  }

  function castRays() {
    var stripIdx = 0;
    var templastray = {}; //we save the last ray's numbers here to avoid rendering duplicates in a row
    for (var i=0;i<numRays;i++) {
      // where on the screen does ray go through?
      var rayScreenPos = (-numRays/2 + i) * stripWidth;
      // the distance from the viewer to the point on the screen, simply Pythagoras.
      var rayViewDist = Math.sqrt(rayScreenPos*rayScreenPos + temp_viewDist2);
      // the angle of the ray, relative to the viewing direction.
      // right triangle: a = sin(A) * c
      var rayAngle = Math.asin(rayScreenPos / rayViewDist);
      castSingleRay(player.rot + rayAngle,stripIdx++,templastray);
    }
  }

  function castSingleRay(rayAngle, stripIdx,templastray) {
    // first make sure the angle is between 0 and 360 degrees
    rayAngle %= twoPI;
    if (rayAngle < 0) rayAngle += twoPI;
    // moving right/left? up/down? Determined by which quadrant the angle is in.
    var right = (rayAngle > twoPI * 0.75 || rayAngle < twoPI * 0.25);
    var up = (rayAngle < 0 || rayAngle > Math.PI);
    var wallType = 0;
    var lightvalue;

    // only do these once
    var angleSin = Math.sin(rayAngle);
    var angleCos = Math.cos(rayAngle);

    var dist = 0;	// the distance to the block we hit
    var xHit = 0; 	// the x and y coord of where the ray hit the block
    var yHit = 0;

    var textureX;	// the x-coord on the texture of the block, ie. what part of the texture are we going to render
    var wallX;	// the (x,y) map coords of the block
    var wallY;

    var wallIsHorizontal = false;

	// first check against the vertical map/wall lines
	// we do this by moving to the right or left edge of the block we're standing in
	// and then moving in 1 map unit steps horizontally. The amount we have to move vertically
	// is determined by the slope of the ray, which is simply defined as sin(angle) / cos(angle).

    var slope = angleSin / angleCos; 	// the slope of the straight line made by the ray
    var dXVer = right ? 1 : -1; 	// we move either 1 map unit to the left or right
    var dYVer = dXVer * slope; 	// how much to move up or down

    var x = right ? Math.ceil(player.x) : Math.floor(player.x);	// starting horizontal position, at one of the edges of the current map block
    var y = player.y + (x - player.x) * slope;			// starting vertical position. We add the small horizontal step we just made, multiplied by the slope.

    while (x >= 0 && x < mapWidth && y >= 0 && y < mapHeight) {
      var wallX = Math.floor(x + (right ? 0 : -1));
      var wallY = Math.floor(y);

      // is this point inside a wall block?
      if (map[wallY][wallX].walltype > 0) {
        var distX = x - player.x;
        var distY = y - player.y;
        dist = distX*distX + distY*distY;	// the distance from the player to this point, squared.
        wallType = map[wallY][wallX].walltype; // we'll remember the type of wall we hit for later
	lightvalue = 6//map[wallY][wallX].light;
        textureX = y % 1;	// where exactly are we on the wall? textureX is the x coordinate on the texture that we'll use later when texturing the wall.
        if (!right) textureX = 1 - textureX; // if we're looking to the left side of the map, the texture should be reversed
        wallIsHorizontal = true;
	break;
      }
      x += dXVer;
      y += dYVer;
    }

	// now check against horizontal lines. It's basically the same, just 'turned around'.
	// the only difference here is that once we hit a map block, 
	// we check if there we also found one in the earlier, vertical run. We'll know that if dist != 0.
	// If so, we only register this hit if this distance is smaller.

    var slope = angleCos / angleSin;
    var dYHor = up ? -1 : 1;
    var dXHor = dYHor * slope;
    var y = up ? Math.floor(player.y) : Math.ceil(player.y);
    var x = player.x + (y - player.y) * slope;

    while (x >= 0 && x < mapWidth && y >= 0 && y < mapHeight) { //////
      var wallY = Math.floor(y + (up ? -1 : 0));
      var wallX = Math.floor(x);
      if (map[wallY][wallX].walltype > 0) {
        var distX = x - player.x;
        var distY = y - player.y;
        var blockDist = distX*distX + distY*distY;
        if (!dist || blockDist < dist) {
          dist = blockDist;
          xHit = x;
          yHit = y;
          wallType = map[wallY][wallX].walltype;
	  lightvalue = 6//map[wallY][wallX].light;
          textureX = x % 1;
          if (up) textureX = 1 - textureX;
        }
        break;
      }
      x += dXHor;
      y += dYHor;
    }

    if (dist) {
      //drawRay(xHit, yHit);
      var strip = screenStrips[stripIdx];
      dist = Math.sqrt(dist);
      // use perpendicular distance to adjust for fish eye
      // distorted_dist = correct_dist / cos(relative_angle_of_ray)
      dist = dist * Math.cos(player.rot - rayAngle);
      
      var shadowlevel = 16-lightvalue; //since we have the lighest texture at position 0, and the darkest at position 16, we have to reverse the lightvalue;
      if (shadowlevel < 0) {shadowlevel = 0;} //nothing can be  lighter than 0
      
      var height = Math.round(viewDist / dist); // 'real' wall height in the game world is 1 unit, the distance from the player to the screen is viewDist, thus the height on the screen is equal to wall_height_real * viewDist / dist
      var width = height * stripWidth; // width is the same, but we have to stretch the texture to a factor of stripWidth to make it fill the strip correctly
      var top = Math.round((screenHeight - height) / 2); // top placement is easy since everything is centered on the x-axis, so we simply move it half way down the screen and then half the wall height back up.
      var texX = Math.round(textureX*width); if (texX > width - stripWidth) {texX = width - stripWidth;} // Here we make the offset for the textures for each ray correct
      var left = (((stripIdx*stripWidth)-texX)-(shadowlevel*width)); // there is so much to account for here, both the texture offset and the shade frame
 
      var diffrence = 2
      if (Math.abs(templastray.top-top) < diffrence && Math.abs(templastray.width-width) < diffrence && Math.abs(templastray.height-height) < diffrence && templastray.shadowlevel == shadowlevel) {
        //strip.element.style.display = 'none';
      } else {
        //strip.element.style.display = 'block';
        templastray.height = height;
        templastray.width = width;
        templastray.top = top;
        templastray.shadowlevel = shadowlevel;
        templastray.strip = strip;
      }
      

      
      
      if(left != strip.left) {strip.left = left;strip.element.style.left =  left+'px';}
      if(top != strip.top) {strip.top = top;strip.element.style.top = top+'px';}
      if(height != strip.height) {strip.height = height;strip.element.style.height = height+'px';}
      if(width != strip.width) {strip.width = width;strip.element.style.width = (width*15)+'px';} //the 15 is the number of shades of light we have.
      
      strip.element.style.clip = 'rect(0px,'+((stripWidth+texX)+(shadowlevel*width))+'px,'+(height)+'px,'+(texX+(shadowlevel*width))+'px)';
      
    }
  }

  function move() {
    var moveStep = player.speed * player.moveSpeed;	// player will move this far along the current direction vector

    player.rot += player.dir * player.rotSpeed; // add rotation if player is rotating (player.dir != 0)
    // make sure the angle is between 0 and 360 degrees
    while (player.rot < 0) player.rot += twoPI;
    while (player.rot >= twoPI) player.rot -= twoPI;

    // this code tried to make the player walk nice angles, like 0,90,180 or 270, because it helps optimization
    if (player.dir == 0) { // we don't want this to happen while the player is in the middle of turning around.
      if (player.rot < 0+radiansnap || player.rot > 6.28318531-radiansnap) {player.rot = 0;} // make the player walk a perfect north if he is very close to walking a perfect north
      if (player.rot <  1.57079633+radiansnap && player.rot >  1.57079633-radiansnap ) {player.rot = 1.57079633;} //perfect west
      if (player.rot <  3.14159265+radiansnap && player.rot >  3.14159265-radiansnap ) {player.rot = 3.14159265;} //perfect south
      if (player.rot <  4.712388985+radiansnap && player.rot >  4.71238898-radiansnap ) {player.rot = 4.71238898;} //perfect east
    }
    
    var newX = player.x + Math.cos(player.rot) * moveStep;	// calculate new player position with simple trigonometry
    var newY = player.y + Math.sin(player.rot) * moveStep;
    if (isBlocking(newX, newY)) {	// are we allowed to move to the new position?
      return; // no, bail out.
    }
    player.x = newX; // set new position
    player.y = newY;
  }

  function isBlocking(x,y) {
    // first make sure that we cannot move outside the boundaries of the level
    if (y < 0 || y >= mapHeight || x < 0 || x >= mapWidth) return true;
    // return true if the map block is not 0, ie. if there is a blocking wall.
    return (map[Math.floor(y)][Math.floor(x)].walltype != 0); 
  }
  
  function createminimap(x,y,type,light) {
        var element = document.createElement('div');
	element.style.position = 'absolute';
	element.style.width = 20;
	element.style.height = 20;
	element.style.left = x*20;
	element.style.top = y*20;
	if (type == 0) {
	element.style.backgroundColor = 'white';
	} else {
	element.style.backgroundColor = 'gray';
	}
	element.style.opacity = (light)/10;
	document.getElementById('minimap').appendChild(element);
  }
 
  function setupmap(data) {
    for (var x=0, length=data.length;x<length;x++) {
      map[x] = [];
      for (var y=0, length=data[x].length;y<length;y++) {
        map[x][y] = {};
        map[x][y].walltype = data[x][y];
	map[x][y].light = [];
	map[x][y].light[0] = 0;
	map[x][y].light[1] = 0;
	map[x][y].light[2] = 0;
	map[x][y].light[3] = 0;
      }
    }
  }

  function calculateLights() {
    tempmap = [];
    for (var x=0, length=map.length;x<length;x++) {
      tempmap[x] = [];
    }
    for (var i=0, length=lightentities.length;i<length;i++) {
      lightloop(lightentities[i].power,lightentities[i].x,lightentities[i].y,1,1);
      lightloop(lightentities[i].power,lightentities[i].x,lightentities[i].y,1,-1);
      lightloop(lightentities[i].power,lightentities[i].x,lightentities[i].y,-1,1);
      lightloop(lightentities[i].power,lightentities[i].x,lightentities[i].y,-1,-1);
    }
  }
  
  function lightloop(power,x,y,xaxis,yaxis) {
    var newx = x+xaxis;
    var newy = y+yaxis;
    if (map[newx] && map[newx][y]) {
      if (map[newx][y].walltype == 0) {
        if (!--power) {return}
        lightloop(power,newx,y,xaxis,yaxis);
      } else {
        if (tempmap[newx][y] != true) {
	//  if(newx == 1) {
            map[newx][y].light = Math.max(power,map[newx][y].light);
	    tempmap[newx][y] = true;
//	  }
	}
      }
    }
    if (map[x] && map[x][newy]) {
      if (map[x][newy].walltype == 0) {
        if (!--power) {return}
        lightloop(power,x,newy,xaxis,yaxis);
      } else {
        if (tempmap[x][newy] != true) {
          map[x][newy].light+=power;
	  tempmap[x][newy] = true;
	}
      }
    }
  }

  setupmap([
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[1,0,0,0,1,1,1,1,0,1,1,1,1,1,1,1],
[1,0,0,1,0,0,0,1,0,1,1,1,1,1,1,1],
[1,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1],
[1,0,1,1,0,0,0,1,0,0,1,0,0,0,0,1],
[1,0,1,1,0,0,0,1,0,0,1,0,0,0,0,1],
[1,0,1,1,1,1,0,1,0,0,0,0,0,0,0,1],
[1,0,1,1,0,0,0,1,0,0,1,0,0,0,0,1],
[1,0,1,1,0,0,0,1,0,1,1,1,1,1,1,1],
[1,0,1,1,0,0,0,1,0,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1],
[1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1],
[1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1],
[1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
]);

  lightentities[lightentities.length] = {
    x: 1,
    y: 1,
    power: 10
  }
  
  //calculateLights();
  initialize();
})();
