#!/usr/bin/env python
#
# extract metadata from uxn rom files.
#
# see https://wiki.xxiivv.com/site/manifest.html
#
# (also converts the icon to a monochrome BMP file.)

from sys import argv, exit

# convert two bytes into an address, subtracting for zero page
def toaddr(x, y):
    return x * 256 + y - 256

# parse null-terminated string
def parsestr(data, start):
    end = start + data[start:].find(b'\0')
    return end + 1, data[start:end].decode('ascii')

# emit 24-bit integer (big endian)
def emit24(n):
    return bytes([(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff])

# convert 64x64 icn to bmp
# since we are always dealing with a 1-bit 64x64
# image we can just hardcode the BMP header data
def tobmp(icn, light, dark):
    bmp =  b"BM"               #  0: bitmap file
    bmp += b"\x20\x10\x00\x00" #  2: file size: 4128 (32 + 4096)
    bmp += b"\x00\x00"         #  6: reserved: 0
    bmp += b"\x00\x00"         #  8: reserved: 0
    bmp += b"\x20\x00\x00\x00" # 10: pixel data offset: 32
    bmp += b"\x0c\x00\x00\x00" # 14: header size: 12
    bmp += b"\x40\x00"         # 18: width in pixels: 64
    bmp += b"\x40\x00"         # 20: height in pixels: 64
    bmp += b"\x01\x00"         # 22: color planes: 1
    bmp += b"\x01\x00"         # 24: bits per pixel: 1
    bmp += emit24(light)       # 26: color 0
    bmp += emit24(dark)        # 29: color 1
                               # 32: start of pixel data

    # ICN is composed of a grid of 8x8 tiles. for BMP we need
    # to create 64 rows of 64 pixels each.
    rows = []
    for offset in range(0, 512, 64):
        for y in range(0, 8):
            rows.append([])
            for x in range(0, 64, 8):
                rows[-1].append(icn[offset + y + x])
    for row in reversed(rows):
        bmp += bytes(row)
    return bmp

# read the icon from the given binary data and address
def parseicon(path, data, pos0, light, dark):
    if len(data[pos0:]) < 512:
        print('needed 512 bytes, only had %d' % len(data[pos0:]))
    icn = data[pos0:pos0+512]
    bmp = tobmp(icn, light, dark)
    return bmp

# try to parse LIT2 x y .System/[rgb] DEO2
# returns (shift, tint)
# shift 16=red, 8=green, 0=blue
def parsecolor(data, pos0):
    a,b,c,d,e,f = data[pos0:pos0+6]
    if a == 0xa0 and d == 0x80 and f == 0x37:
        tint = b * 256 + c
        # we only care about 'b' i.e. the first two colors
        if e == 0x08: return (16, tint)
        if e == 0x0a: return (8, tint)
        if e == 0x0c: return (0, tint)
    return None

# parse the metadata from the given binary data and address
def parsemeta(path, data, pos0):
    a,b,c,d = data[pos0:pos0+4]
    endaddr = toaddr(a, b)
    iconaddr = toaddr(c, d)
    pos1, name = parsestr(data, pos0 + 4)
    pos2, version = parsestr(data, pos1)
    pos3, desc = parsestr(data, pos2)
    pos4, author = parsestr(data, pos3)
    if pos4 != endaddr:
        print('%s: data ended by %04x but expected %04x' % (path, pos4, endaddr))
    meta = {'name': name, 'version': version, 'desc': desc, 'author': author, 'icon': iconaddr}
    return meta

# parse metadata from the given uxn rom
def parse(path, data):
    if len(data) < 6:
        print("%s: file too small (%d bytes)" % (path, len(data)))
        exit(1)

    a,x,y,d,e,f = data[:6]
    if a != 0xa0 or d != 0x80 or e != 0xf0 or f != 0x37:
        print('%s: no metadata detected' % path)
        exit(1)

    return parsemeta(path, data, toaddr(x, y))

# go for it!
def main():
    if len(argv[1:]) != 1:
        print('usage: %s ROMFILE' % argv[0])
        exit(1)

    path = argv[1]
    f = open(path, 'rb')
    data = f.read()
    f.close()

    meta = parse(path, data)

    i = 6
    colors = [0, 0, 0, 0]
    while True:
        res = parsecolor(data, i)
        i += 6
        if res is None:
            break
        shift, tint = res
        colors[0] |= ((((tint >> 12) & 0xf) * 0x11) << shift)
        colors[1] |= ((((tint >>  8) & 0xf) * 0x11) << shift)
        colors[2] |= ((((tint >>  4) & 0xf) * 0x11) << shift)
        colors[3] |= ((((tint >>  0) & 0xf) * 0x11) << shift)

    if colors == [0, 0, 0, 0]:
        dark, light = 0x000000, 0xffffff
    else:
        dark, light = colors[0], colors[1]

    iconpath = path + '.bmp'
    bmp = parseicon(path, data, meta['icon'], light, dark)
    with open(iconpath, 'wb') as f:
        f.write(bmp)

    print('%s:' % path)
    print('  Name:        %s' % meta['name'])
    print('  Version:     %s' % meta['version'])
    print('  Description: %s' % meta['desc'])
    print('  Author:      %s' % meta['author'])
    print('')
    print('  Icon:        %s' % iconpath)
    print('')

if __name__ == "__main__":
    main()