from __future__ import annotations
from pyWebLayout.core.base import Renderable, Queriable
from .box import Box
from pyWebLayout.style import Alignment, Font, FontStyle, FontWeight, TextDecoration
from pyWebLayout.abstract import Word
from PIL import Image, ImageDraw, ImageFont
from typing import Tuple, Union, List, Optional, Protocol
import numpy as np
from abc import ABC, abstractmethod

class AlignmentHandler(ABC):
    """
    Abstract base class for text alignment handlers.
    Each handler implements a specific alignment strategy.
    """

    @abstractmethod
    def calculate_spacing_and_position(self, text_objects: List['Text'],
                                     available_width: int, min_spacing: int,
                                     max_spacing: int) -> Tuple[int, int, bool]:
        """
        Calculate the spacing between words and starting position for the line.

        Args:
            text_objects: List of Text objects in the line
            available_width: Total width available for the line
            min_spacing: Minimum spacing between words
            max_spacing: Maximum spacing between words

        Returns:
            Tuple of (spacing_between_words, starting_x_position)
        """
        pass

class LeftAlignmentHandler(AlignmentHandler):
    """Handler for left-aligned text."""

    def calculate_spacing_and_position(self,
                                    text_objects: List['Text'],
                                    available_width: int,
                                    min_spacing: int,
                                    max_spacing: int) -> Tuple[int, int, bool]:
        """
        Calculate spacing and position for left-aligned text objects.
        CREngine-inspired: never allow negative spacing, always use minimum spacing for overflow.

        Args:
            text_objects (List[Text]): A list of text objects to be laid out.
            available_width (int): The total width available for layout.
            min_spacing (int): Minimum spacing between text objects.
            max_spacing (int): Maximum spacing between text objects.

        Returns:
            Tuple[int, int, bool]: Spacing, start position, and overflow flag.
        """
        # Handle single word case
        if len(text_objects) <= 1:
            return 0, 0, False

        # Calculate the total length of all text objects
        text_length = sum([text.width for text in text_objects])

        # Calculate number of gaps between texts
        num_gaps = len(text_objects) - 1

        # Calculate minimum space needed (text + minimum gaps)
        min_total_width = text_length + (min_spacing * num_gaps)

        # Check if we have overflow (CREngine pattern: always use min_spacing for overflow)
        if min_total_width > available_width:
            return min_spacing, 0, True  # Overflow - but use safe minimum spacing

        # Calculate residual space left after accounting for text lengths
        residual_space = available_width - text_length

        # Calculate ideal spacing
        actual_spacing = residual_space // num_gaps
        # Clamp within bounds (CREngine pattern: respect max_spacing)
        if actual_spacing > max_spacing:
            return max_spacing, 0, False
        elif actual_spacing < min_spacing:
            # Ensure we never return spacing less than min_spacing
            return min_spacing, 0, False
        else:
            return actual_spacing, 0, False  # Use calculated spacing

class CenterRightAlignmentHandler(AlignmentHandler):
    """Handler for center and right-aligned text."""

    def __init__(self, alignment: Alignment):
        self._alignment = alignment

    def calculate_spacing_and_position(self, text_objects: List['Text'],
                                     available_width: int, min_spacing: int,
                                     max_spacing: int) -> Tuple[int, int, bool]:
        """Center/right alignment uses minimum spacing with calculated start position."""
        word_length = sum([word.width for word in text_objects])
        residual_space = available_width - word_length

        # Handle single word case
        if len(text_objects) <= 1:
            if self._alignment == Alignment.CENTER:
                start_position = (available_width - word_length) // 2
            else:  # RIGHT
                start_position = available_width - word_length
            return 0, max(0, start_position), False

        actual_spacing = residual_space // (len(text_objects)-1)
        ideal_space = (min_spacing + max_spacing)/2
        if actual_spacing > 0.5*(min_spacing + max_spacing):
            actual_spacing = 0.5*(min_spacing + max_spacing)

        content_length = word_length + (len(text_objects)-1) * actual_spacing
        if self._alignment == Alignment.CENTER:
            start_position = (available_width - content_length) // 2
        else:
            start_position = available_width - content_length

        if actual_spacing < min_spacing:
            return actual_spacing, max(0, start_position), True

        return ideal_space, max(0, start_position), False

class JustifyAlignmentHandler(AlignmentHandler):
    """Handler for justified text with full justification."""

    def calculate_spacing_and_position(self, text_objects: List['Text'],
                                     available_width: int, min_spacing: int,
                                     max_spacing: int) -> Tuple[int, int, bool]:
        """Justified alignment distributes space to fill the entire line width."""

        word_length = sum([word.width for word in text_objects])
        residual_space = available_width - word_length
        num_gaps = max(1, len(text_objects) - 1)

        actual_spacing = residual_space // num_gaps
        ideal_space = (min_spacing + max_spacing)//2
        # can we touch the end?
        if actual_spacing < max_spacing:
            if actual_spacing < min_spacing:
                # Ensure we never return spacing less than min_spacing
                return min_spacing, 0, True
            return max(min_spacing, actual_spacing), 0, False
        return ideal_space, 0, False

class Text(Renderable, Queriable):
    """
    Concrete implementation for rendering text.
    This class handles the visual representation of text fragments.
    """

    def __init__(self, text: str, style: Font, draw:  ImageDraw.Draw, source: Optional[Word] = None, line: Optional[Line] = None):
        """
        Initialize a Text object.

        Args:
            text: The text content to render
            style: The font style to use for rendering
        """
        super().__init__()
        self._text = text
        self._style = style
        self._line = line
        self._source = source
        self._origin = np.array([0, 0])
        self._draw = draw

        # Calculate dimensions
        self._calculate_dimensions()

    def _calculate_dimensions(self):
        """Calculate the width and height of the text based on the font metrics"""
        # Get the size using PIL's text size functionality
        font = self._style.font
        self._width = self._draw.textlength(self._text, font=font)
        ascent, descent = font.getmetrics()
        self._ascent = ascent
        self._middle_y = ascent - descent / 2

    @classmethod
    def from_word(cls,word:Word, draw: ImageDraw.Draw):
        return cls(word.text,word.style, draw)

    @property
    def text(self) -> str:
        """Get the text content"""
        return self._text

    @property
    def style(self) -> Font:
        """Get the text style"""
        return self._style

    @property
    def origin(self) -> np.ndarray:
        """Get the origin of the text"""
        return self._origin

    @property
    def line(self) -> Optional[Line]:
        """Get the line containing this text"""
        return self._line

    @line.setter
    def line(self, line):
        """Set the line containing this text"""
        self._line = line

    @property
    def width(self) -> int:
        """Get the width of the text"""
        return self._width

    @property
    def size(self) -> int:
        """Get the width of the text"""
        return np.array((self._width, self._style.font_size))

    def set_origin(self, origin:np.generic):
        """Set the origin (left baseline ("ls")) of this text element"""
        self._origin = origin

    def add_line(self, line):
        """Add this text to a line"""
        self._line = line

    def _apply_decoration(self, next_text: Optional['Text'] = None, spacing: int = 0):
        """
        Apply text decoration (underline or strikethrough).
        
        Args:
            next_text: The next Text object in the line (if any)
            spacing: The spacing to the next text object
        """
        if self._style.decoration == TextDecoration.UNDERLINE:
            # Draw underline at about 90% of the height
            y_position = self._origin[1] - 0.1*self._style.font_size
            line_width = max(1, int(self._style.font_size / 15))
            
            # Determine end x-coordinate
            end_x = self._origin[0] + self._width
            
            # If next text also has underline decoration, extend to connect them
            if (next_text is not None and 
                next_text.style.decoration == TextDecoration.UNDERLINE and
                next_text.style.colour == self._style.colour):
                # Extend the underline through the spacing to connect with next word
                end_x += spacing
            
            self._draw.line([(self._origin[0], y_position), (end_x, y_position)],
                      fill=self._style.colour, width=line_width)

        elif self._style.decoration == TextDecoration.STRIKETHROUGH:
            # Draw strikethrough at about 50% of the height
            y_position = self._origin[1] + self._middle_y
            line_width = max(1, int(self._style.font_size / 15))
            
            # Determine end x-coordinate
            end_x = self._origin[0] + self._width
            
            # If next text also has strikethrough decoration, extend to connect them
            if (next_text is not None and 
                next_text.style.decoration == TextDecoration.STRIKETHROUGH and
                next_text.style.colour == self._style.colour):
                # Extend the strikethrough through the spacing to connect with next word
                end_x += spacing
            
            self._draw.line([(self._origin[0], y_position), (end_x, y_position)],
                      fill=self._style.colour, width=line_width)

    def render(self, next_text: Optional['Text'] = None, spacing: int = 0):
        """
        Render the text to an image.
        
        Args:
            next_text: The next Text object in the line (if any)
            spacing: The spacing to the next text object

        Returns:
            A PIL Image containing the rendered text
        """

        # Draw the text background if specified
        if self._style.background and self._style.background[3] > 0:  # If alpha > 0
            self._draw.rectangle([self._origin, self._origin+self._size], fill=self._style.background)

        # Draw the text using baseline as anchor point ("ls" = left-baseline)
        # This ensures the origin represents the baseline, not the top-left
        self._draw.text((self.origin[0], self._origin[1]), self._text, font=self._style.font, fill=self._style.colour, anchor="ls")

        # Apply any text decorations with knowledge of next text
        self._apply_decoration(next_text, spacing)

class Line(Box):
    """
    A line of text consisting of Text objects with consistent spacing.
    Each Text represents a word or word fragment that can be rendered.
    """

    def __init__(self, spacing: Tuple[int, int], origin, size, draw:  ImageDraw.Draw,font: Optional[Font] = None,
                 callback=None, sheet=None, mode=None, halign=Alignment.CENTER,
                 valign=Alignment.CENTER, previous = None,
                 min_word_length_for_brute_force: int = 8,
                 min_chars_before_hyphen: int = 2,
                 min_chars_after_hyphen: int = 2):
        """
        Initialize a new line.

        Args:
            spacing: A tuple of (min_spacing, max_spacing) between words
            origin: The top-left position of the line
            size: The width and height of the line
            font: The default font to use for text in this line
            callback: Optional callback function
            sheet: Optional image sheet
            mode: Optional image mode
            halign: Horizontal alignment of text within the line
            valign: Vertical alignment of text within the line
            previous: Reference to the previous line
            min_word_length_for_brute_force: Minimum word length to attempt brute force hyphenation (default: 8)
            min_chars_before_hyphen: Minimum characters before hyphen in any split (default: 2)
            min_chars_after_hyphen: Minimum characters after hyphen in any split (default: 2)
        """
        super().__init__(origin, size, callback, sheet, mode, halign, valign)
        self._text_objects: List['Text'] = []  # Store Text objects directly
        self._spacing = spacing  # (min_spacing, max_spacing)
        self._font = font if font else Font()  # Use default font if none provided
        self._current_width = 0  # Track the current width used
        self._words : List['Word'] = []
        self._previous = previous
        self._next = None
        ascent, descent = self._font.font.getmetrics()
        # Store baseline as offset from line origin (top), not absolute position
        self._baseline = ascent
        self._draw = draw
        self._spacing_render = (spacing[0] + spacing[1]) //2
        self._position_render = 0
        
        # Hyphenation configuration parameters
        self._min_word_length_for_brute_force = min_word_length_for_brute_force
        self._min_chars_before_hyphen = min_chars_before_hyphen
        self._min_chars_after_hyphen = min_chars_after_hyphen

        # Create the appropriate alignment handler
        self._alignment_handler = self._create_alignment_handler(halign)

    def _create_alignment_handler(self, alignment: Alignment) -> AlignmentHandler:
        """
        Create the appropriate alignment handler based on the alignment type.

        Args:
            alignment: The alignment type

        Returns:
            The appropriate alignment handler instance
        """
        if alignment == Alignment.LEFT:
            return LeftAlignmentHandler()
        elif alignment == Alignment.JUSTIFY:
            return JustifyAlignmentHandler()
        else:  # CENTER or RIGHT
            return CenterRightAlignmentHandler(alignment)

    @property
    def text_objects(self) -> List[Text]:
        """Get the list of Text objects in this line"""
        return self._text_objects

    def set_next(self, line: Line):
        """Set the next line in sequence"""
        self._next = line

    def add_word(self, word: 'Word', part:Optional[Text]=None) -> Tuple[bool, Optional['Text']]:
        """
        Add a word to this line using intelligent word fitting strategies.

        Args:
            word: The word to add to the line
            part: Optional pretext from a previous hyphenated word

        Returns:
            Tuple of (success, overflow_text):
            - success: True if word/part was added, False if it couldn't fit
            - overflow_text: Remaining text if word was hyphenated, None otherwise
        """
        # Import LinkedWord here to avoid circular imports
        from pyWebLayout.abstract.inline import LinkedWord
        from pyWebLayout.concrete.functional import LinkText
        
        # First, add any pretext from previous hyphenation
        if part is not None:
            self._text_objects.append(part)
            self._words.append(word)
            part.add_line(self)

        # Try to add the full word - create LinkText for LinkedWord, regular Text otherwise
        if isinstance(word, LinkedWord):
            # Create a LinkText which includes the link functionality
            # LinkText constructor needs: (link, text, font, draw, source, line)
            # But LinkedWord itself contains the link properties
            # We'll create a Link object from the LinkedWord properties
            from pyWebLayout.abstract.functional import Link
            link = Link(
                location=word.location,
                link_type=word.link_type,
                callback=word.link_callback,
                params=word.params,
                title=word.link_title
            )
            text = LinkText(link, word.text, word.style, self._draw, source=word, line=self)
        else:
            text = Text.from_word(word, self._draw)
        self._text_objects.append(text)
        spacing, position, overflow = self._alignment_handler.calculate_spacing_and_position(
            self._text_objects, self._size[0], self._spacing[0], self._spacing[1]
        )
        
        if not overflow:
            # Word fits! Add it completely
            self._words.append(word)
            word.add_concete(text)
            text.add_line(self)
            self._position_render = position
            self._spacing_render = spacing
            return True, None

        # Word doesn't fit, remove it and try hyphenation
        _ = self._text_objects.pop()
        
        # Step 1: Try pyphen hyphenation
        pyphen_splits = word.possible_hyphenation()
        valid_splits = []
        
        if pyphen_splits:
            # Create Text objects for each possible split and check if they fit
            for pair in pyphen_splits:
                first_part_text = pair[0] + "-"
                second_part_text = pair[1]
                
                # Validate minimum character requirements
                if len(pair[0]) < self._min_chars_before_hyphen:
                    continue
                if len(pair[1]) < self._min_chars_after_hyphen:
                    continue
                
                # Create Text objects
                first_text = Text(first_part_text, word.style, self._draw, line=self, source=word)
                second_text = Text(second_part_text, word.style, self._draw, line=self, source=word)
                
                # Check if first part fits
                self._text_objects.append(first_text)
                spacing, position, overflow = self._alignment_handler.calculate_spacing_and_position(
                    self._text_objects, self._size[0], self._spacing[0], self._spacing[1]
                )
                _ = self._text_objects.pop()
                
                if not overflow:
                    # This split fits! Add it to valid options
                    valid_splits.append((first_text, second_text, spacing, position))
        
        # Step 2: If we have valid pyphen splits, choose the best one
        if valid_splits:
            # Select the split with the best (minimum) spacing
            best_split = min(valid_splits, key=lambda x: x[2])
            first_text, second_text, spacing, position = best_split
            
            # Apply the split
            self._text_objects.append(first_text)
            first_text.line = self
            word.add_concete((first_text, second_text))
            self._spacing_render = spacing
            self._position_render = position
            self._words.append(word)
            return True, second_text
        
        # Step 3: Try brute force hyphenation (only for long words)
        if len(word.text) >= self._min_word_length_for_brute_force:
            # Calculate available space for the word
            word_length = sum([text.width for text in self._text_objects])
            spacing_length = self._spacing[0] * max(0, len(self._text_objects) - 1)
            remaining = self._size[0] - word_length - spacing_length
            
            if remaining > 0:
                # Create a hyphenated version to measure
                test_text = Text(word.text + "-", word.style, self._draw)
                
                if test_text.width > 0:
                    # Calculate what fraction of the hyphenated word fits
                    fraction = remaining / test_text.width
                    
                    # Convert fraction to character position
                    # We need at least min_chars_before_hyphen and leave at least min_chars_after_hyphen
                    max_split_pos = len(word.text) - self._min_chars_after_hyphen
                    min_split_pos = self._min_chars_before_hyphen
                    
                    # Calculate ideal split position based on available space
                    ideal_split = int(fraction * len(word.text))
                    split_pos = max(min_split_pos, min(ideal_split, max_split_pos))
                    
                    # Ensure we meet minimum requirements
                    if (split_pos >= self._min_chars_before_hyphen and 
                        len(word.text) - split_pos >= self._min_chars_after_hyphen):
                        
                        # Create the split
                        first_part_text = word.text[:split_pos] + "-"
                        second_part_text = word.text[split_pos:]
                        
                        first_text = Text(first_part_text, word.style, self._draw, line=self, source=word)
                        second_text = Text(second_part_text, word.style, self._draw, line=self, source=word)
                        
                        # Verify the first part actually fits
                        self._text_objects.append(first_text)
                        spacing, position, overflow = self._alignment_handler.calculate_spacing_and_position(
                            self._text_objects, self._size[0], self._spacing[0], self._spacing[1]
                        )
                        
                        if not overflow:
                            # Brute force split works!
                            first_text.line = self
                            second_text.line = self
                            word.add_concete((first_text, second_text))
                            self._spacing_render = spacing
                            self._position_render = position
                            self._words.append(word)
                            return True, second_text
                        else:
                            # Doesn't fit, remove it
                            _ = self._text_objects.pop()
        
        # Step 4: Word cannot be hyphenated or split, move to next line
        return False, None

    def render(self):
        """
        Render the line with all its text objects using the alignment handler system.

        Returns:
            A PIL Image containing the rendered line
        """
        # Recalculate spacing and position for current text objects to ensure accuracy
        if len(self._text_objects) > 0:
            spacing, position, overflow = self._alignment_handler.calculate_spacing_and_position(
                self._text_objects, self._size[0], self._spacing[0], self._spacing[1]
            )
            self._spacing_render = spacing
            self._position_render = position

        y_cursor = self._origin[1] + self._baseline

        # Start x_cursor at line origin plus any alignment offset
        x_cursor = self._origin[0] + self._position_render
        for i, text in enumerate(self._text_objects):
            # Update text draw context to current draw context
            text._draw = self._draw
            text.set_origin(np.array([x_cursor, y_cursor]))
            
            # Determine next text object for continuous decoration
            next_text = self._text_objects[i + 1] if i + 1 < len(self._text_objects) else None
            
            # Render with next text information for continuous underline/strikethrough
            text.render(next_text, self._spacing_render)
            x_cursor += self._spacing_render + text.width # x-spacing + width of text object
