package ei.game.algo;

import java.util.Arrays;

/**
 * DJK path finder across a tile map
 * 
 * @author Kevin Glass
 */
public class DJKPathFinder implements PathFinder {
	/** The map being searchd */
	private TileMap map;
	/** The tileset describing the tile properties on the map */
	private TileSet set;
	/** The distances from the current destination on the map */
	private int[] distance;
	/** The maximum search depth reached before giving up */
	private int maxsearch;
	/** The starting x coordinate */
	private int sx;
	/** The starting y coordinate */
	private int sy;
	/** The distance from the destination of the best path found so far */
	private int bestPath = 10000;
	
	/**
	 * Create a new DJK based path finder
	 * 
	 * @param map The map being searched
	 * @param set The set describing the tiles on the map
	 */
	public DJKPathFinder(TileMap map,TileSet set) {
		this.map = map;
		this.set = set;
		distance = new int[map.getMapWidth()*map.getMapHeight()];
	}
	
	/**
	 * @see org.newdawn.util.map.PathFinder#reset()
	 */
	public void reset() {
		Arrays.fill(distance,0);
	}
	
	/**
	 * @see org.newdawn.util.map.PathFinder#getSearchData()
	 */
	public int[] getSearchData() {
		return distance;
	}
	
	/**
	 * @see org.newdawn.util.map.PathFinder#findPath(int, int, int, int, int)
	 */
	public synchronized Path findPath(int sx,int sy,int dx,int dy,int maxsearch) {
		int depth = 1;
		this.sx = sx;
		this.sy = sy;
		bestPath = 10000;
		
		this.maxsearch = maxsearch;
		
		if (processNode(dx,dy,depth)) {
			Path path = new Path();
			
			findNextPoint(path,sx,sy,distance[sx+(sy*map.getMapWidth())]-1);
			return path;
		} else {
			return null;
		}
	}
	
	/**
	 * Find the next point on the discovered path by moving 
	 * along the distances. After finding the path by DJK we need
	 * to trace back along the distances working out how we got to the
	 * end.
	 * 
	 * @param path The path being built up
	 * @param x The current x position
	 * @param y The current y position
	 * @param d The distance we're searching for at the moment.
	 */
	private void findNextPoint(Path path,int x,int y,int d) {
		if (d < 0) {
			return;
		}
		path.addPoint(x,y);
		
		for (int xo=-1;xo<2;xo++) {
			for (int yo=-1;yo<2;yo++) {
				if ((xo != 0) || (yo != 0)) {
					if ((xo == 0) || (yo == 0)) {
						int sx = x+xo;
						int sy = y+yo;
						
						if (distance[sx+(sy*map.getMapWidth())] == d) {
							findNextPoint(path,sx,sy,distance[sx+(sy*map.getMapWidth())]-1);
							return;
						}
					}
				}
			}
		}
		
		for (int xo=-1;xo<2;xo++) {
			for (int yo=-1;yo<2;yo++) {
				if ((xo != 0) && (yo != 0)) {
					int sx = x+xo;
					int sy = y+yo;
					
					if (distance[sx+(sy*map.getMapWidth())] == d) {
						findNextPoint(path,sx,sy,distance[sx+(sy*map.getMapWidth())]-1);
						return;
					}
				}
			}
		}
	}
	
	/**
	 * Traverse the specified graph node (in this case grid cell)
	 * 
	 * @param x The x location we're processing
	 * @param y The y location we're processing
	 * @param depth The depth of the search we're at
	 * @return True if we're reached the destination
	 */
	private boolean processNode(int x,int y,int depth) {
		if (depth >= maxsearch) {
			return false;
		}
		if (depth >= bestPath) {
			return false;
		}
		
		if ((x < 0) || (y < 0) || (x >= map.getMapWidth()) || (y >= map.getMapHeight())) {
			return false;
		}
		
		if (isBlocked(x,y)) {
			return false;
		}
		
		int val = distance[x+(y*map.getMapWidth())];
		
		if ((val != 0) && (val <= depth)) {
			return false;
		}
		
		distance[x+(y*map.getMapWidth())] = depth;

		if ((x == sx) && (y == sy)) {
			bestPath = depth;
			return true;
		}
		
		boolean found = false;
		
		for (int xo=-1;xo<2;xo++) {
			for (int yo=-1;yo<2;yo++) {
				if ((xo != 0) || (yo != 0)) {
					if (validMove(x,y,xo,yo)) {
						found |= processNode(x+xo,y+yo,depth+1);
					}
				}
			}
		}
		
		return found;
	}
	
	/**
	 * Check if the move specified is valid 
	 *  
	 * @param x The x position we're starting from
	 * @param y The y position we're starting from
	 * @param xo The x direction we're moving in
	 * @param yo The y direction we're moving in
	 * @return True if the move specified is valid
	 */
	private boolean validMove(int x,int y,int xo,int yo) {
		if ((xo == 0) || (yo == 0)) {
			return true;
		}
		
		if (isBlocked(x+xo,y)) {
			return false;
		}
		if (isBlocked(x,y+yo)) {
			return false;
		}
		
		return true;
	}
	
	/**
	 * Check if a particular location on the map blocks movement
	 * 
	 * @param x The x location to check
	 * @param y The y location to check
	 * @return True if the locaiton is blocked
	 */
	public boolean isBlocked(int x,int y) {
		if (map.isBlocked(x,y)) {
			return true;
		}
		
		return set.blocksMovement(map.getTileAt(x,y,0));
	}
}
