#!/usr/bin/env python3
"""
Enhanced Python Code Documentation Generator

This script creates a PyQt application that generates comprehensive markdown documentation 
from Python source files. It extracts complete information about classes, functions, 
methods, variables, decorators and their associated docstrings.

Author: Generated by LoLLMs
Date: 2025-01-16
"""

from pathlib import Path
import ast
import inspect
from typing import Dict, List, Optional, Tuple, Union, Any
from PyQt5.QtWidgets import (
    QApplication,
    QMainWindow,
    QWidget,
    QVBoxLayout,
    QHBoxLayout,
    QPushButton,
    QTextEdit,
    QFileDialog,
    QMessageBox,
    QLabel,
    QStyle
)
from PyQt5.QtGui import QFont, QIcon
from PyQt5.QtCore import Qt
import sys

class CodeParser:
    """
    A class to parse Python source code and extract comprehensive documentation.
    """
    
    def __init__(self):
        """Initialize the CodeParser."""
        self.markdown: str = ""
        self.current_class: Optional[str] = None
        
    def get_decorator_list(self, node: ast.AST) -> str:
        """
        Extract decorator information from a node.
        
        Args:
            node: AST node containing decorators
            
        Returns:
            str: Formatted decorator string
        """
        decorators = []
        for decorator in node.decorator_list:
            if isinstance(decorator, ast.Name):
                decorators.append(f"@{decorator.id}")
            elif isinstance(decorator, ast.Call):
                if isinstance(decorator.func, ast.Name):
                    args = []
                    for arg in decorator.args:
                        if isinstance(arg, ast.Constant):
                            args.append(str(arg.value))
                    if args:
                        decorators.append(f"@{decorator.func.id}({', '.join(args)})")
                    else:
                        decorators.append(f"@{decorator.func.id}()")
        return "\n".join(decorators)

    def get_arguments(self, node: ast.AST) -> str:
        """
        Extract function arguments information.
        
        Args:
            node: AST node containing function arguments
            
        Returns:
            str: Formatted argument string
        """
        args = []
        
        # Process arguments
        for arg in node.args.args:
            arg_str = arg.arg
            if hasattr(arg, 'annotation') and arg.annotation is not None:
                if isinstance(arg.annotation, ast.Name):
                    arg_str += f": {arg.annotation.id}"
                elif isinstance(arg.annotation, ast.Subscript):
                    arg_str += f": {ast.unparse(arg.annotation)}"
            args.append(arg_str)
            
        # Process default values
        defaults = node.args.defaults
        if defaults:
            default_offset = len(args) - len(defaults)
            for i, default in enumerate(defaults):
                args[default_offset + i] += f" = {ast.unparse(default)}"
        
        # Process variable arguments
        if node.args.vararg:
            args.append(f"*{node.args.vararg.arg}")
        
        # Process keyword arguments
        if node.args.kwarg:
            args.append(f"**{node.args.kwarg.arg}")
            
        return ", ".join(args)

    def get_return_annotation(self, node: ast.AST) -> str:
        """
        Extract return type annotation.
        
        Args:
            node: AST node containing return annotation
            
        Returns:
            str: Formatted return type string
        """
        if node.returns:
            return f" -> {ast.unparse(node.returns)}"
        return ""

    def parse_assign(self, node: ast.AST, level: int) -> None:
        """
        Parse assignment nodes to extract class/module variables.
        
        Args:
            node: AST assignment node
            level: Current nesting level
        """
        for target in node.targets:
            if isinstance(target, ast.Name):
                name = target.id
                value = ast.unparse(node.value)
                if self.current_class:
                    self.markdown += f"{'#' * (level + 3)} Class Variable: {name} = {value}\n\n"
                else:
                    self.markdown += f"{'#' * (level + 2)} Module Variable: {name} = {value}\n\n"

    def parse_class_bases(self, node: ast.ClassDef) -> str:
        """
        Extract class inheritance information.
        
        Args:
            node: Class definition node
            
        Returns:
            str: Formatted base classes string
        """
        bases = []
        for base in node.bases:
            if isinstance(base, ast.Name):
                bases.append(base.id)
            elif isinstance(base, ast.Attribute):
                bases.append(ast.unparse(base))
        return ", ".join(bases)

    def parse_node(self, node: ast.AST, level: int = 0) -> None:
        """
        Parse an AST node and extract relevant documentation.
        
        Args:
            node: The AST node to parse
            level: The current nesting level for markdown formatting
        """
        # Handle class definitions
        if isinstance(node, ast.ClassDef):
            bases = self.parse_class_bases(node)
            class_decor = self.get_decorator_list(node)
            
            if class_decor:
                self.markdown += f"```python\n{class_decor}\n```\n"
            
            self.markdown += f"{'#' * (level + 2)} Class: {node.name}"
            if bases:
                self.markdown += f" ({bases})"
            self.markdown += "\n\n"
            
            if ast.get_docstring(node):
                self.markdown += f"```python\n{ast.get_docstring(node)}\n```\n\n"
            
            # Parse class body
            self.current_class = node.name
            for child in node.body:
                if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Assign)):
                    self.parse_node(child, level + 1)
            self.current_class = None
                    
        # Handle function definitions
        elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            prefix = "Method" if self.current_class else "Function"
            is_async = isinstance(node, ast.AsyncFunctionDef)
            
            # Get decorators
            decorators = self.get_decorator_list(node)
            if decorators:
                self.markdown += f"```python\n{decorators}\n```\n"
            
            # Function signature
            args = self.get_arguments(node)
            returns = self.get_return_annotation(node)
            async_prefix = "async " if is_async else ""
            
            self.markdown += f"{'#' * (level + 2)} {prefix}: {async_prefix}{node.name}({args}){returns}\n\n"
            
            # Function docstring
            if ast.get_docstring(node):
                self.markdown += f"```python\n{ast.get_docstring(node)}\n```\n\n"
                
        # Handle assignments
        elif isinstance(node, ast.Assign):
            self.parse_assign(node, level)

    def parse_imports(self, tree: ast.AST) -> None:
        """
        Parse and format import statements.
        
        Args:
            tree: AST tree
        """
        imports = []
        for node in tree.body:
            if isinstance(node, ast.Import):
                for name in node.names:
                    imports.append(f"import {name.name}")
            elif isinstance(node, ast.ImportFrom):
                names = ", ".join(name.name for name in node.names)
                imports.append(f"from {node.module} import {names}")
        
        if imports:
            self.markdown += "## Imports\n\n```python\n"
            self.markdown += "\n".join(imports)
            self.markdown += "\n```\n\n"
    
    def parse_file(self, file_path: Path) -> str:
        """
        Parse a Python file and generate comprehensive markdown documentation.
        
        Args:
            file_path: Path to the Python file
            
        Returns:
            str: Generated markdown documentation
        """
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                source = f.read()
            
            self.markdown = f"# Documentation for {file_path.name}\n\n"
            
            tree = ast.parse(source)
            
            # Get module docstring
            module_doc = ast.get_docstring(tree)
            if module_doc:
                self.markdown += f"## Module Documentation\n\n```python\n{module_doc}\n```\n\n"
            
            # Parse imports
            self.parse_imports(tree)
            
            # Parse all top-level nodes
            for node in tree.body:
                if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef, ast.Assign)):
                    self.parse_node(node)
                
            return self.markdown
            
        except Exception as e:
            return f"Error parsing file: {str(e)}"

class MainWindow(QMainWindow):
    """
    Main application window.
    """
    
    def __init__(self):
        """Initialize the main window."""
        super().__init__()
        self.setWindowTitle("Python Code Documentation Generator")
        self.setMinimumSize(800, 600)
        
        # Initialize UI
        self.init_ui()
        
    def init_ui(self) -> None:
        """Initialize the user interface."""
        # Create central widget and layout
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)
        
        # Create top button bar
        button_layout = QHBoxLayout()
        
        # Open file button
        self.open_btn = QPushButton("Open Python File")
        self.open_btn.clicked.connect(self.open_file)
        self.open_btn.setIcon(self.style().standardIcon(QStyle.SP_DialogOpenButton))
        button_layout.addWidget(self.open_btn)
        
        # Copy button
        self.copy_btn = QPushButton("Copy to Clipboard")
        self.copy_btn.clicked.connect(self.copy_to_clipboard)
        self.copy_btn.setIcon(self.style().standardIcon(QStyle.SP_DialogSaveButton))
        button_layout.addWidget(self.copy_btn)
        
        # Export button
        self.export_btn = QPushButton("Export Markdown")
        self.export_btn.clicked.connect(self.export_markdown)
        self.export_btn.setIcon(self.style().standardIcon(QStyle.SP_DialogSaveButton))
        button_layout.addWidget(self.export_btn)
        
        layout.addLayout(button_layout)
        
        # Create text display area
        self.text_edit = QTextEdit()
        self.text_edit.setFont(QFont("Consolas", 10))
        self.text_edit.setReadOnly(True)
        layout.addWidget(self.text_edit)
        
        # Status bar
        self.statusBar().showMessage("Ready")
        
    def open_file(self) -> None:
        """Handle file opening."""
        file_path, _ = QFileDialog.getOpenFileName(
            self,
            "Select Python File",
            str(Path.home()),
            "Python Files (*.py)"
        )
        
        if file_path:
            parser = CodeParser()
            markdown = parser.parse_file(Path(file_path))
            self.text_edit.setPlainText(markdown)
            self.statusBar().showMessage(f"Loaded: {file_path}")
            
    def copy_to_clipboard(self) -> None:
        """Copy the generated markdown to clipboard."""
        QApplication.clipboard().setText(self.text_edit.toPlainText())
        self.statusBar().showMessage("Copied to clipboard!")
        
    def export_markdown(self) -> None:
        """Export the markdown to a file."""
        file_path, _ = QFileDialog.getSaveFileName(
            self,
            "Save Markdown File",
            str(Path.home()),
            "Markdown Files (*.md)"
        )
        
        if file_path:
            try:
                with open(file_path, 'w', encoding='utf-8') as f:
                    f.write(self.text_edit.toPlainText())
                self.statusBar().showMessage(f"Saved to: {file_path}")
            except Exception as e:
                QMessageBox.critical(self, "Error", f"Failed to save file: {str(e)}")

def main():
    """Main application entry point."""
    app = QApplication(sys.argv)
    app.setStyle('Fusion')
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()
