# !/bin/env python

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import argparse
import sys
import patterns

# Show every element in a numpy array rather than a portion; useful for debugging
np.set_printoptions(threshold=np.nan)

def get_commandline_options():
    parser = argparse.ArgumentParser()

    parser.add_argument('-r', '--rows',
                        help='set # of rows in the world',
                        action='store',
                        type=int,
                        dest='rows',
                        default=50)

    parser.add_argument('-c', '--columns',
                        help='set # of columns in the world',
                        action='store',
                        type=int,
                        dest='cols',
                        default=50)

    parser.add_argument('-w', '--world',
                        help='type of world to generate',
                        action='store',
                        type=str,
                        dest='world_type',
                        default='empty')

    parser.add_argument('-d', '--framedelay',
                        help='time (in milliseconds) between frames',
                        action='store',
                        type=int,
                        dest='framedelay',
                        default=100)

    opts = parser.parse_args()

    return opts

def report_options(opts):
    """
    Accepts: opts -- a populated command line options class inheritance

    Returns: (Nothing)

    Description: This function simply prints the parameters used to start the
    'Game of Life' simulation.
    """

    print("Conway's Game of Life")
    print("=====================")
    print("  World Size: %i x %i" % (opts.rows, opts.cols))
    print("  World Type: %s" % (opts.world_type))
    print("  Frame Delay: %i (ms)" % (opts.framedelay))

def generate_world(opts):
    """
    Accepts: opts  -- parsed command line options
    Returns: world -- a list of lists that form a 2D pixel buffer

    Description: This function generates a 2D pixel buffer with dimensions
                 opts.cols x opts.rows (in pixels). The initial contents of the
                 generated world is determined by the value provided by
                 opts.world_type: either 'random' or 'empty'. A 'random' world
                 has 10% 'living' pixels and 90% 'dead' pixels. An 'empty'
                 world has 100% 'dead' pixels.
    """
    world = np.zeros(shape=(opts.rows,opts.cols), dtype=int)

    if opts.world_type == "random":     # If the world is specified as "random", generate a world with 90% dead cells and 10% alive
        for row in range(world.shape[0]):
            for col in range(world.shape[1]):
                world[row][col] = np.random.choice(2,1,p=[0.9, 0.1])[0]     # np.random.choice returns an array, so get the only integer in it

    elif opts.world_type == "overpopulated":    # If the world is specified as "overpopulated", generate a world with half dead cells and half alive
        for row in range(world.shape[0]):
            for col in range(world.shape[1]):
                world[row][col] = np.random.choice(2,1,p=[0.5, 0.5])[0]

    return world

def blit(world, sprite, x, y):
    """
    Accepts: world  -- a 2D world pixel buffer generated by generate_world()
             sprite -- a 2D matrix containing a pattern of 1s and 0s
             x      -- x world coord where left edge of sprite will be placed
             y      -- y world coord where top edge of sprite will be placed

    Returns: (Nothing)

    Description: Copies a 2D pixel pattern (i.e. sprite) into the larger 2D
                 world. The sprite will be copied into the 2D world with its
                 top left corner being located at world coordinate (x,y)
    """

    sprite = np.array(sprite)       # Convert the 2D nested list object into a 2D numpy array
    sprite_rows = sprite.shape[0]   # Number of rows in the sprite rows
    sprite_cols = sprite.shape[1]   # Number of items in each sprite row
    for i in range(sprite_rows):
        for j in range(sprite_cols):
            world[y+i][x+j] = sprite[i][j]  # Draw each pixel from the sprite into the world with the specified (x,y) offset

def run_simulation(opts, world):
    """
    Accepts: opts  -- a world command line options class instance
             world -- a 2D world pixel buffer generated by generate_world()

    Returns: (Nothing)

    Description: This function generates the plot that we will use as a
                 rendering surface. 'Living' cells (represented as 1s in the
                 2D world matrix) will be rendered as black pixels and 'dead'
                 cells (represented as 0s) will be rendered as white pixels.
                 The method FuncAnimation() accepts 4 parameters: the figure,
                 the frame update function, a tuple containing arguments to
                 pass to the update function, and the frame update interval (in
                 milliseconds). Once the show() method is called to display the
                 plot, the frame update function will be called every
                 'interval' milliseconds to update the plot image (img).
    """
    if len(world) == 0:
        print("The 'world' was never created. Exiting")
        sys.exit()

    fig = plt.figure()
    img = plt.imshow(world, interpolation='none', cmap='Greys', vmax=1, vmin=0)
    ani = animation.FuncAnimation(fig,
                                  update_frame,
                                  fargs=(opts, world,img),
                                  interval=opts.framedelay)

    plt.show()

def update_frame(frame_num, opts, world, img):
    """
    Accepts: frame_num -- (automatically passed in) current frame number
             opts      -- a populated command line options instance
             world     -- the 2D world pixel buffer
             image     -- the plot image
    """

    # set the current plot image to display the current 2D world matrix
    img.set_array(world)

    # Create a *copy* of 'world' called 'new_world' -- 'new_world' will be your
    # offscreen drawing buffer. We will draw the next frame to 'new_world' so
    # that we may maintain an intact copy of the current 'world' at the same
    # time.

    world_rows = world.shape[0]
    world_cols = world.shape[1]

    new_world = np.zeros((world_rows,world_cols))   # Temporary new_world of zeros with same dimensions as world

    for i in range(world_rows):     # Clone cell data from current world into new_world
        for j in range(world_cols):
            new_world[i][j] = world[i][j]

    for i in range(world_rows):         # Iterate through each row and each pixel within each row
        for j in range(world_cols):
            neighbors = sum([ new_world[(i-1)%world_rows][(j-1)%world_cols],    # upper left pixel
                              new_world[(i-1)%world_rows][(j)%world_cols],      # left pixel
                              new_world[(i-1)%world_rows][(j+1)%world_cols],    # lower left pixel
                              new_world[(i)%world_rows][(j-1)%world_cols],      # upper pixel
                              new_world[(i)%world_rows][(j+1)%world_cols],      # lower pixel
                              new_world[(i+1)%world_rows][(j-1)%world_cols],    # upper right pixel
                              new_world[(i+1)%world_rows][(j)%world_cols],      # right pixel
                              new_world[(i+1)%world_rows][(j+1)%world_cols] ])  # lower right pixel

            # If the current cell is dead and there are 3 adjacent pixels, revive the current cell in "world"
            if world[i][j] == 0 and neighbors == 3:
                world[i][j] = 1
            # If the current cell is alive and there are fewer than 2 or more than 3 adjacent cells, kill the current cell in "world"
            elif world[i][j] == 1 and (neighbors < 2 or neighbors > 3):
                world[i][j] = 0


    # Copy the contents of the new_world into the world
    # (i.e. make the future the present)
    return img,

def main():
    """
    The main function -- everything starts here!
    """
    opts = get_commandline_options()
    world = generate_world(opts)
    report_options(opts)

    # blit(world, patterns.gosper_gun, 10, 10)
    # blit(world, patterns.exploder, 10, 10)
    # blit(world, patterns.hammerhead, 10, 10)
    blit(world, patterns.turbine, 10, 10)

    run_simulation(opts, world)

if __name__ == "__main__":
    main()
