"""
 tubes.py

 Playing with adventofcode.com/2017/day/19 i.e. "a series of tubes".

 running it
 ----------

    $ python tubes.py 
    --- a series of tubes - adventofcode.com/2017/day/19 ---
    Animate "example 1"? (y/n) n
    The symbols of maze "example 1" are ABCDEF.
    Animate "example 2"? (y/n) n
    The symbols of maze "example 2" are SUPER.
    Animate "adventofcode input"? (y/n) n
    The symbols of maze "adventofcode input" are PVBSCMEQHY.

 global variables
 ----------------

   up                  directions (pairs of integers)
   down
   left
   right
   maze_example_1      mazes (lists of strings)
   maze_example_2
   maze_input

 functions
 ---------

   maze = maze_from_string(maze_string)
   maze = maze_from_file(filename)
   (row_min, row_max, col_min, col_max) = get_min_max(maze)
   symbol = look(maze, position) 
   position = (row, column) = start_position(maze)
   new_position = next_position(position, direction)
   partial_maze_string = display(maze, position, window=True, window_width=20)
   (position, direction, syms) = move(maze, position, direction, syms)
   clear_terminal()
   symbols = traverse(maze, animate=False)
   main()

 implementation notes
 --------------------

 A maze is represented as as list of strings which can be treated as
 a matrix of characters i.e. maze[row][col]], 
 where col means column and the top left is the (row=0, col=0) corner.

 A position in the maze is a pair (row, column), and similarly 
 a direction is a pair like (1, 0) giving the row and column offsets.

 While moving through a maze, the state that keeps track of things is
   position         coords of where we are in the maze (pair of integers)
   direction        offsets needed to change position  (ditto)
   symbols          list of letters seen as we travel the maze

 I started with one maze as just a hardcoded string, 
 then decided it would be simpler to access as a list of strings,
 and then when it came time to work on a big one needed a way
 to read one in from a file ... which is why there are several
 maze conversion function here.

 The three mazes looked at are 

   example 1          a small maze in a text string
   example 2          a small maze in the file './maze_example_2.txt'
   maze input         the big maze from adventofcode, './maze_input.txt'

 Are we having fun yet?

 Tested with python 3.5.

 ---------------------------------------------------------------------
 Jim Mahoney | cs.marlboro.college | Oct 2018 | MIT License
"""


import time

# The first example in the advent of code site is :
maze_string_1 = """
     |          
     |  +--+    
     A  |  C    
 F---|----E|--+ 
     |  |  |  D 
     +B-+  +--+ 
"""

# Global variables for the four possible directions :
(up, down, left, right) = ((-1, 0), (1, 0), (0, -1), (0, 1))

def maze_from_string(maze_string):
    """ return maze as list of strings given one long string """
    # split on newlines and leave out empty lines
    lines = maze_string.split('\n')
    result = []
    for line in lines:
        if len(line) > 1:
            result.append(line)
    return result

def maze_from_file(filename):
    """ return maze as list of strings from file """
    # remove newlines and leave out empty lines
    lines = open(filename).readlines()
    result = []
    for line in lines:
        if len(line) > 1:
            if line[-1] == '\n':
                result.append(line[0:-1])  # remove trailing newline
            else:
                result.append(line)
    return result

# And here are the three mazes that we'll play around with.
# (Since these are global names, I can use them in doctests.)
maze_example_1 = maze_from_string(maze_string_1)
maze_example_2 = maze_from_file('maze_example_2.txt')
maze_input = maze_from_file('maze_input.txt')

def get_min_max(maze):
    """ return (row_min, row_max, col_min, col_max) of maze 
        >>> get_min_max(maze_example_1)
        (0, 5, 0, 15)
    """
    # The rows should all be the same length, 
    # so I'll just use the length of maze[0] to get the width.
    (row_min, row_max) = (0, len(maze) - 1)
    (col_min, col_max) = (0, len(maze[0]) - 1)
    return (row_min, row_max, col_min, col_max)

def look(maze, position):
    """ Return character at given (row, col) position in the maze,
        and a space if position is past the edge of the maze.
        >>> look(maze_example_1, (1,8))
        '+'
        >>> look(maze_example_1, (1000, 23))
        ' '
    """
    (row, col) = position
    (row_min, row_max, col_min, col_max) = get_min_max(maze)
    if row < row_min or row > row_max or col < col_min or col > col_max:
        return ' '
    else:
        return maze[row][col]

def start_position(maze):
    """ Return coords of '|' in top row (row=0) of maze 
        >>> start_position(maze_example_1)
        (0, 5)
    """
    row = 0
    for col in range(len(maze[1])):
        if look(maze, (row, col)) == '|':
            return (row, col)
    return (-1, -1)    # error return - we shouldn't get here.
    
def display(maze, position, window=True, window_width=20):
    """ Return the whole maze or a window around the current position 
        as a one string with newlines between rows 
        and a '*' at the current position. """
    result = ''
    (current_row, current_col) = position
    if window:
        halfwidth = int(window_width/2)
        (row_min, row_max, col_min, col_max) = (position[0] - halfwidth,
                                                position[0] + halfwidth,
                                                position[1] - halfwidth,
                                                position[1] + halfwidth)
    else:
        (row_min, row_max, col_min, col_max) = get_min_max(maze)
    # Build the result explicitly symbol by symbol with look()
    # so things work even if the window extends past the maze edges.
    for row in range(row_min, row_max + 1):
        for col in range(col_min, col_max + 1):
            if row == current_row and col == current_col:
                result = result + '*'
            else:
                result = result + look(maze, (row, col))
        result = result + '\n'                              # next line
    return result

def next_position(position, direction):
    """ return position + direction 
        >>> next_position((3, 4), up)
        (2, 4)
        >>> next_position((3, 4), right)
        (3, 5)
    """
    return (position[0] + direction[0], position[1] + direction[1])

def move(maze, position, direction, letters):
    """ return (new_position, new_direction, new_letters) after one move. """
    new_position = next_position(position, direction)
    symbol = look(maze, new_position)
    if symbol in ('|', '-'):
        # keep going in same direction
        return (new_position, direction, letters)             
    elif symbol in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
        # keep going , remember letter
        return (new_position, direction, letters + [symbol])  
    elif symbol == '+':
        if direction in (up, down):
            # Turn to go left or right.
            # Choose which by looking each way and finding a '-' or a letter.
            on_left = next_position(new_position, left)
            on_right = next_position(new_position, right)
            if look(maze, on_left) == '-':
                return (new_position, left, letters)
            elif look(maze, on_right) == '-':
                return (new_position, right, letters)
            elif look(maze, on_left) in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
                return (new_position, left, letters)
            else:
                return (new_position, right, letters)
        else:
            # Direction must be left or right, so turn to go up or down.
            # Choose which by looking each way and finding '|' or a letter.
            above = next_position(new_position, up)
            below = next_position(new_position, down)
            if look(maze, above) == '|':
                return (new_position, up, letters)
            elif look(maze, below) == '|':
                return (new_position, down, letters)
            elif look(maze, above) in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
                return (new_position, up, letters)
            else:
                return (new_position, down, letters)
    else:
        # Something else happened - probably the space at end of the maze.
        return (new_position, direction, letters)

def clear_terminal():
    """ clear terminal and put cursor top left """
    # google "python3 clear terminal"
    # https://stackoverflow.com/questions/2084508/clear-terminal-in-python
    # google "python3 put cursor top left"
    # https://stackoverflow.com/questions/40103919/move-console-cursor-up
    # ... which also gives a link to a list of terminal escape sequences :
    # http://ascii-table.com/ansi-escape-sequences-vt-100.php
    escape = chr(27) # ANSII ESC character
    clear_screen = escape + '[2J'
    cursor_top_left = escape + '[H'
    print(clear_screen)
    print(cursor_top_left)
    
def traverse(maze, animate=False):
    """ Walk through the maze. Return the symbols seen. Optionally
        display animation showing part of maze at current position """
    (where, direction, symbols) = (start_position(maze), down, [])
    while look(maze, where) != ' ':      # Stop when we hit a space.
        if animate:
            clear_terminal()
            print(display(maze, where))  # The default shows part of maze.
            time.sleep(0.1) 
        (where, direction, symbols) = move(maze, where, direction, symbols)
    return symbols

def main():
    print('--- a series of tubes - adventofcode.com/2017/day/19 ---')
    names = ['example 1', 'example 2', 'adventofcode input']
    mazes = [maze_example_1, maze_example_2, maze_input]
    for which in range(3):
        query = input('Animate "{}"? (y/n) '.format(names[which]))
        result = ''.join(traverse(mazes[which], animate=query[0]=='y'))
        print('The symbols of maze "{}" are {}.'.format(names[which], result))

if __name__ == '__main__':
    import doctest
    doctest.testmod()
    main()