#!/usr/bin/env python3
"""anscat — display a CP437 .ans file in a modern UTF-8 terminal.

Usage: anscat file.ans [...]

Real .ans files (including ANSIpants exports since 16M b7 / 16 b6) are CP437
bytes, which a UTF-8 terminal can't `cat`. This strips the SAUCE record,
decodes CP437 to Unicode, wraps rows at the SAUCE width (the files carry no
newlines), and rewrites iCE blink-backgrounds as aixterm bright backgrounds
so they don't literally blink.
"""
import sys, re

# CP437 control-range glyphs (0x20-0x7E are ASCII, 0x80-0xFF via the codec).
# TAB/LF/CR/EOF/ESC keep their control meaning, as in the exporter.
LOW = {0:' ',1:'☺',2:'☻',3:'♥',4:'♦',5:'♣',6:'♠',7:'•',8:'◘',11:'♂',12:'♀',
       14:'♫',15:'☼',16:'►',17:'◄',18:'↕',19:'‼',20:'¶',21:'§',22:'▬',23:'↨',
       24:'↑',25:'↓',28:'∟',29:'↔',30:'▲',31:'▼',127:'⌂'}

def decode(body: bytes) -> str:
    # Old ANSIpants exports (pre 16M b7 / 16 b6) wrongly wrote UTF-8 bodies.
    # If the bytes are valid UTF-8 *and* actually use multi-byte sequences,
    # honor that; otherwise decode as CP437 like every classic renderer.
    if any(b >= 0x80 for b in body):
        try:
            return body.decode('utf-8')
        except UnicodeDecodeError:
            pass
    out = []
    for b in body:
        if b in LOW:               out.append(LOW[b])
        elif b < 0x80:             out.append(chr(b))
        else:                      out.append(bytes([b]).decode('cp437'))
    return ''.join(out)

def show(path):
    d = open(path, 'rb').read()
    width, ice = 80, False
    i = d.rfind(b'\x1aSAUCE00')
    if i >= 0:
        s = d[i+1:]
        width = int.from_bytes(s[96:98], 'little') or 80
        ice = len(s) >= 106 and bool(s[105] & 1)
        d = d[:i]
    text = decode(d)

    sgr = re.compile(r'\x1b\[([0-9;]*)m')
    out, col, pos = [], 0, 0
    for m in sgr.finditer(text):
        chunk = text[pos:m.start()]
        for ch in chunk:
            out.append(ch); col += 1
            if col == width:
                out.append('\x1b[0m\n'); col = 0
        p = [int(x) if x else 0 for x in (m.group(1) or '0').split(';')]
        if ice and 5 in p:  # iCE: blink means bright bg, not blinking
            p = [(x + 60 if 40 <= x <= 47 else x) for x in p if x != 5]
        out.append('\x1b[' + ';'.join(map(str, p)) + 'm')
        pos = m.end()
    for ch in text[pos:]:
        out.append(ch); col += 1
        if col == width:
            out.append('\x1b[0m\n'); col = 0
    out.append('\x1b[0m\n')
    sys.stdout.write(''.join(out))

if __name__ == '__main__':
    if len(sys.argv) < 2:
        sys.exit(__doc__.strip())
    for p in sys.argv[1:]:
        show(p)
