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


def open_dimacs(path):
    if path.endswith(".xz"):
        return lzma.open(path, "rt", encoding="ascii", errors="ignore")
    return open(path, "rt", encoding="ascii", errors="ignore")


def parse(path):
    n = 0
    clauses = []
    cur = []
    with open_dimacs(path) as f:
        for line in f:
            if not line or line[0] in "c\n":
                continue
            if line[0] == "p":
                n = int(line.split()[2])
                continue
            for tok in line.split():
                lit = int(tok)
                if lit == 0:
                    if cur:
                        clauses.append(tuple(cur))
                        cur = []
                else:
                    cur.append(lit)
    if cur:
        clauses.append(tuple(cur))
    return n, clauses


def combos(s, k):
    return itertools.combinations(s, k)


def recover_groups(n, clauses):
    L = max((len(c) for c in clauses if len(c) > 2), default=0)
    if not L or L % 2:
        return None
    b = L // 2
    supports = collections.Counter()
    for c in clauses:
        if len(c) != L:
            continue
        s = tuple(sorted(abs(x) for x in c))
        if len(set(s)) == L:
            supports[s] += 1
    width, freq = collections.Counter(supports.values()).most_common(1)[0]
    support_list = list(supports)

    pair_count = collections.Counter()
    for s in support_list:
        for a, z in itertools.combinations(s, 2):
            pair_count[(a, z)] += 1
    neigh = collections.defaultdict(list)
    for (a, z), ct in pair_count.items():
        if ct > width:
            neigh[a].append((ct, z))
            neigh[z].append((ct, a))
    group_set = set()
    for v in range(1, n + 1):
        ns = sorted(neigh[v], reverse=True)
        if len(ns) >= b - 1:
            g = tuple(sorted([v] + [z for _, z in ns[: b - 1]]))
            group_set.add(g)

    def close():
        changed = True
        while changed:
            changed = False
            for s in support_list:
                ss = set(s)
                for comb in combos(s, b):
                    if comb in group_set:
                        rest = tuple(sorted(ss - set(comb)))
                        if len(rest) == b and rest not in group_set:
                            group_set.add(rest)
                            changed = True

    close()
    by_var = collections.defaultdict(list)
    for i, s in enumerate(support_list):
        for v in s:
            by_var[v].append(i)
    for ids in by_var.values():
        if len(ids) < 2 or len(ids) > 2000:
            continue
        for ii in range(len(ids)):
            A = set(support_list[ids[ii]])
            for jj in range(ii + 1, len(ids)):
                inter = A & set(support_list[ids[jj]])
                if len(inter) == b:
                    group_set.add(tuple(sorted(inter)))
    close()

    groups = sorted(group_set)
    group_of_var = [-1] * (n + 1)
    idx_of_var = [-1] * (n + 1)
    for gi, g in enumerate(groups):
        for i, v in enumerate(g):
            if group_of_var[v] != -1:
                raise ValueError("overlapping groups")
            group_of_var[v] = gi
            idx_of_var[v] = i
    return L, b, width, supports, groups, group_of_var, idx_of_var


def raw_for_clause(clause, g, group_of_var, idx_of_var, b):
    raw = 0
    seen = 0
    for lit in clause:
        v = abs(lit)
        if group_of_var[v] != g:
            continue
        bit = 0 if lit > 0 else 1
        raw |= bit << idx_of_var[v]
        seen += 1
    return raw if seen == b else None


def lit_true_raw(lit, raw, idx_of_var):
    i = idx_of_var[abs(lit)]
    bit = (raw >> i) & 1
    return bool(bit) if lit > 0 else not bool(bit)


def analyze(path):
    n, clauses = parse(path)
    rec = recover_groups(n, clauses)
    if rec is None:
        print(os.path.basename(path), "not bit-vector")
        return
    L, b, width, supports, groups, group_of_var, idx_of_var = rec
    same_binary = [[] for _ in groups]
    binary_between = []
    raw_tables = collections.defaultdict(list)
    ignored = collections.Counter()
    for c in clauses:
        if len(c) == 2:
            g0 = group_of_var[abs(c[0])]
            g1 = group_of_var[abs(c[1])]
            if g0 >= 0 and g1 >= 0:
                if g0 == g1:
                    same_binary[g0].append(c)
                else:
                    binary_between.append((g0, g1, c))
            else:
                ignored["binary"] += 1
        elif len(c) == L:
            gs = {group_of_var[abs(lit)] for lit in c}
            if len(gs) == 2 and -1 not in gs:
                a, z = sorted(gs)
                raw_tables[(a, z)].append(c)
            else:
                ignored["long"] += 1
        else:
            ignored["other"] += 1

    allowed = []
    dom_hist = collections.Counter()
    same_hist = collections.Counter(len(x) for x in same_binary)
    for g in range(len(groups)):
        vals = []
        for raw in range(1 << b):
            ok = True
            for c in same_binary[g]:
                if not any(lit_true_raw(lit, raw, idx_of_var) for lit in c):
                    ok = False
                    break
            if ok:
                vals.append(raw)
        allowed.append(vals)
        dom_hist[len(vals)] += 1

    table_size_hist = collections.Counter(len(v) for v in raw_tables.values())
    edge_degree = [0] * len(groups)
    table_matching = 0
    table_nonmatching = 0
    forbid_shape = collections.Counter()
    table_pairs = set(raw_tables)
    for (a, z), cs in raw_tables.items():
        edge_degree[a] += 1
        edge_degree[z] += 1
        seen_a = collections.Counter()
        seen_z = collections.Counter()
        pairs = []
        for c in cs:
            va = raw_for_clause(c, a, group_of_var, idx_of_var, b)
            vz = raw_for_clause(c, z, group_of_var, idx_of_var, b)
            seen_a[va] += 1
            seen_z[vz] += 1
            pairs.append((va, vz))
        if len(cs) == width and all(seen_a[x] == 1 for x in allowed[a]) and all(seen_z[x] == 1 for x in allowed[z]):
            table_matching += 1
            forbid_shape[(width, 1)] += 1
        else:
            table_nonmatching += 1
            forbid_shape[(len(cs), max(seen_a.values() or [0]), max(seen_z.values() or [0]))] += 1

    print(os.path.basename(path))
    print("  n", n, "clauses", len(clauses), "L", L, "b", b, "width", width)
    print("  supports", len(supports), "support_counts", dict(collections.Counter(supports.values()).most_common(8)))
    print("  groups", len(groups), "covered", sum(1 for x in group_of_var if x >= 0), "ignored", dict(ignored))
    print("  same_binary_hist", dict(sorted(same_hist.items())[:12]), "domain_hist", dict(sorted(dom_hist.items())))
    print("  raw_table_edges", len(raw_tables), "table_size_hist", dict(sorted(table_size_hist.items())[:12]))
    print("  table_matching", table_matching, "table_nonmatching", table_nonmatching, "shape", dict(forbid_shape.most_common(8)))
    print("  binary_between", len(binary_between))
    degs = sorted(edge_degree)
    print("  table_degree min/med/max", degs[0], degs[len(degs)//2], degs[-1])

    side_by_pair = collections.defaultdict(list)
    for g0, g1, c in binary_between:
        if g0 > g1:
            g0, g1 = g1, g0
        side_by_pair[(g0, g1)].append(c)
    if side_by_pair:
        overlap = sum(1 for p in side_by_pair if p in table_pairs)
        print("  side_pairs", len(side_by_pair), "overlap_table", overlap, "side_only", len(side_by_pair) - overlap)
        print("  side_clauses_per_pair", dict(collections.Counter(len(v) for v in side_by_pair.values()).most_common(10)))

        # Reconstruct the color names induced by table matching constraints.
        color_of = [dict() for _ in groups]
        value_of = [dict() for _ in groups]
        table_adj = [[] for _ in groups]
        table_pairs_raw = {}
        for (a, z), cs in raw_tables.items():
            pairs = []
            for c in cs:
                va = raw_for_clause(c, a, group_of_var, idx_of_var, b)
                vz = raw_for_clause(c, z, group_of_var, idx_of_var, b)
                pairs.append((va, vz))
            table_pairs_raw[(a, z)] = pairs
            table_adj[a].append((z, pairs))
            table_adj[z].append((a, [(vz, va) for va, vz in pairs]))
        for s in range(len(groups)):
            if color_of[s]:
                continue
            for ci, raw in enumerate(allowed[s]):
                color_of[s][raw] = ci
                value_of[s][ci] = raw
            q = collections.deque([s])
            while q:
                g = q.popleft()
                for h, pairs in table_adj[g]:
                    hc = {}
                    for vg, vh in pairs:
                        hc[vh] = color_of[g][vg]
                    if not color_of[h]:
                        color_of[h] = hc
                        value_of[h] = {c: raw for raw, c in hc.items()}
                        q.append(h)

        side_forbid_hist = collections.Counter()
        side_clause_shape = collections.Counter()
        combined_row_pop_hist = collections.Counter()
        combined_pair_pop_hist = collections.Counter()
        side_touch_colors = collections.Counter()
        side_perm = 0
        side_nonperm = 0
        side_perm_fixed_hist = collections.Counter()
        for pair, cs in side_by_pair.items():
            a, z = pair
            rows = [set() for _ in range(width)]
            for c in cs:
                local = []
                for ca in range(width):
                    ra = value_of[a][ca]
                    for cz in range(width):
                        rz = value_of[z][cz]
                        if not any(lit_true_raw(lit, ra if group_of_var[abs(lit)] == a else rz, idx_of_var) for lit in c):
                            local.append((ca, cz))
                side_forbid_hist[len(local)] += 1
                for ca, cz in local:
                    rows[ca].add(cz)
                colors_a = {ca for ca, _ in local}
                colors_z = {cz for _, cz in local}
                side_clause_shape[(len(colors_a), len(colors_z), len(local))] += 1
                side_touch_colors[len(colors_a) + len(colors_z)] += 1
            pop = sum(len(r) for r in rows)
            combined_pair_pop_hist[pop] += 1
            for r in rows:
                combined_row_pop_hist[len(r)] += 1
            allowed_pairs = [(ca, cz) for ca in range(width) for cz in range(width) if cz not in rows[ca]]
            row_counts = collections.Counter(ca for ca, _ in allowed_pairs)
            col_counts = collections.Counter(cz for _, cz in allowed_pairs)
            if len(allowed_pairs) == width and all(row_counts[i] == 1 for i in range(width)) and all(col_counts[i] == 1 for i in range(width)):
                side_perm += 1
                fixed = sum(1 for ca, cz in allowed_pairs if ca == cz)
                side_perm_fixed_hist[fixed] += 1
            else:
                side_nonperm += 1
        print("  side_forbidden_pairs_per_clause", dict(side_forbid_hist.most_common(10)))
        print("  side_clause_color_shape", dict(side_clause_shape.most_common(10)))
        print("  combined_side_pair_forbids", dict(combined_pair_pop_hist.most_common(12)))
        print("  combined_side_row_pop", dict(combined_row_pop_hist.most_common(12)))
        print("  side_allowed_permutation", side_perm, "nonperm", side_nonperm, "fixed_points_hist", dict(side_perm_fixed_hist.most_common(12)))


def main(argv):
    for path in argv[1:]:
        analyze(path)


if __name__ == "__main__":
    main(sys.argv)
