public class RayCaster2 extends JApplet {

	protected void processKeyEvent(KeyEvent e) {
		if (e.getID() != KeyEvent.KEY_RELEASED) {
			return;
		}
		
		if (e.getKeyCode() == 38) {
			// up
			p.go(true);
		}
		
		if (e.getKeyCode() == 40) {
			// down
		}
		
		if (e.getKeyCode() == 37) {
			// left
			p.rot(-1);
		}
		
		if (e.getKeyCode() == 39) {
			// right
			p.rot(1);
		}
		
		render();
	}
	
	public void init() {
		enableEvents(AWTEvent.KEY_EVENT_MASK);
		
		
		stripe = new Stripe[320];
		for (int i = 0; i < stripe.length; i++) {
			stripe[i] = new Stripe();
		}
		
		mapWidth = map[0].length;
		mapHeight = map.length;
		
		p = new Player();
		p.x = 4;
		p.y = 4;
		
		p.angle = 0;
		
		setSize(320+4*10+mapWidth*scale, 220);
		setFocusable(true);
		requestFocusInWindow();
		render();
	}

	public void paint(Graphics g) {
		g.setColor(Color.white);
		g.fillRect(0, 0, getWidth(), getHeight());
		
		int xo = 12, yo = 12;
		
		// Draw mini map
		for (int i = 0; i < map.length; i++) {
			for (int j = 0; j < map[i].length; j++) {
				g.setColor(map[i][j] > 0 ? new Color(0x999999) : new Color(0xcccccc));
				g.fillRect(xo+j*scale, yo+i*scale, scale, scale);
				
				g.setColor(new Color(0x666666));
				g.drawRect(xo+j*scale, yo+i*scale, scale, scale);
				
				for (Point h : hit) {
					if (h != null && h.x == j && h.y == i) {
						g.setColor(Color.orange);
						g.fillRect(xo+j*scale, yo+i*scale, scale, scale);
					}
				}
			}
		}
		
		// Draw rays on mini map
		g.setColor(Color.green);
		for (Point2D point : lines) {
			g.drawLine(xo+round(p.x*scale), xo+round(p.y*scale), 
					yo+round(point.getX()*scale), yo+round(point.getY()*scale));
		}
		
		// Draw character
		g.setColor(Color.red);
		g.fillRect(xo-2+round(p.x*scale), yo-2+round(p.y*scale), 4, 4);
		
		// Draw direction of player
		g.drawLine(xo+round(p.x*scale), yo+round(p.y*scale), 
				xo+round((p.x+cos(p.angle)*4))*scale, yo+round((p.y+sin(p.angle)*4))*scale);
		
		// Draw trace points
		g.setColor(Color.orange);
		for (Point2D point : pointVis) {
			g.fillRect(xo+round(point.getX()*scale-1), round(yo+point.getY()*scale-1), 2, 2);
		}
		
		// Draw the view window
		xo = xo+mapWidth*scale+10;
		yo = 10;
		
		// Draw the background
		g.setColor(new Color(0x999999));
		g.fillRect(xo, yo, 320, 200);
		
		g.setColor(new Color(0xbbbbbb));
		g.fillRect(xo, yo, 320, 100);
		
		// Draw the stripes
		g.setColor(new Color(0xffacac));
		for (int i = 0; i < stripe.length; i++) {
			g.drawLine(xo+i, yo+stripe[i].top, xo+i, yo+stripe[i].top+stripe[i].height);
		}
		
		// Draw the frame
		g.setColor(Color.black);
		g.drawRect(xo, yo, 320-1, 200-1);
	}
	
	private int[][] map = {
			{1, 1, 1, 1, 1, 1, 1, 1, 1},
			{1, 0, 0, 0, 0, 0, 0, 0, 1},
			{1, 0, 0, 0, 0, 0, 0, 0, 1},
			{1, 0, 0, 0, 0, 0, 0, 0, 1},
			{1, 0, 0, 0, 0, 0, 0, 0, 1},
			{1, 0, 0, 0, 0, 0, 1, 0, 1},
			{1, 0, 0, 0, 0, 0, 1, 0, 1},
			{1, 0, 0, 0, 0, 0, 1, 0, 1},
			{1, 1, 1, 1, 1, 1, 1, 1, 1}
	};
	
	private int mapWidth, mapHeight, scale = 10;
	
	private Player p;
	
	private Stripe[] stripe;
	
	private List<Point> hit = new ArrayList<Point>();
	private List<Point2D> pointVis = new ArrayList<Point2D>(), lines = new ArrayList<Point2D>();
	
	static class Stripe {
		int height, top;
	}
	
	static class Player {
		float x, y;
		int angle = 0;
		
		public void rot(int dir) {
			angle += dir*6;
		}
		
		public void go(boolean forward) {
			float newX = x+cos(angle)*0.18f;
			float newY = y+sin(angle)*0.18f;
			System.out.println("goto: [" + newX + ", " + newY + "]");
			x = newX;
			y = newY;
		}
		
		public String toString() {
			return "Player[x=" + x + ", y=" + y + "]";
		}

	}

	private void render() {
		hit.clear();
		pointVis.clear();
		lines.clear();
		
		int e = 0;
		long totalTime = 0, tmp = 0;
		for (float i = p.angle-30; i < p.angle+30; i += 60f/320f) {
			tmp = System.nanoTime();
			castSingleRay(i, e++);
			totalTime += System.nanoTime()-tmp;
		}
		
		System.out.println("Render cycle took " + totalTime + " ns");
		
		repaint();
	}
	
	private void castSingleRay(float degree, int i) {
		degree = (360-(degree%360))%360;
		
		boolean right = (degree > 270 || degree < 90);
		boolean up = degree < 180;
		
//		System.out.println(degree);
//		System.out.println("Up? " + up + " Right?" + right);
		
		int hitx = 0, hity = 0;
		
		// Vertikaler check
		
		float m = sin(degree)/cos(degree);
		float dx = right ? -1 : 1;
		float dy = dx*m;
		
		float x = right ? ceil(p.x) : floor(p.x);
		float y = p.y+(x-p.x)*m;
		
		int wallX = 0, wallY = 0;
		
		float dist = 0;
		
		while (x >= 0 && x < mapWidth && y >= 0 && y < mapHeight) {
		    wallX = floor(x + (right ? 0 : -1));
		    wallY = floor(y);
		    
		    if (map[wallY][wallX] > 0) {
		    	dist = sqrt(pow(x-p.x, 2)+pow(y-p.y, 2));
		    	hitx = wallX;
		    	hity = wallY;
		    	break;
		    }
		    
//		    System.out.println(wallX + "|" + wallY);
//		    hit.add(new Point(wallX, wallY));
//		    pointVis.add(new Point2D.Double(x, y));
			
		    x -= dx;
		    y += dy;
		}
		
		// Horizontaler check
		
		m = cos(degree)/sin(degree);
		dy = (up ? -1 : 1);
		dx = dy*m;

		y = up ? floor(p.y) : ceil(p.y);
		x = p.x+(y-p.y)*m;
		
		while (x >= 0 && x < mapWidth && y >= 0 && y < mapHeight) {
			wallY = floor(y + (up ? -1 : 0));
			wallX = floor(x);
			
		    
		    if (-1 < wallY && -1 < wallX && map[wallY][wallX] > 0) {
		    	float dist2 = sqrt(pow(x-p.x, 2)+pow(y-p.y, 2));
		    	
//		    	System.out.println(dist2 + " < " + dist);
		    	
		    	if (dist == 0 || dist2 < dist) {
			    	hitx = wallX;
			    	hity = wallY;
			    	dist = dist2;
		    	}
		    	break;
		    }
			
//		    System.out.println(wallX + "|" + wallY);
//		    hit.add(new Point(wallX, wallY));
//		    pointVis.add(new Point2D.Double(x, y));
			
			
			x -= dx;
			y += dy;
		}
		
		
//		System.out.println("dist=" + dist + "@" + hitx + "|" + hity);
		lines.add(new Point2D.Double(p.x+cos(degree)*dist, p.y-sin(degree)*dist));
		
		Stripe s = stripe[i];
		
		dist = dist*cos(p.angle - degree);
		float viewDist = 277f;//~: (width/2)/tan(60/2)=480/sqrt(3)
		s.height = round(viewDist/dist);
		s.top = round((200 - s.height) / 2);
	}
	
	public static float sqrt(float a) {
		return (float) Math.sqrt(a);
	}
	
	public static float pow(float a, float x) {
		return (float) Math.pow(a, x);
	}
	
	public static float sin(float degree) {
		return (float) Math.sin(d2r(degree));
	}
	
	public static float cos(float degree) {
		return (float) Math.cos(d2r(degree));
	}
	
	public static int ceil(float num) {
		return round(Math.ceil(num));
	}
	
	public static int floor(float num) {
		return round(Math.floor(num));
	}
	
	public static int round(float num) {
		return (int) Math.round(num);
	}
	
	public static int round(double num) {
		return (int) Math.round(num);
	}
	
	public static float d2r(float degree) {
		return ((float) degree)*(((float) Math.PI)/180.0f);
	}
	
}