#!/usr/bin/env python3


## Help

"""
raspi-revcode 1.0

Parse Raspberry Pi revision codes.

Usage:
  raspi-revcode [<code>]
  raspi-revcode -h|--help

Arguments:
  <code>
    A hexadecimal revision code to parse. If not given, '/proc/cpuinfo' will be
    used to get the revision code of the current device.
"""


## Imports

import sys
import re
import docopt


## Constants

# https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#raspberry-pi-revision-codes
CPUINFO_PATH = '/proc/cpuinfo'
CPUINFO_RE   = r'^Revision\s*:\s*(.*)$'

# https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#old-style-revision-codes
# https://forums.raspberrypi.com//viewtopic.php?p=176865#p177014
OLD_STYLE_WARRANTY_BIT   = 24
OLD_STYLE_WARRANTY_FIELD = 'W'
OLD_STYLE = {
    'names': ['Type', 'Revision', 'Memory size', 'Manufacturer'],
    0x0002:  ['B',    '1.0',      '256MB',       'Egoman'      ],
    0x0003:  ['B',    '1.0',      '256MB',       'Egoman'      ],
    0x0004:  ['B',    '2.0',      '256MB',       'Sony UK'     ],
    0x0005:  ['B',    '2.0',      '256MB',       'Qisda'       ],
    0x0006:  ['B',    '2.0',      '256MB',       'Egoman'      ],
    0x0007:  ['A',    '2.0',      '256MB',       'Egoman'      ],
    0x0008:  ['A',    '2.0',      '256MB',       'Sony UK'     ],
    0x0009:  ['A',    '2.0',      '256MB',       'Qisda'       ],
    0x000d:  ['B',    '2.0',      '512MB',       'Egoman'      ],
    0x000e:  ['B',    '2.0',      '512MB',       'Sony UK'     ],
    0x000f:  ['B',    '2.0',      '512MB',       'Egoman'      ],
    0x0010:  ['B+',   '1.2',      '512MB',       'Sony UK'     ],
    0x0011:  ['CM1',  '1.0',      '512MB',       'Sony UK'     ],
    0x0012:  ['A+',   '1.1',      '256MB',       'Sony UK'     ],
    0x0013:  ['B+',   '1.2',      '512MB',       'Embest'      ],
    0x0014:  ['CM1',  '1.0',      '512MB',       'Embest'      ],
    0x0015:  ['A+',   '1.1',      '256MB/512MB', 'Embest'      ],
}

# https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#new-style-revision-codes
NEW_STYLE_BIT  = 23
NEW_STYLE_BITS = 'NOQuuuWuFMMMCCCCPPPPTTTTTTTTRRRR'
NEW_STYLE = {
    'N': ['Overvoltage', {
        0x0: 'Overvoltage allowed',
        0x1: 'Overvoltage disallowed',
    }],
    'O': ['OTP Program', {
        0x0: 'OTP programming allowed',
        0x1: 'OTP programming disallowed',
    }],
    'Q': ['OTP Read', {
        0x0: 'OTP reading allowed',
        0x1: 'OTP reading disallowed',
    }],
    # 'uuu': ['Unused', {
    #     u: 'Unused' for u in range(2**len('uuu'))
    # }],
    'W': ['Warranty bit', {
        0x0: 'Warranty is intact',
        0x1: 'Warranty has been voided by overclocking',
    }],
    # 'u': ['Unused', {
    #     u: 'Unused' for u in range(2**len('u'))
    # }],
    'F': ['New flag', {
        0x1: 'New-style revision',
        0x0: 'Old-style revision',
    }],
    'MMM': ['Memory size', {
        0x0: '256MB',
        0x1: '512MB',
        0x2: '1GB',
        0x3: '2GB',
        0x4: '4GB',
        0x5: '8GB',
    }],
    'CCCC': ['Manufacturer', {
        0x0: 'Sony UK',
        0x1: 'Egoman',
        0x2: 'Embest',
        0x3: 'Sony Japan',
        0x4: 'Embest',
        0x5: 'Stadium',
    }],
    'PPPP': ['Processor', {
        0x0: 'BCM2835',
        0x1: 'BCM2836',
        0x2: 'BCM2837',
        0x3: 'BCM2711',
        0x4: 'BCM2712',
    }],
    'TTTTTTTT': ['Type', {
        0x0:  'A',
        0x1:  'B',
        0x2:  'A+',
        0x3:  'B+',
        0x4:  '2B',
        0x5:  'Alpha (early prototype)',
        0x6:  'CM1',
        0x8:  '3B',
        0x9:  'Zero',
        0xa:  'CM3',
        0xc:  'Zero W',
        0xd:  '3B+',
        0xe:  '3A+',
        0xf:  'Internal use only',
        0x10: 'CM3+',
        0x11: '4B',
        0x12: 'Zero 2 W',
        0x13: '400',
        0x14: 'CM4',
        0x15: 'CM4S',
        0x16: 'Internal use only',
        0x17: '5',
    }],
    'RRRR': ['Revision', {
        rev: f'1.{rev}' for rev in range(2**len('RRRR'))
    }],
}

## Error

def error(msg):
    print(msg, file=sys.stderr)
    exit(1)

## Main

def main():
    args = docopt.docopt(__doc__)
    code = args.get('<code>')
    try:
        if not code:
            with open(CPUINFO_PATH) as file:
                match = re.search(CPUINFO_RE, file.read(), re.MULTILINE)
                if not match:
                    error(f"Could not find revision code in '{CPUINFO_PATH}'.")
                code = match.group(1)
        code = int(code, 16)
    except IOError:
        error(f"Could not open '{CPUINFO_PATH}'.")
    except ValueError:
        error(f"Code '{code}' is not hexadecimal number.")
    if (code >> NEW_STYLE_BIT) & 0x1:
        width = max(1 + len(name) for _, (name, _) in NEW_STYLE.items())
        for field, _ in re.findall(r'((.)\2*)', NEW_STYLE_BITS[::-1]):
            length = len(field)
            value  = code & ((1<<length)-1)
            code   = code >> length
            info   = NEW_STYLE.get(field)
            if info:
                name = info[0]
                desc = info[1].get(value, '<UNKNOWN>')
                print(f"{name:{width}}: {desc}")
    else:
        width    = max(1 + len(name) for name in OLD_STYLE['names'])
        warranty = code &  (1<<OLD_STYLE_WARRANTY_BIT)
        code     = code & ~(1<<OLD_STYLE_WARRANTY_BIT)
        width    = max(width, 1 + len(NEW_STYLE[OLD_STYLE_WARRANTY_FIELD][0]))
        info     = OLD_STYLE.get(code)
        if info:
            for name, desc in zip(OLD_STYLE['names'], info):
                print(f"{name:{width}}: {desc}")
            info = NEW_STYLE[OLD_STYLE_WARRANTY_FIELD]
            name = info[0]
            desc = info[1].get(bool(warranty))
            print(f"{name:{width}}: {desc}")


if __name__ == '__main__':
    main()