"""333: time costs are homomorphisms, fixed by the generators subject to the relations. Checked exactly.

Same coordinates as p3_pairs.py (e1 = (1,0), e2 at 60 degrees, R the anticlockwise 1/3 turn); x, y, z are
the anticlockwise 1/3 turns about a clock, an up-triangle centre and a down-triangle centre, xyz = 1.
A time cost (price list) is a homomorphism phi from 333 to the time circle R/Z (a period = 1).

Claims checked (the page's section 4):
  1. x^3 = y^3 = z^3 = 1 force each of phi(x), phi(y), phi(z) into {0, 1/3, 2/3}: 27 lists.
     xyz = 1 forces phi(x) + phi(y) + phi(z) to be a whole number: at most 9 survive.
  2. Each of those 9 IS a homomorphism of the actual group: phi(t(w) R^k) = alpha(w) + k phi(x), with
     alpha linear and alpha(R w) = alpha(w); checked on a basis (which proves it) and on 5,625 products.
  3. Up to the symmetries of the pattern that keep orientation (they relabel the three kinds of centre),
     the 9 fall into 4 classes -- the price rule's "4 animations": all zero (333); all 1/3 and all 2/3
     (the mirror-twin screws 3_1 3_1 3_1 and 3_2 3_2 3_2); the six orderings of (0, 1/3, 2/3) (r 3 3_1 3_2).
     With mirrors allowed as well, 3 classes.
  4. For the clocks' list (0, 1/3, 2/3): the translations cost 1/3 or 2/3 (the 'r'), and the zero-cost
     subgroup (the film's symmetries with no delay, comparing hand angles without turning the hands, as the
     repelling clocks page does) is exactly the subgroup made by two turns about adjacent clocks, one clock
     spacing apart; its three kinds of centre are the three families of clocks.
"""
from fractions import Fraction as F
from itertools import product

from wallpaper import Aff, T, rot_about, I2, mm, mv, WGroup, closure_mod, find_words, evaluate, word_str

R = ((-1, -1), (1, 0))
R2 = mm(R, R)
G = WGroup("333", "p3", [Aff(I2), Aff(R), Aff(R2)])
G.check_is_group()
x = rot_about(R, (0, 0))
y = rot_about(R, (F(1, 3), F(1, 3)))
z = rot_about(R, (F(-1, 3), F(2, 3)))
e = Aff(I2)
assert x ** 3 == e and y ** 3 == e and z ** 3 == e and x * y * z == e
thirds = [F(0), F(1, 3), F(2, 3)]


def m1(q):
    return F(q) % 1


def make_phi(a, b, c):
    """The unique candidate with phi(x) = a, phi(y) = b, phi(z) = c, if one exists: every element of
    333 is (R^k, w) = t(w) x^k with w in Z^2, and y = t(e1) x, z = t(e2) x."""
    al1, al2 = m1(b - a), m1(c - a)

    def alpha(w):
        return m1(al1 * w[0] + al2 * w[1])

    def phi(g):
        k = {I2: 0, R: 1, R2: 2}[g.M]
        assert g.t[0].denominator == 1 and g.t[1].denominator == 1
        return m1(alpha(g.t) + k * a)

    # is it a homomorphism?  phi(gh) = phi(g) + phi(h) for all g, h  <=>  3a = 0 and alpha(R w) = alpha(w)
    # for w = e1, e2 (alpha is linear, and phi(t(w)x^k t(w')x^j) = alpha(w + R^k w') + (k + j) a).
    ok = m1(3 * a) == 0 and all(alpha(mv(R, w)) == alpha(w) for w in ((1, 0), (0, 1)))
    return phi, ok


lists = list(product(thirds, repeat=3))
assert len(lists) == 27
survivors = []
for (a, b, c) in lists:
    whole = m1(a + b + c) == 0
    phi, ok = make_phi(a, b, c)
    assert ok == whole, (a, b, c)          # the relation is exactly the obstruction
    if ok:
        assert (phi(x), phi(y), phi(z)) == (a, b, c)
        # brute force on a box of elements as well: 75 elements, 5,625 products
        box = [Aff(Mx, (i, j)) for Mx in (I2, R, R2) for i in range(-2, 3) for j in range(-2, 3)]
        for g in box:
            for h in box:
                assert phi(g * h) == m1(phi(g) + phi(h))
        survivors.append((a, b, c))
assert len(survivors) == 9
print("1-2. 27 price lists in thirds; exactly 9 have phi(x) + phi(y) + phi(z) whole, and each of those 9 is a "
      "homomorphism of 333 (proved on a basis, and on all 5,625 products of a 75-element box); "
      "the other 18 break xyz = 1")

# the abelianisation of 333 is exactly (Z/3)^2: at most 9 elements (x, y generate and have order 3, and
# they commute there), and at least 9 (333 maps onto the abelianisation of G/3Z^2, which has 9 elements)
from wallpaper import FiniteQuotient
Q = FiniteQuotient(G, 3)
inv = [next(k for k in range(Q.N) if Q.tab[i][k] == Q.e) for i in range(Q.N)]
comm = {Q.tab[Q.tab[Q.tab[i][j]][inv[i]]][inv[j]] for i in range(Q.N) for j in range(Q.N)}
C = Q.generated(list(comm))
assert Q.N == 27 and Q.N // len(C) == 9
assert all(Q.tab[Q.tab[g][g]][g] in C for g in range(Q.N))      # every cube is a commutator: exponent 3
print("     333's abelianisation is (Z/3)^2: 9 elements, the 9 price lists")

# ---- 3. classes under the symmetries of the pattern
R6 = ((0, -1), (1, 1))
S = ((0, 1), (1, 0))
sym_keep = [Aff(R6), Aff(I2, (F(1, 3), F(1, 3)))]           # 1/6 turn about a clock; clock -> up-centre slide
sym_all = sym_keep + [Aff(S)]                               # plus a mirror
for nrm in sym_all:
    for g in (x, y, z, T((1, 0)), T((0, 1))):
        assert G.contains(nrm * g * nrm.inv()) and G.contains(nrm.inv() * g * nrm)


def act(nrm, lst):
    """(phi . n)(g) = phi(n g n^-1), read off on x, y, z."""
    phi, ok = make_phi(*lst)
    assert ok
    return tuple(phi(nrm * g * nrm.inv()) for g in (x, y, z))


def orbits(syms):
    left = set(survivors)
    out = []
    while left:
        s = left.pop()
        orb = {s}
        frontier = [s]
        while frontier:
            nxt = []
            for t in frontier:
                for nrm in syms:
                    for u in (act(nrm, t), act(nrm.inv(), t)):
                        assert u in survivors
                        if u not in orb:
                            orb.add(u)
                            nxt.append(u)
            frontier = nxt
        left -= orb
        out.append(sorted(orb))
    return sorted(out, key=lambda o: (len(o), o))


def show(lst):
    return "(" + ", ".join(str(q) for q in lst) + ")"


ok_orbits = orbits(sym_keep)
all_orbits = orbits(sym_all)
print(f"3. up to symmetries that keep orientation: {len(ok_orbits)} classes")
for o in ok_orbits:
    phi, _ = make_phi(*o[0])
    tc = sorted({phi(T(w)) for w in [(1, 0), (0, 1), (1, 1), (2, -1)]})
    print(f"     {len(o)} list(s): " + " ".join(show(l) for l in o) + f"   slides cost {', '.join(str(q) for q in tc)}")
print(f"   with mirrors too: {len(all_orbits)} classes (sizes {', '.join(str(len(o)) for o in all_orbits)})")
assert len(ok_orbits) == 4 and len(all_orbits) == 3

# ---- 4. the clocks' list and its zero-cost subgroup
clocks = (F(0), F(1, 3), F(2, 3))
phi, ok = make_phi(*clocks)
assert ok and phi(T((1, 0))) == F(1, 3) and phi(T((0, 1))) == F(2, 3) and phi(T((1, 1))) == 0
x2 = rot_about(R, (1, 0))                     # the turn about the neighbouring clock at e1
H = {"x": x, "x'": x2}
assert phi(x) == 0 and phi(x2) == 0 and phi(y) == F(1, 3)
n = 3
img_H = closure_mod([x.mod(n), x2.mod(n)], n)
ker = {g for g in G.elements_mod(n) if phi(Aff(g[0], (int(g[1][0]), int(g[1][1])))) == 0}
assert img_H == ker and len(ker) * 3 == G.order_mod(n)
# H contains 3 Z^2, so the equality mod 3 Z^2 is an equality of subgroups of 333
found = find_words(H, {"t(3e1)": T((3, 0)), "t(3e2)": T((0, 3))})
assert set(found) == {"t(3e1)", "t(3e2)"}
for lab, w in found.items():
    assert evaluate(w, H) == {"t(3e1)": T((3, 0)), "t(3e2)": T((0, 3))}[lab]
# its translations are M = {w : w1 = w2 mod 3}; its centres are all the clocks, in three classes = cosets of M
orbit0 = set()
for g in img_H:
    orbit0.add(((g[1][0]) % 3, (g[1][1]) % 3))   # image of the origin under g, mod 3
M_mod3 = {(F(i), F(j)) for i in range(3) for j in range(3) if (i - j) % 3 == 0}
assert orbit0 == M_mod3
fam = {}
for i in range(3):
    for j in range(3):
        fam.setdefault(phi(T((i, j))), []).append((i, j))
assert len(fam) == 3 and all(len(v) == 3 for v in fam.values())
print("4. clocks' list (0, 1/3, 2/3): slides by e1, e2, e1+e2 cost 1/3, 2/3, 0 (the time-centred 'r')")
print(f"   the subgroup made by turns about two adjacent clocks (one clock spacing apart) = the zero-cost subgroup "
      f"(equal mod 3Z^2, {len(ker)} of {G.order_mod(n)} elements, and it contains t(3e1) = {word_str(found['t(3e1)'])})")
print("   it moves a clock only to clocks w with w1 = w2 (mod 3): the clocks fall into 3 families, "
      "one per cost of the slide that reaches them (0, 1/3, 2/3)")
print("HEADLINE time: 27 lists -> 9 homomorphisms (xyz = 1 is the only cut) -> 4 classes keeping orientation "
      "(3 with mirrors); the clocks' (0,1/3,2/3) has zero-cost subgroup = <turns about two adjacent clocks>, "
      "index 3")
