#!/usr/bin/env python3
"""
Reorder member functions in a .cpp file to match the order in corresponding .h file.
Uses libclang for AST-based parsing instead of regex.

⚠️  NOT APPLICABLE TO QT-BASED CODE ⚠️

This script uses libclang to parse C++ files via AST (Abstract Syntax Tree) analysis,
which is theoretically superior to regex-based parsing as it properly handles:
- Preprocessor directives
- Complex C++ syntax (templates, nested classes, etc.)
- Comments and string literals
- Function overloads and const variants

However, it CANNOT parse BornAgain code due to Qt dependencies:

WHY IT FAILS:
1. Qt Headers: BornAgain headers include Qt types (QString, QWidget, etc.)
2. Missing Type Resolution: libclang cannot resolve Qt types without full Qt headers
3. Preprocessing Complexity: Qt uses heavy macro preprocessing (Q_OBJECT, signals, slots)
4. moc Dependencies: Qt's Meta-Object Compiler generates additional code
5. Library Mismatch: Bundled libclang may not properly locate system Qt headers

ATTEMPTS MADE:
- Added Qt include paths: Failed, Qt headers have circular dependencies
- Used compilation database: Failed, libclang raises TranslationUnitLoadError
- Stubbed out includes: Failed, types still unresolved
- Used PARSE_INCOMPLETE option: Still fails before building AST

POTENTIAL SOLUTIONS (not implemented):
- Preprocess files first: clang++ -E to expand all includes/macros
- Use LibTooling directly: C++ tool with better compilation database integration
- Mock Qt headers: Create stub declarations for all Qt types used
- Different Python bindings: Try alternatives to pip's libclang package

For BornAgain, use reorder-functions-regex.py instead, which handles most cases
correctly using pattern matching (though it has edge cases with preprocessor
directives and complex initialization).

Usage:
    reorder-functions-ast.py <file_stem>
"""

import sys
from typing import List, Tuple, Dict, Optional
from dataclasses import dataclass
import clang.cindex
from clang.cindex import CursorKind, AccessSpecifier

@dataclass
class FunctionDecl:
    """Represents a function declaration from a header file."""
    class_name: str
    function_name: str
    signature: str  # Canonical signature for matching
    line_number: int
    is_const: bool
    is_virtual: bool
    is_pure_virtual: bool
    is_static: bool

@dataclass
class FunctionDef:
    """Represents a function definition from a cpp file."""
    class_name: str
    function_name: str
    signature: str  # Canonical signature for matching
    start_offset: int
    end_offset: int
    is_const: bool
    is_static: bool
    full_text: str

def init_libclang():
    """Initialize libclang with system includes."""
    # Try to find libclang automatically, or use libclang-19
    import os
    import glob

    # Check if libclang package provides it
    try:
        from clang.cindex import conf
        # libclang package comes with its own library
        return
    except:
        pass

    # Search for system libclang
    search_paths = [
        '/usr/lib/x86_64-linux-gnu/libclang-*.so.1',
        '/usr/lib/llvm-*/lib/libclang-*.so.1',
    ]

    for pattern in search_paths:
        matches = glob.glob(pattern)
        if matches:
            # Use the first match (typically highest version)
            clang.cindex.Config.set_library_file(sorted(matches)[-1])
            return

    # If nothing found, let libclang try its defaults
    pass

def get_canonical_signature(cursor) -> str:
    """
    Get a canonical signature string for a function.
    This is used for matching declarations to definitions.
    """
    # Get parameter types
    param_types = []
    for arg in cursor.get_arguments():
        param_types.append(arg.type.spelling)

    # Include const qualifier
    const_str = " const" if cursor.is_const_method() else ""

    return f"{cursor.spelling}({', '.join(param_types)}){const_str}"

def is_constructor_or_destructor(cursor, class_name: str) -> bool:
    """Check if cursor is a constructor or destructor."""
    name = cursor.spelling
    return name == class_name or name == f"~{class_name}"

def get_compile_args() -> List[str]:
    """Get compilation arguments by reading from compilation database or using defaults."""
    import json
    import os

    # Try to read from compilation database
    comp_db_path = '/G/sw/ba/build/compile_commands.json'
    if os.path.exists(comp_db_path):
        try:
            with open(comp_db_path) as f:
                commands = json.load(f)
            if commands:
                # Extract include paths and defines from first command
                args = []
                for cmd_entry in commands:
                    cmd = cmd_entry.get('command', '')
                    parts = cmd.split()
                    for i, part in enumerate(parts):
                        if part.startswith('-I') or part.startswith('-D') or part == '-std=c++20':
                            if part in ['-I', '-D'] and i + 1 < len(parts):
                                args.append(part + parts[i+1])
                            else:
                                args.append(part)
                if args:
                    return list(set(args))  # Remove duplicates
        except:
            pass

    # Default args
    return [
        '-std=c++20',
        '-I/G/sw/ba',
        '-I/G/sw/ba/build/inc',
        '-I/G/sw/ba/3rdparty/Core',
        '-I/usr/include/x86_64-linux-gnu/qt6',
        '-I/usr/include/x86_64-linux-gnu/qt6/QtCore',
        '-I/usr/include/x86_64-linux-gnu/qt6/QtWidgets',
        '-I/usr/include/x86_64-linux-gnu/qt6/QtGui',
        '-DQT_CORE_LIB',
        '-DBORNAGAIN_PYTHON',
        '-w',  # Suppress warnings
    ]

def extract_function_declarations(header_file: str) -> List[FunctionDecl]:
    """Extract member function declarations from header file using AST."""
    index = clang.cindex.Index.create()

    args = get_compile_args()

    # Parse with options to skip function bodies and continue on errors
    options = (
        clang.cindex.TranslationUnit.PARSE_SKIP_FUNCTION_BODIES |
        clang.cindex.TranslationUnit.PARSE_INCOMPLETE
    )

    try:
        tu = index.parse(header_file, args=args, options=options)
    except clang.cindex.TranslationUnitLoadError:
        # Try with even more lenient options
        print(f"Warning: Initial parse failed, trying with minimal options...")
        args = ['-std=c++20', '-w']
        tu = index.parse(header_file, args=args, options=options)

    if not tu:
        raise RuntimeError(f"Failed to parse {header_file}")

    # Check for fatal errors
    fatal_errors = [d for d in tu.diagnostics
                   if d.severity >= clang.cindex.Diagnostic.Error]
    if fatal_errors:
        print(f"Warning: {len(fatal_errors)} errors while parsing {header_file}:")
        for diag in fatal_errors[:5]:  # Show first 5
            print(f"  {diag}")
        # Continue anyway - we might still extract useful information

    declarations = []

    def visit_class(cursor, class_name):
        """Visit members of a class."""
        for child in cursor.get_children():
            # Only look at member functions
            if child.kind != CursorKind.CXX_METHOD:
                continue

            # Skip if defined inline in header
            if child.is_definition():
                continue

            # Skip constructors and destructors
            if is_constructor_or_destructor(child, class_name):
                continue

            # Skip operators
            if child.spelling.startswith('operator'):
                continue

            # Check if pure virtual
            is_pure = child.is_pure_virtual_method()

            # Skip pure virtual functions
            if is_pure:
                continue

            # Get location
            location = child.location
            if not location.file or location.file.name != header_file:
                continue

            decl = FunctionDecl(
                class_name=class_name,
                function_name=child.spelling,
                signature=get_canonical_signature(child),
                line_number=location.line,
                is_const=child.is_const_method(),
                is_virtual=child.is_virtual_method(),
                is_pure_virtual=is_pure,
                is_static=child.is_static_method()
            )
            declarations.append(decl)

    def visit_node(cursor):
        """Recursively visit AST nodes."""
        if cursor.kind == CursorKind.CLASS_DECL or cursor.kind == CursorKind.STRUCT_DECL:
            # Found a class/struct definition
            class_name = cursor.spelling
            if class_name:  # Skip anonymous classes
                visit_class(cursor, class_name)

        # Recurse into children
        for child in cursor.get_children():
            # Only visit declarations in this file
            if child.location.file and child.location.file.name == header_file:
                visit_node(child)

    visit_node(tu.cursor)

    return declarations

def extract_function_definitions(cpp_file: str) -> List[FunctionDef]:
    """Extract member function definitions from cpp file using AST."""
    index = clang.cindex.Index.create()

    args = get_compile_args()

    # Parse with options to continue on errors
    options = clang.cindex.TranslationUnit.PARSE_INCOMPLETE

    try:
        tu = index.parse(cpp_file, args=args, options=options)
    except clang.cindex.TranslationUnitLoadError:
        # Try with minimal options
        print(f"Warning: Initial parse failed, trying with minimal options...")
        args = ['-std=c++20', '-w']
        tu = index.parse(cpp_file, args=args, options=options)

    if not tu:
        raise RuntimeError(f"Failed to parse {cpp_file}")

    # Check for fatal errors
    fatal_errors = [d for d in tu.diagnostics
                   if d.severity >= clang.cindex.Diagnostic.Error]
    if fatal_errors:
        print(f"Warning: {len(fatal_errors)} errors while parsing {cpp_file}:")
        for diag in fatal_errors[:5]:  # Show first 5
            print(f"  {diag}")
        # Continue anyway

    # Read the source file for text extraction
    with open(cpp_file, 'r') as f:
        source_text = f.read()

    definitions = []

    def visit_node(cursor):
        """Recursively visit AST nodes."""
        # Look for CXX_METHOD that is a definition (not declaration)
        if cursor.kind == CursorKind.CXX_METHOD and cursor.is_definition():
            # Check if this is in the cpp file (not in included headers)
            location = cursor.location
            if not location.file or location.file.name != cpp_file:
                for child in cursor.get_children():
                    visit_node(child)
                return

            # Get the semantic parent (the class)
            semantic_parent = cursor.semantic_parent
            if semantic_parent.kind not in (CursorKind.CLASS_DECL, CursorKind.STRUCT_DECL):
                for child in cursor.get_children():
                    visit_node(child)
                return

            class_name = semantic_parent.spelling

            # Skip constructors and destructors
            if is_constructor_or_destructor(cursor, class_name):
                for child in cursor.get_children():
                    visit_node(child)
                return

            # Skip operators
            if cursor.spelling.startswith('operator'):
                for child in cursor.get_children():
                    visit_node(child)
                return

            # Get extent (source range)
            extent = cursor.extent
            start_offset = extent.start.offset
            end_offset = extent.end.offset

            # Extract the full text
            full_text = source_text[start_offset:end_offset]

            # Make sure we capture complete lines
            # Walk back to start of line
            while start_offset > 0 and source_text[start_offset - 1] not in '\n':
                start_offset -= 1

            # Walk forward to end of line (including newlines after closing brace)
            while end_offset < len(source_text) and source_text[end_offset] in '\n':
                end_offset += 1

            full_text = source_text[start_offset:end_offset]

            definition = FunctionDef(
                class_name=class_name,
                function_name=cursor.spelling,
                signature=get_canonical_signature(cursor),
                start_offset=start_offset,
                end_offset=end_offset,
                is_const=cursor.is_const_method(),
                is_static=cursor.is_static_method(),
                full_text=full_text
            )
            definitions.append(definition)

        # Recurse into children
        for child in cursor.get_children():
            visit_node(child)

    visit_node(tu.cursor)

    return definitions

def match_functions(declarations: List[FunctionDecl],
                   definitions: List[FunctionDef]) -> List[Tuple[FunctionDecl, FunctionDef]]:
    """Match declarations from header to definitions in cpp."""
    matches = []

    # Build a lookup dict for definitions
    def_dict = {}
    for defn in definitions:
        key = (defn.class_name, defn.signature)
        if key in def_dict:
            # Handle overloads - store as list
            if not isinstance(def_dict[key], list):
                def_dict[key] = [def_dict[key]]
            def_dict[key].append(defn)
        else:
            def_dict[key] = defn

    # Match each declaration
    for decl in declarations:
        key = (decl.class_name, decl.signature)
        if key in def_dict:
            defn = def_dict[key]
            if isinstance(defn, list):
                # Multiple definitions - take first one
                defn = defn[0]
            matches.append((decl, defn))

    return matches

def reorder_cpp_file(header_file: str, cpp_file: str) -> bool:
    """Reorder functions in cpp_file to match order in header_file."""
    init_libclang()

    print(f"Parsing {header_file}...")
    declarations = extract_function_declarations(header_file)

    if not declarations:
        print(f"No member functions found in {header_file}")
        return False

    print(f"Found {len(declarations)} declarations in header")

    print(f"Parsing {cpp_file}...")
    definitions = extract_function_definitions(cpp_file)

    if not definitions:
        print(f"No member function implementations found in {cpp_file}")
        return False

    print(f"Found {len(definitions)} definitions in cpp")

    # Match declarations to definitions
    matches = match_functions(declarations, definitions)

    if not matches:
        print(f"No matching functions between {header_file} and {cpp_file}")
        return False

    print(f"Matched {len(matches)} functions")

    # Check if already in order
    current_order = sorted(matches, key=lambda m: m[1].start_offset)
    if matches == current_order:
        print(f"Functions already in correct order in {cpp_file}")
        return False

    # Read the source file
    with open(cpp_file, 'r') as f:
        source_text = f.read()

    # Find the span of all matched functions
    all_defs = [m[1] for m in matches]
    first_offset = min(d.start_offset for d in all_defs)
    last_offset = max(d.end_offset for d in all_defs)

    # Build reordered content
    result = source_text[:first_offset]

    for decl, defn in matches:
        result += defn.full_text
        if not defn.full_text.endswith('\n'):
            result += '\n'

    result += source_text[last_offset:]

    # Write back
    with open(cpp_file, 'w') as f:
        f.write(result)

    print(f"Reordered {len(matches)} functions in {cpp_file}")
    return True

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("Usage: reorder-functions-ast.py <file_stem>")
        sys.exit(1)

    stem = sys.argv[1]
    try:
        changed = reorder_cpp_file(stem + '.h', stem + '.cpp')
        sys.exit(0 if changed else 1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        import traceback
        traceback.print_exc()
        sys.exit(2)
