#!/usr/bin/env python3
from pathlib import Path
import re, sys

SKIP_DESTINATIONS = {
    'fonttbl','colortbl','stylesheet','info','pict','object','datastore','themedata','generator',
    'xmlnstbl','listtable','listoverridetable','rsidtbl','filetbl','header','footer','footnote',
    'annotation','revtbl','latentstyles','fldinst'
}

SPECIAL_CHARS = {
    'par': '\n', 'line': '\n', 'tab': '\t', 'emdash': '—', 'endash': '–',
    'emspace': ' ', 'enspace': ' ', 'qmspace': ' ', 'bullet': '•',
    'lquote': '‘', 'rquote': '’', 'ldblquote': '“', 'rdblquote': '”'
}

def rtf_to_text(data: bytes, encoding='cp1251') -> str:
    # Work bytewise so \'xx escapes retain original cp1251 bytes.
    out = []
    stack = []
    skip_depth = None
    ignorable_next_dest = False
    i = 0
    depth = 0
    n = len(data)
    while i < n:
        c = data[i]
        ch = chr(c)
        if ch == '{':
            depth += 1
            stack.append((skip_depth, ignorable_next_dest))
            ignorable_next_dest = False
            i += 1
            continue
        if ch == '}':
            if skip_depth == depth:
                skip_depth = None
            if stack:
                prev_skip, prev_ign = stack.pop()
                if skip_depth is None:
                    skip_depth = prev_skip
                ignorable_next_dest = prev_ign
            depth -= 1
            i += 1
            continue
        if ch == '\\':
            i += 1
            if i >= n: break
            nc = chr(data[i])
            # escaped literal
            if nc in '\\{}':
                if skip_depth is None:
                    out.append(nc)
                i += 1
                continue
            if nc == '*':
                ignorable_next_dest = True
                i += 1
                continue
            if nc == "'" and i+2 < n:
                hx = data[i+1:i+3]
                try:
                    b = bytes([int(hx.decode('ascii'), 16)])
                    if skip_depth is None:
                        out.append(b.decode(encoding, errors='replace'))
                except Exception:
                    pass
                i += 3
                continue
            # control word or symbol
            m = re.match(rb'([A-Za-z]+)(-?\d+)? ?', data[i:])
            if m:
                word = m.group(1).decode('ascii', errors='ignore')
                arg = m.group(2).decode('ascii', errors='ignore') if m.group(2) else None
                i += len(m.group(0))
                if word in SKIP_DESTINATIONS or (ignorable_next_dest and word not in SPECIAL_CHARS):
                    skip_depth = depth
                ignorable_next_dest = False
                if skip_depth is None:
                    if word in SPECIAL_CHARS:
                        out.append(SPECIAL_CHARS[word])
                    elif word == 'u' and arg is not None:
                        val = int(arg)
                        if val < 0: val += 65536
                        out.append(chr(val))
                continue
            else:
                # one-char control symbol
                if skip_depth is None and nc in '~_-':
                    out.append(' ' if nc == '~' else '')
                i += 1
                continue
        # ordinary char (usually RTF syntax ASCII; Cyrillic normally hex escaped)
        if skip_depth is None and c not in b'\r\n':
            try:
                out.append(bytes([c]).decode(encoding, errors='replace'))
            except Exception:
                pass
        i += 1
    text = ''.join(out)
    # cleanup: remove style/control detritus, normalize whitespace
    text = text.replace('\xa0',' ')
    text = re.sub(r'[ \t]+\n', '\n', text)
    text = re.sub(r'\n[ \t]+', '\n', text)
    text = re.sub(r'\n{3,}', '\n\n', text)
    text = re.sub(r'[ \t]{2,}', ' ', text)
    return text.strip()

if __name__ == '__main__':
    inp = Path(sys.argv[1])
    out = Path(sys.argv[2])
    text = rtf_to_text(inp.read_bytes())
    out.write_text(text, encoding='utf-8')
    print(f'wrote {out} chars={len(text)} lines={text.count(chr(10))+1}')
