from PIL import Image, ImageDraw, ImageFont, ImageColor, ImageOps
import os
import numpy as np
import re
from shoggoth.files import font_dir, icon_dir, overlay_dir
from shoggoth.i18n import tr

#ImageDraw.fontmode = 'L'
image_regex = re.compile(r'<image(\s\w+=\".+?\"){1,}?>', flags=re.IGNORECASE)
size_regex = re.compile(r'<size (\d+?)>', flags=re.IGNORECASE)
margin_regex = re.compile(r'<margin (\d+?)(\s\d+?)*>', flags=re.IGNORECASE)
tag_value_pattern = re.compile(r'(\w+)\s*=\s*"([^"]*)"')


def parse_tag_attributes(tag_string):
    # This regex finds key="value" pairs
    return dict(re.findall(tag_value_pattern, tag_string))


def recolor_icon(icon, color):
    data = np.array(icon)
    red, green, blue, alpha = data.T
    black_areas = (red == 0) & (blue == 0) & (green == 0)
    data[..., :-1][black_areas.T] = ImageColor.getcolor(color, "RGB") # Transpose back needed
    return Image.fromarray(data)


def invert_icon(icon):
    alpha = icon.getchannel('A')
    icon = icon.convert('RGB')
    icon = ImageOps.invert(icon)
    icon.putalpha(alpha)
    return icon


class RichTextRenderer:
    def __init__(self, card_renderer):
        self.card_renderer = card_renderer
        self.icon_cache = {}  # Cache for loaded icons
        self.font_cache = {}  # Cache for loaded fonts by size

        # Add alignment configuration
        self.alignment = 'left'  # Default alignment: 'left', 'center', 'right'
        self.min_font_size = 10  # Minimum font size to use when reducing

        # Define special formatting tags and their replacements
        self.formatting_tags = {
            '<b>': {'start': True, 'font': 'bold'},
            '</b>': {'start': False, 'font': 'bold'},
            '<i>': {'start': True, 'font': 'italic'},
            '</i>': {'start': False, 'font': 'italic'},
            '<bi>': {'start': True, 'font': 'bolditalic'},
            '</bi>': {'start': False, 'font': 'bolditalic'},
            '<t>': {'start': True, 'font': 'bolditalic'},
            '[[': {'start': True, 'font': 'bolditalic'},
            '</t>': {'start': False, 'font': 'bolditalic'},
            ']]': {'start': False, 'font': 'bolditalic'},
            '<icon>': {'start': True, 'font': 'icon'},
            '</icon>': {'start': False, 'font': 'icon'},
            '<center>': {'start': True, 'align': 'center'},
            '</center>': {'start': False, 'align': 'center'},
            '<left>': {'start': True, 'align': 'left'},
            '</left>': {'start': False, 'align': 'left'},
            '<right>': {'start': True, 'align': 'right'},
            '</right>': {'start': False, 'align': 'right'},
            '<story>': {'start': True, 'indent': 4 },
            '</story>': {'start': False, 'indent': 4 },
            '<blockquote>': {'start': True, 'format': 'quote', 'block': True},
            '</blockquote>': {'start': False, 'format': 'quote', 'block': True},
        }

        # Replacement tags - tags that render as different text when encountered
        self.replacement_tags = {
            '<for>': '<b>Forced –</b>',
            '<prey>': '<b>Prey –</b>',
            '<rev>': '<b>Revelation –</b>',
            '<spawn>': '<b>Spawn –</b>',
            '<obj>': '<b>Objective –</b>',
            '<objective>': '<b>Objective –</b>',
            '<quote>': '‘',
            '<dquote>': '“',
            '<quoteend>': '’',
            '<dquoteend>': '”',
            '\'': '’',
            '---': '—',
            '--': '–',
        }

        # Define font-based icon tags and their corresponding characters
        self.font_icon_tags = {
            '<codex>': '#',
            '<star>': '*',
            '<dash>': '-',
            '<sign_1>': '1',
            '<sign_2>': '2',
            '<sign_3>': '3',
            '<sign_4>': '4',
            '<sign_5>': '5',
            '<question>': '?',

            '<tablet>': 'A',
            '<entry>': 'B',
            '<cultist>': 'C',
            '<blessing>': 'D',
            '<elder_sign>': 'E',
            '<fleur>': 'F',
            '<guardian>': 'G',
            '<frost>': 'H',
            '<seeker>': 'K',
            '<elder_thing>': 'L',
            '<mystic>': 'M',
            '<rogue>': 'R',
            '<skull>': 'S',
            '<auto_fail>': 'T',
            '<curse>': 'U',
            '<survivor>': 'V',

            '<agility>': 'a',
            '<agi>': 'a',
            '[agility]': 'a',
            '<bullet>': 'b',
            '<com>': 'c',
            '<combat>': 'c',
            '[combat]': 'c',
            '<horror>': 'd',
            '<resolution>': 'e',
            '<free>': 'f',
            '[fast]': 'f',
            '<damage>': 'h',
            '<intellect>': 'i',
            '[intellect]': 'i',
            '<int>': 'i',
            '<resource>': 'm',
            '<act>': 'n',
            '<action>': 'n',
            '[action]': 'n',
            '<open>': 'o',
            '<per>': 'p',
            '[per_investigator]': 'p',
            '<reaction>': 'r',
            '<unique>': 'u',
            '<willpower>': 'w',
            '[willpower]': 'w',

            '<day>': '<',
            '<night>': '>',
        }

        # Font configurations
        self.fonts = {
            'regular': {
                'path': font_dir / "Arno Pro" / "arnopro_regular.otf",
                'scale': 1,
                'fallback': None
            },
            'caption': {
                'path': font_dir / "Arno Pro" / "arnopro_caption.otf",
                'scale': 1,
                'fallback': None
            },
            'bold': {
                'path': font_dir / "Arno Pro" / "arnopro_bold.otf",
                'scale': 1,
                'fallback': None
            },
            'semibold': {
                'path': font_dir / "Arno Pro" / "arnopro_semibold.ttf",
                'scale': 1,
                'fallback': None
            },
            'italic': {
                'path': font_dir / "Arno Pro" / "arnopro_italic.otf",
                'scale': 1,
                'fallback': None
            },
            'bolditalic': {
                'path': font_dir / "Arno Pro" / "arnopro_bolditalic.otf",
                'scale': 1,
                'fallback': None
            },
            'icon': {
                'path': font_dir / "AHLCGSymbol.otf",
                'scale': 1,
                'fallback': None
            },
            'cost': {
                'path': font_dir / "Arkhamic.ttf",
                'scale': 1,
                'fallback': None
            },
            'title': {
                'path': font_dir / "Arkhamic.ttf",
                'scale': 1,
                'fallback': None
            },
            'skill': {
                'path': font_dir / "Bolton.ttf",
                'scale': 1,
                'fallback': None
            }
        }

    def get_help_text(self):
        """ Returns a helpful text """
        text = tr("HELP_SPECIAL_TAGS_INTRO") + "\n\n"
        text += tr("HELP_FORMATTING_TAGS") + "\n"
        for tag, options in self.formatting_tags.items():
            if options['start']:
                text += f'{tag}{options.get("font") or options.get("align")}'
            else:
                text += f'{tag}\n'

        text += "\n" + tr("HELP_REPLACEMENT_TAGS") + "\n"
        for tag, result in self.replacement_tags.items():
            text += f"{tag} = {result}\n"

        text += "\n" + tr("HELP_ICON_TAGS") + "\n"
        for tag in self.font_icon_tags:
            text += f"{tag}\n"

        text += "\n" + tr("HELP_AVAILABLE_FONTS") + "\n"
        for tag, options in self.fonts.items():
            text += f"{tag}: {options['path']}\n"
        return text


    def load_fonts(self, size):
        """Load all fonts at the specified size, with caching"""
        if size in self.font_cache:
            return self.font_cache[size]

        loaded_fonts = {}
        for font_type, font_info in self.fonts.items():
            loaded_fonts[font_type] = ImageFont.truetype(font_info['path'], size)

        self.font_cache[size] = loaded_fonts
        return loaded_fonts

    def load_icon(self, icon_path, height):
        """Load an image icon and resize to match text height"""
        if str(icon_path)[-4:] == '.svg':
            return self.card_renderer.get_resized_cached(icon_path, (height, height))

        if (icon_path, height) in self.icon_cache:
            return self.icon_cache[(icon_path, height)]

        if os.path.exists(icon_path):
            full_path = icon_path
        else:
            full_path = icon_dir / f"{icon_path}.png"
            if not os.path.exists(full_path):
                return None

        try:
            height *= 1
            icon = Image.open(full_path).convert("RGBA")
            # Maintain aspect ratio
            aspect_ratio = icon.width / icon.height
            new_width = int(height * aspect_ratio)
            icon = icon.resize((new_width, height), Image.LANCZOS)

            # Cache the icon
            self.icon_cache[(icon_path, height)] = icon
            return icon
        except Exception:
            return None

    def parse_text(self, text):
        """Parse rich text into tokens with preserved whitespace"""
        tokens = []
        current_pos = 0

        # replacement tags
        for tag, new in self.replacement_tags.items():
            text = text.replace(tag, new)

        # Find all special tags or icons
        while current_pos < len(text):
            # Check for formatting tags
            format_match = False
            if text[current_pos] in ('<', '[', ']'):
                for tag, info in self.formatting_tags.items():
                    if text[current_pos:].startswith(tag):
                        if 'font' in info:
                            tokens.append({
                                'type': 'format',
                                'value': info['font'],
                                'start': info['start']
                            })
                            current_pos += len(tag)
                            format_match = True
                            break
                        elif 'align' in info:
                            tokens.append({
                                'type': 'align',
                                'value': info['align'],
                                'start': info['start']
                            })
                            current_pos += len(tag)
                            format_match = True
                            break
                        elif 'indent' in info:
                            tokens.append({
                                'type': 'indent',
                                'value': info['indent'],
                                'start': info['start']
                            })
                            current_pos += len(tag)
                            format_match = True
                            break
                        elif 'format' in info:
                            tokens.append({
                                'type': 'story',
                                'value': info['format'],
                                'start': info['start']
                            })
                            current_pos += len(tag)
                            format_match = True
                            break

            if format_match:
                continue

            # Check for font-based icons
            font_icon_match = False
            for tag, char in self.font_icon_tags.items():
                if text[current_pos:].startswith(tag):
                    tokens.append({
                        'type': 'font_icon',
                        'value': char
                    })
                    current_pos += len(tag)
                    font_icon_match = True
                    break

            if font_icon_match:
                continue

            # size tag
            if match := size_regex.match(text[current_pos:]):
                tag = match[0]
                size = match[1]
                tokens.append({
                    'type': 'size',
                    'value': size,
                })
                current_pos += len(tag)
                continue

            # margin tag
            if match := margin_regex.match(text[current_pos:]):
                tag = match[0]
                margin_top = int(match[1])
                #margin_left = match[2]
                tokens.append({
                    'type': 'margin',
                    'value': margin_top,
                })
                current_pos += len(tag)
                continue

            # image tag
            if match := image_regex.match(text[current_pos:]):
                tag = match[0]
                attributes = parse_tag_attributes(tag)
                if 'src' in attributes:
                    tokens.append({
                        'type': 'image_icon',
                        'value': attributes.get('src'),
                    })
                current_pos += len(tag)
                continue

            # Check for newlines
            if text[current_pos:].startswith('\n'):
                tokens.append({
                    'type': 'newline'
                })
                current_pos += 1
                continue

            # Check for whitespace
            if text[current_pos] == ' ':
                tokens.append({
                    'type': 'text',
                    'value': ' ',
                })
                current_pos += 1
                continue

            # Regular text - find the next special character or tag
            next_special = len(text)

            # Find the next special tag
            all_tags = (
                list(self.formatting_tags.keys()) +
                list(self.font_icon_tags.keys()) +
                [' ', '\n']
            )

            for tag in all_tags:
                pos = text.find(tag, current_pos)
                if pos != -1 and pos < next_special:
                    next_special = pos

            # Add text token
            text_content = text[current_pos:next_special]
            if text_content:
                tokens.append({
                    'type': 'text',
                    'value': text_content
                })

            current_pos = next_special

        return tokens

    def _measure_fits(self, tokens, region, polygon, font_size, base_font='regular'):
        """
        Fast check if text fits at given font size. Returns (fits: bool, percentage: float).
        percentage indicates how much of the tokens were processed before overflow.
        """
        fonts = self.load_fonts(font_size)
        line_height = int(font_size * 1.30)

        x, y = region.x, region.y
        max_width = region.width
        max_height = region.height

        current_font = base_font
        font_stack = []
        current_line_width = 0

        def polygon_width_at_y(target_y, polygon_points):
            x_intersections = []
            for i in range(len(polygon_points) - 1):
                (x1, y1), (x2, y2) = polygon_points[i], polygon_points[i + 1]
                if (y1 < target_y and y2 < target_y) or (y1 > target_y and y2 > target_y) or y1 == y2:
                    continue
                t = (target_y - y1) / (y2 - y1)
                x_val = x1 + t * (x2 - x1)
                x_intersections.append(x_val)
            if not x_intersections:
                return region.x, region.x + region.width
            return min(x_intersections), max(x_intersections)

        if polygon:
            left, right = polygon_width_at_y(y, polygon)
            max_width = right - left

        def get_token_width(token, font_name):
            if token['type'] == 'text':
                return fonts[font_name].getlength(token['value'])
            elif token['type'] == 'font_icon':
                return fonts['icon'].getlength(token['value'])
            elif token['type'] == 'image_icon':
                icon = self.load_icon(token['value'], font_size)
                return icon.width if icon else 0
            return 0

        for i, token in enumerate(tokens):
            if token['type'] == 'format':
                if token['start']:
                    font_stack.append(current_font)
                    current_font = token['value']
                else:
                    current_font = font_stack.pop() if font_stack else base_font

            elif token['type'] == 'margin':
                y += token['value']

            elif token['type'] == 'story':
                if token['start']:
                    font_stack.append(current_font)
                    current_font = 'italic'
                else:
                    current_font = font_stack.pop() if font_stack else base_font

            elif token['type'] == 'newline':
                y += line_height
                current_line_width = 0
                if polygon:
                    left, right = polygon_width_at_y(y, polygon)
                    max_width = right - left
                if y + line_height > region.y + max_height:
                    return False, i / len(tokens)

            elif token['type'] in ('text', 'font_icon', 'image_icon'):
                token_width = get_token_width(token, current_font)

                if current_line_width + token_width > max_width:
                    # Wrap to next line
                    y += line_height
                    current_line_width = token_width
                    if polygon:
                        left, right = polygon_width_at_y(y, polygon)
                        max_width = right - left
                    if y + line_height > region.y + max_height:
                        return False, i / len(tokens)
                else:
                    current_line_width += token_width

        # Check final line
        if y + line_height > region.y + max_height:
            return False, 1.0

        return True, 1.0

    def render_text(self, image, text, region, polygon=None, alignment='left', font_size=32, min_font_size=None, font=None, outline=0, outline_fill=None, fill='#231f20'):
        """
        Render rich text with specified alignment and automatic font size reduction.

        Parameters:
            image: PIL Image to draw on
            text: The rich text string to render
            region: Dictionary with x, y, width, height of text region
            polygon: Optional list of points defining text boundary
            alignment: Text alignment - 'left', 'center', or 'right'
            font_size: Initial font size
            min_font_size: Minimum font size when auto-reducing
            font: The initial font
        """

        if not text:
            return
        # Parse the rich text
        tokens = self.parse_text(text)

        # Try rendering with progressively smaller font sizes until it fits
        current_size = font_size  # Start with default size

        if not font:
            font = 'regular'

        if min_font_size is None:
            min_font_size = font_size // 2

        # Test each font size to find the largest that fits
        while current_size >= min_font_size:
            force = current_size == min_font_size

            # Use fast measurement to check if text fits
            fits, percentage = self._measure_fits(tokens, region, polygon, current_size, base_font=font)
            if text == 'ÉVÈNEMENT':
                print('ÉVÈNEMENT', fits, percentage, current_size, min_font_size)

            if fits or force:
                # Render directly - single pass
                self._render_with_font_size(image, tokens, region, polygon, current_size, font=font, force=force, outline=outline, outline_fill=outline_fill, fill=fill, alignment=alignment)
                break

            # If a lot of the text is left, we skip a few font sizes down.
            current_size -= 1
            if 0 < percentage < .8:
                current_size -= 1
            if 0 < percentage < .5:
                current_size -= 1
            if 0 < percentage < .3:
                current_size -= 1


    def _render_with_font_size(self, image, tokens, region, polygon, font_size, font='regular', force=False, outline=0, outline_fill=None, fill='#231f20', alignment='left'):
        """
        Render text at the specified font size. Called after _measure_fits confirms text fits.
        """
        draw = ImageDraw.Draw(image)

        # Load fonts at the current size
        fonts = self.load_fonts(font_size)

        # Current position and state
        x, y = region.x, region.y
        max_width = region.width
        max_height = region.height

        # Current formatting state
        current_font = font
        font_stack = []  # Stack to track nested font changes
        current_alignment = alignment
        current_indent = 0
        indent_current = False
        alignment_stack = []
        quote = False
        quote_last = False

        # some run-on-line states
        previous_bullet = 0
        bullet_indent = False

        # Word wrapping data
        current_line = []
        line_height = int(font_size * 1.30)  # Line height with spacing
        current_line_width = 0

        # Keep track of whether we're going to overflow
        will_overflow = False

        def polygon_width_at_y(target_y, polygon_points):
            x_intersections = []

            for i in range(len(polygon_points) - 1):
                (x1, y1), (x2, y2) = polygon_points[i], polygon_points[i + 1]

                # Skip horizontal edges or those that don't straddle target_y
                if (y1 < target_y and y2 < target_y) or (y1 > target_y and y2 > target_y) or y1 == y2:
                    continue

                # Linear interpolation to find x at target_y
                t = (target_y - y1) / (y2 - y1)
                x_val = x1 + t * (x2 - x1)
                x_intersections.append(x_val)

            if not x_intersections:
                return region.x, region.x+region.width  # target_y is outside the vertical range of the polygon

            left = min(x_intersections)
            right = max(x_intersections)
            return left, right

        if polygon:
            left, right = polygon_width_at_y(y, polygon)
            max_width = right-left
            x = left

        # Polygon check function (same as before)
        def is_point_in_polygon(point, polygon_points):
            """Check if a point is inside a polygon"""
            if polygon is None:
                return True

            x, y = point
            n = len(polygon_points)
            inside = False

            p1x, p1y = polygon_points[0]
            for i in range(1, n + 1):
                p2x, p2y = polygon_points[i % n]
                if y > min(p1y, p2y):
                    if y <= max(p1y, p2y):
                        if x <= max(p1x, p2x):
                            if p1y != p2y:
                                xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
                            if p1x == p2x or x <= xinters:
                                inside = not inside
                p1x, p1y = p2x, p2y

            return inside

        def get_token_width(token):
            """Calculate the width of a token"""
            if token['type'] == 'text':
                x = draw.textlength(token['value'], font=fonts[token['font']])
                return x
            elif token['type'] == 'font_icon':
                x = draw.textlength(token['value'], font=fonts['icon'])
                return x
            elif token['type'] == 'image_icon':
                icon = self.load_icon(token['value'], font_size)
                return icon.width//1 if icon else 0
            return 0

        def get_line_width(line):
            """Calculate the total width of a line"""
            return sum(get_token_width(t) for t in line if t['type'] != 'format')

        def get_line_start_x(line, y):
            """Calculate starting X position based on alignment"""
            line_width = get_line_width(line)
            if polygon:
                left, right = polygon_width_at_y(y, polygon)
                if current_alignment == 'center':
                    return left + ((right-left) - line_width) // 2
                elif current_alignment == 'right':
                    return left + (right-left) - line_width
                else:
                    return left

            if current_alignment == 'center':
                return x + (max_width - line_width) // 2
            elif current_alignment == 'right':
                return x + max_width - line_width
            else:  # left alignment
                return x

        def render_line(line, y_pos, run_on_line=True, indent=0):
            """Render a line of tokens with proper alignment"""
            if quote or quote_last:
                indent = 20
                if not quote:
                    draw.line((x, y_pos, x, y_pos+font_size*.8), fill=fill, width=2)
                    draw.line((x+10, y_pos, x+10, y_pos+font_size*.8), fill=fill, width=2)
                else:
                    draw.line((x, y_pos, x, y_pos+line_height), fill=fill, width=2)
                    draw.line((x+10, y_pos, x+10, y_pos+line_height), fill=fill, width=2)

            x_pos = get_line_start_x(line, y_pos)
            x_pos += indent
            if bullet_indent:
                nonlocal previous_bullet
                if previous_bullet:
                    x_pos = previous_bullet
                previous_bullet = x_pos

            if not line:
                return

            # remove spaces on newlines, if exactly 1, and it's a
            # continued line.
            if run_on_line and line[0]['type'] == 'text':
                if line[0]['value'] == ' ':
                    line = line[1:]

            for token in line:
                if token['type'] in ('format', 'indent'):
                    continue  # Format tokens don't render

                if token['type'] == 'text':
                    draw.text(
                        (x_pos, y_pos),
                        token['value'],
                        fill=fill,
                        font=fonts[token['font']],
                        stroke_width=outline,
                        stroke_fill=outline_fill,
                    )
                    x_pos += token['width']

                elif token['type'] == 'font_icon':
                    draw.text(
                        (x_pos, y_pos),
                        token['value'],
                        fill=fill,
                        font=fonts['icon'],
                        stroke_width=outline,
                        stroke_fill=outline_fill,
                    )
                    x_pos += token['width']

                elif token['type'] == 'image_icon':
                    icon = self.load_icon(token['value'], font_size)
                    if icon:
                        # Center the icon vertically with text
                        icon = invert_icon(icon)
                        icon_y = y_pos - (icon.height - font_size) // 2
                        image.paste(icon, (int(x_pos), int(icon_y)), icon)
                        x_pos += icon.width


        # Process each token to create lines
        for i, token in enumerate(tokens):
            if token['type'] == 'format':
                token['width'] = 0
                # Update current font
                if token['start']:
                    font_stack.append(current_font)  # Save current font
                    current_font = token['value']
                else:
                    # Restore previous font if stack isn't empty
                    if font_stack:
                        current_font = font_stack.pop()
                    else:
                        current_font = 'regular'

                # Add format change to current line (it has no width impact)
                current_line.append(token)

            elif token['type'] == 'story':
                # Whether this is the beginning or the end of the block
                # a new line starts before/after this.
                if token['start']:
                    quote = True
                    font_stack.append(current_font)  # Save current font
                    current_font = 'italic'
                else:
                    # update quote stuff after render
                    quote = False
                    quote_last = True
                    current_font = font_stack.pop()

            elif token['type'] == 'align':
                token['width'] = 0
                # Update current alignment
                if token['start']:
                    alignment_stack.append(current_alignment)
                    current_alignment = token['value']
                else:
                    if font_stack:
                        current_alignment = alignment_stack.pop()
                    else:
                        current_alignment = self.alignment

                # Add alignment change to current line (it has no width impact)
                current_line.append(token)

            elif token['type'] == 'margin':
                y += token['value']

            elif token['type'] == 'newline':
                # Render current line and start a new one
                if current_line:
                    render_line(current_line, y, indent=(current_indent if indent_current else 0))
                    current_indent = 0
                    indent_current = False
                    quote_last = False

                # Move to next line
                y += line_height
                if polygon:
                    l, r = polygon_width_at_y(y, polygon)
                    max_width = r - l
                current_line = []
                current_line_width = 0

                # Check if we've exceeded the region height
                if y + line_height > region.y + max_height:
                    if not force:
                        return False, i/len(tokens)  # Text doesn't fit
                    will_overflow = True

            elif token['type'] in ['font_icon', 'image_icon']:
                # Calculate token width
                token['width'] = get_token_width(token)
                token_width = token['width']

                # if this is a bullet, at the start of a new line, indent it
                if token['value'] == 'b':
                    current_indent = current_line_width + token_width
                    indent_current = True
                    bullet_indent = True

                # Check if adding this icon would exceed the max width
                if current_line_width + token_width > max_width:
                    # Icon doesn't fit, render current line first
                    if current_line:
                        render_line(current_line, y, indent=(current_indent if indent_current else 0))
                        indent_current = current_indent > 0
                        quote_last = False

                        # Move to next line
                        y += font_size
                        if polygon:
                            l,r = polygon_width_at_y(y, polygon)
                            max_width = r-l
                        current_line = []
                        current_line_width = 0

                        # Check if we've exceeded the region height
                        if y + line_height > region.y + max_height:
                            if not force:
                                return False, i/len(tokens)  # Text doesn't fit
                            will_overflow = True

                # Add icon to current line
                current_line.append(token)
                current_line_width += token_width

            elif token['type'] == 'text':
                # We'll process text as a unit to preserve all whitespace
                text_value = token['value']
                token_with_font = {
                    'type': 'text',
                    'value': text_value,
                    'font': current_font
                }

                # Calculate width if we add this token
                segment_width = get_token_width(token_with_font)
                token_with_font['width'] = segment_width
                token['width'] = segment_width

                if current_line_width + segment_width > max_width:
                    # Text doesn't fit on current line

                    # Render current line and move to next
                    render_line(current_line, y, indent=(current_indent if indent_current else 0))
                    indent_current = current_indent > 0
                    quote_last = False
                    y += font_size
                    if polygon:
                        l,r = polygon_width_at_y(y, polygon)
                        max_width = r-l

                    # Check if we've exceeded the region height
                    if y + line_height > region.y + max_height:
                        if not force:
                            return False, i/len(tokens)  # Text doesn't fit
                        will_overflow = True

                    # Start new line with this token
                    current_line = []
                    current_line_width = 0

                    # Special case: if the token is still too wide for a line by itself
                    # and we're forcing rendering, we'll still add it
                    if segment_width > max_width and force:
                        current_line.append(token_with_font)
                        current_line_width = segment_width
                    elif segment_width <= max_width:  # Only add if it fits
                        current_line.append(token_with_font)
                        current_line_width = segment_width
                else:
                    # Token fits on current line
                    current_line.append(token_with_font)
                    current_line_width += segment_width
        # Render any remaining line
        if current_line:
            render_line(current_line, y, indent=(current_indent if indent_current else 0))
            quote_last = False

            # Check if we ran out of vertical space
            if y + line_height > region.y + max_height and not force:
                return False, 1

        # If we get here and aren't forcing, the text fit successfully
        return not will_overflow or force, 1
