#!/usr/bin/env python3
import lzma
import sys
from pathlib import Path


def open_dimacs(path: Path):
    if path.suffix == ".xz":
        return lzma.open(path, "rt", encoding="ascii")
    return path.open("rt", encoding="ascii")


def parse(path: Path):
    nvars = 0
    clauses = []
    sizes = {}
    with open_dimacs(path) as f:
        pending = []
        for line in f:
            if not line or line[0] == "c":
                continue
            if line[0] == "p":
                nvars = int(line.split()[2])
                continue
            for tok in line.split():
                lit = int(tok)
                if lit == 0:
                    if pending:
                        clauses.append(tuple(pending))
                        sizes[len(pending)] = sizes.get(len(pending), 0) + 1
                    pending = []
                else:
                    pending.append(lit)
    return nvars, clauses, sizes


class WatchProbe:
    def __init__(self, nvars, clauses):
        self.nvars = nvars
        self.clauses = [list(c) for c in clauses]
        self.watches = [[] for _ in range(2 * nvars + 2)]
        self.assign = [0] * (nvars + 1)
        self.trail = []
        self.qhead = 0
        for i, c in enumerate(self.clauses):
            if len(c) == 1:
                if not self.enqueue(c[0]):
                    raise RuntimeError("root conflict")
            else:
                self.watches[self.lidx(c[0])].append(i)
                self.watches[self.lidx(c[1])].append(i)
        confl = self.propagate()
        if confl is not None:
            raise RuntimeError("root conflict")
        self.root_len = len(self.trail)

    @staticmethod
    def lidx(lit):
        return ((abs(lit) - 1) << 1) ^ (1 if lit < 0 else 0)

    def value(self, lit):
        val = self.assign[abs(lit)]
        if val == 0:
            return 0
        return 1 if ((lit > 0) == (val > 0)) else -1

    def enqueue(self, lit):
        v = abs(lit)
        val = 1 if lit > 0 else -1
        if self.assign[v] != 0:
            return self.assign[v] == val
        self.assign[v] = val
        self.trail.append(lit)
        return True

    def propagate(self):
        while self.qhead < len(self.trail):
            lit = self.trail[self.qhead]
            self.qhead += 1
            ws = self.watches[self.lidx(-lit)]
            j = 0
            i = 0
            while i < len(ws):
                ci = ws[i]
                i += 1
                c = self.clauses[ci]
                if c[0] == -lit:
                    c[0], c[1] = c[1], c[0]
                other = c[0]
                if self.value(other) > 0:
                    ws[j] = ci
                    j += 1
                    continue
                found = False
                for k in range(2, len(c)):
                    if self.value(c[k]) >= 0:
                        c[1], c[k] = c[k], c[1]
                        self.watches[self.lidx(c[1])].append(ci)
                        found = True
                        break
                if found:
                    continue
                ws[j] = ci
                j += 1
                if self.value(other) < 0 or not self.enqueue(other):
                    while i < len(ws):
                        ws[j] = ws[i]
                        j += 1
                        i += 1
                    del ws[j:]
                    return ci
            del ws[j:]
        return None

    def reset(self):
        for lit in self.trail[self.root_len:]:
            self.assign[abs(lit)] = 0
        del self.trail[self.root_len:]
        self.qhead = self.root_len

    def failed(self, lit):
        self.reset()
        if not self.enqueue(lit):
            return True, 0
        confl = self.propagate()
        return confl is not None, len(self.trail) - self.root_len


def interesting_clauses(clauses):
    # Table clauses of length 33 are common and not useful here. The unusual
    # long clauses are the final miters/assertions in this family.
    return [(i, c) for i, c in enumerate(clauses) if len(c) in (16, 56, 184, 257)]


def main():
    for arg in sys.argv[1:]:
        path = Path(arg)
        nvars, clauses, sizes = parse(path)
        probe = WatchProbe(nvars, clauses)
        print(path.name, f"vars={nvars}", f"clauses={len(clauses)}", f"root={probe.root_len}")
        for ci, c in interesting_clauses(clauses):
            failed = 0
            maxprop = 0
            samples = []
            for lit in c:
                ok, props = probe.failed(lit)
                failed += int(ok)
                maxprop = max(maxprop, props)
                if len(samples) < 8:
                    samples.append((lit, ok, props))
            print(f"  clause#{ci} len={len(c)} failed={failed}/{len(c)} maxprop={maxprop} samples={samples}")


if __name__ == "__main__":
    main()
