import heapq
import time
import codecs
import csv

class Cell:
    def __init__(self,x,y,isWall,isStart,isExit):
        self.x=x
        self.y=y
        self.isWall=isWall
        self.isStart=isStart
        self.isExit=isExit
    def isPassable(self):
        if self.isWall==False:
            return True
        else:
            return False
        
class Maze:
    def __init__(self,grid):
        self.grid=grid
        self.width=len(self.grid[0])
        self.height=len(self.grid) 
        self.start=None
        self.exit=None
        for i in range(self.height):
            for j in range(self.width):
                if self.grid[i][j].isStart==True:
                    self.start=(j,i)
                if self.grid[i][j].isExit==True:
                    self.exit=(j,i)
    
    def getCell(self,x,y):
        return self.grid[y][x]
    
    def getNeighbours(self,cell):
        x=cell.x
        y=cell.y
        cell_up=None
        cell_down=None
        cell_left=None
        cell_right=None
        
        #up
        if y>0:
            if self.grid[y-1][x].isPassable()==True:
                cell_up=self.grid[y-1][x]     
        #down
        if y<(self.height-1):
            if self.grid[y+1][x].isPassable()==True:
                cell_down=self.grid[y+1][x]
        #left
        if x>0:
            if self.grid[y][x-1].isPassable()==True:
                cell_left=self.grid[y][x-1]
        #right
        if x<(self.width-1):
            if self.grid[y][x+1].isPassable()==True:
                cell_right=self.grid[y][x+1]
        
        #neighbours=[cell_up, cell_down, cell_left, cell_right]
        neighbours=[cell_down, cell_right, cell_up, cell_left]
        return neighbours

class TextFileMazeBuilder:
    def build(self,filename):
        with open(filename,"r") as f:
            lines=f.readlines()
        grid=[]
        x=0
        y=0
        for i in range(len(lines)):
            line=lines[i]
            x=0
            row=[]
            for j in range(len(line)):
                if line[j]=="#":
                    row.append(Cell(x,y,True,False,False))
                elif line[j]==" ":
                    row.append(Cell(x,y,False,False,False))
                elif line[j]=="S":
                    row.append(Cell(x,y,False,True,False))
                elif line[j]=="E":
                    row.append(Cell(x,y,False,False,True))
                x+=1
            grid.append(row)
            y+=1
        return grid
    
class MazeBuilder:
    def buildFromFile(self,filename):
        grid = TextFileMazeBuilder().build(filename)
        return Maze(grid)

class SearchStats:
    def __init__(self,ttime,visited_cells,path_length):
        self.ttime=ttime
        self.visited_cells=visited_cells
        self.path_length=path_length

class BFSStrategy:
    def findPath(self,maze,start,exxit):
        tstart=time.perf_counter()
        queue=[]
        queue.append((maze.getCell(start[0],start[1]),0))
        visited_cells=[maze.getCell(start[0],start[1])]
        Path={}
        f=0
        count=0
        while len(queue)!=0:
            temp=queue.pop(0)
            cell=temp[0]
            steps=temp[1]
            directions=maze.getNeighbours(cell)
            count+=1
               
            if (cell.x,cell.y)==exxit:
                end=time.perf_counter()
                #print(end-tstart)
                f=1
                break
                
            for i in directions:
                if i!=None:
                    flag=0
                    for j in visited_cells:
                        if i==j:
                            flag=1
                            break
                    if flag==0:
                        queue.append((i,steps+1))
                        visited_cells.append(i)
                        Path[(i.x,i.y)]=(cell.x,cell.y)
        reversePath=[]
        if f==1:
            cell=exxit
            while cell!=start:
                reversePath.append(cell)
                cell=Path[cell]
            reversePath.append(cell)
            reversePath.reverse()
            #print(len(reversePath)-1)
        return reversePath, count

class DFSStrategy:
    def findPath(self,maze,start,exxit):
        tstart=time.perf_counter()
        queue=[]
        queue.append((maze.getCell(start[0],start[1]),0))
        visited_cells=[maze.getCell(start[0],start[1])]
        Path={}
        f=0
        count=0
        while len(queue)!=0:
            temp=queue.pop()
            cell=temp[0]
            steps=temp[1]
            directions=maze.getNeighbours(cell)
            count+=1
               
            if (cell.x,cell.y)==exxit:
                end=time.perf_counter()
                #print(end-tstart)
                f=1
                break
                
            for i in directions:
                if i!=None:
                    flag=0
                    for j in visited_cells:
                        if i==j:
                            flag=1
                            break
                    if flag==0:
                        queue.append((i,steps+1))
                        visited_cells.append(i)
                        Path[(i.x,i.y)]=(cell.x,cell.y)
        reversePath=[]
        if f==1:
            cell=exxit
            while cell!=start:
                reversePath.append(cell)
                cell=Path[cell]
            reversePath.append(cell)
            reversePath.reverse()
            #print(len(reversePath)-1)
        return reversePath, count



class AStarStrategy:
    def H(self,exxit,cell):
        Hn=0
        if exxit!=None:
            Hn=abs(exxit[0]-cell.x)+abs(exxit[1]-cell.y)
        return Hn
    def findPath(self,maze,start,exxit):
        tstart=time.perf_counter()
        queue=[]
        f=self.H(exxit,maze.getCell(start[0],start[1]))
        heapq.heappush(queue, (f, f, (start[0],start[1])))
        
        f_scores=[]
        for i in range(maze.height):
            row=[]
            for j in range(maze.width):
                row.append(None)
            f_scores.append(row)
            
        g_scores=[]
        for i in range(maze.height):
            row=[]
            for j in range(maze.width):
                row.append(None)
            g_scores.append(row)
            
        f_scores[start[1]][start[0]]=f
        g_scores[start[1]][start[0]]=0
        Path={}
        flag=0
        count=0
        while len(queue)!=0:
            temp=heapq.heappop(queue)
            cell=temp[2]
            directions=maze.getNeighbours(maze.getCell(cell[0], cell[1]))
            count+=1
               
            if (cell[0],cell[1])==exxit:
                end=time.perf_counter()
                #print(end-tstart)
                flag=1
                break
                
            for i in directions:
                if i!=None:
                    temp_g=g_scores[cell[1]][cell[0]]+1
                    temp_f=temp_g+self.H(exxit,i)
                    
                    if f_scores[i.y][i.x]==None:
                        g_scores[i.y][i.x]=temp_g
                        f_scores[i.y][i.x]=temp_f
                        heapq.heappush(queue,(temp_f,self.H(exxit,i),(i.x,i.y)))
                        Path[(i.x,i.y)]=cell
                    elif temp_f<f_scores[i.y][i.x]:
                        f_scores[i.y][i.x]=temp_f
                        g_scores[i.y][i.x]=temp_g
                        heapq.heappush(queue,(temp_f,self.H(exxit,i),(i.x,i.y)))
                        Path[(i.x,i.y)]=cell
        
        reversePath=[]
        if flag==1:
            cell=exxit
            while cell!=start:
                reversePath.append(cell)
                cell=Path[cell]
            reversePath.append(cell)
            reversePath.reverse()
            #print(len(reversePath)-1)
        return reversePath, count
            

class PathFindingStrategy:
    def __init__(self,strategy):
        self.strategy=strategy
        
    def findPath(self,maze,start,exxit):
        if self.strategy=="BFS":
            return BFSStrategy().findPath(maze,start,exxit)
        elif self.strategy=="DFS":
            return DFSStrategy().findPath(maze,start,exxit)
        elif self.strategy=="AStar":
            return AStarStrategy().findPath(maze,start,exxit)
        
class MazeSolver:
    def __init__(self, maze, strategy):
        self.maze=maze
        self.strategy=PathFindingStrategy(strategy)
        
    def setStrategy(self, strategy):
        self.strategy=PathFindingStrategy(strategy)     
    
    def solve(self):
        start=time.perf_counter()
        path, visited = self.strategy.findPath(self.maze, self.maze.start, self.maze.exit)
        end=time.perf_counter()
        if len(path)>0:
            path_length=len(path)-1
        else:
            path_length=0
        return SearchStats(end-start, visited, path_length)
        
def test():
    #maze=MazeBuilder().buildFromFile("1.txt")
    #maze=MazeBuilder().buildFromFile("2.txt")
    maze=MazeBuilder().buildFromFile("v.txt")
    #maze=MazeBuilder().buildFromFile("maze_10x10.txt")
    #maze=MazeBuilder().buildFromFile("maze_50x50.txt")
    #maze=MazeBuilder().buildFromFile("maze_100x100.txt")
    #maze=MazeBuilder().buildFromFile("maze_no_walls.txt")
    #maze=MazeBuilder().buildFromFile("maze_no_exit.txt")
    print(maze.exit)
    print(maze.start)
    stats=MazeSolver(maze, "BFS").solve()
    print(stats.ttime,stats.visited_cells,stats.path_length)
    stats=MazeSolver(maze, "DFS").solve()
    print(stats.ttime,stats.visited_cells,stats.path_length)
    stats=MazeSolver(maze, "AStar").solve()
    print(stats.ttime,stats.visited_cells,stats.path_length)
    #print(PathFindingStrategy("BFS").findPath(maze,maze.start,maze.exit))
    #print(PathFindingStrategy("DFS").findPath(maze,maze.start,maze.exit))
    #print(PathFindingStrategy("AStar").findPath(maze,maze.start,maze.exit))
    #PathFindingStrategy("BFS").findPath(maze,maze.start,maze.exit)
    #PathFindingStrategy("DFS").findPath(maze,maze.start,maze.exit)
    #PathFindingStrategy("AStar").findPath(maze,maze.start,maze.exit)
    
def run():
    maze=MazeBuilder().buildFromFile("maze_10x10.txt")
    results = [
    [u"Лабиринт", u"Стратегия", u"Среднее время (мс)", u"Посещено клеток",u"Длина пути"]
    ]
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "BFS").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append(["10x10", "BFS", temp, stats.visited_cells, stats.path_length])
    
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "DFS").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append(["10x10", "DFS", temp, stats.visited_cells, stats.path_length])
        
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "AStar").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append(["10x10", "AStar", temp, stats.visited_cells, stats.path_length])
    
    with codecs.open("docs/data/[2]results.csv", "w", "utf-16") as f:
        writer = csv.writer(f)
        writer.writerows(results)
        writer.writerow("")
        
    maze=MazeBuilder().buildFromFile("maze_50x50.txt")
    results = []
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "BFS").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append(["50x50", "BFS", temp, stats.visited_cells, stats.path_length])
    
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "DFS").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append(["50x50", "DFS", temp, stats.visited_cells, stats.path_length])
        
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "AStar").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append(["50x50", "AStar", temp, stats.visited_cells, stats.path_length])
    
    with codecs.open("docs/data/[2]results.csv", "a+", "utf-16") as f:
        writer = csv.writer(f)
        writer.writerows(results)
        writer.writerow("")
    
    maze=MazeBuilder().buildFromFile("maze_100x100.txt")
    results = []
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "BFS").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append(["100x100", "BFS", temp, stats.visited_cells, stats.path_length])
    
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "DFS").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append(["100x100", "DFS", temp, stats.visited_cells, stats.path_length])
        
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "AStar").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append(["100x100", "AStar", temp, stats.visited_cells, stats.path_length])
    
    with codecs.open("docs/data/[2]results.csv", "a+", "utf-16") as f:
        writer = csv.writer(f)
        writer.writerows(results)
        writer.writerow("")
    
    maze=MazeBuilder().buildFromFile("maze_no_walls.txt")
    results = []
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "BFS").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append([u"Без стен", "BFS", temp, stats.visited_cells, stats.path_length])
    
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "DFS").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append([u"Без стен", "DFS", temp, stats.visited_cells, stats.path_length])
        
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "AStar").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append([u"Без стен", "AStar", temp, stats.visited_cells, stats.path_length])
    
    with codecs.open("docs/data/[2]results.csv", "a+", "utf-16") as f:
        writer = csv.writer(f)
        writer.writerows(results)
        writer.writerow("")
    
    maze=MazeBuilder().buildFromFile("maze_no_exit.txt")
    results = []
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "BFS").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append([u"Без выхода", "BFS", temp, stats.visited_cells, stats.path_length])
    
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "DFS").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append([u"Без выхода", "DFS", temp, stats.visited_cells, stats.path_length])
        
    temp=0
    for i in range(10):
        stats=MazeSolver(maze, "AStar").solve()
        temp+=stats.ttime
    temp=temp/10
    temp=temp*(10**3)
    results.append([u"Без выхода", "AStar", temp, stats.visited_cells, stats.path_length])
    with codecs.open("docs/data/[2]results.csv", "a+", "utf-16") as f:
        writer = csv.writer(f)
        writer.writerows(results)
        writer.writerow("")

run()

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        