# Principled Programming / Tim Teitelbaum / Chapter 19.
from mrp import MRP
import random

class RunMaze:
    """Run a maze given as input, if possible."""
    @classmethod
    def main(cls) -> None:
        # Input a maze of arbitrary size, or output “malformed input”
        #   and stop if the input is improper. Input format: TBD.
            RunMaze._input()
        # Compute a direct path through the maze, if one exists.
            RunMaze._solve()
        # Output the direct path found, or “unreachable” if there is
        #    none. Output format: TBD.
            RunMaze._output()
        # Stop execution if path found is not a solution.
            assert MRP.is_solution(), "internal program error"

    @classmethod
    def _input(cls) -> None:
        """
            Input a maze of arbitrary size, or output “malformed input”
            and stop if the input is improper.
        """
        MRP.input()

    @classmethod
    def _solve(cls) -> None:
        """Compute a direct path through the maze, if one exists."""
        while not (MRP.is_at_cheese()) and not (MRP.is_about_to_repeat()):
            if MRP.is_facing_wall(): MRP.turn_clockwise()
            elif MRP.is_facing_unvisited():
                MRP.step_forward()
                MRP.turn_counter_clockwise()
            else:
                RunMaze._retract()

    @classmethod
    def _retract(cls) -> None:
        """Unwind abortive exploration."""
        MRP.record_neighbor_and_direction()
        while not (MRP.is_at_neighbor()):
            MRP.face_previous()
            MRP.step_backward()
        MRP.restore_direction()
        MRP.turn_counter_clockwise()

    @classmethod
    def _output(cls) -> None:
        """Output the direct path found, or “unreachable” if there is none."""
        if not (MRP.is_at_cheese()):
            print("Unreachable")
        else:
            MRP.print_maze()

    @classmethod
    def exhaustive_test(cls) -> None:
        """Generate/solve all mazes of sizes up through 3, and validate paths found."""
        for n in range(1, 5):
            for w in range(0, 2 ** (2 * n * (n - 1))):
                MRP.generate_input(n, w)
                RunMaze._solve()
                assert MRP.is_solution(), "internal program error"
                if (w > 0) and (w % 100000 == 0): print(w)  # Heartbeat.
            print("passed for size:", n)
        print("passed")

    @classmethod
    def fuzz_test(cls, f: int, n: int) -> None:
        """Fuzz f mazes of size n."""
        for k in range(0, f): RunMaze.random_test(n)

    @classmethod
    def random_test(cls, n: int) -> None:
        """Create a random maze of size n."""
        # Let w be random walls for the n-by-n maze.
        w = random.randint(0, 2 ** (2 * n * (n - 1)))
        # Generate maze of size n with walls w, solve, and validate (or abort).
        print("Size:", n, "Walls:", w)  # (Comment out for serious test.)
        MRP.generate_input(n, w)  # Create n-by-n maze with walls w.
        RunMaze._solve()  # Attempt a solution.
        assert MRP.is_solution()  # Validate solution (or abort).
        MRP.print_maze()  # (Comment out for serious test.)


RunMaze.main()  # Uncomment to run on builtin example.
# RunMaze.exhaustive_test()  # Uncomment to run on all mazes of size <= 4.
# RunMaze.fuzz_test(25, 5)   # Uncomment to run on 25 random mazes of size 5.