package org.newdawn.slick.examples.scroller;

import org.newdawn.slick.Animation;
import org.newdawn.slick.AppGameContainer;
import org.newdawn.slick.BasicGame;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.SpriteSheet;
import org.newdawn.slick.tiled.TiledMap;
import org.newdawn.slick.util.Log;

/**
 * An example to show scrolling around a tilemap smoothly. This seems to have caused confusion
 * a couple of times so here's "a" way to do it.
 *
 * @author kevin
 */
public class Scroller extends BasicGame {
	/** The size of the tank sprite - used for finding the centre */
	private static final int TANK_SIZE = 32;
	/** The size of the tiles - used to determine the amount to draw */
	private static final int TILE_SIZE = 32;
	/** The speed the tank moves at */
	private static final float TANK_MOVE_SPEED = 0.003f;
	/** The speed the tank rotates at */
	private static final float TANK_ROTATE_SPEED = 0.2f;
	
	/** The player's x position in tiles */
	private float playerX = 15;
	/** The player's y position in tiles */
	private float playerY = 16;
	
	/** The width of the display in tiles */
	private int widthInTiles;
	/** The height of the display in tiles */
	private int heightInTiles;
	
	/** The offset from the centre of the screen to the top edge in tiles */
	private int topOffsetInTiles;
	/** The offset from the centre of the screen to the left edge in tiles */
	private int leftOffsetInTiles;
	
	/** The map that we're going to drive around */
	private TiledMap map;
	
	/** The animation representing the player's tank */
	private Animation player;
	
	/** The angle the player is facing */
	private float ang;
	/** The x component of the movement vector */
	private float dirX;
	/** The y component of themovement vector */
	private float dirY;
	
	/** The collision map indicating which tiles block movement - generated based on tile properties */
	private boolean[][] blocked;
	
	/**
	 * Scroller example
	 */
	public Scroller() {
		super("Scroller");
	}
	
	/**
	 * @see org.newdawn.slick.BasicGame#init(org.newdawn.slick.GameContainer)
	 */
	public void init(GameContainer container) throws SlickException {
		// load the sprites and tiles, note that underneath the texture
		// will be shared between the sprite sheet and tilemap
		SpriteSheet sheet = new SpriteSheet("testdata/scroller/sprites.png",32,32);
		// load the tilemap created the TileD tool 
		map = new TiledMap("testdata/scroller/map.tmx");
		
		// build a collision map based on tile properties in the TileD map
		blocked = new boolean[map.getWidth()][map.getHeight()];
		for (int x=0;x<map.getWidth();x++) {
			for (int y=0;y<map.getHeight();y++) {
				int tileID = map.getTileId(x, y, 0);
				String value = map.getTileProperty(tileID, "blocked", "false");
				if ("true".equals(value)) {
					blocked[x][y] = true;
				}
			}
		}
		
		// caculate some layout values for rendering the tilemap. How many tiles
		// do we need to render to fill the screen in each dimension and how far is
		// it from the centre of the screen
		widthInTiles = container.getWidth() / TILE_SIZE;
		heightInTiles = container.getHeight() / TILE_SIZE;
		topOffsetInTiles = heightInTiles / 2;
		leftOffsetInTiles = widthInTiles / 2;
		
		// create the player sprite based on a set of sprites from the sheet loaded
		// above (tank tracks moving)
		player = new Animation();
		for (int frame=0;frame<7;frame++) {
			player.addFrame(sheet.getSprite(frame,1), 150);
		}
		player.setAutoUpdate(false);

		// update the vector of movement based on the initial angle
		updateMovementVector();
		
		Log.info("Window Dimensions in Tiles: "+widthInTiles+"x"+heightInTiles);
	}

	/**
	 * @see org.newdawn.slick.BasicGame#update(org.newdawn.slick.GameContainer, int)
	 */
	public void update(GameContainer container, int delta) throws SlickException {
		// check the controls, left/right adjust the rotation of the tank, up/down 
		// move backwards and forwards
		if (container.getInput().isKeyDown(Input.KEY_LEFT)) {
			ang -= delta * TANK_ROTATE_SPEED;
			updateMovementVector();
		}
		if (container.getInput().isKeyDown(Input.KEY_RIGHT)) {
			ang += delta * TANK_ROTATE_SPEED;
			updateMovementVector();
		}
		if (container.getInput().isKeyDown(Input.KEY_UP)) {
			if (tryMove(dirX * delta * TANK_MOVE_SPEED, dirY * delta * TANK_MOVE_SPEED)) {
				// if we managed to move update the animation
				player.update(delta);
			}
		}
		if (container.getInput().isKeyDown(Input.KEY_DOWN)) {
			if (tryMove(-dirX * delta * TANK_MOVE_SPEED, -dirY * delta * TANK_MOVE_SPEED)) {
				// if we managed to move update the animation
				player.update(delta);
			}
		}
	}

	/**
	 * Check if a specific location of the tank would leave it 
	 * on a blocked tile
	 * 
	 * @param x The x coordinate of the tank's location
	 * @param y The y coordinate of the tank's location
	 * @return True if the location is blocked
	 */
	private boolean blocked(float x, float y) {
		return blocked[(int) x][(int) y];
	}
	
	/**
	 * Try to move in the direction specified. If it's blocked, try sliding. If that
	 * doesn't work just don't bother
	 * 
	 * @param x The amount on the X axis to move
	 * @param y The amount on the Y axis to move
	 * @return True if we managed to move
	 */
	private boolean tryMove(float x, float y) {
		float newx = playerX + x;
		float newy = playerY + y;
		
		// first we try the real move, if that doesn't work
		// we try moving on just one of the axis (X and then Y) 
		// this allows us to slide against edges
		if (blocked(newx,newy)) {
			if (blocked(newx, playerY)) {
				if (blocked(playerX, newy)) {
					// can't move at all!
					return false;
				} else {
					playerY = newy;
					return true;
				}
			} else {
				playerX = newx;
				return true;
			}
		} else {
			playerX = newx;
			playerY = newy;
			return true;
		}
	}
	
	/**
	 * Update the direction that will be moved in based on the
	 * current angle of rotation
	 */
	private void updateMovementVector() {
		dirX = (float) Math.sin(Math.toRadians(ang));
		dirY = (float) -Math.cos(Math.toRadians(ang));
	}
	
	/**
	 * @see org.newdawn.slick.Game#render(org.newdawn.slick.GameContainer, org.newdawn.slick.Graphics)
	 */
	public void render(GameContainer container, Graphics g) throws SlickException {
		// draw the appropriate section of the tilemap based on the centre (hence the -(TANK_SIZE/2)) of
		// the player
		int playerTileX = (int) playerX;
		int playerTileY = (int) playerY;
		
		// caculate the offset of the player from the edge of the tile. As the player moves around this
		// varies and this tells us how far to offset the tile based rendering to give the smooth
		// motion of scrolling
		int playerTileOffsetX = (int) ((playerTileX - playerX) * TILE_SIZE);
		int playerTileOffsetY = (int) ((playerTileY - playerY) * TILE_SIZE);
		
		// render the section of the map that should be visible. Notice the -1 and +3 which renders
		// a little extra map around the edge of the screen to cope with tiles scrolling on and off
		// the screen
		map.render(playerTileOffsetX - (TANK_SIZE / 2), playerTileOffsetY - (TANK_SIZE / 2), 
				   playerTileX - leftOffsetInTiles - 1, 
				   playerTileY - topOffsetInTiles - 1,
				   widthInTiles + 3, heightInTiles + 3);
		
		// draw entities relative to the player that must appear in the centre of the screen
		g.translate(400 - (int) (playerX * 32), 300 - (int) (playerY * 32));
		
		drawTank(g, playerX, playerY, ang);
		// draw other entities here if there were any
		
		g.resetTransform();
	}

	/**
	 * Draw a single tank to the game
	 *  
	 * @param g The graphics context on which we're drawing
	 * @param xpos The x coordinate in tiles the tank is at
	 * @param ypos The y coordinate in tiles the tank is at
	 * @param rot The rotation of the tank
	 */
	public void drawTank(Graphics g, float xpos, float ypos, float rot) {
		// work out the centre of the tank in rendering coordinates and then
		// spit onto the screen
		int cx = (int) (xpos * 32);
		int cy = (int) (ypos * 32);
		g.rotate(cx,cy,rot);
		player.draw(cx-16,cy-16);
		g.rotate(cx,cy,-rot);
	}
	
	/**
	 * Entry point to the scroller example
	 * 
	 * @param argv The argument passed on the command line (if any)
	 */
	public static void main(String[] argv) {
		try {
			// create a new container for our example game. This container
			// just creates a normal native window for rendering OpenGL accelerated
			// elements to
			AppGameContainer container = new AppGameContainer(new Scroller(), 800, 600, false);
			container.start();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

