import sympy as sym
from sympy import poly

a = sym.Symbol('a')
b = sym.Symbol('b')
c = sym.Symbol('c')
d = sym.Symbol('d')
e = sym.Symbol('e')

def make_T(Fpair): #Subfunction for the group builder
    def T(L):
        return Fpair[0][0](Fpair[1][0](L))
    return T

#Takes in a specific set of maps and genereates the group G_k using those maps as a base.
#The 120 benchmark is an optimization made with foresight that this group only gets that big,
#removing the constraint has no effect other than making group generation longer
def Build_Group(S, base): 
    G = []
    out = set()
    i = 0
    for x in S:
        out.add(tuple(x[1]))
        G.append(x)
    while i < len(G) and len(G) < 120:
        for f in G:
            T = make_T([G[i], f])
            res = T(base)
            if not(tuple(res) in out):
                out.add(tuple(res))
                G.append([T, res])
            T = make_T([f, G[i]])
            res = T(base)
            if not(tuple(res) in out):
                out.add(tuple(res))
                G.append([T, res])
        i += 1
    return G

def genXk(k): #Generates the set G_k will act upon
    X_k = set()
    for m in range(1,k):
        for n in range(1,k):
            for l in range(1,k):
                for p in [t for t in range(0,k) if t not in [m,n,l]]:
                    for q in [t for t in range(0,k) if t not in [m,n,l, (m+n+l - p) %k]]:
                        X_k.add((m,n,l,p,q))
    return X_k

#Takes in G_k and an integer k >= 2 and determines the orbits of the action of G_k on X_k
def genorbits(G, k):
    X_k = genXk(k)
    Y = genXk(k)
    orbits = set()
    while len(Y) > 0:
        for x in X_k:
            if not(x in Y):
                continue
            orbitx = set()
            for f in G:
                y = tuple([l % k for l in f[0](list(x))])
                if(y != x and not(y in orbitx)):
                    orbitx.add(y)
                    Y.discard(y)
            orbitx.add(x)
            orbits.add(tuple(orbitx))
            Y.discard(x)
    return orbits

#Handler for large scale orbit generation
def orbit_lists(G, n, m):
    for k in range(n, m+1):
        print(f"Generating orbit file for k = {k}...")
        O = genorbits(G, k)
        with open("Orbits_k=" + str(k) + ".txt", mode = "w") as file:
            if len(O) == 1:
                file.write(f"The 1 orbit of the action of G_k on X_k for k = {k} is,\n\n")
            else:
                file.write(f"The {len(O)} orbits of the action of G_k on X_k for k = {k} are,\n\n")
            
            for orb in O:
                file.write(f"The orbit generated by {orb[len(orb)-1]} is listed below and is of size {len(orb)}\n")
                for x in orb:
                    file.write(str(x) + "\n")
                file.write("\n")
    return 1

#main
def main():
    def T_1(L):
        NL = [L[1] - L[3], L[0] - L[3], L[2] - L[3], -L[3], L[4] - L[3]]
        return NL

    def T_2(L):
        NL = [L[0], L[0] - L[3], L[0] - L[4], L[0] - L[1], L[0] - L[2]]
        return NL

    def T_6(L): 
        NL = [L[3] - L[0], L[1], L[2], L[3], L[1] + L[2] - L[4]]
        return NL

    def T_8(L):
        NL = [L[0], L[2], L[1], L[4], L[3]]
        return NL

    def T_16(L):
        return T_1(T_6(L))

    base = [a, b, c, d, e]

    S = [[T_2, T_2(base)], [T_16, T_16(base)], [T_8, T_8(base)]]

    G = Build_Group(S, base)
    print("This program will generate orbit files for the action of G_k on X_k for all integers k >=2 in a user given range.\nThese files will be saved as 'orbit_k=i.txt' as they are generated.\nWarning: This code may take exceedingly long to run on some machines for large values of k")
    n = int(input("Enter a starting integer n (this number must be >= 2): "))
    m = int(input("Enter a finishing integer m (only one file will be generated if m = n): "))
    orbit_lists(G, n, m)
    
main()
