import sys
import pygame
import requests
from bs4 import BeautifulSoup, NavigableString

def parse_css(css_text):
    """
    A simple CSS parser mapping selectors to a dictionary
    of property names and values.
    """
    styles = {}
    css_text = css_text.replace("\n", "")
    blocks = css_text.split("}")
    for block in blocks:
        if "{" in block:
            selector, props = block.split("{", 1)
            selector = selector.strip()
            props = props.strip()
            prop_dict = {}
            for declaration in props.split(";"):
                if ":" in declaration:
                    key, value = declaration.split(":", 1)
                    key = key.strip()
                    value = value.strip()
                    # For font-size, store the raw value so percentages can be processed.
                    if key == "font-size":
                        prop_dict[key] = value
                    else:
                        prop_dict[key] = value
            styles[selector] = prop_dict
    return styles

def get_computed_style(element, css_styles):
    """
    Merge CSS rules (by tag name) with inline style declarations.
    """
    style = {}
    if element.name in css_styles:
        style.update(css_styles[element.name])
    if 'style' in element.attrs:
        declarations = element['style'].split(";")
        for decl in declarations:
            if ":" in decl:
                key, value = decl.split(":", 1)
                style[key.strip()] = value.strip()
    return style

def render_element(surface, element, x, y, css_styles, base_font):
    """
    Recursively render an element (or text node) on the surface.
    Returns the total height rendered.
    """
    total_height = 0

    # Handle text nodes.
    if isinstance(element, NavigableString):
        text = str(element).strip()
        if text:
            text_surface = base_font.render(text, True, pygame.Color("black"))
            surface.blit(text_surface, (x, y))
            return text_surface.get_height()
        return 0

    # For tag nodes, compute the style.
    computed_style = get_computed_style(element, css_styles)

    # Handle margins (defaults: top=0, bottom=5).
    try:
        margin_top = int(computed_style.get("margin-top", "0"))
    except ValueError:
        margin_top = 0
    try:
        margin_bottom = int(computed_style.get("margin-bottom", "5"))
    except ValueError:
        margin_bottom = 5

    total_height += margin_top

    # Process font size.
    fs_val = computed_style.get("font-size", None)
    if fs_val:
        fs_val = fs_val.strip()
        if fs_val.endswith('%'):
            try:
                percent = float(fs_val.strip('%'))
                font_size = int(base_font.get_height() * percent / 100.0)
            except Exception:
                font_size = base_font.get_height()
        else:
            try:
                font_size = int(fs_val)
            except ValueError:
                font_size = base_font.get_height()
    else:
        font_size = base_font.get_height()

    # Process color, with fallback.
    color_str = computed_style.get("color", "black")
    try:
        color = pygame.Color(color_str)
    except ValueError:
        color = pygame.Color("black")
    
    # Create a new font with the computed font size.
    font = pygame.font.SysFont(None, font_size)
    # Apply simple tag-based styling.
    if element.name == "b":
        font.set_bold(True)
    elif element.name == "i":
        font.set_italic(True)

    # Recursively render children.
    for child in element.children:
        child_height = render_element(surface, child, x, y + total_height, css_styles, font)
        total_height += child_height

    total_height += margin_bottom
    return total_height

def render_html(surface, html, css_text):
    """
    Renders the HTML content onto a Pygame surface.
    Returns the total height used.
    """
    css_styles = parse_css(css_text)
    soup = BeautifulSoup(html, "html.parser")
    
    # Remove tags that are non-renderable.
    for tag in soup.find_all(["script", "style", "meta", "link", "head"]):
        tag.decompose()
    
    y = 10
    base_font = pygame.font.SysFont(None, 24)
    
    # Prefer content inside <body>, if available.
    body = soup.find("body")
    if body:
        for element in body.children:
            y += render_element(surface, element, 10, y, css_styles, base_font)
    else:
        for element in soup.contents:
            y += render_element(surface, element, 10, y, css_styles, base_font)
    return y

def fetch_html(url):
    """
    Download the HTML content from a URL.
    """
    response = requests.get(url)
    response.raise_for_status()
    return response.text

def render_html_to_surface(html, css_text, width):
    """
    Render the HTML to an offscreen surface for scrolling.
    """
    # Create a temporary surface with extra height.
    temp_surface = pygame.Surface((width, 3000))
    rendered_height = render_html(temp_surface, html, css_text)
    
    # If nothing was rendered (common with complex pages), fallback to plain text.
    if rendered_height < 20:
        plain_text = BeautifulSoup(html, "html.parser").get_text()
        temp_surface.fill((255, 255, 255))
        font = pygame.font.SysFont(None, 24)
        y = 10
        for line in plain_text.splitlines():
            line = line.strip()
            if line:
                text_surface = font.render(line, True, pygame.Color("black"))
                temp_surface.blit(text_surface, (10, y))
                y += text_surface.get_height() + 5
        rendered_height = y

    # Create a surface exactly as tall as the rendered content.
    content_surface = pygame.Surface((width, rendered_height))
    content_surface.blit(temp_surface, (0, 0))
    return content_surface, rendered_height

def main():
    pygame.init()
    screen_width, screen_height = 800, 600
    screen = pygame.display.set_mode((screen_width, screen_height))
    pygame.display.set_caption("HTML Renderer with URL Fetch, CSS, and Scrolling")

    # Fetch HTML from a URL if provided; otherwise, use sample HTML.
    if len(sys.argv) > 1:
        url = sys.argv[1]
        try:
            html = fetch_html(url)
        except Exception as e:
            print(f"Error fetching {url}: {e}")
            return
    else:
        html = """
        <html>
          <body>
            <h1>Hello, Pygame HTML Renderer!</h1>
            <p>This is a <b>proof of concept</b> for rendering <i>HTML</i> with CSS in Pygame.</p>
            <p style="color: red;">This paragraph is red!</p>
            <p style="font-size: 150;">This paragraph is larger.</p>
            <p style="margin-top: 20; margin-bottom: 20;">This paragraph has margins!</p>
            <p>Scroll down for more content!</p>
          </body>
        </html>
        """

    # A minimal default CSS.
    css_text = """
    h1 {
      font-size: 36;
      color: blue;
      margin-bottom: 10;
    }
    p {
      font-size: 24;
      color: black;
    }
    """

    # Render the HTML to an offscreen content surface.
    content_surface, content_height = render_html_to_surface(html, css_text, screen_width)
    scroll_offset = 0
    clock = pygame.time.Clock()
    max_scroll = max(0, content_height - screen_height)

    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

            # Handle keyboard scrolling.
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_DOWN:
                    scroll_offset = min(max_scroll, scroll_offset + 20)
                elif event.key == pygame.K_UP:
                    scroll_offset = max(0, scroll_offset - 20)
            # Handle mouse wheel scrolling.
            if event.type == pygame.MOUSEBUTTONDOWN:
                if event.button == 4:  # Wheel up.
                    scroll_offset = max(0, scroll_offset - 20)
                elif event.button == 5:  # Wheel down.
                    scroll_offset = min(max_scroll, scroll_offset + 20)

        screen.fill((255, 255, 255))
        screen.blit(content_surface, (0, -scroll_offset))
        pygame.display.flip()
        clock.tick(30)

    pygame.quit()

if __name__ == "__main__":
    main()