# Principled Programming / Tim Teitelbaum / Chapter 17.
# Maze, Rat, and Path (MRP) Representations.
class MRP:
    # GRAPH REPRESENTATIONS

    # Maze. Cells of a maze are represented by N*N nodes of an
    #   undirected graph. G[n] is an edge list for node n, i.e., for
    #   0<=e<len(G[n]), G[n][e] is an adjacent node n, i.e., a cell
    #   adjacent to n with no Wall between them. The upper-left cell is
    #   node 0. Cheese is at cheese_node. */
    _G: list[list[int]]  # Edge lists.
    _cheese_node: int  # Cheese.

    # Path. Array path[0..path_length-1] is a list of adjacent nodes in
    #   G reaching from node 0 to some node path[path_length-1]. */
    _path: list[int]
    _path_length: int

    @classmethod
    def is_at_cheese(cls) -> bool:
        return MRP._path[MRP._path_length - 1] == MRP._cheese_node

    # SOLUTION BY DEPTH-FIRST SEARCH
    @classmethod
    def search(cls) -> None:
        """
        Convert representation M[N][N] to graph G, then perform DFS from upper-left,
        then convert computed path to representation M[N][N].
        """

        MRP._make_graph_from_input()
        try:
            MRP._DFS(0, 0)
        except Exception:
            pass
        MRP._make_output_from_path()

    _mark: list[bool]  # mark[n] iff DFS reached node n.

    @classmethod
    def _DFS(cls, n: int, p: int) -> None:
        """Depth First Search (DFS) of node n for cheese_node at depth p."""
        if not MRP._mark[n]:  # Node n has not been visited before.
            MRP._mark[n] = True  # Mark that n has been visited.
            MRP._path[p] = n  # Extend the path to include n.
            if n == MRP._cheese_node:  # Terminate search if cheese found.
                MRP._path_length = p + 1  # Length of path is one longer than p.
                raise Exception("found cheese")
            for e in range(0, len(MRP._G[n])):
                MRP._DFS(MRP._G[n][e], p + 1)

    # LEGACY REPRESENTATIONS (Chapter 15)
    
    # Maze. Cells of an N-by-N maze are represented by elements of a
    #   (2*N+1)-by(2*N+1) array M. Maze cell ⟨r,c⟩ is represented by array
    #   element M[2*r+1][2*c+1]. The possible walls ⟨top, right, bottom,
    #   left⟩ of the maze cell corresponding to ⟨r,c⟩ are represented by
    #   WALL or NO_WALL in ⟨M[r-1][c], M[r][c+1], M[r+1][c], M[r][c-1]⟩.
    #   The remaining elements of M are unused. lo is 1, and hi is 2*N-1.
    _N: int = 0               # _N is size of maze.
    _M: list[list[int]] = []  # _M is _N-by-_N maze, walls, and path.
    _WALL: int = -1           # _WALL encodes presence of a wall.
    _NO_WALL: int = 0         # _NO_WALL encodes absence of a wall.
    _lo: int = 1              # _lo is left and top maze indices.
    _hi: int = 2 * _N - 1     # _hi is right and bottom maze indices.

    # Rat. The rat is located in cell M[r][c] facing direction d, where
    #   d=⟨0,1,2,3⟩ represents the orientation ⟨up,right,down,left⟩,
    #   respectively.
    _r: int = _lo
    _c: int = _lo
    _d: int = 0

    # Path. When the rat has traveled to cell ⟨r,c⟩ via a given path
    #   through cells of the maze, the elements of M that correspond to
    #   those cells will be 1, 2, 3, etc., and all other elements of M
    #   that correspond to cells of the maze will be UNVISITED. The
    #   number of the last step in the path is move.
    _UNVISITED: int = 0
    _move: int = _lo

    # Unit vectors in direction d
    #                   d = 0,     1,    2,    3
    #                      up, right, down, left
    _deltaR: list[int] = [ -1,     0,    1,    0 ]
    _deltaC: list[int] = [  0,     1,    0,   -1 ]

    # Input N-by-N maze.
    @classmethod
    def input(cls) -> None:
        # Input file split into lines.
            # file = "3\nxxxxxxx\nx     x\nx x   x\nx xx  x\nx xxxxx\nx     x\nxxxxxxx"  # 3x3, with room.
            # file = "1\nxxx\nx x\nxxx\nxxx" # 1x1.
            # file = "2\nxxxxx\nx x x\nx   x\nx   x\nxxxxx" # 2x2, with obstacle.
            # file = "2\nxxxxx\nx   x\nx   x\nx   x\nxxxxx" # 2x2, no obstacle.
            file = "5\nxxxxxxxxxxx\nx     x   x\nx  x      x\nx x     x x\nx    x   xx\nx x   x   x\nx      x  x\nx     x x x x   x\nx  x   x xx\nx   x     x\nxxxxxxxxxxx\n" # 5x5, for Chapter 19.
            lines = file.split("\n")

        # Maze. As per representation invariant.
            MRP._N = int(lines[0]); del lines[0]
            MRP._lo = 1;  MRP._hi = 2 * MRP._N - 1
            MRP._M = [[0 for _ in range(2 * MRP._N + 1)] for _ in range(2 * MRP._N + 1)]
            for r in range(MRP._lo - 1, MRP._hi + 2):
                line = lines[0]; del lines[0]
                for c in range(MRP._lo - 1, MRP._hi + 2):
                    if (r % 2 == 1) and (c % 2 == 1): MRP._M[r][c] = MRP._UNVISITED
                    elif line[c:c+1] == " ":
                        MRP._M[r][c] = MRP._NO_WALL
                    else: MRP._M[r][c] = MRP._WALL

        # Rat. Place rat in upper-left cell facing up.
            MRP._r = MRP._lo;  MRP._c = MRP._lo;  MRP._d = 0
            
        # Path. Establish the rat in the upper-left cell.
            MRP._move = 1;  MRP._M[MRP._r][MRP._c] = MRP._move

    @classmethod
    def print_maze(cls) -> None:
        """Output N-by-N maze, with walls and path."""
        for r in range(MRP._lo - 1, MRP._hi + 2):
            for c in range(MRP._lo - 1, MRP._hi + 2):
                if MRP._M[r][c] == MRP._WALL: s = "#"
                elif ((MRP._M[r][c] == MRP._NO_WALL) or 
                            (MRP._M[r][c] == MRP._UNVISITED)): s = " "
                else: s = str(MRP._M[r][c]) + ""
                print((s + "  ")[0:3], end='')
            print()

    # CONVERSION LEGACY ==> GRAPH
    @classmethod
    def _make_graph_from_input(cls) -> None:
        """Create undirected graph representation of maze and path."""
        MRP._G = [[0] for _ in range(MRP._N)
                      for _ in range(MRP._N)]
        MRP._cheese_node = (MRP._N * MRP._N) - 1     # Node containing cheese.
        MRP._path = [0] * (MRP._N * MRP._N)         # Path of rat from 0 to cheese_node, if possible.
        MRP._mark = [False] * (MRP._N * MRP._N)     # mark[n] iff DFS has reached n.
        MRP._path_length = 0
        n: int = 0                                  # Node number.
        for r in range(MRP._lo, MRP._hi + 1, 2):
            for c in range (MRP._lo, MRP._hi + 1, 2):
                MRP._G[n] = [0] * MRP._count_edges(r, c)
                e: int = 0                          # Edge number. 
                for d in range(0, 4):
                    if MRP._M[r + MRP._deltaR[d]][c + MRP._deltaC[d]] == MRP._NO_WALL:
                        MRP._G[n][e] = MRP._node(r + 2 * MRP._deltaR[d], c + 2 * MRP._deltaC[d])
                        e += 1
                n += 1
                
    # CONVERSION GRAPH ==> LEGACY
    @classmethod
    def _count_edges(cls, r: int, c: int) -> int:
         """Return cell <r,c>'s number of outgoing edges."""
         e: int = 0                                 # Number of edges.
         for d in range(0, 4):
            if MRP._M[r + MRP._deltaR[d]][c + MRP._deltaC[d]] == MRP._NO_WALL:
                e += 1
         return e
    @classmethod
    def _node(cls, r: int, c: int) -> int:
        """Return node number from <r,c> in M."""
        return MRP._N  * (r//2) + (c//2)
    @classmethod
    def _row(cls, n: int) -> int:
        """Return r of <r,c> in M given node n."""
        return 2 * (n // MRP._N)  + 1
    @classmethod
    def _column(cls, n: int) -> int:
        """Return c of <r,c> in M given node n."""
        return 2 * (n % MRP._N ) + 1
    @classmethod
    def _make_output_from_path(cls) -> None:
        """Create undirected graph representation of maze and path."""
        for q in range(0, MRP._path_length):
            MRP._M[MRP._row(MRP._path[q])][MRP._column(MRP._path[q])] = q + 1

    # MISC LEGACY SELF-CHECKING, EXHAUSTIVE TESTING, AND FUZZ TESTING.  (Chapter15)
    @classmethod
    def _is_valid_path(cls, r: int, c: int) -> bool:
        """Return False iff rat reached cell ⟨r,c⟩ via an invalid path."""
        if MRP._M[r][c] == MRP._UNVISITED: return True # No claim if UNVISITED.
        else: 
            while not((r == MRP._lo) and (c == MRP._lo)):
                # Go to any valid predecessor; return False if there is none.
                    d = 0
                    while (d < 4) and ( (
                          MRP._M[r + MRP._deltaR[d]
                                ][c + MRP._deltaC[d]] == MRP._WALL
                     ) or (MRP._M[r + 2 * MRP._deltaR[d]][
                                   c + 2 * MRP._deltaC[d]] != (MRP._M[r][c] - 1)
                        ) ): d += 1
                    if d == 4: return False
                    r += 2 * MRP._deltaR[d]; c += 2 * MRP._deltaC[d]
        return True # Reached upper-left cell.
    
    @classmethod
    def is_solution(cls) -> bool:
        """Return False iff rat reached lower-right cell via an invalid path."""
        return MRP._is_valid_path(MRP._hi, MRP._hi)

    @classmethod
    def generate_input(cls, n: int, w: int) -> None:
            """Create an N-by-N maze with walls given by the bits of w."""
        # Maze.
            MRP._N = n;  lo = MRP._lo = 1;  hi = MRP._hi = 2 * n - 1
            MRP._M = [[0 for _ in range(2 * n + 1)] for _ in range(2 * n + 1)]
        # Set boundary walls.
            for i in range(0, hi+2):
                MRP._M[lo - 1][i] = MRP._M[hi + 1][i] = MRP._WALL
                MRP._M[i][lo - 1] = MRP._M[i][hi + 1] = MRP._WALL
        # Set 2*n*(n-1) interior walls to the corresponding bits of w.
            for r in range(lo, hi + 1):
                for c in range(lo, hi + 1):
                    if (r%2 == 0 and c%2 == 1) or (r%2 == 1 and c%2 == 0):
                        if w%2 == 1: MRP._M[r][c] = MRP._WALL 
                        else: MRP._M[r][c] = MRP._NO_WALL
                        w = w // 2
     # Rat. 
            MRP._r = lo;  MRP._c = lo;  MRP._d = 0
     # Path.
            MRP._move = 1;  MRP._M[lo][lo] = MRP._move