#!/usr/bin/env python3
"""
Simple HTML Browser using pyWebLayout

This browser can render basic HTML content using the pyWebLayout concrete objects.
It supports text, images, links, forms, and basic styling.
"""

import re
import tkinter as tk
from tkinter import ttk, messagebox, filedialog, simpledialog
from PIL import Image, ImageTk, ImageDraw
from typing import Dict, List, Optional, Tuple, Any
import webbrowser
import os
from urllib.parse import urljoin, urlparse
import requests
from io import BytesIO
import pyperclip

# Import pyWebLayout components
from pyWebLayout.concrete import (
    Page, Container, Box, Text, RenderableImage, 
    RenderableLink, RenderableButton, RenderableForm, RenderableFormField
)
from pyWebLayout.abstract.functional import (
    Link, Button, Form, FormField, LinkType, FormFieldType
)
from pyWebLayout.abstract.block import Paragraph
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style.fonts import Font, FontWeight, FontStyle, TextDecoration
from pyWebLayout.style import Alignment
from pyWebLayout.layout.paragraph_layout import ParagraphLayout, ParagraphLayoutResult


class HTMLParser:
    """Simple HTML parser that converts HTML to pyWebLayout objects"""
    
    def __init__(self):
        self.font_stack = [Font(font_size=14)]  # Default font
        self.current_container = None
        
    def parse_html_string(self, html_content: str, base_url: str = "") -> Page:
        """Parse HTML string and return a Page object"""
        # Create the main page
        page = Page(size=(800, 10000), background_color=(255, 255, 255))
        self.current_container = page
        self.base_url = base_url
        
        # Simple HTML parsing using regex (not production-ready, but works for demo)
        # Remove comments and scripts
        html_content = re.sub(r'<!--.*?-->', '', html_content, flags=re.DOTALL)
        html_content = re.sub(r'<script.*?</script>', '', html_content, flags=re.DOTALL)
        html_content = re.sub(r'<style.*?</style>', '', html_content, flags=re.DOTALL)
        
        # Extract title
        title_match = re.search(r'<title>(.*?)</title>', html_content, re.IGNORECASE)
        if title_match:
            page.title = title_match.group(1)
        
        # Extract body content
        body_match = re.search(r'<body[^>]*>(.*?)</body>', html_content, re.DOTALL | re.IGNORECASE)
        if body_match:
            body_content = body_match.group(1)
        else:
            # If no body tag, use the entire content
            body_content = html_content
        
        # Parse the body content
        self._parse_content(body_content, page)
        
        return page
    
    def parse_html_file(self, file_path: str) -> Page:
        """Parse HTML file and return a Page object"""
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                html_content = f.read()
            base_url = os.path.dirname(os.path.abspath(file_path))
            return self.parse_html_string(html_content, base_url)
        except Exception as e:
            # Create error page
            page = Page(size=(800, 10000), background_color=(255, 255, 255))
            error_text = Text(f"Error loading file: {str(e)}", Font(font_size=16, colour=(255, 0, 0)))
            page.add_child(error_text)
            return page
    
    def _parse_content(self, content: str, container: Container):
        """Parse HTML content and add elements to container"""
        # Simple token-based parsing
        tokens = self._tokenize_html(content)
        
        # Group tokens into paragraphs and other elements
        self._process_tokens_into_elements(tokens, container)
    
    def _process_tokens_into_elements(self, tokens: List[Dict], container: Container):
        """Process tokens and create appropriate elements (paragraphs, images, etc.)"""
        i = 0
        current_paragraph_content = []
        
        while i < len(tokens):
            token = tokens[i]
            
            if token['type'] == 'text':
                if token['content'].strip():  # Only add non-empty text
                    current_paragraph_content.append((token['content'].strip(), self.font_stack[-1]))
            
            elif token['type'] == 'tag':
                tag_name = token['name']
                is_closing = token['closing']
                
                # Handle block-level elements that should end the current paragraph
                if tag_name in ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'br', 'img'] and not is_closing:
                    # Finalize any pending paragraph content
                    if current_paragraph_content:
                        self._create_and_add_paragraph(current_paragraph_content, container)
                        current_paragraph_content = []
                    
                    # Handle the block element
                    if tag_name == 'p':
                        # Start a new paragraph
                        i = self._handle_paragraph_tag(token, tokens, i, container)
                        continue
                    elif tag_name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']:
                        # Handle header
                        i = self._handle_header_tag(token, tokens, i, container)
                        continue
                    elif tag_name == 'br':
                        # Add line break
                        spacer = Box((0, 0), (1, 10))
                        container.add_child(spacer)
                    elif tag_name == 'img':
                        # Handle image
                        self._handle_tag(token, container)
                    elif tag_name == 'div':
                        # Continue processing div content
                        pass
                
                # Handle inline elements or continue processing
                elif tag_name in ['b', 'strong', 'i', 'em', 'u', 'a']:
                    i = self._handle_inline_tag_with_content(token, tokens, i, current_paragraph_content)
                    continue
                else:
                    # Handle other tags normally
                    self._handle_tag(token, container)
            
            i += 1
        
        # Finalize any remaining paragraph content
        if current_paragraph_content:
            self._create_and_add_paragraph(current_paragraph_content, container)
    
    def _create_and_add_paragraph(self, content_list: List[Tuple[str, Font]], container: Container):
        """Create a paragraph from content and add it to the container using proper layout"""
        if not content_list:
            return
        
        # Create a paragraph object
        paragraph = Paragraph(style=content_list[0][1])  # Use first font as paragraph style
        
        # Add words to the paragraph
        for text_content, font in content_list:
            words = text_content.split()
            for word_text in words:
                if word_text.strip():
                    word = Word(word_text.strip(), font)
                    paragraph.add_word(word)
        
        # Use paragraph layout to break into lines
        layout = ParagraphLayout(
            line_width=750,  # Page width minus margins
            line_height=20,
            word_spacing=(3, 8),
            line_spacing=3,
            halign=Alignment.LEFT
        )
        
        # Layout the paragraph into lines
        lines = layout.layout_paragraph(paragraph)
        
        # Add each line to the container
        for line in lines:
            container.add_child(line)
        
        # Add some space after the paragraph
        spacer = Box((0, 0), (1, 5))
        container.add_child(spacer)
    
    def _handle_paragraph_tag(self, token, tokens, current_index, container):
        """Handle paragraph tags with proper text flow"""
        content_start = current_index + 1
        content_end = self._find_matching_closing_tag(tokens, current_index, 'p')
        
        # Collect content within the paragraph
        paragraph_content = []
        
        i = content_start
        while i < content_end:
            content_token = tokens[i]
            if content_token['type'] == 'text':
                if content_token['content'].strip():
                    paragraph_content.append((content_token['content'].strip(), self.font_stack[-1]))
            elif content_token['type'] == 'tag' and not content_token['closing']:
                # Handle inline formatting within paragraph
                if content_token['name'] in ['b', 'strong', 'i', 'em', 'u', 'a']:
                    i = self._handle_inline_tag_with_content(content_token, tokens, i, paragraph_content)
                    continue
            i += 1
        
        # Create and add the paragraph
        if paragraph_content:
            self._create_and_add_paragraph(paragraph_content, container)
        
        return content_end + 1 if content_end < len(tokens) else len(tokens)
    
    def _handle_header_tag(self, token, tokens, current_index, container):
        """Handle header tags with proper styling"""
        tag_name = token['name']
        
        # Push header font onto stack
        size_map = {'h1': 24, 'h2': 20, 'h3': 18, 'h4': 16, 'h5': 14, 'h6': 12}
        font = self.font_stack[-1].with_size(size_map[tag_name]).with_weight(FontWeight.BOLD)
        self.font_stack.append(font)
        
        content_start = current_index + 1
        content_end = self._find_matching_closing_tag(tokens, current_index, tag_name)
        
        # Collect header content
        header_content = []
        
        i = content_start
        while i < content_end:
            content_token = tokens[i]
            if content_token['type'] == 'text':
                if content_token['content'].strip():
                    header_content.append((content_token['content'].strip(), self.font_stack[-1]))
            elif content_token['type'] == 'tag' and not content_token['closing']:
                # Handle inline formatting within header
                if content_token['name'] in ['b', 'strong', 'i', 'em', 'u']:
                    i = self._handle_inline_tag_with_content(content_token, tokens, i, header_content)
                    continue
            i += 1
        
        # Pop the header font
        if len(self.font_stack) > 1:
            self.font_stack.pop()
        
        # Create and add the header paragraph with extra spacing
        if header_content:
            self._create_and_add_paragraph(header_content, container)
            # Add extra space after headers
            spacer = Box((0, 0), (1, 10))
            container.add_child(spacer)
        
        return content_end + 1 if content_end < len(tokens) else len(tokens)
    
    def _handle_inline_tag_with_content(self, token, tokens, current_index, paragraph_content):
        """Handle inline formatting tags and collect their content"""
        tag_name = token['name']
        
        # Push formatted font onto stack
        if tag_name in ['b', 'strong']:
            font = self.font_stack[-1].with_weight(FontWeight.BOLD)
            self.font_stack.append(font)
        elif tag_name in ['i', 'em']:
            font = self.font_stack[-1].with_style(FontStyle.ITALIC)
            self.font_stack.append(font)
        elif tag_name == 'u':
            font = self.font_stack[-1].with_decoration(TextDecoration.UNDERLINE)
            self.font_stack.append(font)
        elif tag_name == 'a':
            font = self.font_stack[-1].with_colour((0, 0, 255)).with_decoration(TextDecoration.UNDERLINE)
            self.font_stack.append(font)
        
        content_start = current_index + 1
        content_end = self._find_matching_closing_tag(tokens, current_index, tag_name)
        
        # Collect content with the formatting applied
        i = content_start
        while i < content_end:
            content_token = tokens[i]
            if content_token['type'] == 'text':
                if content_token['content'].strip():
                    paragraph_content.append((content_token['content'].strip(), self.font_stack[-1]))
            elif content_token['type'] == 'tag' and not content_token['closing']:
                # Handle nested inline formatting
                if content_token['name'] in ['b', 'strong', 'i', 'em', 'u']:
                    i = self._handle_inline_tag_with_content(content_token, tokens, i, paragraph_content)
                    continue
            i += 1
        
        # Pop the formatting font
        if len(self.font_stack) > 1:
            self.font_stack.pop()
        
        return content_end + 1 if content_end < len(tokens) else len(tokens)
    
    def _handle_tag_with_content(self, token, tokens, current_index, container):
        """Handle tags and their content, returning the new index position"""
        tag_name = token['name']
        is_closing = token['closing']
        
        if is_closing:
            # Handle closing tags
            if tag_name in ['b', 'strong', 'i', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']:
                if len(self.font_stack) > 1:  # Don't pop the last font
                    self.font_stack.pop()
            return current_index + 1
        
        # For opening tags that affect text styling, parse their content with the new style
        if tag_name in ['b', 'strong', 'i', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']:
            # Push new font onto stack
            self._handle_tag(token, container)
            
            # Find the matching closing tag and parse content in between
            content_start = current_index + 1
            content_end = self._find_matching_closing_tag(tokens, current_index, tag_name)
            
            if content_end > content_start:
                # Parse content between opening and closing tags with current font style
                for j in range(content_start, content_end):
                    content_token = tokens[j]
                    if content_token['type'] == 'text':
                        if content_token['content'].strip():
                            text_obj = Text(content_token['content'].strip(), self.font_stack[-1])
                            container.add_child(text_obj)
                    elif content_token['type'] == 'tag' and not content_token['closing']:
                        # Handle nested tags
                        self._handle_tag(content_token, container)
            
            # Pop the font from stack
            if len(self.font_stack) > 1:
                self.font_stack.pop()
            
            return content_end + 1 if content_end < len(tokens) else len(tokens)
        
        else:
            # Handle other tags normally
            self._handle_tag(token, container)
            return current_index + 1
    
    def _find_matching_closing_tag(self, tokens, start_index, tag_name):
        """Find the index of the matching closing tag"""
        open_count = 1
        i = start_index + 1
        
        while i < len(tokens) and open_count > 0:
            token = tokens[i]
            if token['type'] == 'tag' and token['name'] == tag_name:
                if token['closing']:
                    open_count -= 1
                else:
                    open_count += 1
            i += 1
        
        return i - 1 if open_count == 0 else len(tokens)
    
    def _tokenize_html(self, content: str) -> List[Dict]:
        """Simple HTML tokenizer"""
        tokens = []
        tag_pattern = r'<(/?)([^>]+)>'
        
        last_end = 0
        for match in re.finditer(tag_pattern, content):
            # Add text before tag
            text_content = content[last_end:match.start()]
            if text_content:
                tokens.append({'type': 'text', 'content': text_content})
            
            # Add tag
            is_closing = bool(match.group(1))
            tag_content = match.group(2)
            tag_parts = tag_content.split()
            tag_name = tag_parts[0].lower()
            
            # Parse attributes
            attributes = {}
            if len(tag_parts) > 1:
                attr_text = ' '.join(tag_parts[1:])
                attr_pattern = r'(\w+)=(?:"([^"]*)"|\'([^\']*)\'|([^\s>]+))'
                for attr_match in re.finditer(attr_pattern, attr_text):
                    attr_name = attr_match.group(1).lower()
                    attr_value = attr_match.group(2) or attr_match.group(3) or attr_match.group(4)
                    attributes[attr_name] = attr_value
            
            tokens.append({
                'type': 'tag',
                'name': tag_name,
                'closing': is_closing,
                'attributes': attributes,
                'content': tag_content
            })
            
            last_end = match.end()
        
        # Add remaining text
        if last_end < len(content):
            text_content = content[last_end:]
            if text_content:
                tokens.append({'type': 'text', 'content': text_content})
        
        return tokens
    
    def _handle_tag(self, token: Dict, container: Container):
        """Handle HTML tags"""
        tag_name = token['name']
        is_closing = token['closing']
        attributes = token['attributes']
        
        if is_closing:
            # Handle closing tags
            if tag_name in ['b', 'strong']:
                self.font_stack.pop()
            elif tag_name in ['i', 'em']:
                self.font_stack.pop()
            elif tag_name == 'u':
                self.font_stack.pop()
            return
        
        # Handle opening tags
        if tag_name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']:
            # Headers
            size_map = {'h1': 24, 'h2': 20, 'h3': 18, 'h4': 16, 'h5': 14, 'h6': 12}
            font = self.font_stack[-1].with_size(size_map[tag_name]).with_weight(FontWeight.BOLD)
            self.font_stack.append(font)
            
        elif tag_name in ['b', 'strong']:
            # Bold text
            font = self.font_stack[-1].with_weight(FontWeight.BOLD)
            self.font_stack.append(font)
            
        elif tag_name in ['i', 'em']:
            # Italic text
            font = self.font_stack[-1].with_style(FontStyle.ITALIC)
            self.font_stack.append(font)
            
        elif tag_name == 'u':
            # Underlined text
            font = self.font_stack[-1].with_decoration(TextDecoration.UNDERLINE)
            self.font_stack.append(font)
            
        elif tag_name == 'a':
            # Links
            href = attributes.get('href', '#')
            title = attributes.get('title', href)
            
            # Determine link type
            if href.startswith('http'):
                link_type = LinkType.EXTERNAL
            elif href.startswith('#'):
                link_type = LinkType.INTERNAL
            else:
                link_type = LinkType.INTERNAL
            
            # Create link callback
            def link_callback(location, **kwargs):
                return f"Navigate to: {location}"
            
            link = Link(href, link_type, link_callback, title=title)
            link_font = self.font_stack[-1].with_colour((0, 0, 255)).with_decoration(TextDecoration.UNDERLINE)
            
            # For now, just add the link text with link styling
            link_text = attributes.get('title', href)
            renderable_link = RenderableLink(link, link_text, link_font)
            container.add_child(renderable_link)
            
        elif tag_name == 'img':
            # Images
            src = attributes.get('src', '')
            alt = attributes.get('alt', 'Image')
            width = attributes.get('width')
            height = attributes.get('height')
            
            if src:
                # Resolve relative URLs
                if self.base_url and not src.startswith(('http://', 'https://')):
                    if os.path.isdir(self.base_url):
                        src = os.path.join(self.base_url, src)
                    else:
                        src = urljoin(self.base_url, src)
                
                try:
                    # Create abstract image
                    from pyWebLayout.abstract.block import Image as AbstractImage
                    abstract_img = AbstractImage(src, alt)
                    
                    # Parse dimensions if provided
                    max_width = int(width) if width and width.isdigit() else None
                    max_height = int(height) if height and height.isdigit() else None
                    
                    renderable_img = RenderableImage(abstract_img, max_width, max_height)
                    container.add_child(renderable_img)
                    
                except Exception as e:
                    # Add error text if image fails to load
                    error_text = Text(f"[Image Error: {alt}]", Font(colour=(255, 0, 0)))
                    container.add_child(error_text)
            
        elif tag_name == 'br':
            # Line breaks - add some vertical space
            spacer = Box((0, 0), (1, 10))
            container.add_child(spacer)
            
        elif tag_name == 'p':
            # Paragraphs - add some vertical space
            spacer = Box((0, 0), (1, 5))
            container.add_child(spacer)
            
        elif tag_name in ['div', 'span']:
            # Generic containers - just continue parsing
            pass


class BrowserWindow:
    """Main browser window using Tkinter"""
    
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("pyWebLayout HTML Browser")
        self.root.geometry("900x700")
        
        self.current_page = None
        self.history = []
        self.history_index = -1
        
        # Text selection variables
        self.selection_start = None
        self.selection_end = None
        self.is_selecting = False
        self.selected_text = ""
        self.text_elements = []  # Store text elements with positions
        self.selection_overlay = None  # Canvas overlay for selection highlighting
        
        self.setup_ui()
        
    def setup_ui(self):
        """Setup the user interface"""
        # Create main frame
        main_frame = ttk.Frame(self.root)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        # Navigation frame
        nav_frame = ttk.Frame(main_frame)
        nav_frame.pack(fill=tk.X, pady=(0, 5))
        
        # Navigation buttons
        self.back_btn = ttk.Button(nav_frame, text="←", command=self.go_back, state=tk.DISABLED)
        self.back_btn.pack(side=tk.LEFT, padx=(0, 5))
        
        self.forward_btn = ttk.Button(nav_frame, text="→", command=self.go_forward, state=tk.DISABLED)
        self.forward_btn.pack(side=tk.LEFT, padx=(0, 5))
        
        self.refresh_btn = ttk.Button(nav_frame, text="⟳", command=self.refresh)
        self.refresh_btn.pack(side=tk.LEFT, padx=(0, 10))
        
        # Address bar
        ttk.Label(nav_frame, text="URL:").pack(side=tk.LEFT)
        self.url_var = tk.StringVar()
        self.url_entry = ttk.Entry(nav_frame, textvariable=self.url_var, width=50)
        self.url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 5))
        self.url_entry.bind('<Return>', self.navigate_to_url)
        
        self.go_btn = ttk.Button(nav_frame, text="Go", command=self.navigate_to_url)
        self.go_btn.pack(side=tk.LEFT, padx=(0, 10))
        
        # File operations
        self.open_btn = ttk.Button(nav_frame, text="Open File", command=self.open_file)
        self.open_btn.pack(side=tk.LEFT)
        
        # Content frame with scrollbars
        content_frame = ttk.Frame(main_frame)
        content_frame.pack(fill=tk.BOTH, expand=True)
        
        # Create canvas with scrollbars
        self.canvas = tk.Canvas(content_frame, bg='white')
        
        v_scrollbar = ttk.Scrollbar(content_frame, orient=tk.VERTICAL, command=self.canvas.yview)
        h_scrollbar = ttk.Scrollbar(content_frame, orient=tk.HORIZONTAL, command=self.canvas.xview)
        
        self.canvas.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set)
        
        v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X)
        self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        
        # Status bar
        self.status_var = tk.StringVar(value="Ready")
        status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
        status_bar.pack(fill=tk.X, pady=(5, 0))
        
        # Bind mouse events
        self.canvas.bind('<Button-1>', self.on_click)
        self.canvas.bind('<B1-Motion>', self.on_drag)
        self.canvas.bind('<ButtonRelease-1>', self.on_release)
        self.canvas.bind('<Motion>', self.on_mouse_move)
        
        # Keyboard shortcuts
        self.root.bind('<Control-c>', self.copy_selection)
        self.root.bind('<Control-a>', self.select_all)
        
        # Context menu
        self.setup_context_menu()
        
        # Make canvas focusable
        self.canvas.config(highlightthickness=1)
        self.canvas.focus_set()
        
        # Load default page
        self.load_default_page()
    
    def setup_context_menu(self):
        """Setup the right-click context menu"""
        self.context_menu = tk.Menu(self.root, tearoff=0)
        self.context_menu.add_command(label="Copy", command=self.copy_selection)
        self.context_menu.add_command(label="Select All", command=self.select_all)
        
        # Bind right-click to show context menu
        self.canvas.bind('<Button-3>', self.show_context_menu)
    
    def show_context_menu(self, event):
        """Show context menu at mouse position"""
        try:
            self.context_menu.tk_popup(event.x_root, event.y_root)
        finally:
            self.context_menu.grab_release()
    
    def on_drag(self, event):
        """Handle mouse dragging for text selection"""
        canvas_x = self.canvas.canvasx(event.x)
        canvas_y = self.canvas.canvasy(event.y)
        
        if not self.is_selecting:
            # Start selection
            self.is_selecting = True
            self.selection_start = (canvas_x, canvas_y)
            self.selection_end = (canvas_x, canvas_y)
        else:
            # Update selection end
            self.selection_end = (canvas_x, canvas_y)
        
        # Update visual selection
        self.update_selection_visual()
        
        # Update status
        self.status_var.set("Selecting text...")
    
    def on_release(self, event):
        """Handle mouse release to complete text selection"""
        if self.is_selecting:
            canvas_x = self.canvas.canvasx(event.x)
            canvas_y = self.canvas.canvasy(event.y)
            self.selection_end = (canvas_x, canvas_y)
            
            # Extract selected text
            self.extract_selected_text()
            
            # Update status
            if self.selected_text:
                self.status_var.set(f"Selected: {len(self.selected_text)} characters")
            else:
                self.status_var.set("No text selected")
                self.clear_selection()
    
    def update_selection_visual(self):
        """Update the visual representation of text selection"""
        # Remove existing selection overlay
        if self.selection_overlay:
            self.canvas.delete(self.selection_overlay)
        
        if self.selection_start and self.selection_end:
            # Create selection rectangle
            x1, y1 = self.selection_start
            x2, y2 = self.selection_end
            
            # Ensure proper coordinates (top-left to bottom-right)
            left = min(x1, x2)
            top = min(y1, y2)
            right = max(x1, x2)
            bottom = max(y1, y2)
            
            # Draw selection rectangle with transparency effect
            self.selection_overlay = self.canvas.create_rectangle(
                left, top, right, bottom,
                fill='blue', stipple='gray50', outline='blue', width=1
            )
    
    def extract_selected_text(self):
        """Extract text that falls within the selection area"""
        if not self.selection_start or not self.selection_end:
            self.selected_text = ""
            return
        
        # Get selection bounds
        x1, y1 = self.selection_start
        x2, y2 = self.selection_end
        left = min(x1, x2)
        top = min(y1, y2)
        right = max(x1, x2)
        bottom = max(y1, y2)
        
        # Extract text elements in selection area
        selected_elements = []
        self._collect_text_in_area(self.current_page, (0, 0), left, top, right, bottom, selected_elements)
        
        # Sort by position (top to bottom, left to right)
        selected_elements.sort(key=lambda x: (x[2], x[1]))  # Sort by y, then x
        
        # Combine text
        self.selected_text = " ".join([element[0] for element in selected_elements])
    
    def _collect_text_in_area(self, container, offset, left, top, right, bottom, collected):
        """Recursively collect text elements within the selection area"""
        if not hasattr(container, '_children'):
            return
        
        for child in container._children:
            if hasattr(child, '_origin') and hasattr(child, '_size'):
                # Calculate absolute position
                child_origin = tuple(child._origin) if hasattr(child._origin, '__iter__') else child._origin
                child_size = tuple(child._size) if hasattr(child._size, '__iter__') else child._size
                
                abs_x = offset[0] + child_origin[0]
                abs_y = offset[1] + child_origin[1]
                abs_w = child_size[0]
                abs_h = child_size[1]
                
                # Check if element intersects with selection area
                if (abs_x < right and abs_x + abs_w > left and 
                    abs_y < bottom and abs_y + abs_h > top):
                    
                    # If it's a text element, add its text
                    if isinstance(child, Text):
                        text_content = getattr(child, '_text', '')
                        if text_content.strip():
                            collected.append((text_content.strip(), abs_x, abs_y))
                    
                    # If it's a line with words, extract word text
                    elif hasattr(child, '_words'):
                        for word in child._words:
                            if hasattr(word, 'text'):
                                word_text = word.text
                                if word_text.strip():
                                    collected.append((word_text.strip(), abs_x, abs_y))
                    
                    # Recursively check children
                    if hasattr(child, '_children'):
                        self._collect_text_in_area(child, (abs_x, abs_y), left, top, right, bottom, collected)
    
    def copy_selection(self, event=None):
        """Copy selected text to clipboard"""
        if self.selected_text:
            try:
                pyperclip.copy(self.selected_text)
                self.status_var.set(f"Copied {len(self.selected_text)} characters to clipboard")
            except Exception as e:
                self.status_var.set(f"Error copying to clipboard: {str(e)}")
        else:
            self.status_var.set("No text selected to copy")
    
    def select_all(self, event=None):
        """Select all text on the page"""
        if not self.current_page:
            return
        
        # Set selection to entire canvas area
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()
        
        self.selection_start = (0, 0)
        self.selection_end = (canvas_width, canvas_height)
        self.is_selecting = True
        
        # Extract all text
        self.extract_selected_text()
        
        # Update visual
        self.update_selection_visual()
        
        if self.selected_text:
            self.status_var.set(f"Selected all text: {len(self.selected_text)} characters")
        else:
            self.status_var.set("No text found to select")
    
    def clear_selection(self):
        """Clear the current text selection"""
        self.selection_start = None
        self.selection_end = None
        self.is_selecting = False
        self.selected_text = ""
        
        # Remove visual selection
        if self.selection_overlay:
            self.canvas.delete(self.selection_overlay)
            self.selection_overlay = None
        
        self.status_var.set("Selection cleared")
    
    def load_default_page(self):
        """Load a default welcome page"""
        html_content = """
        <html>
        <head><title>pyWebLayout Browser - Welcome</title></head>
        <body>
            <h1>Welcome to pyWebLayout Browser</h1>
            <p>This is a simple HTML browser built using pyWebLayout components.</p>
            
            <h2>Features:</h2>
            <ul>
                <li>Basic HTML rendering</li>
                <li>Text formatting (bold, italic, underline)</li>
                <li>Headers (H1-H6)</li>
                <li>Links (clickable)</li>
                <li>Images</li>
                <li>Forms (basic support)</li>
            </ul>
            
            <h2>Try these features:</h2>
            <p><b>Bold text</b>, <i>italic text</i>, and <u>underlined text</u></p>
            
            <p>Sample link: <a href="https://www.example.com" title="External link">Visit Example.com</a></p>
            
            <h3>File Operations</h3>
            <p>Use the "Open File" button to load local HTML files.</p>
            
            <p>Or enter a URL in the address bar above.</p>
        </body>
        </html>
        """
        
        parser = HTMLParser()
        self.current_page = parser.parse_html_string(html_content)
        self.render_page()
        self.status_var.set("Welcome page loaded")
        
    def navigate_to_url(self, event=None):
        """Navigate to the URL in the address bar"""
        url = self.url_var.get().strip()
        if not url:
            return
            
        self.status_var.set(f"Loading {url}...")
        self.root.update()
        
        try:
            if url.startswith(('http://', 'https://')):
                # Web URL
                response = requests.get(url, timeout=10)
                response.raise_for_status()
                html_content = response.text
                
                parser = HTMLParser()
                self.current_page = parser.parse_html_string(html_content, url)
                
            elif os.path.isfile(url):
                # Local file
                parser = HTMLParser()
                self.current_page = parser.parse_html_file(url)
                
            else:
                # Try to treat as a local file path
                if not url.startswith('file://'):
                    url = 'file://' + os.path.abspath(url)
                
                file_path = url.replace('file://', '')
                if os.path.isfile(file_path):
                    parser = HTMLParser()
                    self.current_page = parser.parse_html_file(file_path)
                else:
                    raise FileNotFoundError(f"File not found: {file_path}")
            
            # Add to history
            self.add_to_history(url)
            self.render_page()
            self.status_var.set(f"Loaded {url}")
            
        except Exception as e:
            self.status_var.set(f"Error loading {url}: {str(e)}")
            messagebox.showerror("Error", f"Failed to load {url}:\n{str(e)}")
    
    def open_file(self):
        """Open a local HTML file"""
        file_path = filedialog.askopenfilename(
            title="Open HTML File",
            filetypes=[("HTML files", "*.html *.htm"), ("All files", "*.*")]
        )
        
        if file_path:
            self.url_var.set(file_path)
            self.navigate_to_url()
    
    def render_page(self):
        """Render the current page to the canvas"""
        if not self.current_page:
            return
            
        # Clear canvas
        self.canvas.delete("all")
        
        # Render the page to PIL Image
        page_image = self.current_page.render()
        
        # Convert to PhotoImage
        self.photo = ImageTk.PhotoImage(page_image)
        
        # Display on canvas
        self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo)
        
        # Update scroll region
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))
        
        # Store page elements for interaction
        self.page_elements = self._get_clickable_elements(self.current_page)
    
    def _get_clickable_elements(self, container, offset=(0, 0)) -> List[Tuple]:
        """Get list of clickable elements with their positions"""
        elements = []
        
        if hasattr(container, '_children'):
            for child in container._children:
                if hasattr(child, '_origin'):
                    # Convert numpy arrays to tuples for consistent coordinate handling
                    child_origin = tuple(child._origin) if hasattr(child._origin, '__iter__') else child._origin
                    child_size = tuple(child._size) if hasattr(child._size, '__iter__') else child._size
                    
                    child_offset = (offset[0] + child_origin[0], offset[1] + child_origin[1])
                    
                    # Check if element is clickable
                    if isinstance(child, (RenderableLink, RenderableButton)):
                        elements.append((child, child_offset, child_size))
                    
                    # Recursively check children
                    if hasattr(child, '_children'):
                        elements.extend(self._get_clickable_elements(child, child_offset))
        
        return elements
    
    def on_click(self, event):
        """Handle mouse clicks on the canvas"""
        # Convert canvas coordinates to image coordinates
        canvas_x = self.canvas.canvasx(event.x)
        canvas_y = self.canvas.canvasy(event.y)
        
        # Check if click is on any clickable element
        for element, offset, size in self.page_elements:
            element_x, element_y = offset
            element_w, element_h = size
            
            if (element_x <= canvas_x <= element_x + element_w and
                element_y <= canvas_y <= element_y + element_h):
                
                # Handle the click
                if isinstance(element, RenderableLink):
                    result = element._callback()
                    if result:
                        self.status_var.set(result)
                        # For external links, open in system browser
                        if element._link.link_type == LinkType.EXTERNAL:
                            webbrowser.open(element._link.location)
                        
                elif isinstance(element, RenderableButton):
                    result = element._callback()
                    if result:
                        self.status_var.set(f"Button clicked: {result}")
                
                break
    
    def on_mouse_move(self, event):
        """Handle mouse movement for hover effects"""
        # Convert canvas coordinates to image coordinates
        canvas_x = self.canvas.canvasx(event.x)
        canvas_y = self.canvas.canvasy(event.y)
        
        # Check if mouse is over any clickable element
        cursor = "arrow"
        for element, offset, size in self.page_elements:
            element_x, element_y = offset
            element_w, element_h = size
            
            if (element_x <= canvas_x <= element_x + element_w and
                element_y <= canvas_y <= element_y + element_h):
                cursor = "hand2"
                break
        
        self.canvas.configure(cursor=cursor)
    
    def add_to_history(self, url):
        """Add URL to navigation history"""
        # Remove any forward history
        self.history = self.history[:self.history_index + 1]
        
        # Add new URL
        self.history.append(url)
        self.history_index = len(self.history) - 1
        
        # Update navigation buttons
        self.update_nav_buttons()
    
    def update_nav_buttons(self):
        """Update the state of navigation buttons"""
        self.back_btn.configure(state=tk.NORMAL if self.history_index > 0 else tk.DISABLED)
        self.forward_btn.configure(state=tk.NORMAL if self.history_index < len(self.history) - 1 else tk.DISABLED)
    
    def go_back(self):
        """Navigate back in history"""
        if self.history_index > 0:
            self.history_index -= 1
            url = self.history[self.history_index]
            self.url_var.set(url)
            self.navigate_to_url()
    
    def go_forward(self):
        """Navigate forward in history"""
        if self.history_index < len(self.history) - 1:
            self.history_index += 1
            url = self.history[self.history_index]
            self.url_var.set(url)
            self.navigate_to_url()
    
    def refresh(self):
        """Refresh the current page"""
        if self.current_page:
            current_url = self.url_var.get()
            if current_url:
                self.navigate_to_url()
            else:
                self.load_default_page()
    
    def run(self):
        """Start the browser"""
        self.root.mainloop()


def main():
    """Main function to run the browser"""
    print("Starting pyWebLayout HTML Browser...")
    
    try:
        browser = BrowserWindow()
        browser.run()
    except Exception as e:
        print(f"Error starting browser: {e}")
        import traceback
        traceback.print_exc()


if __name__ == "__main__":
    main()
