"""
RTF content extractor.

Parses RTF (Rich Text Format) files and extracts text, metadata, fonts,
colors, styles, headers/footers, hyperlinks, images, footnotes, and more.
"""

import io
import logging
import re
from typing import Any, Generator, List, Optional

from sharepoint2text.extractors.data_types import (
    RtfAnnotation,
    RtfBookmark,
    RtfColor,
    RtfContent,
    RtfField,
    RtfFont,
    RtfFootnote,
    RtfHeaderFooter,
    RtfHyperlink,
    RtfImage,
    RtfMetadata,
    RtfParagraph,
    RtfStyle,
)

logger = logging.getLogger(__name__)


class _RtfParser:
    """Low-level RTF parser that tokenizes and processes RTF content."""

    # RTF special characters mapping
    SPECIAL_CHARS = {
        "par": "\n",
        "line": "\n",
        "tab": "\t",
        "lquote": "'",
        "rquote": "'",
        "ldblquote": '"',
        "rdblquote": '"',
        "bullet": "•",
        "endash": "–",
        "emdash": "—",
        "~": "\u00a0",  # Non-breaking space
        "_": "\u00ad",  # Optional hyphen
        "-": "\u00ad",  # Optional hyphen
        "enspace": "\u2002",
        "emspace": "\u2003",
        "qmspace": "\u2005",
    }

    # Unicode escapes for common characters
    UNICODE_MAP = {
        0x2018: "'",  # Left single quote
        0x2019: "'",  # Right single quote
        0x201C: '"',  # Left double quote
        0x201D: '"',  # Right double quote
        0x2013: "–",  # En dash
        0x2014: "—",  # Em dash
        0x2026: "...",  # Ellipsis
    }

    def __init__(self, data: bytes):
        self.data = data
        self.pos = 0
        self.length = len(data)

        # Extracted data
        self.fonts: List[RtfFont] = []
        self.colors: List[RtfColor] = []
        self.styles: List[RtfStyle] = []
        self.metadata = RtfMetadata()
        self.paragraphs: List[RtfParagraph] = []
        self.headers: List[RtfHeaderFooter] = []
        self.footers: List[RtfHeaderFooter] = []
        self.hyperlinks: List[RtfHyperlink] = []
        self.bookmarks: List[RtfBookmark] = []
        self.fields: List[RtfField] = []
        self.images: List[RtfImage] = []
        self.footnotes: List[RtfFootnote] = []
        self.annotations: List[RtfAnnotation] = []
        self.pages: List[str] = []
        self.raw_text_blocks: List[str] = []

        # Parsing state
        self._current_text: List[str] = []
        self._current_para = RtfParagraph()
        self._in_header_footer: Optional[str] = None
        self._in_footnote = False
        self._in_field = False
        self._field_instruction = ""
        self._field_result = ""
        self._skip_destination = False
        self._group_depth = 0
        self._skip_until_depth = -1
        self._current_hyperlink_url = ""

    def parse(self) -> RtfContent:
        """Parse the RTF document and return extracted content."""
        try:
            # Decode the raw bytes to string
            text = self._decode_rtf()

            # Check RTF signature
            if not text.startswith("{\\rtf"):
                logger.warning("Not a valid RTF file - missing RTF header")
                return RtfContent(full_text=text)

            # Extract various components
            self._extract_fonts(text)
            self._extract_colors(text)
            self._extract_styles(text)
            self._extract_metadata(text)
            self._extract_headers_footers(text)
            self._extract_body_text(text)
            self._extract_hyperlinks(text)
            self._extract_fields(text)
            self._extract_images(text)
            self._extract_footnotes(text)

            # Build full text
            full_text = "\n".join(p.text for p in self.paragraphs if p.text.strip())

            return RtfContent(
                metadata=self.metadata,
                fonts=self.fonts,
                colors=self.colors,
                styles=self.styles,
                paragraphs=self.paragraphs,
                headers=self.headers,
                footers=self.footers,
                hyperlinks=self.hyperlinks,
                bookmarks=self.bookmarks,
                fields=self.fields,
                images=self.images,
                footnotes=self.footnotes,
                annotations=self.annotations,
                pages=self.pages,
                full_text=full_text,
                raw_text_blocks=self.raw_text_blocks,
            )
        except Exception as e:
            logger.error(f"RTF parsing failed: {e}")
            # Return basic content with raw text if parsing fails
            try:
                text = self._decode_rtf()
                plain = self._strip_rtf_simple(text)
                return RtfContent(full_text=plain)
            except Exception:
                return RtfContent()

    def _decode_rtf(self) -> str:
        """Decode RTF bytes to string, handling various encodings."""
        # Try UTF-8 first, then fall back to cp1252 (Windows-1252)
        for encoding in ["utf-8", "cp1252", "latin-1"]:
            try:
                return self.data.decode(encoding)
            except UnicodeDecodeError:
                continue
        # Last resort: decode with error handling
        return self.data.decode("cp1252", errors="replace")

    def _extract_fonts(self, text: str) -> None:
        """Extract font table from RTF."""
        # Find the font table
        fonttbl_match = re.search(r"\{\\fonttbl(.*?)\}", text, re.DOTALL)
        if not fonttbl_match:
            return

        fonttbl = fonttbl_match.group(1)

        # Parse individual font definitions
        # Pattern: {\f<id>\f<family>\fcharset<charset> <name>;}
        font_pattern = re.compile(
            r"\{\\f(\d+)"  # Font ID
            r"(?:\\fbidi\s*)?"  # Optional bidi
            r"\\f(\w+)"  # Font family
            r"(?:\\fcharset(\d+))?"  # Optional charset
            r"(?:\\fprq(\d+))?"  # Optional pitch
            r"(?:\{[^}]*\})?"  # Optional panose
            r"\s*([^;}]*)"  # Font name
            r";?\}",
            re.IGNORECASE,
        )

        for match in font_pattern.finditer(fonttbl):
            font_id = int(match.group(1))
            font_family = match.group(2) or ""
            charset = int(match.group(3)) if match.group(3) else 0
            pitch = int(match.group(4)) if match.group(4) else 0
            font_name = match.group(5).strip()

            self.fonts.append(
                RtfFont(
                    font_id=font_id,
                    font_family=font_family,
                    font_name=font_name,
                    charset=charset,
                    pitch=pitch,
                )
            )

    def _extract_colors(self, text: str) -> None:
        """Extract color table from RTF."""
        colortbl_match = re.search(r"\{\\colortbl\s*;?(.*?)\}", text, re.DOTALL)
        if not colortbl_match:
            return

        colortbl = colortbl_match.group(1)

        # Parse colors: \red<n>\green<n>\blue<n>;
        color_pattern = re.compile(r"\\red(\d+)\\green(\d+)\\blue(\d+)", re.IGNORECASE)

        idx = 0
        for match in color_pattern.finditer(colortbl):
            self.colors.append(
                RtfColor(
                    index=idx,
                    red=int(match.group(1)),
                    green=int(match.group(2)),
                    blue=int(match.group(3)),
                )
            )
            idx += 1

    def _extract_styles(self, text: str) -> None:
        """Extract stylesheet from RTF."""
        stylesheet_match = re.search(
            r"\{\\stylesheet(.*?)\}\s*(?=\{|\\)", text, re.DOTALL
        )
        if not stylesheet_match:
            return

        stylesheet = stylesheet_match.group(1)

        # Parse style definitions
        # Pattern: {\s<id>...<style name>;}
        style_pattern = re.compile(
            r"\{(?:\\[*]?)?\\s(\d+)"  # Style ID
            r"[^}]*?"  # Style properties
            r"(?:\\sbasedon(\d+))?"  # Based on
            r"(?:\\snext(\d+))?"  # Next style
            r"[^}]*?"
            r"\s+([^;}]+)"  # Style name
            r";?\}",
            re.IGNORECASE,
        )

        for match in style_pattern.finditer(stylesheet):
            style_id = int(match.group(1))
            based_on = int(match.group(2)) if match.group(2) else None
            next_style = int(match.group(3)) if match.group(3) else None
            style_name = match.group(4).strip()

            # Determine style type
            style_type = "paragraph"
            if "\\cs" in match.group(0):
                style_type = "character"
            elif "\\ts" in match.group(0):
                style_type = "table"

            self.styles.append(
                RtfStyle(
                    style_id=style_id,
                    style_type=style_type,
                    style_name=style_name,
                    based_on=based_on,
                    next_style=next_style,
                )
            )

    def _extract_metadata(self, text: str) -> None:
        """Extract document info/metadata from RTF."""
        info_match = re.search(
            r"\{\\info\s*(.*?)\}(?=\s*\\|\s*\{[^\\])", text, re.DOTALL
        )
        if not info_match:
            # Try alternate pattern
            info_match = re.search(r"\{\\info\s*((?:\{[^}]*\}\s*)+)\}", text, re.DOTALL)

        if not info_match:
            return

        info = info_match.group(1)

        # Extract individual metadata fields
        def get_info_value(pattern: str) -> str:
            match = re.search(pattern, info, re.IGNORECASE | re.DOTALL)
            if match:
                value = match.group(1).strip()
                # Handle hex escapes first (before removing control sequences)
                value = re.sub(
                    r"\\'([0-9a-fA-F]{2})",
                    lambda m: chr(int(m.group(1), 16)),
                    value,
                )
                # Handle non-breaking space
                value = value.replace("\\~", " ")
                # Remove RTF control sequences from the value
                value = re.sub(r"\\[a-z]+\d*\s*", "", value)
                # Remove braces
                value = value.replace("{", "").replace("}", "")
                return value.strip()
            return ""

        self.metadata.title = get_info_value(r"\{\\title\s+([^}]*)\}")
        self.metadata.subject = get_info_value(r"\{\\subject\s+([^}]*)\}")
        self.metadata.author = get_info_value(r"\{\\author\s+([^}]*)\}")
        self.metadata.keywords = get_info_value(r"\{\\keywords\s+([^}]*)\}")
        self.metadata.comments = get_info_value(r"\{\\comment\s+([^}]*)\}")
        self.metadata.operator = get_info_value(r"\{\\operator\s+([^}]*)\}")
        self.metadata.category = get_info_value(r"\{\\[*]?\\?category\s+([^}]*)\}")
        self.metadata.manager = get_info_value(r"\{\\manager\s+([^}]*)\}")
        self.metadata.company = get_info_value(r"\{\\company\s+([^}]*)\}")
        self.metadata.doc_comment = get_info_value(r"\{\\doccomm\s+([^}]*)\}")

        # Extract numeric values
        version_match = re.search(r"\{\\version(\d+)\}", info)
        if version_match:
            self.metadata.version = int(version_match.group(1))

        revision_match = re.search(r"\{\\vern(\d+)\}", info)
        if revision_match:
            self.metadata.revision = int(revision_match.group(1))

        nofpages_match = re.search(r"\{\\nofpages(\d+)\}", info)
        if nofpages_match:
            self.metadata.num_pages = int(nofpages_match.group(1))

        nofwords_match = re.search(r"\{\\nofwords(\d+)\}", info)
        if nofwords_match:
            self.metadata.num_words = int(nofwords_match.group(1))

        nofchars_match = re.search(r"\{\\nofchars(\d+)\}", info)
        if nofchars_match:
            self.metadata.num_chars = int(nofchars_match.group(1))

        nofcharsws_match = re.search(r"\{\\nofcharsws(\d+)\}", info)
        if nofcharsws_match:
            self.metadata.num_chars_with_spaces = int(nofcharsws_match.group(1))

        # Extract dates
        def parse_rtf_date(pattern: str) -> str:
            match = re.search(pattern, info, re.IGNORECASE)
            if match:
                date_str = match.group(1)
                yr_match = re.search(r"\\yr(\d+)", date_str)
                mo_match = re.search(r"\\mo(\d+)", date_str)
                dy_match = re.search(r"\\dy(\d+)", date_str)
                hr_match = re.search(r"\\hr(\d+)", date_str)
                min_match = re.search(r"\\min(\d+)", date_str)

                if yr_match and mo_match and dy_match:
                    year = int(yr_match.group(1))
                    month = int(mo_match.group(1))
                    day = int(dy_match.group(1))
                    hour = int(hr_match.group(1)) if hr_match else 0
                    minute = int(min_match.group(1)) if min_match else 0

                    return (
                        f"{year:04d}-{month:02d}-{day:02d}T{hour:02d}:{minute:02d}:00"
                    )
            return ""

        self.metadata.created = parse_rtf_date(r"\{\\creatim([^}]*)\}")
        self.metadata.modified = parse_rtf_date(r"\{\\revtim([^}]*)\}")

    def _extract_headers_footers(self, text: str) -> None:
        """Extract headers and footers from RTF."""
        header_types = [
            ("header", "header"),
            ("headerl", "headerl"),  # Left page header
            ("headerr", "headerr"),  # Right page header
            ("headerf", "headerf"),  # First page header
            ("footer", "footer"),
            ("footerl", "footerl"),  # Left page footer
            ("footerr", "footerr"),  # Right page footer
            ("footerf", "footerf"),  # First page footer
        ]

        for rtf_keyword, hf_type in header_types:
            pattern = re.compile(
                r"\{\\" + rtf_keyword + r"\s+(.*?)\}(?=\s*\{|\s*\\)",
                re.DOTALL | re.IGNORECASE,
            )

            for match in pattern.finditer(text):
                content = match.group(1)
                extracted_text = self._strip_rtf_simple(content)

                if extracted_text.strip():
                    hf = RtfHeaderFooter(type=hf_type, text=extracted_text.strip())
                    if "header" in hf_type:
                        self.headers.append(hf)
                    else:
                        self.footers.append(hf)

    def _extract_body_text(self, text: str) -> None:
        """Extract body text from RTF, building paragraphs and pages.

        Pages are split on explicit \\page breaks. If no page breaks exist,
        the entire document is treated as a single page.
        """
        # Remove destination groups we don't want in the body
        destinations_to_remove = [
            r"\{\\fonttbl[^{}]*(?:\{[^{}]*\}[^{}]*)*\}",
            r"\{\\colortbl[^{}]*\}",
            r"\{\\stylesheet[^{}]*(?:\{[^{}]*\}[^{}]*)*\}",
            r"\{\\info[^{}]*(?:\{[^{}]*\}[^{}]*)*\}",
            r"\{\\[*]?\\?header[lrf]?\s+[^{}]*(?:\{[^{}]*\}[^{}]*)*\}",
            r"\{\\[*]?\\?footer[lrf]?\s+[^{}]*(?:\{[^{}]*\}[^{}]*)*\}",
            r"\{\\\*\\[a-z]+[^{}]*(?:\{[^{}]*\}[^{}]*)*\}",  # Destination groups
        ]

        body = text
        for pattern in destinations_to_remove:
            body = re.sub(pattern, "", body, flags=re.DOTALL | re.IGNORECASE)

        # Now extract text from the body with page break detection
        extracted = self._strip_rtf_full_with_pages(body)

        # Split into paragraphs (for backward compatibility)
        para_texts = re.split(r"\n+", extracted)
        for para_text in para_texts:
            cleaned = para_text.strip()
            if cleaned:
                self.paragraphs.append(RtfParagraph(text=cleaned))
                self.raw_text_blocks.append(cleaned)

    def _extract_hyperlinks(self, text: str) -> None:
        """Extract hyperlinks from RTF."""
        # Pattern for HYPERLINK fields
        hyperlink_pattern = re.compile(
            r'\\field\s*\{[^}]*\\fldinst\s*\{[^}]*HYPERLINK\s+"([^"]+)"[^}]*\}[^}]*\{[^}]*\\fldrslt\s*\{([^}]*)\}',
            re.IGNORECASE | re.DOTALL,
        )

        for match in hyperlink_pattern.finditer(text):
            url = match.group(1).strip()
            link_text = self._strip_rtf_simple(match.group(2)).strip()

            if url:
                self.hyperlinks.append(RtfHyperlink(text=link_text, url=url))

    def _extract_fields(self, text: str) -> None:
        """Extract fields (page numbers, dates, etc.) from RTF."""
        field_pattern = re.compile(
            r"\\field\s*\{[^}]*\\fldinst\s*\{([^}]*)\}[^}]*\{[^}]*\\fldrslt\s*\{([^}]*)\}",
            re.IGNORECASE | re.DOTALL,
        )

        for match in field_pattern.finditer(text):
            instruction = self._strip_rtf_simple(match.group(1)).strip()
            result = self._strip_rtf_simple(match.group(2)).strip()

            # Skip hyperlinks (handled separately)
            if "HYPERLINK" in instruction.upper():
                continue

            # Determine field type
            field_type = "unknown"
            if "PAGE" in instruction.upper():
                field_type = "page"
            elif "DATE" in instruction.upper():
                field_type = "date"
            elif "TIME" in instruction.upper():
                field_type = "time"
            elif "STYLEREF" in instruction.upper():
                field_type = "styleref"
            elif "TOC" in instruction.upper():
                field_type = "toc"

            self.fields.append(
                RtfField(
                    field_type=field_type,
                    field_instruction=instruction,
                    field_result=result,
                )
            )

    def _extract_images(self, text: str) -> None:
        """Extract embedded images from RTF."""
        # Pattern for picture groups
        pict_pattern = re.compile(
            r"\{\\pict([^}]*(?:\{[^}]*\}[^}]*)*)\}", re.DOTALL | re.IGNORECASE
        )

        for match in pict_pattern.finditer(text):
            pict_content = match.group(1)

            # Determine image type
            image_type = "unknown"
            if "\\pngblip" in pict_content:
                image_type = "png"
            elif "\\jpegblip" in pict_content:
                image_type = "jpeg"
            elif "\\emfblip" in pict_content:
                image_type = "emf"
            elif "\\wmetafile" in pict_content:
                image_type = "wmf"

            # Extract dimensions
            width = 0
            height = 0
            width_match = re.search(r"\\picw(\d+)", pict_content)
            height_match = re.search(r"\\pich(\d+)", pict_content)
            if width_match:
                width = int(width_match.group(1))
            if height_match:
                height = int(height_match.group(1))

            # Extract hex data (simplified - just note that there's image data)
            hex_data_match = re.search(r"([0-9a-fA-F]{20,})", pict_content)
            data = None
            if hex_data_match:
                try:
                    hex_str = hex_data_match.group(1)
                    data = bytes.fromhex(hex_str)
                except ValueError:
                    pass

            self.images.append(
                RtfImage(
                    image_type=image_type,
                    width=width,
                    height=height,
                    data=data,
                )
            )

    def _extract_footnotes(self, text: str) -> None:
        """Extract footnotes from RTF."""
        # Pattern for footnotes
        footnote_pattern = re.compile(
            r"\{\\footnote\s*([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}",
            re.DOTALL | re.IGNORECASE,
        )

        footnote_id = 1
        for match in footnote_pattern.finditer(text):
            content = match.group(1)
            footnote_text = self._strip_rtf_simple(content).strip()

            if footnote_text:
                self.footnotes.append(RtfFootnote(id=footnote_id, text=footnote_text))
                footnote_id += 1

    def _strip_rtf_simple(self, text: str) -> str:
        """Simple RTF stripping - removes control words and groups."""
        result = text

        # Handle unicode escapes \u<decimal>?
        result = re.sub(
            r"\\u(-?\d+)\??",
            lambda m: chr(int(m.group(1)) & 0xFFFF),
            result,
        )

        # Handle hex escapes \'xx
        result = re.sub(
            r"\\'([0-9a-fA-F]{2})",
            lambda m: chr(int(m.group(1), 16)),
            result,
        )

        # Replace special RTF characters
        for keyword, char in self.SPECIAL_CHARS.items():
            result = re.sub(r"\\" + re.escape(keyword) + r"(?:\s|$)", char, result)

        # Remove control words with parameters
        result = re.sub(r"\\[a-z]+(-?\d+)?\s?", "", result, flags=re.IGNORECASE)

        # Remove braces
        result = result.replace("{", "").replace("}", "")

        # Normalize whitespace
        result = re.sub(r"[ \t]+", " ", result)
        result = re.sub(r"\n{3,}", "\n\n", result)

        return result.strip()

    def _strip_rtf_full(self, text: str) -> str:
        """Full RTF stripping with proper group handling."""
        result = []
        i = 0
        n = len(text)
        group_depth = 0
        skip_group = False
        skip_depth = 0

        while i < n:
            char = text[i]

            if char == "{":
                group_depth += 1
                # Check if this is a destination to skip
                if i + 1 < n and text[i + 1] == "\\":
                    # Look ahead for destination markers
                    ahead = text[i + 1 : i + 30]
                    if ahead.startswith("\\*") or any(
                        ahead.startswith("\\" + kw)
                        for kw in [
                            "fonttbl",
                            "colortbl",
                            "stylesheet",
                            "info",
                            "pict",
                            "object",
                            "datafield",
                            "fldinst",
                            "ftnsep",
                            "ftnsepc",
                            "aftnsep",
                            "aftnsepc",
                            "header",
                            "footer",
                            "headerl",
                            "headerr",
                            "headerf",
                            "footerl",
                            "footerr",
                            "footerf",
                            "pnseclvl",
                            "xmlnstbl",
                            "rsidtbl",
                            "mmathPr",
                            "generator",
                            "listtable",
                            "listoverridetable",
                            "revtbl",
                        ]
                    ):
                        skip_group = True
                        skip_depth = group_depth
                i += 1

            elif char == "}":
                if skip_group and group_depth == skip_depth:
                    skip_group = False
                group_depth -= 1
                i += 1

            elif skip_group:
                i += 1

            elif char == "\\":
                # Control word or escape
                if i + 1 >= n:
                    i += 1
                    continue

                next_char = text[i + 1]

                # Escape sequences
                if next_char in "\\{}":
                    result.append(next_char)
                    i += 2

                # Unicode escape
                elif next_char == "u":
                    unicode_match = re.match(r"\\u(-?\d+)\??", text[i:])
                    if unicode_match:
                        code = int(unicode_match.group(1))
                        result.append(chr(code & 0xFFFF))
                        i += len(unicode_match.group(0))
                    else:
                        i += 2

                # Hex escape
                elif next_char == "'":
                    if i + 3 < n:
                        hex_val = text[i + 2 : i + 4]
                        try:
                            result.append(chr(int(hex_val, 16)))
                        except ValueError:
                            pass
                        i += 4
                    else:
                        i += 2

                # Control word
                elif next_char.isalpha():
                    # Find end of control word
                    j = i + 1
                    while j < n and text[j].isalpha():
                        j += 1
                    # Skip optional numeric parameter
                    if j < n and (text[j].isdigit() or text[j] == "-"):
                        while j < n and (text[j].isdigit() or text[j] == "-"):
                            j += 1
                    # Skip optional trailing space
                    if j < n and text[j] == " ":
                        j += 1

                    control_word = text[i + 1 : j].rstrip()
                    # Remove numeric suffix for lookup
                    word_only = re.sub(r"-?\d+$", "", control_word)

                    if word_only in self.SPECIAL_CHARS:
                        result.append(self.SPECIAL_CHARS[word_only])

                    i = j

                else:
                    # Handle \~ (non-breaking space), \- (optional hyphen), etc.
                    if next_char == "~":
                        result.append("\u00a0")
                    elif next_char == "-":
                        pass  # Optional hyphen - don't add
                    elif next_char == "_":
                        result.append("\u00ad")
                    i += 2

            else:
                # Regular character
                if char not in "\r":  # Skip carriage returns
                    result.append(char)
                i += 1

        return "".join(result)

    def _strip_rtf_full_with_pages(self, text: str) -> str:
        """Full RTF stripping with page break detection.

        Detects \\page control words and splits content into pages.
        Populates self.pages with text content per page.
        Returns the full text as a single string.
        """
        result = []
        current_page = []
        i = 0
        n = len(text)
        group_depth = 0
        skip_group = False
        skip_depth = 0

        def flush_page():
            """Save current page content and start a new page."""
            page_text = "".join(current_page).strip()
            # Normalize whitespace
            page_text = re.sub(r"[ \t]+", " ", page_text)
            page_text = re.sub(r"\n{3,}", "\n\n", page_text)
            if page_text:
                self.pages.append(page_text)
            current_page.clear()

        while i < n:
            char = text[i]

            if char == "{":
                group_depth += 1
                # Check if this is a destination to skip
                if i + 1 < n and text[i + 1] == "\\":
                    # Look ahead for destination markers
                    ahead = text[i + 1 : i + 30]
                    if ahead.startswith("\\*") or any(
                        ahead.startswith("\\" + kw)
                        for kw in [
                            "fonttbl",
                            "colortbl",
                            "stylesheet",
                            "info",
                            "pict",
                            "object",
                            "datafield",
                            "fldinst",
                            "ftnsep",
                            "ftnsepc",
                            "aftnsep",
                            "aftnsepc",
                            "header",
                            "footer",
                            "headerl",
                            "headerr",
                            "headerf",
                            "footerl",
                            "footerr",
                            "footerf",
                            "pnseclvl",
                            "xmlnstbl",
                            "rsidtbl",
                            "mmathPr",
                            "generator",
                            "listtable",
                            "listoverridetable",
                            "revtbl",
                        ]
                    ):
                        skip_group = True
                        skip_depth = group_depth
                i += 1

            elif char == "}":
                if skip_group and group_depth == skip_depth:
                    skip_group = False
                group_depth -= 1
                i += 1

            elif skip_group:
                i += 1

            elif char == "\\":
                # Control word or escape
                if i + 1 >= n:
                    i += 1
                    continue

                next_char = text[i + 1]

                # Escape sequences
                if next_char in "\\{}":
                    current_page.append(next_char)
                    result.append(next_char)
                    i += 2

                # Unicode escape
                elif next_char == "u":
                    unicode_match = re.match(r"\\u(-?\d+)\??", text[i:])
                    if unicode_match:
                        code = int(unicode_match.group(1))
                        char_val = chr(code & 0xFFFF)
                        current_page.append(char_val)
                        result.append(char_val)
                        i += len(unicode_match.group(0))
                    else:
                        i += 2

                # Hex escape
                elif next_char == "'":
                    if i + 3 < n:
                        hex_val = text[i + 2 : i + 4]
                        try:
                            char_val = chr(int(hex_val, 16))
                            current_page.append(char_val)
                            result.append(char_val)
                        except ValueError:
                            pass
                        i += 4
                    else:
                        i += 2

                # Control word
                elif next_char.isalpha():
                    # Find end of control word
                    j = i + 1
                    while j < n and text[j].isalpha():
                        j += 1
                    # Skip optional numeric parameter
                    if j < n and (text[j].isdigit() or text[j] == "-"):
                        while j < n and (text[j].isdigit() or text[j] == "-"):
                            j += 1
                    # Skip optional trailing space
                    if j < n and text[j] == " ":
                        j += 1

                    control_word = text[i + 1 : j].rstrip()
                    # Remove numeric suffix for lookup
                    word_only = re.sub(r"-?\d+$", "", control_word)

                    # Check for page break
                    # \page is explicit page break
                    # \sect with \sbkpage is section break with page break
                    if word_only == "page" or word_only == "sbkpage":
                        flush_page()
                    elif word_only in self.SPECIAL_CHARS:
                        char_val = self.SPECIAL_CHARS[word_only]
                        current_page.append(char_val)
                        result.append(char_val)

                    i = j

                else:
                    # Handle \~ (non-breaking space), \- (optional hyphen), etc.
                    if next_char == "~":
                        current_page.append("\u00a0")
                        result.append("\u00a0")
                    elif next_char == "-":
                        pass  # Optional hyphen - don't add
                    elif next_char == "_":
                        current_page.append("\u00ad")
                        result.append("\u00ad")
                    i += 2

            else:
                # Regular character
                if char not in "\r":  # Skip carriage returns
                    current_page.append(char)
                    result.append(char)
                i += 1

        # Flush the last page
        flush_page()

        # If no page breaks were found, treat the entire document as one page
        if not self.pages:
            full_text = "".join(result).strip()
            full_text = re.sub(r"[ \t]+", " ", full_text)
            full_text = re.sub(r"\n{3,}", "\n\n", full_text)
            if full_text:
                self.pages.append(full_text)

        return "".join(result)


def read_rtf(
    file_like: io.BytesIO, path: str | None = None
) -> Generator[RtfContent, Any, None]:
    """
    Extract all relevant content from an RTF file.

    Args:
        file_like: A BytesIO object containing the RTF file data.
        path: Optional file path to populate file metadata fields.

    Yields:
        RtfContent dataclass with all extracted content.
    """
    logger.debug("Reading RTF file")
    file_like.seek(0)
    data = file_like.read()

    parser = _RtfParser(data)
    content = parser.parse()

    content.metadata.populate_from_path(path)

    yield content
