#!/usr/bin/env python3
import importlib.util
import lzma
import sys
import time

spec = importlib.util.spec_from_file_location("dmb", "/workspace/solver/tools/detect_min_blocks.py")
dmb = importlib.util.module_from_spec(spec)
spec.loader.exec_module(dmb)


def read(path):
    op = lzma.open if path.endswith(".xz") else open
    clauses = []
    with op(path, "rt") as fh:
        for line in fh:
            if not line or line[0] == "c":
                continue
            if line[0] == "p":
                parts = line.split()
                V = int(parts[2])
                continue
            cl = tuple(int(x) for x in line.split() if x != "0")
            if cl:
                clauses.append(cl)
    P = abs(clauses[0][2]) - 1
    return V, P, clauses


def detect_blocks(clauses, P):
    blocks = []
    pos = 0
    while pos + 92 <= len(clauses):
        cl = clauses[pos]
        matched = False
        if len(cl) == 3 and cl[0] > 0 and cl[1] > 0 and cl[2] > P:
            x0, y0, aux0 = cl
            if y0 == x0 + 6:
                row = 0
                p = pos
                sels = []
                bitslist = []
                while p + 92 <= len(clauses):
                    if row == 0:
                        bits = list(range(x0, x0 + 6))
                        sel = x0 + 16
                    else:
                        s = x0 + 17 + 7 * (row - 1)
                        bits = list(range(s, s + 6))
                        sel = s + 6
                    if clauses[p : p + 92] != dmb.gen_row(bits, y0, sel, aux0 + 18 * row):
                        break
                    sels.append(sel)
                    bitslist.append(bits)
                    row += 1
                    p += 92
                if row > 0 and p < len(clauses) and clauses[p] == tuple(reversed(sels)):
                    blocks.append((x0, y0, row, sels, bitslist))
                    pos = p + 1
                    matched = True
        if not matched:
            pos += 1
    return blocks


class BCP:
    def __init__(self, V, clauses):
        self.V = V
        self.clauses = clauses
        n = len(clauses)
        self.w0 = [0] * n
        self.w1 = [0] * n
        self.watches = [[] for _ in range(2 * V + 2)]
        for ci, cl in enumerate(clauses):
            self.w1[ci] = 0 if len(cl) == 1 else 1
            self.watches[self.wi(cl[0])].append(ci)
            if len(cl) > 1:
                self.watches[self.wi(cl[1])].append(ci)
        self.val = bytearray(V + 1)
        self.trail = []
        self.q = 0

    def wi(self, lit):
        v = abs(lit)
        return 2 * (v - 1) + (1 if lit > 0 else 0)

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

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

    def propagate(self):
        while self.q < len(self.trail):
            lit = self.trail[self.q]
            self.q += 1
            fl = -lit
            wl = self.watches[self.wi(fl)]
            i = 0
            while i < len(wl):
                ci = wl[i]
                cl = self.clauses[ci]
                if cl[self.w0[ci]] == fl:
                    fw = 0
                    ow = self.w1[ci]
                elif cl[self.w1[ci]] == fl:
                    fw = 1
                    ow = self.w0[ci]
                else:
                    wl[i] = wl[-1]
                    wl.pop()
                    continue
                if self.lit_val(cl[ow]) == 1:
                    i += 1
                    continue
                found = False
                for k, l in enumerate(cl):
                    if k == self.w0[ci] or k == self.w1[ci]:
                        continue
                    if self.lit_val(l) != -1:
                        if fw == 0:
                            self.w0[ci] = k
                        else:
                            self.w1[ci] = k
                        wl[i] = wl[-1]
                        wl.pop()
                        self.watches[self.wi(l)].append(ci)
                        found = True
                        break
                if found:
                    continue
                other = cl[ow]
                lv = self.lit_val(other)
                if lv == -1:
                    return ci
                if lv == 0 and not self.enqueue(other):
                    return ci
                i += 1
        return None


def run(path, pick):
    t = time.time()
    V, P, clauses = read(path)
    blocks = detect_blocks(clauses, P)
    print("read", V, len(clauses), "P", P, "blocks", len(blocks), "time", time.time() - t)
    b = BCP(V, clauses)
    for cl in clauses:
        if len(cl) == 1 and not b.enqueue(cl[0]):
            print("unit enqueue conflict")
            return False
    for _x0, y0, _k, sels, bitslist in blocks:
        b.enqueue(sels[0])
        for s in sels[1:]:
            b.enqueue(-s)
        for i in range(10):
            b.enqueue(-(y0 + i))
        for bit in bitslist[0]:
            b.enqueue(-bit)
        for bits in bitslist[1:]:
            for bit in bits:
                b.enqueue(bit)
    conf = b.propagate()
    print("after init", None if conf is None else conf + 1, "trail", len(b.trail), "time", time.time() - t)
    if conf is not None:
        print(clauses[conf])
        return False
    pos = [ci for ci, cl in enumerate(clauses) if all(l > 0 for l in cl)]
    dec = 0
    for rounds in range(20):
        made = False
        for ci in pos:
            cl = clauses[ci]
            if any(b.lit_val(l) == 1 for l in cl):
                continue
            un = [l for l in cl if b.lit_val(l) == 0]
            if not un:
                print("pos conflict", ci + 1, cl)
                return False
            lit = un[0] if pick == "first" else un[-1] if pick == "last" else min(un)
            if not b.enqueue(lit):
                print("enqueue conflict", lit, ci + 1, cl)
                return False
            dec += 1
            made = True
            conf = b.propagate()
            if conf is not None:
                print("conf after pos", conf + 1, clauses[conf], "dec", dec, "lit", lit, "poscl", ci + 1, cl)
                return False
        print("round", rounds, "made", made, "dec", dec, "trail", len(b.trail), "time", time.time() - t)
        if not made:
            break
    false = 0
    for ci, cl in enumerate(clauses):
        if all(b.lit_val(l) == -1 for l in cl):
            false += 1
            print("false clause", ci + 1, cl)
            break
    print("partial false", false, "trail", len(b.trail), "time", time.time() - t)
    return false == 0


if __name__ == "__main__":
    run(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else "first")
