#!/usr/bin/env python3
import collections
import itertools
import lzma
import sys


def op(path):
    return lzma.open(path, "rt") if path.endswith(".xz") else open(path)


def clauses(path):
    with op(path) as f:
        for line in f:
            if not line or line[0] == "c":
                continue
            if line[0] == "p":
                parts = line.split()
                yield ("p", int(parts[2]), int(parts[3]))
                continue
            xs = [int(x) for x in line.split()]
            if xs and xs[-1] == 0:
                xs.pop()
            if xs:
                yield xs


def sat_clause(c, assign):
    for lit in c:
        v = abs(lit)
        b = assign[v]
        if (lit > 0 and b) or (lit < 0 and not b):
            return True
    return False


def truth_class(v, cs):
    vars_ = sorted({abs(x) for c in cs for x in c if abs(x) != v})
    if len(vars_) > 5:
        return None
    out = {}
    for bits in itertools.product([False, True], repeat=len(vars_)):
        base = dict(zip(vars_, bits))
        okvals = []
        for bv in [False, True]:
            a = dict(base)
            a[v] = bv
            if all(sat_clause(c, a) for c in cs):
                okvals.append(bv)
        if len(okvals) != 1:
            return ("nonfunc", tuple(vars_), None)
        out[bits] = okvals[0]
    if len(vars_) == 0:
        return ("const", tuple(vars_), next(iter(out.values())))
    # Test OR of signed literals or negated OR.
    lits = []
    for signs in itertools.product([False, True], repeat=len(vars_)):
        vals = []
        for bits in itertools.product([False, True], repeat=len(vars_)):
            val = any((b if s else (not b)) for b, s in zip(bits, signs))
            vals.append(val)
        tab = [out[bits] for bits in itertools.product([False, True], repeat=len(vars_))]
        if vals == tab:
            return ("or", tuple((var if sign else -var) for var, sign in zip(vars_, signs)), None)
        if [not x for x in vals] == tab:
            return ("nor", tuple((var if sign else -var) for var, sign in zip(vars_, signs)), None)
    if len(vars_) == 1:
        tab = [out[(False,)], out[(True,)]]
        if tab == [False, True]:
            return ("buf", (vars_[0],), None)
        if tab == [True, False]:
            return ("not", (vars_[0],), None)
    if len(vars_) == 2:
        tab = [out[bits] for bits in [(False, False), (False, True), (True, False), (True, True)]]
        if tab == [False, True, True, False]:
            return ("xor", tuple(vars_), None)
        if tab == [True, False, False, True]:
            return ("xnor", tuple(vars_), None)
    return ("other", tuple(vars_), tuple(out.items()))


def classify(v, group):
    units = [c[0] for c in group if len(c) == 1]
    cs = [c for c in group if len(c) > 1]
    if not cs:
        return ("unitonly", [], units)
    # OR/NOR style: n binaries plus one long.
    longs = [c for c in cs if len(c) > 2]
    bins = [c for c in cs if len(c) == 2]
    if len(longs) == 1 and len(bins) == len(longs[0]) - 1:
        long = longs[0]
        first = long[0]
        ok = all(abs(c[0]) == v and c[0] == -first for c in bins)
        if ok:
            in_lits = long[1:]
            # first == -v means v = OR(in_lits). first == v means v = NOR(in_lits).
            typ = "or" if first == -v else "nor"
            return (typ, in_lits, units)
    if len(cs) <= 6:
        tc = truth_class(v, cs)
        if tc:
            typ, data, extra = tc
            return (typ, list(data), units)
    return ("unknown", [], units)


def main():
    path = sys.argv[1]
    nvars = 0
    cur_v = None
    cur = []
    defs = {}
    units = []
    type_hist = collections.Counter()
    fanin_hist = collections.Counter()
    samples = collections.defaultdict(list)

    def flush():
        nonlocal cur_v, cur
        if not cur:
            return
        typ, ins, us = classify(cur_v, cur)
        type_hist[typ] += 1
        fanin_hist[(typ, len(ins))] += 1
        if typ != "unitonly":
            defs[cur_v] = (typ, ins)
        units.extend(us)
        if len(samples[typ]) < 4:
            samples[typ].append((cur_v, cur[:10], ins, us))

    for item in clauses(path):
        if item[0] == "p":
            nvars = item[1]
            continue
        c = item
        v = abs(c[0])
        if cur and v != cur_v:
            flush()
            cur = [c]
            cur_v = v
        else:
            cur.append(c)
            cur_v = v
    flush()
    print("nvars", nvars, "defs", len(defs), "units", len(units))
    print("types", type_hist)
    print("fanin top")
    for k, v in fanin_hist.most_common(50):
        print(k, v)
    print("unknown samples")
    for typ in sorted(samples):
        print("TYPE", typ)
        for s in samples[typ]:
            print(" var", s[0], "ins", s[2][:12], "units", s[3])
            for c in s[1][:5]:
                print("  ", c)
    # Topological check.
    indeg = {v: 0 for v in defs}
    children = collections.defaultdict(list)
    for v, (_, ins) in defs.items():
        for lit in ins:
            u = abs(lit)
            if u in defs:
                indeg[v] += 1
                children[u].append(v)
    q = collections.deque([v for v, d in indeg.items() if d == 0])
    seen = 0
    while q:
        u = q.popleft()
        seen += 1
        for w in children[u]:
            indeg[w] -= 1
            if indeg[w] == 0:
                q.append(w)
    print("topo seen", seen, "of", len(defs), "cyclic", len(defs) - seen)
    if seen != len(defs):
        print("cycle sample", [v for v, d in indeg.items() if d > 0][:50])


if __name__ == "__main__":
    main()
