from math import ceil

def dither(r, g, b, /, log=lambda *x: None, results={}):
    log('initial r', r, 'g', g, 'b', b)
    swapRB = False
    swapRG = False
    swapGB = False
    # Map the RGB values into tetrahedral space 0, with r >= g >= b
    # This is "ComputeSymmetry" in the patent code.
    if r < b:
        swapRB = True
        r, b = b, r
    if g < b:
        swapGB = True
        g, b = b, g
    if r < g:
        swapRG = True
        r, g = g, r

    log('swapRB', swapRB, 'swapGB', swapGB, 'swapRG', swapRG)
    log('swapped r', r, 'g', g, 'b', b)
    results['swapRB'] = swapRB
    results['swapGB'] = swapGB
    results['swapRG'] = swapRG

    
    # Find which tetrahedral subspace of space 0 this RGB value is in
    # These are defined by the following IBGR coordinates:
    #   0: 0, 1, 3, 7
    #   1: 1, 3, 7, 9
    #   2: 3, 7, 9, 11
    #   3: 7, 9, 11, 15
    if r < 128:
        subspace = 0
    elif r + g < 256:
        subspace = 1
    elif r + b < 256:
        subspace = 2
    else:
        subspace = 3

    log('subspace', subspace)
    results['subspace'] = subspace

    # Scale RGB values into 0-64 range
    r = ceil(r / 2) // 2
    g = ceil(g / 2) // 2
    b = ceil(b / 2) // 2

    log('scaled r', r, 'g', g, 'b', b)
    results['scaledR'] = r
    results['scaledG'] = g
    results['scaledB'] = b

    # We now have three things to do according to each subspace:
    # * Transform coordinates according to its origin. Subspace 3 uses
    #   (64, 0, 0), and the others (32, 32, 0)
    # * Determine a number of pixels for each colour based on how close
    #   they are to the defining vertex.
    # * Choose four specific colour values for the next step.
    # Each subspace is defined by four IBGR colour coordinates:
    # 0: 3 0  1  7
    # 1: 3 1  9  7
    # 2: 3 9 11  7
    # 3: 9 7 11 15
    # These are mapped back into the full set of colours later.
    match subspace:
        case 0:
            c1 = (r - 32) * -2
            c2 = (r - 32) *  2 - 2 * (g - 32)
            c3 = b *  2
            [v1, v2, v3, v4] = [3, 0, 1, 7]
        case 1:
            c1 = (r - 32) * -2 - (g - 32) * 2
            c2 = (r - 32) *  2
            c3 = b *  2
            [v1, v2, v3, v4] = [3, 1, 9, 7]
        case 2:
            c1 = (r - 32) - (g - 32)
            c2 = (r - 32) + (g - 32)
            c3 = b *  2
            [v1, v2, v3, v4] = [3, 9, 11, 7]
        case 3:
            c1 = (r - 64) * -2
            c2 = g - b
            c3 = (r - 64) + b
            [v1, v2, v3, v4] = [9, 7, 11, 15]

    log('c1', c1, 'c2', c2, 'c3', c3)
    log('v1', v1, 'v2', v2, 'v3', v3, 'v4', v4)

    results['c1'] = c1
    results['c2'] = c2
    results['c3'] = c3

    results['v1'] = v1
    results['v1'] = v2
    results['v1'] = v3
    results['v1'] = v4

    table = {}

    if c1 + c2 + c3 != 64:
        table[v1] = 64 - c1 - c2 - c3
    if c1 != 0:
        table[v2] = c1
    if c2 != 0:
        table[v3] = c2
    if c3 != 0:
        table[v4] = c3

    results['countTable'] = table


    # Translate the pseudo-colours back to real ones.
    # This swaps bits of the pseudo-colour according to the
    # swapRG/GB/RB values determined earlier:
    # R is the low bit, G the 2, B the 4.
    colourCounts = {}
    for pseudo, count in table.items():
        R = bool(pseudo & 1)
        G = bool(pseudo & 2)
        B = bool(pseudo & 4)
        I = bool(pseudo & 8)
        if swapRG:
            R, G = G, R
        if swapGB:
            G, B = B, G
        if swapRB:
            R, B = B, R
        colour = R + G * 2 + B * 4 + I * 8
        colourCounts[colour] = count

    results['realColourTable'] = colourCounts

    # Sort the table by a predetermined "intensity" score
    # The colour scores are:
    # [1, 3, 4, 7, 2, 5, 6, 8, -1, 10, 11, 14, 9, 12, 13, 15]
    # So they sort into this order:
    # 0, 4, 1, 2, 5, 6, 3, 7, 12, 9, 10, 13, 14, 11, 15
    # (colour 8 is never produced)
    orderedColourCounts = {}
    for i in [0, 4, 1, 2, 5, 6, 3, 7, 12, 9, 10, 13, 14, 11, 15]:
        if i in colourCounts:
            orderedColourCounts[i] = colourCounts[i]
    results['orderedColourTable'] = orderedColourCounts

    # Make the bitmap using a predefined Bayer matrix
    # The pixels are filled in the order of that matrix,
    # with the least-intense colour put in place first.
    xyorder = [
            (0, 0), (4, 4), (4, 0), (0, 4), (2, 2), (6, 6), (6, 2), (2, 6),
            (2, 0), (6, 4), (6, 0), (2, 4), (0, 2), (4, 6), (4, 2), (0, 6),
            (1, 1), (5, 5), (5, 1), (1, 5), (3, 3), (7, 7), (7, 3), (3, 7),
            (3, 1), (7, 5), (7, 1), (3, 5), (1, 3), (5, 7), (5, 3), (1, 7),
            (1, 0), (5, 4), (5, 0), (1, 4), (3, 2), (7, 6), (7, 2), (3, 6),
            (3, 0), (7, 4), (7, 0), (3, 4), (1, 2), (5, 6), (5, 2), (1, 6),
            (0, 1), (4, 5), (4, 1), (0, 5), (2, 3), (6, 7), (6, 3), (2, 7),
            (2, 1), (6, 5), (6, 1), (2, 5), (0, 3), (4, 7), (4, 3), (0, 7)
        ]
    pattern = [[None] * 8 for _ in range(8)]
    pos = 0
    for colour, count in orderedColourCounts.items():
        for i in range(count):
            x, y = xyorder[pos + i]
            pattern[y][x] = colour
        pos += count
    return pattern

# result = dither(128, 31, 190)
result = dither(35, 160, 59, log=print)
print(result)

for row in result:
    print(' '.join(f'{x:3d}' for x in row))

import random

found = {}
while True:
    r = random.randint(0, 255)
    g = random.randint(0, 255)
    b = random.randint(0, 255)
    out = {}
    dither(r, g, b, results=out)
    tup = (out['swapRB'], out['swapGB'], out['swapRG'], out['subspace'])
    if tup not in found:
        found[tup] = (r, g, b)
    if len(found) == 24:
        break

for tup in sorted(found):
    if False:
        if tup[0]:
            print('+', end='')
        else:
            print(' ', end='')
        if tup[1]:
            print('+', end='')
        else:
            print(' ', end='')
        if tup[2]:
            print('+', end='')
        else:
            print(' ', end='')
        print(tup[3], end='\t')
        r, g, b = found[tup]
        print(f'{r}\t{g}\t{b}')
    else:
        print('  <tr><td>', end='')
        if tup[0]:
            print('Y', end='')
        print('</td><td>', end='')
        if tup[1]:
            print('Y', end='')
        print('</td><td>', end='')
        if tup[2]:
            print('Y', end='')
        print('</td><td>', end='')
        print(tup[3], end='')
        print('</td>', end='')
        r, g, b = found[tup]
        rgb = '#' + hex(r)[2:].rjust(2, '0') + hex(g)[2:].rjust(2, '0') + hex(b)[2:].rjust(2, '0')
        print(f'<td>{r}</td><td>{g}</td><td>{b}</td><td><button style="background-color:{rgb}; height: 1.2em; width: 1.2em;" onclick="useColour(\'{rgb}\')"></button></td></tr>')


