#!/usr/bin/env python3
"""Inventory an RTF file before translation, or compare a translated RTF with its source.

Usage:
  python3 check_rtf.py source.rtf
  python3 check_rtf.py source.rtf translated.rtf

Standard library only. It reads the RTF markup directly; it is a pre-flight
check, not a full RTF parser.
"""
import re
import sys

DESTINATIONS = {
    "footnote": "footnotes",
    "header": "headers", "headerl": "headers", "headerr": "headers", "headerf": "headers",
    "footer": "footers", "footerl": "footers", "footerr": "footers", "footerf": "footers",
    "pict": "images",
    "annotation": "comments",
}
SKIP_GROUPS = ("fonttbl", "colortbl", "stylesheet", "info", "listtable", "listoverridetable")


def read(path):
    data = open(path, "rb").read()
    return data.decode("latin-1")  # RTF is 7-bit; \'hh and \uN carry the rest


def markup_counts(rtf):
    words = re.findall(r"\\([a-zA-Z]+)(-?\d+)? ?", rtf)
    names = [w for w, _ in words]
    counts = {
        "par_breaks": len(re.findall(r"(?<!\\)(?:\\\\)*\\par\b", rtf)),
        "backslash_newline_breaks": len(re.findall(r"(?<!\\)(?:\\\\)*\\\r?\n", rtf)),
        "table_cells": names.count("cell"),
        "table_rows": names.count("row"),
        "hyperlinks": len(re.findall(r"HYPERLINK", rtf)),
        "fields": names.count("field"),
        "bold_on": sum(1 for w, p in words if w == "b" and p in ("", "1")),
        "italic_on": sum(1 for w, p in words if w == "i" and p in ("", "1")),
        "page_breaks": names.count("page"),
        "fonts": len(set(re.findall(r"\\f(\d+)\\f(?:roman|swiss|modern|script|decor|tech|nil)", rtf)))
                 or len(set(re.findall(r"\\f(\d+)", rtf))),
    }
    for word, label in DESTINATIONS.items():
        counts[label] = counts.get(label, 0) + names.count(word)
    return counts


def plain_text(rtf):
    """Rough visible text: drop header groups and destinations, decode escapes."""
    out, i, depth, skip_depth = [], 0, 0, None
    uc, skip_chars = 1, 0
    while i < len(rtf):
        ch = rtf[i]
        if ch == "{":
            depth += 1
            m = re.match(r"\{\\\*?\\?([a-zA-Z]+)", rtf[i:])
            if skip_depth is None and m and (m.group(1) in SKIP_GROUPS or rtf[i:i + 3] == "{\\*"
                                             or m.group(1) in ("pict", "fldinst")):
                skip_depth = depth
            i += 1
            continue
        if ch == "}":
            if skip_depth == depth:
                skip_depth = None
            depth -= 1
            i += 1
            continue
        if ch == "\\":
            m = re.match(r"\\([a-zA-Z]+)(-?\d+)? ?", rtf[i:])
            if m:
                i += len(m.group(0))
                word, param = m.group(1), m.group(2)
                if skip_depth is not None:
                    continue
                if word == "uc":
                    uc = int(param or 1)
                elif word == "u" and param:
                    n = int(param)
                    out.append(chr(n + 65536 if n < 0 else n))
                    skip_chars = uc
                elif word in ("par", "line", "row", "page"):
                    out.append("\n")
                elif word in ("cell", "tab"):
                    out.append("\t")
                continue
            nxt = rtf[i + 1:i + 2]
            if nxt == "'":
                if skip_depth is None:
                    hexpair = rtf[i + 2:i + 4]
                    if skip_chars:
                        skip_chars -= 1
                    elif re.fullmatch(r"[0-9a-fA-F]{2}", hexpair):
                        out.append(bytes([int(hexpair, 16)]).decode("cp1252", "replace"))
                i += 4
                continue
            if skip_depth is None:
                if nxt in ("\\", "{", "}"):
                    out.append(nxt)
                elif nxt in ("\n", "\r"):
                    out.append("\n")
            i += 2
            continue
        i += 1
        if skip_depth is not None or ch in "\r\n":
            continue
        if skip_chars:
            skip_chars -= 1
            continue
        out.append(ch)
    return "".join(out)


def risky_tokens(text):
    """Visible strings of 20+ hex characters (0-9, a-f): long reference numbers, hashes."""
    return sorted(set(re.findall(r"[0-9a-fA-F]{20,}", text)))


def report(path):
    rtf = read(path)
    if not rtf.startswith("{\\rtf"):
        print(f"{path}: does not start with {{\\rtf; not an RTF file")
        sys.exit(1)
    c = markup_counts(rtf)
    text = plain_text(rtf)
    paragraphs = [p for p in text.split("\n") if p.strip()]
    print(f"== {path}")
    for k, v in c.items():
        if v:
            print(f"  {k}: {v}")
    print(f"  visible_paragraphs: {len(paragraphs)}")
    return c, text, paragraphs


def warn_source(c, text):
    warnings = []
    if c["backslash_newline_breaks"] > c["par_breaks"]:
        warnings.append("Paragraphs end with a backslash + line break (TextEdit/Cocoa style), "
                        "not \\par. Tools that only split on \\par will see one long paragraph.")
    if c["table_cells"]:
        warnings.append(f"{c['table_cells']} table cells: check that cell boundaries survive.")
    for label in ("footnotes", "headers", "footers", "comments"):
        if c[label]:
            warnings.append(f"{c[label]} {label}: check where this text ends up in the output.")
    if c["hyperlinks"]:
        warnings.append(f"{c['hyperlinks']} hyperlinks: the link text may survive without the URL.")
    if c["images"]:
        warnings.append(f"{c['images']} images: keep the original for the artwork.")
    for tok in risky_tokens(text):
        warnings.append(f"Long hex-like string in the text: {tok} (confirm it is still there after translation).")
    return warnings


def main():
    if len(sys.argv) not in (2, 3):
        print(__doc__)
        sys.exit(2)
    c, text, paras = report(sys.argv[1])
    for w in warn_source(c, text):
        print("  WARN", w)
    if len(sys.argv) == 3:
        c2, text2, paras2 = report(sys.argv[2])
        print("== compare")
        for key in ("table_cells", "footnotes", "headers", "footers", "hyperlinks", "images",
                    "bold_on", "italic_on", "page_breaks"):
            if c[key] != c2[key]:
                print(f"  CHANGED {key}: {c[key]} -> {c2[key]}")
        if len(paras) != len(paras2):
            print(f"  CHANGED visible_paragraphs: {len(paras)} -> {len(paras2)}")
        for tok in risky_tokens(text):
            if tok not in text2:
                print(f"  MISSING {tok}")


if __name__ == "__main__":
    main()
