from colorama import init
from termcolor import colored
from time import sleep
import random


def display_space_time(row, t="", boxed=True):
    """
    Print one row of the automaton, optionally boxed, followed by its time step.
    """

    # Colour the initial state and display
    space = colour_state(row, 'black', 'white')
    time = str(t)
    space_time = ""
    if boxed:
        edge = colored("|", "green")
        space_time = edge + space + edge + time
    else:
        space_time = space + time
    print(space_time)


def colour_state(states, on="black", off="white", character='██'):
    """
    Return a coloured string representing a row of cell states.
    """
    coloured_state = ''
    for cell in states:
        if cell:
            coloured_state += colored(character, on)
        else:
            coloured_state += colored(character, off)
    return coloured_state


def run(initial_state, max_time, boolean_function):
    """
    Evolve the initial state for max_time steps using the rule, displaying each row.
    """

    current_state = initial_state
    display_space_time(current_state, 0)

    # Propagate forwards in time and display results
    length = len(current_state)
    for tick in range(1, max_time):

        # create a new state from current state
        new_state = []
        for i in range(length):

            # Wrapped neighbours
            left = current_state[(i - 1) % length]  # Wrap around using %
            middle = current_state[i]
            right = current_state[(i + 1) % length]  # Wrap around using %

            # Apply Rule to (left, center, right) to get output
            output = boolean_function[(left, middle, right)]
            new_state.append(output)

        # Copy new state to be next current state (parallel update)
        current_state = new_state
        display_space_time(current_state, tick)


def simulate_wolfram(space, time, rule):
    """
    Run a Wolfram rule from a single centred live cell and display the result.
    """

    padding = colored("--" * space, "green")
    print(" " + padding)
    title = colored(f"Wolfram Rule {rule}", "red")
    print(title)
    bf = create_boolean_function(rule)
    initial_state = [0] * space
    initial_state[space // 2] = 1
    run(initial_state, time, bf)


def create_boolean_function(rule):
    """
    Create a boolean_function from all possible neighbourhood inputs
    and map it to a given rule, which is coded into a left-padded
    binary list.

    Example rule = 30: https://mathworld.wolfram.com/Rule30.html
        111 110 101 100 011 010 001 000
         0   0   0   1   1   1   1   0
    Returns boolean function (dict type) as:
        {
        (1, 1, 1): 0,
        (1, 1, 0): 0,
        (1, 0, 1): 0,
        (1, 0, 0): 1,
        (0, 1, 1): 1,
        (0, 1, 0): 1,
        (0, 0, 1): 1,
        (0, 0, 0): 0
        }
    """

    # 1: Convert integer rule to 8-bit string, 'outputs'
    # ... e.g. 30 -> '00011110'
    outputs = bin(rule)  # bin converts to binary
    outputs = outputs.replace('0b', '')  # remove 0b prefix
    outputs = outputs.zfill(8)  # padd left shortfall with 0's

    # 2: All 3bit neighborhoods are 'inputs':
    inputs = [(1, 1, 1), (1, 1, 0), (1, 0, 1), (1, 0, 0), (0, 1, 1), (0, 1, 0), (0, 0, 1), (0, 0, 0)]

    # 3: boolean_function of input:output values
    boolean_function = {}
    for i in range(len(inputs)):
        # Load an input and map to an output bit
        boolean_function[inputs[i]] = int(outputs[i])

    return boolean_function


def main():
    """
    Run Rule 30, then either cycle through all 256 rules or show a random one.
    """

    width = 31
    steps = 31

    # Do rule 30
    # ----------
    rule = 30
    simulate_wolfram(width, steps, rule)

    message = colored("Press q (quit) or enter (iterate 256 rules)\n> ", "green")
    choice = input(message)
    if choice != "q":
        for rule in range(256):
            simulate_wolfram(width, steps, rule)
            sleep(0.5)
    else:
        rule = random.randint(0, 255)
        simulate_wolfram(width, steps, rule)
        message = f"Better things to do than watch a reem of 256 1D cellular automata?\nNo way! Anyway, above is a random one for the road, i.e.: rule {rule}\nHope it produced a pleasing pattern.\nApart from that...have a great day!!"
        message = colored(message, "yellow")
        print(message)


if __name__ == "__main__":

    # Initialize colorama
    init()

    # Call the main function
    main()