package squidpony.squidgrid;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

import squidpony.annotation.GwtIncompatible;
import squidpony.squidmath.Bresenham;
import squidpony.squidmath.Coord;
import squidpony.squidmath.DDALine;
import squidpony.squidmath.Elias;
import squidpony.squidmath.OrderedSet;

/**
 * Line of Sight (LOS) algorithms find if there is or is not a path between two
 * given points. <br>
 * The line found between two points will end at either the target, the
 * obstruction closest to the start, or the edge of the map. <br>
 * For normal line of sight usage, you should prefer Bresenham lines, and these
 * are the default (they can also be specified by passing {@link #BRESENHAM} to
 * the constructor). For more specialized usage, there are other kinds of LOS in
 * this class, like lines that make no diagonal moves between cells (using
 * {@link #ORTHO}, or lines that check a wide path (but these use different
 * methods, like {@link #thickReachable(Radius)}). <br>
 * Performance-wise, all of these methods are rather fast and about the same
 * speed. {@link #RAY} is a tiny fraction faster than {@link #BRESENHAM} but
 * produces rather low-quality lines in comparison. Calculating the visibility
 * of 40,000 lines in a 102x102 dungeon takes within 3% of 950ms (on an Intel
 * i7-4700MQ laptop processor) for every one of BRESENHAM, DDA, ORTHO, and RAY,
 * even with ORTHO finding a different kind of line by design.
 *
 * @author Eben Howard - http://squidpony.com - howard@squidpony.com
 * @author Tommy Ettinger Added DDA, ORTHO, and the thick lines; some cleanup
 * @author smelC optimized several methods
 */
public class LOS {

	// constants to indicate desired type of solving algorithm to use
	/**
	 * A Bresenham-based line-of-sight algorithm.
	 */
	public static final int BRESENHAM = 1;
	/**
	 * Uses Wu's Algorithm as modified by Elias to draw the line. Does not end at an
	 * obstruction but rather returns one of the possible attempted paths in full.
	 *
	 * <p>
	 * Be aware, it is GWT-incompatible.
	 * </p>
	 */
	public static final int ELIAS = 2;
	/**
	 * Uses a series of rays internal to the start and end point to determine
	 * visibility. Appearance is extremely close to DDA, which is also probably a
	 * faster algorithm, so BRESENHAM (which can look a little better) and DDA are
	 * recommended instead of RAY.
	 */
	public static final int RAY = 3;
	/**
	 * Optimized algorithm for Bresenham-like lines. There are slight differences in
	 * many parts of the lines this draws when compared to Bresenham lines, but it
	 * may also perform significantly better, and may also be useful as a building
	 * block for more complex LOS. Virtually identical in results to RAY, and just a
	 * hair slower, but better-tested and more predictable.
	 */
	public static final int DDA = 5;
	/**
	 * Draws a line as if with a thick brush, going from a point between a corner of
	 * the starting cell and the center of the starting cell to the corresponding
	 * corner of the target cell, and considers the target visible if any portion of
	 * the thick stroke reached it. Will result in 1-width lines for
	 * exactly-orthogonal or exactly-diagonal lines and some parts of other lines,
	 * but usually is 2 cells wide.
	 */
	public static final int THICK = 6;
	private LinkedList<Coord> lastPath = new LinkedList<>();
	private int type;
	private double[][] resistanceMap;
	private int startx, starty, targetx, targety;
	private Elias elias = null;

	/**
	 * Gets the radius strategy this uses.
	 * 
	 * @return the current Radius enum used to measure distance; starts at CIRCLE if
	 *         not specified
	 */
	public Radius getRadiusStrategy() {
		return radiusStrategy;
	}

	/**
	 * Set the radius strategy to the given Radius; the default is CIRCLE if this is
	 * not called.
	 * 
	 * @param radiusStrategy
	 *            a Radius enum to determine how distances are measured
	 */
	public void setRadiusStrategy(Radius radiusStrategy) {
		this.radiusStrategy = radiusStrategy;
	}

	private Radius radiusStrategy = Radius.CIRCLE;

	/**
	 * Constructs an LOS that will draw Bresenham lines and measure distances using
	 * the CIRCLE radius strategy.
	 */
	public LOS() {
		this(BRESENHAM);
	}

	/**
	 * Constructs an LOS with the given type number, which must equal a static field
	 * in this class such as BRESENHAM.
	 * 
	 * @param type
	 *            an int that must correspond to the value of a static field in this
	 *            class (such as BRESENHAM)
	 */
	public LOS(int type) {
		this.type = type;
		if (type == ELIAS)
			elias = new Elias();
	}

	/**
	 * Returns true if a line can be drawn from the start point to the target point
	 * without intervening obstructions.
	 *
	 * Uses RadiusStrategy.CIRCLE, or whatever RadiusStrategy was set with
	 * setRadiusStrategy .
	 *
	 * @param walls
	 *            '#' is fully opaque, anything else is fully transparent, as always
	 *            this uses x,y indexing.
	 * @param startx
	 *            starting x position on the grid
	 * @param starty
	 *            starting y position on the grid
	 * @param targetx
	 *            ending x position on the grid
	 * @param targety
	 *            ending y position on the grid
	 * @return true if a line can be drawn without being obstructed, false otherwise
	 */
	public boolean isReachable(char[][] walls, int startx, int starty, int targetx, int targety) {
		if (walls.length < 1)
			return false;
		double[][] resMap = new double[walls.length][walls[0].length];
		for (int x = 0; x < walls.length; x++) {
			for (int y = 0; y < walls[0].length; y++) {
				resMap[x][y] = (walls[x][y] == '#') ? 1.0 : 0.0;
			}
		}
		return isReachable(resMap, startx, starty, targetx, targety, radiusStrategy);
	}

	/**
	 * Returns true if a line can be drawn from the start point to the target point
	 * without intervening obstructions.
	 *
	 * Does not take into account resistance less than opaque or distance cost.
	 *
	 * Uses RadiusStrategy.CIRCLE, or whatever RadiusStrategy was set with
	 * setRadiusStrategy .
	 *
	 * @param resistanceMap
	 *            0.0 is fully transparent, 1.0 is fully opaque, as always this uses
	 *            x,y indexing.
	 * @param startx
	 *            starting x position on the grid
	 * @param starty
	 *            starting y position on the grid
	 * @param targetx
	 *            ending x position on the grid
	 * @param targety
	 *            ending y position on the grid
	 * @return true if a line can be drawn without being obstructed, false otherwise
	 */
	public boolean isReachable(double[][] resistanceMap, int startx, int starty, int targetx, int targety) {
		return isReachable(resistanceMap, startx, starty, targetx, targety, radiusStrategy);
	}

	/**
	 * Returns true if a line can be drawn from the start point to the target point
	 * without intervening obstructions.
	 *
	 * @param resistanceMap
	 *            0.0 is fully transparent, 1.0 is fully opaque, as always this uses
	 *            x,y indexing.
	 * @param startx
	 *            starting x position on the grid
	 * @param starty
	 *            starting y position on the grid
	 * @param targetx
	 *            ending x position on the grid
	 * @param targety
	 *            ending y position on the grid
	 * @param radiusStrategy
	 *            the strategy to use in computing unit distance
	 * @return true if a line can be drawn without being obstructed, false otherwise
	 */
	public boolean isReachable(double[][] resistanceMap, int startx, int starty, int targetx, int targety,
			Radius radiusStrategy) {
		if (resistanceMap.length < 1)
			return false;
		this.resistanceMap = resistanceMap;
		this.startx = startx;
		this.starty = starty;
		this.targetx = targetx;
		this.targety = targety;
		switch (type) {
		case BRESENHAM:
			return bresenhamReachable(radiusStrategy);
		case ELIAS:
			throw new IllegalStateException("Elias LOS is Gwt Incompatible");
			// Comment required to compile with GWT:
			// return eliasReachable(radiusStrategy);
		case RAY:
			return rayReachable(radiusStrategy);
		case DDA:
			return ddaReachable(radiusStrategy);
		case THICK:
			return thickReachable(radiusStrategy);
		}
		return false;
	}

	/**
	 * Returns true if a line can be drawn from the start point to the target point
	 * without intervening obstructions.
	 *
	 * @param walls
	 *            '#' is fully opaque, anything else is fully transparent, as always
	 *            this uses x,y indexing.
	 * @param startx
	 *            starting x position on the grid
	 * @param starty
	 *            starting y position on the grid
	 * @param targetx
	 *            ending x position on the grid
	 * @param targety
	 *            ending y position on the grid
	 * @param radiusStrategy
	 *            the strategy to use in computing unit distance
	 * @return true if a line can be drawn without being obstructed, false otherwise
	 */
	public boolean isReachable(char[][] walls, int startx, int starty, int targetx, int targety,
			Radius radiusStrategy) {
		if (walls.length < 1)
			return false;
		double[][] resMap = new double[walls.length][walls[0].length];
		for (int x = 0; x < walls.length; x++) {
			for (int y = 0; y < walls[0].length; y++) {
				resMap[x][y] = (walls[x][y] == '#') ? 1.0 : 0.0;
			}
		}
		return isReachable(resMap, startx, starty, targetx, targety, radiusStrategy);
	}

	/**
	 * Returns true if a line can be drawn from the any of the points within spread
	 * cells of the start point, to any of the corresponding points at the same
	 * direction and distance from the target point, without intervening
	 * obstructions. Primarily useful to paint a broad line that can be retrieved
	 * with getLastPath.
	 *
	 * @param walls
	 *            '#' is fully opaque, anything else is fully transparent, as always
	 *            this uses x,y indexing.
	 * @param startx
	 *            starting x position on the grid
	 * @param starty
	 *            starting y position on the grid
	 * @param targetx
	 *            ending x position on the grid
	 * @param targety
	 *            ending y position on the grid
	 * @param radiusStrategy
	 *            the strategy to use in computing unit distance
	 * @param spread
	 *            the number of cells outward, measured by radiusStrategy, to place
	 *            extra start and target points
	 * @return true if a line can be drawn without being obstructed, false otherwise
	 */
	public boolean spreadReachable(char[][] walls, int startx, int starty, int targetx, int targety,
			Radius radiusStrategy, int spread) {
		if (walls.length < 1)
			return false;
		resistanceMap = new double[walls.length][walls[0].length];
		for (int x = 0; x < walls.length; x++) {
			for (int y = 0; y < walls[0].length; y++) {
				resistanceMap[x][y] = (walls[x][y] == '#') ? 1.0 : 0.0;
			}
		}
		this.startx = startx;
		this.starty = starty;
		this.targetx = targetx;
		this.targety = targety;

		return brushReachable(radiusStrategy, spread);
	}

	/**
	 * Returns true if a line can be drawn from the any of the points within spread
	 * cells of the start point, to any of the corresponding points at the same
	 * direction and distance from the target point, without intervening
	 * obstructions. Primarily useful to paint a broad line that can be retrieved
	 * with getLastPath.
	 *
	 * @param resistanceMap
	 *            0.0 is fully transparent, 1.0 is fully opaque, as always this uses
	 *            x,y indexing.
	 * @param startx
	 *            starting x position on the grid
	 * @param starty
	 *            starting y position on the grid
	 * @param targetx
	 *            ending x position on the grid
	 * @param targety
	 *            ending y position on the grid
	 * @param radiusStrategy
	 *            the strategy to use in computing unit distance
	 * @param spread
	 *            the number of cells outward, measured by radiusStrategy, to place
	 *            extra start and target points
	 * @return true if a line can be drawn without being obstructed, false otherwise
	 */
	public boolean spreadReachable(double[][] resistanceMap, int startx, int starty, int targetx, int targety,
			Radius radiusStrategy, int spread) {
		if (resistanceMap.length < 1)
			return false;
		this.resistanceMap = resistanceMap;
		this.startx = startx;
		this.starty = starty;
		this.targetx = targetx;
		this.targety = targety;

		return brushReachable(radiusStrategy, spread);
	}

	/**
	 * Returns the path of the last LOS calculation, with the starting point as the
	 * head of the queue.
	 *
	 * @return
	 */
	public LinkedList<Coord> getLastPath() {
		return lastPath;
	}

	/*
	 * private boolean bresenhamReachable(Radius radiusStrategy) { Queue<Coord> path
	 * = Bresenham.line2D(startx, starty, targetx, targety); lastPath = new
	 * LinkedList<>(); lastPath.add(Coord.get(startx, starty)); double decay = 1 /
	 * radiusStrategy.radius(startx, starty, targetx, targety); double currentForce
	 * = 1; for (Coord p : path) { lastPath.offer(p); if (p.x == targetx && p.y ==
	 * targety) { return true;//reached the end } if (p.x != startx || p.y !=
	 * starty) {//don't discount the start location even if on resistant cell
	 * currentForce -= resistanceMap[p.x][p.y]; } double r =
	 * radiusStrategy.radius(startx, starty, p.x, p.y); if (currentForce - (r *
	 * decay) <= 0) { return false;//too much resistance } } return false;//never
	 * got to the target point }
	 */
	private boolean bresenhamReachable(Radius radiusStrategy) {
		Coord[] path = Bresenham.line2D_(startx, starty, targetx, targety);
		lastPath = new LinkedList<>();
		double rad = radiusStrategy.radius(startx, starty, targetx, targety);
		if (rad == 0.0) {
			lastPath.add(Coord.get(startx, starty));
			return true; // already at the point; we can see our own feet just fine!
		}
		double decay = 1 / rad;
		double currentForce = 1;
		Coord p;
		for (int i = 0; i < path.length; i++) {
			p = path[i];
			lastPath.offer(p);
			if (p.x == targetx && p.y == targety) {
				return true;// reached the end
			}
			if (p.x != startx || p.y != starty) {// don't discount the start location even if on resistant cell
				currentForce -= resistanceMap[p.x][p.y];
			}
			double r = radiusStrategy.radius(startx, starty, p.x, p.y);
			if (currentForce - (r * decay) <= 0) {
				return false;// too much resistance
			}
		}
		return false;// never got to the target point
	}

	private boolean ddaReachable(Radius radiusStrategy) {
		Coord[] path = DDALine.line_(startx, starty, targetx, targety);
		lastPath = new LinkedList<>();
		double rad = radiusStrategy.radius(startx, starty, targetx, targety);
		if (rad == 0.0) {
			lastPath.add(Coord.get(startx, starty));
			return true; // already at the point; we can see our own feet just fine!
		}
		double decay = 1 / rad;
		double currentForce = 1;
		Coord p;
		for (int i = 0; i < path.length; i++) {
			p = path[i];
			if (p.x == targetx && p.y == targety) {
				lastPath.offer(p);
				return true;// reached the end
			}
			if (p.x != startx || p.y != starty) {// don't discount the start location even if on resistant cell
				currentForce -= resistanceMap[p.x][p.y];
			}
			double r = radiusStrategy.radius(startx, starty, p.x, p.y);
			if (currentForce - (r * decay) <= 0) {
				return false;// too much resistance
			}
			lastPath.offer(p);
		}
		return false;// never got to the target point
	}

	private boolean thickReachable(Radius radiusStrategy) {
		lastPath = new LinkedList<>();
		double dist = radiusStrategy.radius(startx, starty, targetx, targety);
		double decay = 1.0 / dist; // note: decay can be positive infinity if dist is 0; this is actually OK
		OrderedSet<Coord> visited = new OrderedSet<>((int) dist + 3);
		List<List<Coord>> paths = new ArrayList<>(4);
		/*
		 * // actual corners paths.add(DDALine.line(startx, starty, targetx, targety, 0,
		 * 0)); paths.add(DDALine.line(startx, starty, targetx, targety, 0, 0xffff));
		 * paths.add(DDALine.line(startx, starty, targetx, targety, 0xffff, 0));
		 * paths.add(DDALine.line(startx, starty, targetx, targety, 0xffff, 0xffff));
		 */
		// halfway between the center and a corner
		paths.add(DDALine.line(startx, starty, targetx, targety, 0x3fff, 0x3fff));
		paths.add(DDALine.line(startx, starty, targetx, targety, 0x3fff, 0xbfff));
		paths.add(DDALine.line(startx, starty, targetx, targety, 0xbfff, 0x3fff));
		paths.add(DDALine.line(startx, starty, targetx, targety, 0xbfff, 0xbfff));

		int length = Math.max(paths.get(0).size(),
				Math.max(paths.get(1).size(), Math.max(paths.get(2).size(), paths.get(3).size())));
		double[] forces = new double[] { 1, 1, 1, 1 };
		boolean[] go = new boolean[] { true, true, true, true };
		Coord p;
		for (int d = 0; d < length; d++) {
			for (int pc = 0; pc < 4; pc++) {
				List<Coord> path = paths.get(pc);
				if (d < path.size() && go[pc])
					p = path.get(d);
				else
					continue;
				if (p.x == targetx && p.y == targety) {
					visited.add(p);
					lastPath.addAll(visited);
					return true;// reached the end
				}
				if (p.x != startx || p.y != starty) {// don't discount the start location even if on resistant cell
					forces[pc] -= resistanceMap[p.x][p.y];
				}
				double r = radiusStrategy.radius(startx, starty, p.x, p.y);
				if (forces[pc] - (r * decay) <= 0) {
					go[pc] = false;
					continue;// too much resistance
				}
				visited.add(p);
			}
		}
		lastPath.addAll(visited);
		return false;// never got to the target point
	}

	private boolean brushReachable(Radius radiusStrategy, int spread) {
		lastPath = new LinkedList<>();
		double dist = radiusStrategy.radius(startx, starty, targetx, targety) + spread * 2, decay = 1 / dist;
		OrderedSet<Coord> visited = new OrderedSet<>((int) (dist + 3) * spread);
		List<List<Coord>> paths = new ArrayList<>((int) (radiusStrategy.volume2D(spread) * 1.25));
		int length = 0;
		List<Coord> currentPath;
		for (int i = -spread; i <= spread; i++) {
			for (int j = -spread; j <= spread; j++) {
				if (radiusStrategy.inRange(startx, starty, startx + i, starty + j, 0, spread) && startx + i >= 0
						&& starty + j >= 0 && startx + i < resistanceMap.length && starty + j < resistanceMap[0].length
						&& targetx + i >= 0 && targety + j >= 0 && targetx + i < resistanceMap.length
						&& targety + j < resistanceMap[0].length) {
					for (int q = 0x3fff; q < 0xffff; q += 0x8000) {
						for (int r = 0x3fff; r < 0xffff; r += 0x8000) {
							currentPath = DDALine.line(startx + i, starty + j, targetx + i, targety + j, q, r);
							paths.add(currentPath);
							length = Math.max(length, currentPath.size());
						}
					}
				}
			}
		}
		double[] forces = new double[paths.size()];
		Arrays.fill(forces, 1.0);
		boolean[] go = new boolean[paths.size()];
		Arrays.fill(go, true);
		Coord p;
		boolean found = false;
		for (int d = 0; d < length; d++) {
			for (int pc = 0; pc < paths.size(); pc++) {
				List<Coord> path = paths.get(pc);
				if (d < path.size() && go[pc])
					p = path.get(d);
				else
					continue;
				if (p.x == targetx && p.y == targety) {
					found = true;
				}
				if (p.x != startx || p.y != starty) {// don't discount the start location even if on resistant cell
					forces[pc] -= resistanceMap[p.x][p.y];
				}
				double r = radiusStrategy.radius(startx, starty, p.x, p.y);
				if (forces[pc] - (r * decay) <= 0) {
					go[pc] = false;
					continue;// too much resistance
				}
				visited.add(p);
			}
		}
		lastPath.addAll(visited);
		return found;// never got to the target point
	}

	private boolean rayReachable(Radius radiusStrategy) {
		lastPath = new LinkedList<>();// save path for later retrieval
		if (startx == targetx && starty == targety) {// already there!
			lastPath.add(Coord.get(startx, starty));
			return true;
		}

		int width = resistanceMap.length;
		int height = resistanceMap[0].length;

		Coord end = Coord.get(targetx, targety);
		// find out which direction to step, on each axis
		int stepX = (int) Math.signum(targetx - startx), stepY = (int) Math.signum(targety - starty);

		int deltaY = Math.abs(targetx - startx), deltaX = Math.abs(targety - starty);

		int testX = startx, testY = starty;

		int maxX = deltaX, maxY = deltaY;

		while (testX >= 0 && testX < width && testY >= 0 && testY < height && (testX != targetx || testY != targety)) {
			lastPath.add(Coord.get(testX, testY));
			if (maxY - maxX > deltaX) {
				maxX += deltaX;
				testX += stepX;
				if (resistanceMap[testX][testY] >= 1f) {
					end = Coord.get(testX, testY);
					break;
				}
			} else if (maxX - maxY > deltaY) {
				maxY += deltaY;
				testY += stepY;
				if (resistanceMap[testX][testY] >= 1f) {
					end = Coord.get(testX, testY);
					break;
				}
			} else {// directly on diagonal, move both full step
				maxY += deltaY;
				testY += stepY;
				maxX += deltaX;
				testX += stepX;
				if (resistanceMap[testX][testY] >= 1f) {
					end = Coord.get(testX, testY);
					break;
				}
			}
			if (radiusStrategy.radius(testX, testY, startx, starty) > radiusStrategy.radius(startx, starty, end.x,
					end.y)) {// went too far
				break;
			}
		}

		if (end.x >= 0 && end.x < width && end.y >= 0 && end.y < height) {
			lastPath.add(Coord.get(end.x, end.y));
		}

		return end.x == targetx && end.y == targety;
	}

	@GwtIncompatible /* Because of Thread */
	private boolean eliasReachable(Radius radiusStrategy) {
		if (elias == null)
			elias = new Elias();
		List<Coord> ePath = elias.line(startx, starty, targetx, targety);
		lastPath = new LinkedList<>(ePath);// save path for later retreival

		HashMap<EliasWorker, Thread> pool = new HashMap<>();

		for (Coord p : ePath) {
			EliasWorker worker = new EliasWorker(p.x, p.y, radiusStrategy);
			Thread thread = new Thread(worker);
			thread.start();
			pool.put(worker, thread);
		}

		for (EliasWorker w : pool.keySet()) {
			try {
				pool.get(w).join();
			} catch (InterruptedException ex) {
			}
			if (w.succeeded) {
				lastPath = w.path;
				return true;
			}
		}

		return false;// never got to the target point
	}

	private class EliasWorker implements Runnable {

		private LinkedList<Coord> path;
		private boolean succeeded = false;
		private int testx, testy;
		private Radius eliasRadiusStrategy;

		EliasWorker(int testx, int testy, Radius radiusStrategy) {
			this.testx = testx;
			this.testy = testy;
			this.eliasRadiusStrategy = radiusStrategy;
		}

		@Override
		public void run() {
			LOS los1 = new LOS(BRESENHAM);
			LOS los2 = new LOS(BRESENHAM);
			// if a non-solid midpoint on the path can see both the start and end, consider
			// the two ends to be able to see each other
			if (resistanceMap[testx][testy] < 1
					&& eliasRadiusStrategy.radius(startx, starty, testx, testy) <= eliasRadiusStrategy.radius(startx,
							starty, targetx, targety)
					&& los1.isReachable(resistanceMap, testx, testy, targetx, targety, eliasRadiusStrategy)
					&& los2.isReachable(resistanceMap, startx, starty, testx, testy, eliasRadiusStrategy)) {

				// record actual sight path used
				path = new LinkedList<>(los2.lastPath);
				path.addAll(los1.lastPath);
				succeeded = true;
			}
		}
	}
}
