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


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


def parse(path):
    clauses = []
    nvars = 0
    with open_maybe_xz(path) as f:
        for line in f:
            if not line or line[0] in "c%":
                continue
            parts = line.split()
            if not parts:
                continue
            if parts[0] == "p":
                nvars = int(parts[2])
                continue
            lits = [int(x) for x in parts]
            if lits and lits[-1] == 0:
                lits.pop()
            clauses.append(lits)
    return nvars, clauses


def relation_from_group(group):
    vars_ = sorted({abs(lit) for c in group for lit in c})
    if any(len(c) != len(vars_) for c in group):
        return None
    k = len(vars_)
    if k < 1 or k > 5:
        return None
    forbidden = set()
    for c in group:
        mask = 0
        seen = set()
        for lit in c:
            v = abs(lit)
            if v in seen:
                return None
            seen.add(v)
            pos = vars_.index(v)
            if lit < 0:
                mask |= 1 << pos
        forbidden.add(mask)
    allowed = [0, 0]
    for mask in range(1 << k):
        if mask not in forbidden:
            allowed[mask.bit_count() & 1] += 1
    half = 1 << (k - 1)
    if allowed == [half, 0]:
        return vars_, 0
    if allowed == [0, half]:
        return vars_, 1
    return None


def is_and_clause(c, n):
    base = 2 * n
    andmax = base + n * n
    if len(c) == 2 and c[0] < 0:
        z = -c[0]
        if not (base < z <= andmax):
            return False
        k = z - base - 1
        return c[1] == k // n + 1 or c[1] == n + k % n + 1
    if len(c) == 3 and c[0] < 0 and c[1] < 0 and base < c[2] <= andmax:
        k = c[2] - base - 1
        return -c[0] == k // n + 1 and -c[1] == n + k % n + 1
    return False


class Basis:
    def __init__(self, nbits):
        self.rows = {}
        self.rhs = {}
        self.nbits = nbits

    @staticmethod
    def top(mask):
        return mask.bit_length() - 1

    def insert_reduce(self, mask, rhs, stop_below):
        while mask:
            p = self.top(mask)
            if p < stop_below:
                return mask, rhs, True
            if p not in self.rows:
                self.rows[p] = mask
                self.rhs[p] = rhs
                return 0, 0, True
            mask ^= self.rows[p]
            rhs ^= self.rhs[p]
        return 0, 0, rhs == 0


def extract(path):
    nvars, clauses = parse(path)
    n = clauses[2][0] - 1
    base = 2 * n
    andmax = base + n * n
    buckets = collections.defaultdict(list)
    rels = []
    units = []
    long_clauses = []
    other = []
    for c in clauses:
        if is_and_clause(c, n):
            continue
        if len(c) == 1:
            units.append(c[0])
            if abs(c[0]) > base:
                rels.append(([abs(c[0])], 1 if c[0] > 0 else 0, "unit"))
            continue
        if len(c) == n - 1:
            long_clauses.append(c)
            continue
        if 2 <= len(c) <= 4:
            buckets[tuple(sorted(abs(x) for x in c))].append(c)
        else:
            other.append(c)
    grouped = []
    for key, group in buckets.items():
        rel = relation_from_group(group)
        if rel is None:
            other.extend(group)
        else:
            grouped.append((key, group, rel))
            rels.append((rel[0], rel[1], "xor"))
    xb = Basis(nvars)
    projected = []
    inconsistent = False
    for vars_, rhs, kind in rels:
        mask = 0
        for v in vars_:
            mask ^= 1 << (v - 1)
        pm, pr, ok = xb.insert_reduce(mask, rhs, andmax)
        if not ok:
            inconsistent = True
        if pm:
            pairs = []
            for v in range(base + 1, andmax + 1):
                if (pm >> (v - 1)) & 1:
                    k = v - base - 1
                    pairs.append((k // n, k % n))
            projected.append((pairs, pr))
    return nvars, n, base, andmax, units, grouped, projected, inconsistent, other


def main():
    for path in sys.argv[1:]:
        nvars, n, base, andmax, units, grouped, projected, inconsistent, other = extract(path)
        print("FILE", path)
        print("nvars", nvars, "N", n, "aux", nvars - andmax, "units", len(units), "xorrels", len(grouped), "projected", len(projected), "inconsistent", inconsistent, "other", len(other))
        print("unit signs", collections.Counter(1 if u > 0 else -1 for u in units), "unit ranges", collections.Counter("xy" if abs(u) <= base else "aux" for u in units))
        print("xor arity", collections.Counter(len(rel[0]) for _, _, rel in grouped))
        print("xor group sizes", collections.Counter(len(group) for _, group, _ in grouped))
        print("projected sizes", collections.Counter(len(pairs) for pairs, _ in projected).most_common(20))
        print("projected rhs", collections.Counter(rhs for _, rhs in projected))
        for pairs, rhs in projected[:5]:
            print(" row", len(pairs), rhs, pairs[:20])
        print()


if __name__ == "__main__":
    main()
