#!/usr/bin/env python3

import random

######## ENCODING and DECODING ########

FIELD_BITS = 32
FIELD_MODULUS = (1 << FIELD_BITS) + 0b10001101

def mul2(x):
    """Compute 2*x in GF(2^FIELD_BITS)"""
    return (x << 1) ^ (FIELD_MODULUS if x.bit_length() >= FIELD_BITS else 0)

def mul(x, y):
    """Compute x*y in GF(2^FIELD_BITS)"""
    ret = 0
    for bit in [(x >> i) & 1 for i in range(x.bit_length())]:
        ret ^= bit * y
        y = mul2(y)
    return ret

######## ENCODING only ########

def sketch(shortids, capacity):
    """Compute the bytes of a sketch for given shortids and given capacity."""
    odd_sums = [0 for _ in range(capacity)]
    for shortid in shortids:
        squared = mul(shortid, shortid)
        for i in range(capacity):
            odd_sums[i] ^= shortid
            shortid = mul(shortid, squared)
    return b''.join(elem.to_bytes(4, 'little') for elem in odd_sums)

######## DECODING only ########

def inv(x):
    """Compute 1/x in GF(2^FIELD_BITS)"""
    t = x
    for i in range(FIELD_BITS - 2):
        t = mul(mul(t, t), x)
    return mul(t, t)


def berlekamp_massey(s):
    """Given a sequence of LFSR outputs, find the coefficients of the LFSR."""
    C, B, L, m, b = [1], [1], 0, 1, 1
    for n in range(len(s)):
        d = s[n]
        for i in range(1, L + 1):
            d ^= mul(C[i], s[n - i])
        if d == 0:
            m += 1
        else:
            T = list(C)
            while len(C) <= len(B) + m:
                C += [0]
            t = mul(d, inv(b))
            for i in range(len(B)):
                C[i + m] ^= mul(t, B[i])
            if 2 * L <= n:
                L, B, b, m = n + 1 - L, T, d, 1
            else:
                m += 1
    return C[0:L + 1]

def poly_monic(p):
    """Return the monic multiple of p, or 0 if the input is 0."""
    if len(p) == 0:
        return []
    i = inv(p[-1])
    return [mul(v, i) for v in p]

def poly_divmod(m, p):
    """Compute the polynomial quotient p/m, and replace p with p mod m."""
    assert(len(m) > 0 and m[-1] == 1)
    div = [0 for _ in range(len(p) - len(m) + 1)]
    while len(p) >= len(m):
        div[len(p) - len(m)] = p[-1]
        for i in range(len(m)):
            p[len(p) - len(m) + i] ^= mul(p[-1], m[i])
        assert(p[-1] == 0)
        p.pop()
    while (len(p) > 0 and p[-1] == 0):
        p.pop()
    return div

def poly_gcd(a, b):
    """Compute the GCD of a and b (destroys the inputs)."""
    if len(a) < len(b):
        a, b = b, a
    while len(b):
        if len(b) == 1:
            return [1]
        b = poly_monic(b)
        poly_divmod(b, a)
        a, b = b, a
    return a

def poly_sqr(p):
    """Compute the coefficients of the square of polynomial with coefficients p."""
    return [0 if i & 1 else mul(p[i // 2], p[i // 2]) for i in range(2 * len(p))]

def poly_trace(m, a):
    """Compute the coefficients of the trace polynomial of (a*x) mod m."""
    out = [0, a]
    for i in range(FIELD_BITS - 1):
        out = poly_sqr(out)
        while len(out) < 2:
            out += [0]
        out[1] = a
        poly_divmod(m, out)
    return out

def find_roots_inner(p, a):
    """Recursive helper function for find_roots (destroys p). a is randomizer."""
    # p must be monic
    assert(len(p) > 0 and p[-1] == 1)
    # Deal with degree 0 and degree 1 inputs
    if len(p) == 1:
        return []
    elif len(p) == 2:
        return [p[0]]
    # Otherwise, split p in left*right using parameter a_vals[0].
    t = poly_monic(poly_trace(p, a))
    left = poly_gcd(list(p), t)
    right = poly_divmod(list(left), p)
    # Invoke recursion with the remaining a_vals.
    ret_right = find_roots_inner(right, mul2(a))
    ret_left = find_roots_inner(left, mul2(a))
    # Concatenate roots
    return ret_left + ret_right

def find_roots(p):
    """Find the roots of polynomial with coefficients p."""
    # Compute x^(2^FIELD_BITS)+x mod p in a roundabout way.
    t = poly_trace(p, 1)
    t2 = poly_sqr(t)
    for i in range(len(t)):
        t2[i] ^= t[i]
    poly_divmod(p, t2)
    # If distinct from 0, p is not fully factorizable into non-repeating roots.
    if len(t2):
        return None
    # Invoke the recursive splitting algorithm
    return find_roots_inner(list(p), random.randrange(1, 2**32-1))

def decode(sketch):
    """Recover the shortids from a sketch."""
    odd_sums = [int.from_bytes(sketch[i*4:(i+1)*4], 'little') for i in range(len(sketch) // 4)]
    sums = []
    for i in range(len(odd_sums) * 2):
        if i & 1:
            sums.append(mul(sums[(i-1)//2], sums[(i-1)//2]))
        else:
            sums.append(odd_sums[(i+1)//2])
    return find_roots(list(reversed(berlekamp_massey(sums))))
