# @file bignum.003.py
# @ingroup experimental
# Experimental big-number representation.
# (n's complement version)
# @date 11/24/2024

import itertools
import operator
import random

class Num:
    base = 2**8

    def __init__(self, init):
        if isinstance(init, list):
            self.m = init
        else:
            self.m = iton(init, Num.base)

    def __neg__(self):
        return Num(nneg(self.m, Num.base))

    def __add__(self, other):
        return Num(nadd(self.m, other.m, Num.base))

    def __sub__(self, other):
        return Num(nsub(self.m, other.m, Num.base))

    def __mul__(self, other):
        return Num(nmul(self.m, other.m, Num.base))

    def __floordiv__(self, other):
        return Num(ndiv(self.m, other.m, Num.base)[0])

    def __mod__(self, other):
        return Num(ndiv(self.m, other.m, Num.base)[1])

    def __str__(self):
        return '-' * _isneg(self.m) + ntos(self.m, Num.base)

    def __repr__(self):
        return str(self.m)

# Utility.

def _isneg(a):
    return a[-1] != 0

def _iszero(a):
    return len(a) == 2 and (a[0] | a[1]) == 0

def _samesign(a, b):
    return a[-1] == b[-1]

def _signextend(a, n):
    return a[:] + [a[-1]] * (n - len(a))

def _signreduce(a):
    n = len(a)
    while n != 2 and a[n-1] == a[n-2]:
        n -= 1
    return a[:n]

def _inplace_mul1(r, x, c, base):
    for i in range(len(r)):
        c, r[i] = divmod(r[i] * x + c, base)
    return c, r

def _complement(a, base):
    return [base-1-x for x in a]

def _abs(a, base):
    return nneg(a, base) if _isneg(a) else a

# Convert.

def iton(z, base):
    r = []
    x = abs(z)
    while True:
        x, y = divmod(x, base)
        r.append(y)
        if x == 0:
            break
    r.append(0)
    return nneg(r, base) if z < 0 else r

def nton(a, base1, base2):
    r = [0]
    for x in reversed(a[:-1]):
        c = _inplace_mul1(r, base1, x, base2)[0]
        while c != 0:
            c, y = divmod(c, base2)
            r.append(y)
    r.append(base2-1 if _isneg(a) else 0)
    return r

def ntos(a, base):
    return ''.join(map(str, reversed(nton(_abs(a, base), base, 10)[:-1])))

# Compare.

def _cmp(x, y):
    return -1 if x < y else 1

def ncmp(a, b):
    n = len(a)
    m = len(b)
    if n != m:
        return _cmp(n, m)
    for x, y in zip(reversed(a), reversed(b)):
        if x != y:
            return _cmp(x, y)
    return 0

# Shift.

def nshl(a, n):
    return _signreduce([0] * n + a[:])

def nshr(a, n):
    n = min(n, len(a)-1)
    return _signextend(a[n:], 2)

# Negate.

def nneg(a, base):
    return nadd(_complement(a, base), [1, 0], base)

# Add.

def nadd(a, b, base):
    n = 1 + max(len(a), len(b))
    r = _signextend(a, n)
    b = _signextend(b, n)
    c = 0
    for i in range(n):
        c, r[i] = divmod(r[i] + b[i] + c, base)
    return _signreduce(r)

# Subtract.

def nsub(a, b, base):
    return nadd(a, nneg(b, base), base)

# Multiply.

def _mul1(a, x, base):
    n = 1 + len(a)
    return _signreduce(_inplace_mul1(_signextend(a, n), x, 0, base)[1])

def _mul(a, b, base):
    if _iszero(b):
        return [0, 0]
    return nadd(_mul1(a, b[0], base), _mul(nshl(a, 1), nshr(b, 1), base),
                base)

def nmul(a, b, base):
    r = _mul(a, _abs(b, base), base)
    if _isneg(b):
        r = nneg(r, base)
    return r

# Divide.

def _div2(a, b):
    if len(a) < len(b):
        q, r = [0, 0], a
    else:
        q, r = _div2(a, nshl(b, 1))
        q = nshl(q, 1)
        if ncmp(r, b) >= 0:
            q = nadd(q, [1, 0], 2)
            r = nsub(r, b, 2)
    return q, r

def _div(a, b, base):
    a = nton(a, base, 2)
    b = nton(b, base, 2)
    q, r = _div2(a, b)
    q = nton(q, 2, base)
    r = nton(r, 2, base)
    return q, r

def ndiv(a, b, base):
    if _iszero(b):
        raise ZeroDivisionError
    t = _abs(b, base)
    q, r = _div(_abs(a, base), t, base)
    if not _samesign(a, b) and not _iszero(r):
        q = nadd(q, [1, 0], base)
        r = nsub(t, r, base)
    if not _samesign(a, b):
        q = nneg(q, base)
    if _isneg(b):
        r = nneg(r, base)
    return q, r

# Test.

def _test(n, z, op, nozeroy=False):
    for x, y in itertools.product(z, repeat=2):
        if not nozeroy or y != 0:
            u = int(str(op(Num(x), Num(y))))
            v = op(x, y)
            assert u == v, f'{op}({x}, {y})\nExpected:{v}, Got:{u}'
    z = Num.base ** 3
    for _ in range(n):
        x = random.randint(-z, z)
        y = random.randint(-z, z)
        if not nozeroy or y != 0:
            u = int(str(op(Num(x), Num(y))))
            v = op(x, y)
            assert u == v, f'{op}({x}, {y})\nExpected:{v}, Got:{u}'

n = 1000
w = Num.base
z = (0, 1, -1, w, -w, w-1, 1-w)
_test(n, z, operator.add)
_test(n, z, operator.sub)
_test(n, z, operator.mul)
_test(n, z, operator.floordiv, True)
_test(n, z, operator.mod, True)

def show1(a):
    print(repr(a), a, sep='; ')

def _show(z, op, nozeroy=False):
    print(op)
    for x, y in itertools.product(z, repeat=2):
        if not nozeroy or y != 0:
            show1(op(Num(x), Num(y)))

w = Num.base
z = (0, 1, -1, w, -w)
_show(z, operator.add)
_show(z, operator.sub)
_show(z, operator.mul)
_show(z, operator.floordiv, True)
_show(z, operator.mod, True)

def factorial(n):
    r = Num(1)
    for i in range(2, n+1):
        r *= Num(i)
    return r

print(factorial)
for i in range(5):
    show1(factorial(10*i))