#!/usr/bin/env python3
"""Generate high-quality HTML API reference documentation for McRogueFace."""
import os
import sys
import datetime
import html
from pathlib import Path
import mcrfpy
def escape_html(text: str) -> str:
    """Escape HTML special characters."""
    return html.escape(text) if text else ""
def format_docstring_as_html(docstring: str) -> str:
    """Convert docstring to properly formatted HTML."""
    if not docstring:
        return ""
    
    # Split and process lines
    lines = docstring.strip().split('\n')
    result = []
    in_code_block = False
    
    for line in lines:
        # Convert \n to actual newlines
        line = line.replace('\\n', '\n')
        
        # Handle code blocks
        if line.strip().startswith('```'):
            if in_code_block:
                result.append('')
                in_code_block = False
            else:
                result.append('')
    
    return '\n'.join(result)
def get_class_details(cls):
    """Get detailed information about a class."""
    info = {
        'name': cls.__name__,
        'doc': cls.__doc__ or "",
        'methods': {},
        'properties': {},
        'bases': []
    }
    
    # Get real base classes (excluding object)
    for base in cls.__bases__:
        if base.__name__ != 'object':
            info['bases'].append(base.__name__)
    
    # Special handling for Entity which doesn't inherit from Drawable
    if cls.__name__ == 'Entity' and 'Drawable' in info['bases']:
        info['bases'].remove('Drawable')
    
    # Get methods and properties
    for attr_name in dir(cls):
        if attr_name.startswith('__') and attr_name != '__init__':
            continue
            
        try:
            attr = getattr(cls, attr_name)
            
            if isinstance(attr, property):
                info['properties'][attr_name] = {
                    'doc': (attr.fget.__doc__ if attr.fget else "") or "",
                    'readonly': attr.fset is None
                }
            elif callable(attr) and not attr_name.startswith('_'):
                info['methods'][attr_name] = attr.__doc__ or ""
        except:
            pass
    
    return info
def generate_class_init_docs(class_name):
    """Generate initialization documentation for specific classes."""
    init_docs = {
        'Entity': {
            'signature': 'Entity(x=0, y=0, sprite_id=0)',
            'description': 'Game entity that can be placed in a Grid.',
            'args': [
                ('x', 'int', 'Grid x coordinate. Default: 0'),
                ('y', 'int', 'Grid y coordinate. Default: 0'),
                ('sprite_id', 'int', 'Sprite index for rendering. Default: 0')
            ],
            'example': '''entity = mcrfpy.Entity(5, 10, 42)
entity.move(1, 0)  # Move right one tile'''
        },
        'Color': {
            'signature': 'Color(r=255, g=255, b=255, a=255)',
            'description': 'RGBA color representation.',
            'args': [
                ('r', 'int', 'Red component (0-255). Default: 255'),
                ('g', 'int', 'Green component (0-255). Default: 255'),
                ('b', 'int', 'Blue component (0-255). Default: 255'),
                ('a', 'int', 'Alpha component (0-255). Default: 255')
            ],
            'example': 'red = mcrfpy.Color(255, 0, 0)'
        },
        'Font': {
            'signature': 'Font(filename)',
            'description': 'Load a font from file.',
            'args': [
                ('filename', 'str', 'Path to font file (TTF/OTF)')
            ]
        },
        'Texture': {
            'signature': 'Texture(filename)',
            'description': 'Load a texture from file.',
            'args': [
                ('filename', 'str', 'Path to image file (PNG/JPG/BMP)')
            ]
        },
        'Vector': {
            'signature': 'Vector(x=0.0, y=0.0)',
            'description': '2D vector for positions and directions.',
            'args': [
                ('x', 'float', 'X component. Default: 0.0'),
                ('y', 'float', 'Y component. Default: 0.0')
            ]
        },
        'Animation': {
            'signature': 'Animation(property_name, start_value, end_value, duration, transition="linear", loop=False)',
            'description': 'Animate UI element properties over time.',
            'args': [
                ('property_name', 'str', 'Property to animate (e.g., "x", "y", "scale")'),
                ('start_value', 'float', 'Starting value'),
                ('end_value', 'float', 'Ending value'),
                ('duration', 'float', 'Duration in seconds'),
                ('transition', 'str', 'Easing function. Default: "linear"'),
                ('loop', 'bool', 'Whether to loop. Default: False')
            ],
            'properties': ['current_value', 'elapsed_time', 'is_running', 'is_finished']
        },
        'GridPoint': {
            'description': 'Represents a single tile in a Grid.',
            'properties': ['x', 'y', 'texture_index', 'solid', 'transparent', 'color']
        },
        'GridPointState': {
            'description': 'State information for a GridPoint.',
            'properties': ['visible', 'discovered', 'custom_flags']
        },
        'Timer': {
            'signature': 'Timer(name, callback, interval_ms)',
            'description': 'Create a recurring timer.',
            'args': [
                ('name', 'str', 'Unique timer identifier'),
                ('callback', 'callable', 'Function to call'),
                ('interval_ms', 'int', 'Interval in milliseconds')
            ]
        }
    }
    
    return init_docs.get(class_name, {})
def generate_method_docs(method_name, class_name):
    """Generate documentation for specific methods."""
    method_docs = {
        # Base Drawable methods (inherited by all UI elements)
        'Drawable': {
            'get_bounds': {
                'signature': 'get_bounds()',
                'description': 'Get the bounding rectangle of this drawable element.',
                'returns': 'tuple: (x, y, width, height) representing the element\'s bounds',
                'note': 'The bounds are in screen coordinates and account for current position and size.'
            },
            'move': {
                'signature': 'move(dx, dy)',
                'description': 'Move the element by a relative offset.',
                'args': [
                    ('dx', 'float', 'Horizontal offset in pixels'),
                    ('dy', 'float', 'Vertical offset in pixels')
                ],
                'note': 'This modifies the x and y position properties by the given amounts.'
            },
            'resize': {
                'signature': 'resize(width, height)',
                'description': 'Resize the element to new dimensions.',
                'args': [
                    ('width', 'float', 'New width in pixels'),
                    ('height', 'float', 'New height in pixels')
                ],
                'note': 'Behavior varies by element type. Some elements may ignore or constrain dimensions.'
            }
        },
        
        # Caption-specific methods
        'Caption': {
            'get_bounds': {
                'signature': 'get_bounds()',
                'description': 'Get the bounding rectangle of the text.',
                'returns': 'tuple: (x, y, width, height) based on text content and font size',
                'note': 'Bounds are automatically calculated from the rendered text dimensions.'
            },
            'move': {
                'signature': 'move(dx, dy)',
                'description': 'Move the caption by a relative offset.',
                'args': [
                    ('dx', 'float', 'Horizontal offset in pixels'),
                    ('dy', 'float', 'Vertical offset in pixels')
                ]
            },
            'resize': {
                'signature': 'resize(width, height)',
                'description': 'Set text wrapping bounds (limited support).',
                'args': [
                    ('width', 'float', 'Maximum width for text wrapping'),
                    ('height', 'float', 'Currently unused')
                ],
                'note': 'Full text wrapping is not yet implemented. This prepares for future multiline support.'
            }
        },
        
        # Entity-specific methods
        'Entity': {
            'at': {
                'signature': 'at(x, y)',
                'description': 'Get the GridPointState at the specified grid coordinates relative to this entity.',
                'args': [
                    ('x', 'int', 'Grid x offset from entity position'),
                    ('y', 'int', 'Grid y offset from entity position')
                ],
                'returns': 'GridPointState: State of the grid point at the specified position',
                'note': 'Requires entity to be associated with a grid. Raises ValueError if not.'
            },
            'die': {
                'signature': 'die()',
                'description': 'Remove this entity from its parent grid.',
                'returns': 'None',
                'note': 'The entity object remains valid but is no longer rendered or updated.'
            },
            'index': {
                'signature': 'index()',
                'description': 'Get the index of this entity in its grid\'s entity collection.',
                'returns': 'int: Zero-based index in the parent grid\'s entity list',
                'note': 'Raises RuntimeError if not associated with a grid, ValueError if not found.'
            },
            'get_bounds': {
                'signature': 'get_bounds()',
                'description': 'Get the bounding rectangle of the entity\'s sprite.',
                'returns': 'tuple: (x, y, width, height) of the sprite bounds',
                'note': 'Delegates to the internal sprite\'s get_bounds method.'
            },
            'move': {
                'signature': 'move(dx, dy)',
                'description': 'Move the entity by a relative offset in pixels.',
                'args': [
                    ('dx', 'float', 'Horizontal offset in pixels'),
                    ('dy', 'float', 'Vertical offset in pixels')
                ],
                'note': 'Updates both sprite position and entity grid position.'
            },
            'resize': {
                'signature': 'resize(width, height)',
                'description': 'Entities do not support direct resizing.',
                'args': [
                    ('width', 'float', 'Ignored'),
                    ('height', 'float', 'Ignored')
                ],
                'note': 'This method exists for interface compatibility but has no effect.'
            }
        },
        
        # Frame-specific methods
        'Frame': {
            'get_bounds': {
                'signature': 'get_bounds()',
                'description': 'Get the bounding rectangle of the frame.',
                'returns': 'tuple: (x, y, width, height) representing the frame bounds'
            },
            'move': {
                'signature': 'move(dx, dy)',
                'description': 'Move the frame and all its children by a relative offset.',
                'args': [
                    ('dx', 'float', 'Horizontal offset in pixels'),
                    ('dy', 'float', 'Vertical offset in pixels')
                ],
                'note': 'Child elements maintain their relative positions within the frame.'
            },
            'resize': {
                'signature': 'resize(width, height)',
                'description': 'Resize the frame to new dimensions.',
                'args': [
                    ('width', 'float', 'New width in pixels'),
                    ('height', 'float', 'New height in pixels')
                ],
                'note': 'Does not automatically resize children. Set clip_children=True to clip overflow.'
            }
        },
        
        # Grid-specific methods
        'Grid': {
            'at': {
                'signature': 'at(x, y) or at((x, y))',
                'description': 'Get the GridPoint at the specified grid coordinates.',
                'args': [
                    ('x', 'int', 'Grid x coordinate (0-based)'),
                    ('y', 'int', 'Grid y coordinate (0-based)')
                ],
                'returns': 'GridPoint: The grid point at (x, y)',
                'note': 'Raises IndexError if coordinates are out of range. Accepts either two arguments or a tuple.',
                'example': 'point = grid.at(5, 3)  # or grid.at((5, 3))'
            },
            'get_bounds': {
                'signature': 'get_bounds()',
                'description': 'Get the bounding rectangle of the entire grid.',
                'returns': 'tuple: (x, y, width, height) of the grid\'s display area'
            },
            'move': {
                'signature': 'move(dx, dy)',
                'description': 'Move the grid display by a relative offset.',
                'args': [
                    ('dx', 'float', 'Horizontal offset in pixels'),
                    ('dy', 'float', 'Vertical offset in pixels')
                ],
                'note': 'Moves the entire grid viewport. Use center property to pan within the grid.'
            },
            'resize': {
                'signature': 'resize(width, height)',
                'description': 'Resize the grid\'s display viewport.',
                'args': [
                    ('width', 'float', 'New viewport width in pixels'),
                    ('height', 'float', 'New viewport height in pixels')
                ],
                'note': 'Changes the visible area, not the grid dimensions. Use zoom to scale content.'
            }
        },
        
        # Sprite-specific methods
        'Sprite': {
            'get_bounds': {
                'signature': 'get_bounds()',
                'description': 'Get the bounding rectangle of the sprite.',
                'returns': 'tuple: (x, y, width, height) based on texture size and scale',
                'note': 'Bounds account for current scale. Returns (x, y, 0, 0) if no texture.'
            },
            'move': {
                'signature': 'move(dx, dy)',
                'description': 'Move the sprite by a relative offset.',
                'args': [
                    ('dx', 'float', 'Horizontal offset in pixels'),
                    ('dy', 'float', 'Vertical offset in pixels')
                ]
            },
            'resize': {
                'signature': 'resize(width, height)',
                'description': 'Resize the sprite by adjusting its scale.',
                'args': [
                    ('width', 'float', 'Target width in pixels'),
                    ('height', 'float', 'Target height in pixels')
                ],
                'note': 'Calculates and applies uniform scale to best fit the target dimensions.'
            }
        },
        
        'Animation': {
            'get_current_value': {
                'signature': 'get_current_value()',
                'description': 'Get the current interpolated value.',
                'returns': 'float: Current animation value'
            },
            'start': {
                'signature': 'start(target)',
                'description': 'Start the animation on a target UI element.',
                'args': [('target', 'UIDrawable', 'The element to animate')]
            }
        },
        
        # Collection methods (shared by EntityCollection and UICollection)
        'EntityCollection': {
            'append': {
                'signature': 'append(entity)',
                'description': 'Add an entity to the end of the collection.',
                'args': [
                    ('entity', 'Entity', 'The entity to add')
                ]
            },
            'remove': {
                'signature': 'remove(entity)',
                'description': 'Remove the first occurrence of an entity from the collection.',
                'args': [
                    ('entity', 'Entity', 'The entity to remove')
                ],
                'note': 'Raises ValueError if entity is not found.'
            },
            'extend': {
                'signature': 'extend(iterable)',
                'description': 'Add multiple entities from an iterable.',
                'args': [
                    ('iterable', 'iterable', 'An iterable of Entity objects')
                ]
            },
            'count': {
                'signature': 'count(entity)',
                'description': 'Count occurrences of an entity in the collection.',
                'args': [
                    ('entity', 'Entity', 'The entity to count')
                ],
                'returns': 'int: Number of times the entity appears'
            },
            'index': {
                'signature': 'index(entity)',
                'description': 'Find the index of the first occurrence of an entity.',
                'args': [
                    ('entity', 'Entity', 'The entity to find')
                ],
                'returns': 'int: Zero-based index of the entity',
                'note': 'Raises ValueError if entity is not found.'
            }
        },
        
        'UICollection': {
            'append': {
                'signature': 'append(drawable)',
                'description': 'Add a drawable element to the end of the collection.',
                'args': [
                    ('drawable', 'Drawable', 'Any UI element (Frame, Caption, Sprite, Grid)')
                ]
            },
            'remove': {
                'signature': 'remove(drawable)',
                'description': 'Remove the first occurrence of a drawable from the collection.',
                'args': [
                    ('drawable', 'Drawable', 'The drawable to remove')
                ],
                'note': 'Raises ValueError if drawable is not found.'
            },
            'extend': {
                'signature': 'extend(iterable)',
                'description': 'Add multiple drawables from an iterable.',
                'args': [
                    ('iterable', 'iterable', 'An iterable of Drawable objects')
                ]
            },
            'count': {
                'signature': 'count(drawable)',
                'description': 'Count occurrences of a drawable in the collection.',
                'args': [
                    ('drawable', 'Drawable', 'The drawable to count')
                ],
                'returns': 'int: Number of times the drawable appears'
            },
            'index': {
                'signature': 'index(drawable)',
                'description': 'Find the index of the first occurrence of a drawable.',
                'args': [
                    ('drawable', 'Drawable', 'The drawable to find')
                ],
                'returns': 'int: Zero-based index of the drawable',
                'note': 'Raises ValueError if drawable is not found.'
            }
        }
    }
    
    return method_docs.get(class_name, {}).get(method_name, {})
def generate_function_docs():
    """Generate documentation for all mcrfpy module functions."""
    function_docs = {
        # Scene Management
        'createScene': {
            'signature': 'createScene(name: str) -> None',
            'description': 'Create a new empty scene.',
            'args': [
                ('name', 'str', 'Unique name for the new scene')
            ],
            'returns': 'None',
            'exceptions': [
                ('ValueError', 'If a scene with this name already exists')
            ],
            'note': 'The scene is created but not made active. Use setScene() to switch to it.',
            'example': '''mcrfpy.createScene("game")
mcrfpy.createScene("menu")
mcrfpy.setScene("game")'''
        },
        
        'setScene': {
            'signature': 'setScene(scene: str, transition: str = None, duration: float = 0.0) -> None',
            'description': 'Switch to a different scene with optional transition effect.',
            'args': [
                ('scene', 'str', 'Name of the scene to switch to'),
                ('transition', 'str', 'Transition type ("fade", "slide_left", "slide_right", "slide_up", "slide_down"). Default: None'),
                ('duration', 'float', 'Transition duration in seconds. Default: 0.0 for instant')
            ],
            'returns': 'None',
            'exceptions': [
                ('KeyError', 'If the scene doesn\'t exist'),
                ('ValueError', 'If the transition type is invalid')
            ],
            'example': '''mcrfpy.setScene("menu")
mcrfpy.setScene("game", "fade", 0.5)
mcrfpy.setScene("credits", "slide_left", 1.0)'''
        },
        
        'currentScene': {
            'signature': 'currentScene() -> str',
            'description': 'Get the name of the currently active scene.',
            'args': [],
            'returns': 'str: Name of the current scene',
            'example': '''scene = mcrfpy.currentScene()
print(f"Currently in scene: {scene}")'''
        },
        
        'sceneUI': {
            'signature': 'sceneUI(scene: str = None) -> list',
            'description': 'Get all UI elements for a scene.',
            'args': [
                ('scene', 'str', 'Scene name. If None, uses current scene. Default: None')
            ],
            'returns': 'list: All UI elements (Frame, Caption, Sprite, Grid) in the scene',
            'exceptions': [
                ('KeyError', 'If the specified scene doesn\'t exist')
            ],
            'example': '''# Get UI for current scene
ui_elements = mcrfpy.sceneUI()
# Get UI for specific scene
menu_ui = mcrfpy.sceneUI("menu")
for element in menu_ui:
    print(f"{element.name}: {type(element).__name__}")'''
        },
        
        'keypressScene': {
            'signature': 'keypressScene(handler: callable) -> None',
            'description': 'Set the keyboard event handler for the current scene.',
            'args': [
                ('handler', 'callable', 'Function that receives (key_name: str, is_pressed: bool)')
            ],
            'returns': 'None',
            'note': 'The handler is called for every key press and release event. Key names are single characters (e.g., "A", "1") or special keys (e.g., "Space", "Enter", "Escape").',
            'example': '''def on_key(key, pressed):
    if pressed:
        if key == "Space":
            player.jump()
        elif key == "Escape":
            mcrfpy.setScene("pause_menu")
    else:
        # Handle key release
        if key in ["A", "D"]:
            player.stop_moving()
            
mcrfpy.keypressScene(on_key)'''
        },
        
        # Audio Functions
        'createSoundBuffer': {
            'signature': 'createSoundBuffer(filename: str) -> int',
            'description': 'Load a sound effect from a file and return its buffer ID.',
            'args': [
                ('filename', 'str', 'Path to the sound file (WAV, OGG, FLAC)')
            ],
            'returns': 'int: Buffer ID for use with playSound()',
            'exceptions': [
                ('RuntimeError', 'If the file cannot be loaded')
            ],
            'note': 'Sound buffers are stored in memory for fast playback. Load sound effects once and reuse the buffer ID.',
            'example': '''# Load sound effects
jump_sound = mcrfpy.createSoundBuffer("assets/sounds/jump.wav")
coin_sound = mcrfpy.createSoundBuffer("assets/sounds/coin.ogg")
# Play later
mcrfpy.playSound(jump_sound)'''
        },
        
        'loadMusic': {
            'signature': 'loadMusic(filename: str, loop: bool = True) -> None',
            'description': 'Load and immediately play background music from a file.',
            'args': [
                ('filename', 'str', 'Path to the music file (WAV, OGG, FLAC)'),
                ('loop', 'bool', 'Whether to loop the music. Default: True')
            ],
            'returns': 'None',
            'note': 'Only one music track can play at a time. Loading new music stops the current track.',
            'example': '''# Play looping background music
mcrfpy.loadMusic("assets/music/theme.ogg")
# Play music once without looping
mcrfpy.loadMusic("assets/music/victory.ogg", loop=False)'''
        },
        
        'playSound': {
            'signature': 'playSound(buffer_id: int) -> None',
            'description': 'Play a sound effect using a previously loaded buffer.',
            'args': [
                ('buffer_id', 'int', 'Sound buffer ID returned by createSoundBuffer()')
            ],
            'returns': 'None',
            'exceptions': [
                ('RuntimeError', 'If the buffer ID is invalid')
            ],
            'note': 'Multiple sounds can play simultaneously. Each call creates a new sound instance.',
            'example': '''# Load once
explosion_sound = mcrfpy.createSoundBuffer("explosion.wav")
# Play multiple times
for enemy in destroyed_enemies:
    mcrfpy.playSound(explosion_sound)'''
        },
        
        'getMusicVolume': {
            'signature': 'getMusicVolume() -> int',
            'description': 'Get the current music volume level.',
            'args': [],
            'returns': 'int: Current volume (0-100)',
            'example': '''volume = mcrfpy.getMusicVolume()
print(f"Music volume: {volume}%")'''
        },
        
        'getSoundVolume': {
            'signature': 'getSoundVolume() -> int',
            'description': 'Get the current sound effects volume level.',
            'args': [],
            'returns': 'int: Current volume (0-100)',
            'example': '''volume = mcrfpy.getSoundVolume()
print(f"Sound effects volume: {volume}%")'''
        },
        
        'setMusicVolume': {
            'signature': 'setMusicVolume(volume: int) -> None',
            'description': 'Set the global music volume.',
            'args': [
                ('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')
            ],
            'returns': 'None',
            'example': '''# Mute music
mcrfpy.setMusicVolume(0)
# Half volume
mcrfpy.setMusicVolume(50)
# Full volume
mcrfpy.setMusicVolume(100)'''
        },
        
        'setSoundVolume': {
            'signature': 'setSoundVolume(volume: int) -> None',
            'description': 'Set the global sound effects volume.',
            'args': [
                ('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')
            ],
            'returns': 'None',
            'example': '''# Audio settings from options menu
mcrfpy.setSoundVolume(sound_slider.value)
mcrfpy.setMusicVolume(music_slider.value)'''
        },
        
        # UI Utilities
        'find': {
            'signature': 'find(name: str, scene: str = None) -> UIDrawable | None',
            'description': 'Find the first UI element with the specified name.',
            'args': [
                ('name', 'str', 'Exact name to search for'),
                ('scene', 'str', 'Scene to search in. Default: current scene')
            ],
            'returns': 'Frame, Caption, Sprite, Grid, or Entity if found; None otherwise',
            'note': 'Searches scene UI elements and entities within grids. Returns the first match found.',
            'example': '''# Find in current scene
player = mcrfpy.find("player")
if player:
    player.x = 100
    
# Find in specific scene
menu_button = mcrfpy.find("start_button", "main_menu")'''
        },
        
        'findAll': {
            'signature': 'findAll(pattern: str, scene: str = None) -> list',
            'description': 'Find all UI elements matching a name pattern.',
            'args': [
                ('pattern', 'str', 'Name pattern with optional wildcards (* matches any characters)'),
                ('scene', 'str', 'Scene to search in. Default: current scene')
            ],
            'returns': 'list: All matching UI elements and entities',
            'note': 'Supports wildcard patterns for flexible searching.',
            'example': '''# Find all enemies
enemies = mcrfpy.findAll("enemy*")
for enemy in enemies:
    enemy.sprite_id = 0  # Reset sprite
    
# Find all buttons
buttons = mcrfpy.findAll("*_button")
for btn in buttons:
    btn.visible = True
    
# Find exact matches
health_bars = mcrfpy.findAll("health_bar")  # No wildcards = exact match'''
        },
        
        # System Functions
        'exit': {
            'signature': 'exit() -> None',
            'description': 'Cleanly shut down the game engine and exit the application.',
            'args': [],
            'returns': 'None',
            'note': 'This immediately closes the window and terminates the program. Ensure any necessary cleanup is done before calling.',
            'example': '''def quit_game():
    # Save game state
    save_progress()
    
    # Exit
    mcrfpy.exit()'''
        },
        
        'getMetrics': {
            'signature': 'getMetrics() -> dict',
            'description': 'Get current performance metrics.',
            'args': [],
            'returns': '''dict: Performance data with keys:
    - frame_time: Last frame duration in seconds
    - avg_frame_time: Average frame time
    - fps: Frames per second
    - draw_calls: Number of draw calls
    - ui_elements: Total UI element count
    - visible_elements: Visible element count
    - current_frame: Frame counter
    - runtime: Total runtime in seconds''',
            'example': '''metrics = mcrfpy.getMetrics()
print(f"FPS: {metrics['fps']}")
print(f"Frame time: {metrics['frame_time']*1000:.1f}ms")
print(f"Draw calls: {metrics['draw_calls']}")
print(f"Runtime: {metrics['runtime']:.1f}s")
# Performance monitoring
if metrics['fps'] < 30:
    print("Performance warning: FPS below 30")'''
        },
        
        'setTimer': {
            'signature': 'setTimer(name: str, handler: callable, interval: int) -> None',
            'description': 'Create or update a recurring timer.',
            'args': [
                ('name', 'str', 'Unique identifier for the timer'),
                ('handler', 'callable', 'Function called with (runtime: float) parameter'),
                ('interval', 'int', 'Time between calls in milliseconds')
            ],
            'returns': 'None',
            'note': 'If a timer with this name exists, it will be replaced. The handler receives the total runtime in seconds as its argument.',
            'example': '''# Simple repeating timer
def spawn_enemy(runtime):
    enemy = mcrfpy.Entity()
    enemy.x = random.randint(0, 800)
    grid.entities.append(enemy)
    
mcrfpy.setTimer("enemy_spawner", spawn_enemy, 2000)  # Every 2 seconds
# Timer with runtime check
def update_timer(runtime):
    time_left = 60 - runtime
    timer_text.text = f"Time: {int(time_left)}"
    if time_left <= 0:
        mcrfpy.delTimer("game_timer")
        game_over()
        
mcrfpy.setTimer("game_timer", update_timer, 100)  # Update every 100ms'''
        },
        
        'delTimer': {
            'signature': 'delTimer(name: str) -> None',
            'description': 'Stop and remove a timer.',
            'args': [
                ('name', 'str', 'Timer identifier to remove')
            ],
            'returns': 'None',
            'note': 'No error is raised if the timer doesn\'t exist.',
            'example': '''# Stop spawning enemies
mcrfpy.delTimer("enemy_spawner")
# Clean up all game timers
for timer_name in ["enemy_spawner", "powerup_timer", "score_updater"]:
    mcrfpy.delTimer(timer_name)'''
        },
        
        'setScale': {
            'signature': 'setScale(multiplier: float) -> None',
            'description': 'Scale the game window size.',
            'args': [
                ('multiplier', 'float', 'Scale factor (e.g., 2.0 for double size)')
            ],
            'returns': 'None',
            'exceptions': [
                ('ValueError', 'If multiplier is not between 0.2 and 4.0')
            ],
            'note': 'The internal resolution remains 1024x768, but the window is scaled. This is deprecated - use Window.resolution instead.',
            'example': '''# Double the window size
mcrfpy.setScale(2.0)
# Half size window
mcrfpy.setScale(0.5)
# Better approach (not deprecated):
mcrfpy.Window.resolution = (1920, 1080)'''
        }
    }
    
    return function_docs
def generate_collection_docs(class_name):
    """Generate documentation for collection classes."""
    collection_docs = {
        'EntityCollection': {
            'description': 'Container for Entity objects in a Grid. Supports iteration and indexing.',
            'methods': {
                'append': 'Add an entity to the collection',
                'remove': 'Remove an entity from the collection',
                'extend': 'Add multiple entities from an iterable',
                'count': 'Count occurrences of an entity',
                'index': 'Find the index of an entity'
            }
        },
        'UICollection': {
            'description': 'Container for UI drawable elements. Supports iteration and indexing.',
            'methods': {
                'append': 'Add a UI element to the collection',
                'remove': 'Remove a UI element from the collection',
                'extend': 'Add multiple UI elements from an iterable',
                'count': 'Count occurrences of a UI element',
                'index': 'Find the index of a UI element'
            }
        },
        'UICollectionIter': {
            'description': 'Iterator for UICollection. Automatically created when iterating over a UICollection.'
        },
        'UIEntityCollectionIter': {
            'description': 'Iterator for EntityCollection. Automatically created when iterating over an EntityCollection.'
        }
    }
    
    return collection_docs.get(class_name, {})
def format_class_html(cls_info, class_name):
    """Format a class as HTML with proper structure."""
    html_parts = []
    
    # Class header
    html_parts.append(f'')
                in_code_block = True
            continue
            
        # Convert markdown-style code to HTML
        if '`' in line and not in_code_block:
            import re
            line = re.sub(r'`([^`]+)`', r'\1', line)
        
        if in_code_block:
            result.append(escape_html(line))
        else:
            result.append(escape_html(line) + '
')
    
    if in_code_block:
        result.append('
Inherits from: {", ".join(cls_info["bases"])}
') # Get additional documentation init_info = generate_class_init_docs(class_name) collection_info = generate_collection_docs(class_name) # Constructor signature for classes with __init__ if init_info.get('signature'): html_parts.append('')
        html_parts.append(escape_html(init_info['signature']))
        html_parts.append('')
        html_parts.append('{format_docstring_as_html(description)}
') html_parts.append('{arg_name} ({arg_type}){prop_name}{prop_name}{readonly}{method_info["signature"]}{escape_html(method_info["description"])}
') if method_info.get('args'): html_parts.append('Arguments:
') html_parts.append('{arg[0]} ({arg[1]}): {arg[2]}{arg[0]} ({arg[1]})Returns: {escape_html(method_info["returns"])}
') if method_info.get('note'): html_parts.append(f'Note: {escape_html(method_info["note"])}
') else: # Use docstring html_parts.append(f'{method_name}(...){escape_html(method_doc)}
') html_parts.append('')
        html_parts.append(escape_html(init_info['example']))
        html_parts.append('')
        html_parts.append('')
            elif line.strip() and not line.startswith(' '):
                html_parts.append(f'{escape_html(line)}
')
            elif line.strip():
                # Code line
                html_parts.append(escape_html(line))
        html_parts.append('')
        html_parts.append('The mcrfpy.automation module provides testing and automation capabilities for simulating user input and capturing screenshots.
automation.{name}{escape_html(description)}
') html_parts.append('{escape_html(signature)}{escape_html(doc_info["description"])}
') # Arguments if 'args' in doc_info and doc_info['args']: html_parts.append('{escape_html(arg_name)} : {escape_html(arg_type)}{escape_html(doc_info["returns"])}
') html_parts.append('{escape_html(exc_type)}Note: {escape_html(doc_info["note"])}
') html_parts.append('')
            html_parts.append(escape_html(doc_info['example']))
            html_parts.append('')
            html_parts.append('{escape_html(signature)}{in_section}:
') elif in_section == 'Example': if not stripped: continue if stripped.startswith('>>>') or (len(lines) > lines.index(line) + 1 and lines[lines.index(line) + 1].strip().startswith('>>>')): html_parts.append('')
                        html_parts.append(escape_html(stripped))
                        # Get rest of example
                        idx = lines.index(line) + 1
                        while idx < len(lines) and lines[idx].strip():
                            html_parts.append(escape_html(lines[idx]))
                            idx += 1
                        html_parts.append('')
                        break
                elif in_section and stripped:
                    if in_section == 'Args':
                        # Format arguments nicely
                        if ':' in stripped:
                            param, desc = stripped.split(':', 1)
                            html_parts.append(f'{escape_html(param.strip())}: {escape_html(desc.strip())}
{escape_html(stripped)}
') else: html_parts.append(f'{escape_html(stripped)}
') elif stripped and not in_section: html_parts.append(f'{escape_html(stripped)}
') html_parts.append('