from ast import literal_eval

# Named neighbourhoods:
nhoods = {'Moore': [(0, 0), (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, 0)],
          'vonNeumann': [(0, 0), (0, -1), (1, 0), (0, 1), (-1, 0), (0, 0)],
          'hexagonal': [(0, 0), (0, -1), (1, 0), (1, 1), (0, 1), (-1, 0), (-1, -1), (0, 0)],
          'oneDimensional': [(0, 0), (-1, 0), (1, 0), (0, 0)]}

# Named symmetries. Each symmetry group is expressed as a list of generators,
# where each generator is expressed as a list of disjoint cycles:
syms = {('Moore', 'rotate2'):                 [[(1, 5), (2, 6), (3, 7), (4, 8)]],
        ('Moore', 'rotate2reflect'):          [[(1, 5), (2, 6), (3, 7), (4, 8)], [(2, 8), (3, 7), (4, 6)]],
        ('Moore', 'rotate4'):                 [[(1, 3, 5, 7), (2, 4, 6, 8)]],
        ('Moore', 'rotate4reflect'):          [[(1, 3, 5, 7), (2, 4, 6, 8)], [(2, 8), (3, 7), (4, 6)]],
        ('Moore', 'rotate8'):                 [[(1, 2, 3, 4, 5, 6, 7, 8)]],
        ('Moore', 'rotate8reflect'):          [[(1, 2, 3, 4, 5, 6, 7, 8)], [(2, 8), (3, 7), (4, 6)]],
        ('Moore', 'reflect_horizontal'):      [[(2, 8), (3, 7), (4, 6)]],

        ('vonNeumann', 'rotate2'):            [[(1, 3), (2, 4)]],
        ('vonNeumann', 'rotate2reflect'):     [[(1, 3)], [(2, 4)]],
        ('vonNeumann', 'rotate4'):            [[(1, 2, 3, 4)]],
        ('vonNeumann', 'rotate4reflect'):     [[(1, 2, 3, 4)], [(2, 4)]],
        ('vonNeumann', 'reflect_horizontal'): [[(2, 4)]],

        ('hexagonal', 'rotate2'):             [[(1, 4), (2, 5), (3, 6)]],
        ('hexagonal', 'rotate3'):             [[(1, 3, 5), (2, 4, 6)]],
        ('hexagonal', 'rotate6'):             [[(1, 2, 3, 4, 5, 6)]],
        ('hexagonal', 'rotate6reflect'):      [[(1, 2, 3, 4, 5, 6)], [(1, 6), (2, 5), (3, 4)]],
        ('hexagonal', 'reflect_horizontal'):  [[(1, 6), (2, 5), (3, 4)]],

        ('oneDimensional', 'reflect'):        [[(1, 2)]]}

def cycles2perm(n, g):
    '''
    Idempotent function for converting a list of disjoint cycles
    into a permutation.
    '''

    if len(g) == 0:
        return tuple(range(n))
    elif hasattr(g[0], '__iter__'):
        perm = list(range(n))
        for cyc in g:
            for i in range(len(cyc)):
                perm[cyc[i-1]] = cyc[i]
        return tuple(perm)
    elif set(g) == set(range(n)):
        return tuple(g)
    else:
        raise ValueError("%s is not in S_%d" % (g, n))

def permute_by_generators(generators, initial, used=set([])):
    '''
    Produce the subgroup of S_n generated by the generators.
    '''

    initial = tuple(initial) # ensure hashable

    if initial in used:
        return []

    pg = [initial]
    used.add(initial)

    # The next four lines allow us to iterate over a list whilst
    # appending to it. The more Pythonic 'for h in pg' is not
    # guaranteed by the standard to work as intended.
    i = 0
    while (i < len(pg)):
        h = pg[i]
        i += 1

        # Compose h with each of our generators and include any
        # previously-unseen elements to the permutation group:
        for g in generators:
            f = tuple([h[x] for x in g]) # Compose permutations
            if f not in used:
                used.add(f)
                pg.append(f)

    # We now have the desired subgroup:
    return pg

def GetNumberOfInputs(nhood):

    # Find which elements are inputs and which are outputs:
    numInputs = len(nhood) - 1
    for i in range(len(nhood)):
        if nhood[i] in nhood[:i]:
            numInputs = i
            break

    return numInputs

def permute_symmetry(nhood):
    '''
    Produces generators for the 'permute' symmetry group:
    '''

    numInputs = GetNumberOfInputs(nhood)
    indices = [[], [], [], []]
    symmetry = []

    for (i, j) in enumerate(nhood):
        if j in nhood[i+1:]:
            indices[2].append(i)
            indices[3].append(i+1+nhood[i+1:].index(j))
        elif i < numInputs:
            indices[0].append(i)
        else:
            indices[1].append(i)

    if len(indices[0]) >= 2:
        symmetry.append([tuple(indices[0])])
    if len(indices[0]) >= 3:
        symmetry.append([tuple(indices[0][:2])])
    if len(indices[1]) >= 2:
        symmetry.append([tuple(indices[1])])
    if len(indices[1]) >= 3:
        symmetry.append([tuple(indices[1][:2])])
    if len(indices[2]) >= 2:
        symmetry.append([tuple(indices[2]), tuple(indices[3])])
    if len(indices[2]) >= 3:
        symmetry.append([tuple(indices[2][:2]), tuple(indices[3][:2])])

    return symmetry


def read_transition(line, variables, transitions, used, numParams, symmetry):
    '''
    Subroutine of ReadRuleTable()
    '''

    if ',' in line:
        entries = line.replace('\n','').replace(',',' ').replace(';',' ').replace(':',' ').split()
    else:
        entries = list(line.strip()) # special no-comma format

    if len(entries) != numParams:
        raise RuntimeError('Wrong number of entries on line: '+line+' (expected '+str(numParams)+')')

    # retrieve the variables that repeat within the transition, these are 'bound'
    bound_vars = [ e for e in set(entries) if entries.count(e)>1 and e in variables ]

    # iterate through all the possible values of each bound variable
    var_val_indices = {k : 0 for k in bound_vars}

    while True:

        def f(e):
            if e in bound_vars:
                return [variables[e][var_val_indices[e]]]
            elif e in variables:
                return variables[e]
            else:
                return [int(e)]

        transition = [tuple(f(e)) for e in entries]

        # expand for symmetry using the explicit list
        pg = permute_by_generators(symmetry, transition, used)
        transitions += [list(map(list, x)) for x in pg]

        # increment the variable values (or break out if done)
        var_val_to_change = 0
        while var_val_to_change<len(bound_vars):
            var_label = bound_vars[var_val_to_change]
            if var_val_indices[var_label]<len(variables[var_label])-1:
                var_val_indices[var_label] += 1
                break
            else:
                var_val_indices[var_label] = 0
                var_val_to_change += 1
        if var_val_to_change >= len(bound_vars):
            break

def ReadRuleTable(list_of_lines):
    '''
    Based on the ReadRuleTable code from Golly's glife module, largely
    written by Tim Hutton, Tom Rokicki, and Andrew Trevorrow.
    '''

    variables   = {}
    transitions = []
    used = set([])

    for line in list_of_lines:

        line = line.split('#')[0].strip()

        if (line == '') or (line[0] == '@'):
            continue

        if ':' in line:

            command, thing = tuple([x.strip() for x in line.split(':')])

            if command in ['n_states', 'states']:
                n_states = int(thing)
                if (n_states < 2) or (n_states > 65536):
                    raise ValueError('n_states must be between 2 and 65536, inclusive')
            elif command in ['neighbourhood', 'neighborhood']:
                nstring = thing
                if nstring in nhoods:
                    nhood = nhoods[nstring]
                else:
                    nhood = [tuple(x) for x in literal_eval(nstring)]
                numParams = len(nhood)
            elif command == 'symmetries':
                symstring = thing
                if (nstring, symstring) in syms:
                    symmetry = syms[nstring, symstring]
                elif symstring == 'permute':
                    # Compute cycle type of full permutation group:
                    symmetry = permute_symmetry(nhood)
                elif symstring == 'none':
                    symmetry = []
                else:
                    symmetry = literal_eval(symstring)
                # Convert cycles to permutations:
                symmetry = [cycles2perm(numParams, x) for x in symmetry]
            else:
                read_transition(line, variables, transitions, used, numParams, symmetry)

        elif line[0:4] == 'var ':

            line = line[4:].strip() # strip var keyword
            # read each variable into a dictionary mapping string to list of ints
            line = line.replace('=',' ').replace('{',' ').replace(',',' ')
            entries = line.replace(':',' ').replace('}',' ').split()
            curvar = set([])
            for e in entries[1:]:
                if e in variables:
                    curvar.update(variables[e])
                else:
                    curvar.add(int(e))
            variables[entries[0]] = sorted(curvar)

        else:

            # assume line is a transition
            read_transition(line, variables, transitions, used, numParams, symmetry)

    return n_states, nhood, transitions
