"""Noita Eye Messages: an evening's re-derivation, scored against Pyry's nine conditions.

Input: data.csv from github.com/Doctor-Ned/NoitaEyeGlyphResearch (NoitaEyeGlyphResearchLib/data.csv), eye digits 0-4
in the community's orthodox trigram reading order; cross-checked against eyes.json from
github.com/ngraham20/NoitaCryptographyResearch (eye/eyes.json).
Neither repo carries a licence, so the data is NOT copied here. Fetch it (pinned commits, verify the hashes):
  gh api "repos/Doctor-Ned/NoitaEyeGlyphResearch/contents/NoitaEyeGlyphResearchLib/data.csv?ref=37100742603f7e8cfd0bfa4e42f5502e8576f284" --jq .content | base64 -d > data.csv
      sha256 853400aec1ab0141882888ab4b39efa8fe1da5b9e0e9d8370970837da781062a
  gh api "repos/ngraham20/NoitaCryptographyResearch/contents/eye/eyes.json?ref=0cf791d5e1a0777059fe0ac26ed645704a4d2d9b" --jq .content | base64 -d > eyes.json
      sha256 dab680e5482d233322b746b80bca12ff1983c1f976be1536254a25f8ecc9f9ec
Run: python eyes_evening_c7005.py data.csv eyes.json   (output: eyes_evening_c7005.out.txt)
Every number in the essay is printed by this script.
"""
import collections
import csv
import itertools
import json
import random
import sys


def load(path):
    rows = list(csv.reader(open(path, encoding="utf-8")))[1:]
    msgs = {}
    for r in rows:
        d = [int(x) for x in r[2:] if x.strip() != ""]
        assert len(d) % 3 == 0, r[1]
        msgs[r[1]] = [d[i] * 25 + d[i + 1] * 5 + d[i + 2] for i in range(0, len(d), 3)]
    return msgs


def ioc(seq):
    n = len(seq); c = collections.Counter(seq)
    return sum(k * (k - 1) for k in c.values()) / (n * (n - 1))


def pattern(w):
    first = {}
    return tuple(first.setdefault(v, len(first)) for v in w)


def isomorphs(msgs, L, min_repeats):
    """(msg_a, pos_a, msg_b, pos_b) window pairs with the same non-trivial repeat pattern but different values."""
    idx = collections.defaultdict(list)
    for k, m in msgs.items():
        for i in range(len(m) - L + 1):
            w = m[i:i + L]; p = pattern(w)
            if L - len(set(w)) >= min_repeats:
                idx[p].append((k, i, tuple(w)))
    out = []
    for p, occ in idx.items():
        for (ka, ia, wa), (kb, ib, wb) in itertools.combinations(occ, 2):
            if wa != wb:
                out.append((ka, ia, kb, ib))
    return out


def main():
    msgs = load(sys.argv[1])
    allv = [v for m in msgs.values() for v in m]
    print("messages %d, trigrams %d, values %d..%d, distinct %d" % (len(msgs), len(allv), min(allv), max(allv), len(set(allv))))
    if len(sys.argv) > 2:  # independent transcription cross-check (raw eye digits, '5' = row break)
        e = json.load(open(sys.argv[2], encoding="utf-8"))["messages"]
        raw = {k: [int(c) for c in v if c in "01234"] for k, v in e.items()}
        print("eyes.json: %d messages, eye counts %s" % (len(raw), sorted(len(v) for v in raw.values())))
        print("data.csv eye counts %s" % sorted(3 * len(m) for m in msgs.values()))
    print("IoC %.4f (uniform over 83 = %.4f)" % (ioc(allv), 1 / 83))
    pairs = sum(len(m) - 1 for m in msgs.values())
    dbl = sum(1 for m in msgs.values() for a, b in zip(m, m[1:]) if a == b)
    c = collections.Counter(allv); n = len(allv); p_same = sum((k / n) ** 2 for k in c.values())
    print("adjacent doubles %d of %d; expected if independent %.1f; P(0) ~ %.1e" % (dbl, pairs, pairs * p_same, (1 - p_same) ** pairs))
    firsts = [m[0] for m in msgs.values()]; seconds = [m[1] for m in msgs.values()]
    print("first trigram distinct %d of %d; second trigram distinct %d" % (len(set(firsts)), len(msgs), len(set(seconds))))
    for M in (83,):
        diffs = [(m[i] - m[i - 1]) % M for m in msgs.values() for i in range(1, len(m))]
        random.seed(1)
        sh = [random.sample(m, len(m)) for m in msgs.values()]
        ctl = [(m[i] - m[i - 1]) % M for m in sh for i in range(1, len(m))]
        print("lag-1 difference mod %d IoC %.4f vs shuffled control %.4f" % (M, ioc(diffs), ioc(ctl)))
    same = shifted = 0
    for a, b in itertools.combinations(msgs, 2):
        ma, mb = msgs[a], msgs[b]
        for off in range(-len(mb) + 1, len(ma)):
            run = 0
            for j in range(len(mb) + 1):
                i = j + off
                if j < len(mb) and 0 <= i < len(ma) and ma[i] == mb[j]:
                    run += 1
                else:
                    if run >= 4:
                        same += off == 0; shifted += off != 0
                    run = 0
    print("identical runs >= 4 across pairs: same offset %d, shifted offset %d" % (same, shifted))
    for L, r in ((12, 3), (16, 4), (20, 5)):
        iso = isomorphs(msgs, L, r)
        sh_iso = [x for x in iso if x[1] != x[3] or x[0] != x[2]]
        shifted_iso = [x for x in iso if x[1] != x[3]]
        random.seed(2)
        ctrl = {k: random.sample(v, len(v)) for k, v in msgs.items()}
        print("isomorph window L=%d (>=%d repeats): %d pairs with different values, %d at DIFFERENT positions; shuffled control %d"
              % (L, r, len(sh_iso), len(shifted_iso), len(isomorphs(ctrl, L, r))))
        for x in shifted_iso[:3]:
            print("   e.g.", x)
    # charlie (writing stage): WHERE the same-offset identical runs sit, and one isomorph printed in full.
    runs = []
    for a, b in itertools.combinations(msgs, 2):
        ma, mb = msgs[a], msgs[b]; n = min(len(ma), len(mb)); i = 0
        while i < n:
            if ma[i] != mb[i]:
                i += 1; continue
            j = i
            while j < n and ma[j] == mb[j]:
                j += 1
            if j - i >= 4:
                runs.append((a, b, i, j - i))
            i = j
    at1 = [r for r in runs if r[2] == 1]
    print("same-offset runs: %d, of which %d start at position 1 (the second trigram); lengths from position 1: %s"
          % (len(runs), len(at1), sorted(set(r[3] for r in at1))))
    for r in runs:
        if r[2] != 1:
            print("   later run: %s / %s at position %d, %d long" % r)
    ea, wa = msgs["East 1"], msgs["West 1"]
    diff = [i for i in range(min(len(ea), len(wa))) if ea[i] != wa[i]]
    print("East 1 vs West 1 first differ at positions %s" % diff[1:6])
    L = 12
    for k, i in (("East 1", 37), ("East 1", 65)):
        w = msgs[k][i:i + L]
        print("   %s @%d: %s  pattern %s" % (k, i, w, pattern(w)))


if __name__ == "__main__":
    main()
