# @file bignum.002.py
# @ingroup experimental
# Experimental big-number representation.
# @date 11/22/2024

import itertools
import operator
import random

class Num:
    base = 2**8

    def __init__(self, init):
        if isinstance(init, tuple):
            self.m = init[0]
            self.s = init[1]
        else:
            self.m = iton(abs(init), Num.base)
            self.s = init < 0

    def __neg__(self):
        return Num(_fixsign(self.m.copy(), not self.s))

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

    def __sub__(self, other):
        t = _fixsign(other.m, not other.s)[1]
        return Num(nadd(self.m, self.s, other.m, t, Num.base))

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

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

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

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

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

# Utility.

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

def _fixsign(a, s):
    return a, s and not _iszero(a)

def _zeroextend(a, n):
    return a[:] + [0] * (n - len(a))

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

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

# Convert.

def iton(x, b):
    r = []
    while True:
        x, y = divmod(x, b)
        r.append(y)
        if x == 0:
            return r

def nton(a, bi, bo):
    r = [0]
    for x in reversed(a):
        c = _inplace_mul1(r, bi, x, bo)[0]
        if c != 0:
            r.extend(iton(c, bo))
    return r

def ntos(a, b):
    return ''.join(map(str, reversed(nton(a, b, 10))))

# 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 _zeroreduce([0] * n + a[:])

def nshr(a, n):
    return _zeroextend(a[n:], 1)

# Add.

def _add(a, b, base, op):
    n = 1 + max(len(a), len(b))
    r = _zeroextend(a, n)
    b = _zeroextend(b, n)
    c = 0
    for i in range(n):
        c, r[i] = divmod(op(r[i], b[i] + abs(c)), base)
    return _zeroreduce(r)

def nadd(a, s, b, t, base):
    if s == t:
        op = operator.add
    else:
        op = operator.sub
        if ncmp(a, b) < 0:
            a, b, s = b, a, t
    return _fixsign(_add(a, b, base, op), s)

# Multiply.

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

def _mul(a, b, base):
    if _iszero(b):
        return [0]
    return _add(_mul1(a, b[0], base), _mul(nshl(a, 1), nshr(b, 1), base),
                base, operator.add)

def nmul(a, s, b, t, base):
    return _fixsign(_mul(a, b, base), s != t)

# Divide.

def _div2(a, b):
    if len(a) < len(b):
        q, r = [0], a
    else:
        q, r = _div2(a, nshl(b, 1))
        q = nshl(q, 1)
        if ncmp(r, b) >= 0:
            q = _add(q, [1], 2, operator.add)
            r = _add(r, b, 2, operator.sub)
    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, s, b, t, base):
    if _iszero(b):
        raise ZeroDivisionError
    q, r = _div(a, b, base)
    if s != t and not _iszero(r):
        q = _add(q, [1], base, operator.add)
        r = _add(b, r, base, operator.sub)
    return _fixsign(q, s != t), _fixsign(r, t)

# 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))