# A* Shortest Path Algorithm
# http://en.wikipedia.org/wiki/A*
# Taken from http://code.activestate.com/recipes/577519-a-star-shortest-path-algorithm/
# FB - 201012256
from heapq import heappush, heappop # for priority queue
import math
import time
import random
#from termcolor import colored

class node:
    xPos = 0 # x position
    yPos = 0 # y position
    distance = 0 # total distance already travelled to reach the node. This is "g"
    priority = 0 # priority = distance + remaining distance estimate, low is better. This is "f"
    def __init__(self, xPos, yPos, distance, priority): # Node Constructor
        self.xPos = xPos
        self.yPos = yPos
        self.distance = distance
        self.priority = priority
    def __lt__(self, other): # comparison method for priority queue
        return self.priority < other.priority
    def updatePriority(self, xDest, yDest):
        self.priority = self.distance + self.estimate(xDest, yDest) * 10 # A*
    # give higher priority to going straight instead of diagonally
    def nextMove(self, dirs, d): # d: direction to move
        if dirs == 8 and d % 2 != 0:
            self.distance += 14
        else:
            self.distance += 10
    # Estimation function for the remaining distance to the goal. This is "h"
    def estimate(self, xDest, yDest):
        xd = xDest - self.xPos
        yd = yDest - self.yPos
        # Euclidian Distance
        d = math.sqrt(xd * xd + yd * yd)
        # Manhattan distance
        # d = abs(xd) + abs(yd)
        # Chebyshev distance
        # d = max(abs(xd), abs(yd))
        return(d)

# A-star algorithm.
# The path "route" returned will be a string of digits of directions.
def pathFind(the_map, n, m, dirs, dx, dy, xA, yA, xB, yB):
    closed_nodes_map = [] # map of closed (tried-out) nodes = non-leaf nodes
    open_nodes_map = [] # map of open (not-yet-tried) nodes = leaf nodes
    dir_map = [] # map of dirs
    row = [0] * n
    for i in range(m): # create 2d arrays
        closed_nodes_map.append(list(row))
        open_nodes_map.append(list(row))
        dir_map.append(list(row))

    pq = [[], []] # priority queues of open (not-yet-expanded) nodes
    pqi = 0 # priority queue index
    # create the start node and push into list of open nodes
    n0 = node(xA, yA, 0, 0) # calls init in node class
    n0.updatePriority(xB, yB) # first time it's all heuristic guess on dist
    heappush(pq[pqi], n0)
    open_nodes_map[int(yA)][int(xA)] = n0.priority # mark it on the open nodes map

    # A* search itself:
    while len(pq[pqi]) > 0:
        # get the current node w/ the highest priority
        # from the list of open nodes
        n1 = pq[pqi][0] # top node
        n0 = node(n1.xPos, n1.yPos, n1.distance, n1.priority)
        x = n0.xPos
        y = n0.yPos
        heappop(pq[pqi]) # remove the node from the open list
        open_nodes_map[int(y)][int(x)] = 0
        closed_nodes_map[int(y)][int(x)] = 1 # mark it on the closed nodes map

        # quit searching when the goal is reached
        # If n0.estimate(xB, yB) == 0:
        if x == xB and y == yB:
            # generate the path from finish to start
            # by following the rules: "dirs" = 4 means straight only, "dirs" = 8 means also diagonally
            path = ''
            while not (x == xA and y == yA):
                j = int(dir_map[int(y)][int(x)])
                c = str(int((j + dirs / 2) % dirs)) # tricky use of "dirs" value
                path = c + path
                x += dx[j]
                y += dy[j]
            return path

        # Else generate moves (child nodes) in all possible dirs and continue
        for i in range(dirs):
            xdx = x + dx[i]
            ydy = y + dy[i]
            if not (xdx < 0 or xdx > n-1 or ydy < 0 or ydy > m - 1
                    or the_map[int(ydy)][int(xdx)] == 1 or closed_nodes_map[int(ydy)][int(xdx)] == 1):
                # generate a child node
                m0 = node(xdx, ydy, n0.distance, n0.priority)
                m0.nextMove(dirs, i)
                m0.updatePriority(xB, yB)
                # if it is not in the open list then add into that
                if open_nodes_map[int(ydy)][int(xdx)] == 0:
                    open_nodes_map[int(ydy)][int(xdx)] = m0.priority
                    heappush(pq[pqi], m0)
                    # mark its parent node direction
                    dir_map[int(ydy)][int(xdx)] = (i + dirs / 2) % dirs
                elif open_nodes_map[int(ydy)][int(xdx)] > m0.priority:
                    # update the priority
                    open_nodes_map[int(ydy)][int(xdx)] = m0.priority
                    # update the parent direction
                    dir_map[int(ydy)][int(xdx)] = (i + dirs / 2) % dirs
                    # replace the node
                    # by emptying one pq to the other one
                    # except the node to be replaced will be ignored
                    # and the new node will be pushed in instead
                    while not (pq[pqi][0].xPos == xdx and pq[pqi][0].yPos == ydy):
                        heappush(pq[1 - pqi], pq[pqi][0])
                        heappop(pq[pqi])
                    heappop(pq[pqi]) # remove the target node
                    # empty the larger size priority queue to the smaller one
                    if len(pq[pqi]) > len(pq[1 - pqi]):
                        pqi = 1 - pqi
                    while len(pq[pqi]) > 0:
                        heappush(pq[1-pqi], pq[pqi][0])
                        heappop(pq[pqi])       
                    pqi = 1 - pqi
                    heappush(pq[pqi], m0) # add the better node instead
    return '' # if no route found

# MAIN
dirs = 8 # number of possible directions to move on the map
if dirs == 4:
    dx = [1, 0, -1, 0]
    dy = [0, 1, 0, -1]
elif dirs == 8:
    dx = [1, 1, 0, -1, -1, -1, 0, 1]
    dy = [0, 1, 1, 1, 0, -1, -1, -1]

n = 30 # horizontal size of the map
m = 30 # vertical size of the map
the_map = []
row = [0] * n
for i in range(m): # create empty map
    the_map.append(list(row))

# fillout the map with a '+' pattern
for x in range(int(n / 8), int(n * 7 / 8)):
    the_map[int(m / 2)][x] = 1
for y in range(int(m/8), int(m * 7 / 8)):
    the_map[y][int(n / 2)] = 1

# add a few more sizeable obstacles for fun
big_obstacles = 1
if big_obstacles == 1:
    #print ("Debug - adding obstacles")
    for x in range(int(n * 2 / 8), int(n * 3 / 8)):
        the_map[int(m / 4)][x] = 1
        the_map[int(m * 3 / 4)][x] = 1
    for y in range(int(m * 5 /8), int(m * 6 / 8)):
        the_map[y][int(n / 4)] = 1
        the_map[y][int(n * 3/ 4)] = 1

# sprinkle some single-celled obstacles into it
little_obstacles = 30
if little_obstacles > 0:
    for x in range (little_obstacles):
        the_map[int(m*random.random())][int(n*random.random())] = 1

# And sprinkle some double-celled obstacles into it, vert and horiz
double_obstacles = 30
if double_obstacles > 0:
    for x in range (little_obstacles):
        x1 = int((m-1)*random.random())
        y1 = int((n-1)*random.random())
        x2 = int(random.random())
        if x2 == 0:
            y2 = 1
        else:
            y2 = 0        
        the_map[x1][y1] = 1
        the_map[x1+x2][y1+y2] = 1

# randomly select start and finish locations from a list
sf = []
sf.append((0, 0, n - 1, m - 1))
sf.append((0, m - 1, n - 1, 0))
sf.append((n / 2 - 1, m / 2 - 1, n / 2 + 1, m / 2 + 1))
sf.append((n / 2 - 1, m / 2 + 1, n / 2 + 1, m / 2 - 1))
sf.append((n / 2 - 1, 0, n / 2 + 1, m - 1))
sf.append((n / 2 + 1, m - 1, n / 2 - 1, 0))
sf.append((0, m / 2 - 1, n - 1, m / 2 + 1))
sf.append((n - 1, m / 2 + 1, 0, m / 2 - 1))
(xA, yA, xB, yB) = random.choice(sf)
xA = int(xA)
yA = int(yA)
xB = int(xB)
yB = int(yB)
# print ("Debug: Starting the_map is ", the_map)
print ('Map size (X,Y): ', n, m)
print ('Start: ', xA, yA)
print ('Finish: ', xB, yB)
t = time.time()
route = pathFind(the_map, n, m, dirs, dx, dy, xA, yA, xB, yB)
print ('Time to generate the route (seconds): ', time.time() - t)
print ('Route:')
print (route)
#print ("Debug: Ending the_map is ")
#print (the_map)

# mark the route on the map
if len(route) > 0:
    x = int(xA)
    y = int(yA)
    the_map[y][x] = 2
    for i in range(len(route)):
        #print("Debug pt 1", i, len(route), route[i])
        if (route[i] != "."): # Not needed - removed "."'s from ending routes
            j = int(route[i])
            x += dx[j]
            y += dy[j]
            #print("Debug pt 2", x, y, the_map[y][x])
            the_map[y][x] = 3 # Problems here!
    the_map[y][x] = 4
#print("Debug: Map with route is:")
#print (the_map)
#input('Debug - Press Enter...')

# display the map with the route added
print ('Map:')
for y in range(m):
    for x in range(n):
        xy = the_map[y][x]
        if xy == 0:
            print ('.',end=" ") # space
        elif xy == 1:
            print ('O',end=" ") # obstacle
        elif xy == 2:
            print ('S',end=" ") # start
        elif xy == 3:
            print ('R',end=" ") # route
        elif xy == 4:
            print ('F',end=" ") # finish
    print ()

input('Press Enter...')