#!/usr/bin/env python3
# Copyright (c) 2025 OPEN CASCADE SAS
#
# This file is part of Open CASCADE Technology software library.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License version 2.1 as published
# by the Free Software Foundation, with special exception defined in the file
# OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
# distribution for complete text of the license and disclaimer of any warranty.
#
# Alternatively, this file may be used under the terms of Open CASCADE
# commercial license or contractual agreement.

"""
OCCT 8.0.0 Typedef Cleanup Apply Script

Applies changes from the JSON file generated by collect_unused_typedefs.py:
- Deletes typedef-only header files
- Updates files that included deleted headers (adds includes and forward declarations)
- Removes unused typedef lines from files with other content

Usage:
    python3 apply_typedef_cleanup.py [options] <src_directory>

Options:
    --dry-run    Show what would be done without making changes
    --verbose    Show detailed progress
    --input      Input JSON file (default: unused_typedefs.json)
"""

import argparse
import json
import re
from pathlib import Path
from typing import Dict, List, Set
from dataclasses import dataclass, field


@dataclass
class ApplyResult:
    """Result of apply operations."""
    files_deleted: List[str] = field(default_factory=list)
    files_modified: List[str] = field(default_factory=list)
    errors: List[str] = field(default_factory=list)


class TypedefCleanupApplier:
    """Applies typedef cleanup changes from JSON."""

    def __init__(self, src_dir: str, dry_run: bool = False, verbose: bool = False):
        self.src_dir = Path(src_dir)
        self.dry_run = dry_run
        self.verbose = verbose

    def log(self, message: str):
        """Print message if verbose."""
        if self.verbose:
            print(message)

    def update_includer_file(self, filepath: Path, old_include: str,
                              new_includes: List[str],
                              forward_decls: List[str]) -> bool:
        """
        Update a file that included a deleted typedef-only header:
        - Replace the old include with the new includes
        - Add forward declarations after all #include lines
        """
        try:
            content = filepath.read_text(encoding='utf-8', errors='replace')
        except Exception as e:
            self.log(f"Error reading {filepath}: {e}")
            return False

        lines = content.split('\n')
        new_lines = []
        include_pattern = re.compile(
            r'#include\s*[<"]' + re.escape(old_include) + r'[>"]'
        )

        # Track the last #include line position for inserting forward decls
        last_include_idx = -1
        include_replaced = False

        for line in lines:
            stripped = line.strip()

            # Track last include position
            if stripped.startswith('#include'):
                last_include_idx = len(new_lines)

            # Check if this is the include to replace
            if include_pattern.match(stripped):
                # Replace with new includes (avoiding duplicates)
                for new_inc in new_includes:
                    # Check if this include already exists in the file
                    inc_name = re.search(r'#include\s*[<"]([^>"]+)[>"]', new_inc)
                    if inc_name:
                        inc_file = inc_name.group(1)
                        if not re.search(r'#include\s*[<"]' + re.escape(inc_file) + r'[>"]', content):
                            new_lines.append(new_inc)
                            last_include_idx = len(new_lines) - 1
                include_replaced = True
            else:
                new_lines.append(line)

        if not include_replaced:
            return False

        # Insert forward declarations after the last #include line
        if forward_decls and last_include_idx >= 0:
            # Find existing forward declarations to avoid duplicates
            existing_fwd = set()
            for line in new_lines:
                fwd_match = re.match(r'^(class|struct)\s+(\w+)\s*;$', line.strip())
                if fwd_match:
                    existing_fwd.add(line.strip())

            # Filter out duplicates
            new_fwd_decls = [fd for fd in forward_decls if fd not in existing_fwd]

            if new_fwd_decls:
                # Insert after last include with empty line before
                insert_pos = last_include_idx + 1
                # Check if there's already an empty line
                if insert_pos < len(new_lines) and new_lines[insert_pos].strip():
                    new_lines.insert(insert_pos, '')
                    insert_pos += 1
                for fd in new_fwd_decls:
                    new_lines.insert(insert_pos, fd)
                    insert_pos += 1

        new_content = '\n'.join(new_lines)

        # Clean up multiple blank lines
        new_content = re.sub(r'\n{3,}', '\n\n', new_content)

        if new_content != content:
            if not self.dry_run:
                filepath.write_text(new_content, encoding='utf-8')
            return True

        return False

    def delete_file(self, filepath: Path) -> bool:
        """Delete a file."""
        if not self.dry_run:
            try:
                filepath.unlink()
                return True
            except Exception as e:
                self.log(f"Error deleting {filepath}: {e}")
                return False
        return True  # Dry run always succeeds

    def remove_from_files_cmake(self, filepath: Path) -> bool:
        """Remove a file entry from the FILES.cmake in the same directory."""
        files_cmake = filepath.parent / "FILES.cmake"
        if not files_cmake.exists():
            self.log(f"  No FILES.cmake found in {filepath.parent}")
            return False

        try:
            content = files_cmake.read_text(encoding='utf-8', errors='replace')
        except Exception as e:
            self.log(f"Error reading {files_cmake}: {e}")
            return False

        filename = filepath.name
        lines = content.split('\n')
        new_lines = []
        removed = False

        for line in lines:
            # Check if this line contains the filename
            # Handle both "  filename.hxx" and "  filename.hxx)" formats
            stripped = line.strip()
            if stripped == filename or stripped == filename + ')':
                removed = True
                # If line ends with ), we need to add ) to previous line
                if stripped.endswith(')') and new_lines:
                    # Find the last non-empty line and add ) to it
                    for i in range(len(new_lines) - 1, -1, -1):
                        if new_lines[i].strip():
                            if not new_lines[i].rstrip().endswith(')'):
                                new_lines[i] = new_lines[i].rstrip()
                            break
                continue
            new_lines.append(line)

        if removed:
            new_content = '\n'.join(new_lines)
            if not self.dry_run:
                files_cmake.write_text(new_content, encoding='utf-8')
            self.log(f"  Removed {filename} from FILES.cmake")
            return True

        return False

    def remove_typedefs_from_file(self, filepath: Path,
                                   typedefs: List[Dict]) -> bool:
        """Remove specified typedef lines from a file."""
        try:
            content = filepath.read_text(encoding='utf-8', errors='replace')
        except Exception as e:
            self.log(f"Error reading {filepath}: {e}")
            return False

        lines = content.split('\n')

        # Collect all line ranges to remove
        lines_to_remove: Set[int] = set()
        for typedef in typedefs:
            for line_num in range(typedef['start_line'], typedef['end_line'] + 1):
                lines_to_remove.add(line_num)

        # Build new content without the typedef lines
        new_lines = []
        for i, line in enumerate(lines, 1):
            if i not in lines_to_remove:
                new_lines.append(line)

        new_content = '\n'.join(new_lines)

        # Clean up multiple blank lines
        new_content = re.sub(r'\n{3,}', '\n\n', new_content)

        if new_content != content:
            if not self.dry_run:
                filepath.write_text(new_content, encoding='utf-8')
            return True

        return False

    def apply(self, data: Dict) -> ApplyResult:
        """Apply changes from the JSON data."""
        result = ApplyResult()

        files_to_delete = data.get('files_to_delete', [])
        files_to_modify = data.get('files_to_modify', [])

        # Process typedef-only files first (delete and propagate includes)
        if files_to_delete:
            print(f"\n{'DRY RUN: ' if self.dry_run else ''}Deleting {len(files_to_delete)} typedef-only header files...")

            for file_info in files_to_delete:
                rel_path = file_info['file']
                filepath = self.src_dir / rel_path
                header_name = filepath.name

                includes = file_info.get('includes', [])
                forward_decls = file_info.get('forward_decls', [])
                includers = file_info.get('includers', [])

                self.log(f"  {header_name}: {len(includers)} includers")

                # Update includer files
                for includer_path in includers:
                    includer_filepath = self.src_dir / includer_path
                    if self.update_includer_file(includer_filepath, header_name,
                                                  includes, forward_decls):
                        if str(includer_filepath) not in result.files_modified:
                            result.files_modified.append(str(includer_filepath))
                        self.log(f"    Updated: {includer_path}")

                # Delete the typedef-only file and remove from FILES.cmake
                if filepath.exists():
                    if self.delete_file(filepath):
                        result.files_deleted.append(rel_path)
                        self.log(f"  Deleted: {rel_path}")
                        # Also remove from FILES.cmake
                        self.remove_from_files_cmake(filepath)
                else:
                    self.log(f"  File not found (already deleted?): {rel_path}")

        # Remove unused typedefs from remaining files
        if files_to_modify:
            print(f"\n{'DRY RUN: ' if self.dry_run else ''}Removing unused typedefs from {len(files_to_modify)} files...")

            for file_info in files_to_modify:
                rel_path = file_info['file']
                filepath = self.src_dir / rel_path
                typedefs = file_info.get('typedefs', [])

                if filepath.exists():
                    if self.remove_typedefs_from_file(filepath, typedefs):
                        result.files_modified.append(str(filepath))
                        typedef_names = [t['name'] for t in typedefs]
                        self.log(f"  Modified: {rel_path} (removed: {', '.join(typedef_names)})")
                else:
                    self.log(f"  File not found: {rel_path}")

        return result


def print_summary(result: ApplyResult, dry_run: bool):
    """Print apply summary."""
    print("\n" + "=" * 60)
    print("TYPEDEF CLEANUP APPLY SUMMARY")
    print("=" * 60)

    if dry_run:
        print("(DRY RUN - no files were modified)")

    print(f"\nFiles {'to be ' if dry_run else ''}deleted: {len(result.files_deleted)}")
    print(f"Files {'to be ' if dry_run else ''}modified: {len(result.files_modified)}")

    if result.errors:
        print(f"\nErrors: {len(result.errors)}")
        for error in result.errors[:10]:
            print(f"  {error}")


def main():
    parser = argparse.ArgumentParser(
        description='OCCT 8.0.0 Typedef Cleanup Apply Script'
    )
    parser.add_argument(
        'src_directory',
        nargs='?',
        default='.',
        help='Source directory (default: current directory)'
    )
    parser.add_argument(
        '--dry-run',
        action='store_true',
        help='Preview changes without modifying files'
    )
    parser.add_argument(
        '--verbose', '-v',
        action='store_true',
        help='Show detailed progress'
    )
    parser.add_argument(
        '--input', '-i',
        default=None,
        help='Input JSON file (default: unused_typedefs.json)'
    )

    args = parser.parse_args()

    print("OCCT Typedef Cleanup Applier")
    print("=" * 60)

    # Load JSON data
    script_dir = Path(__file__).parent
    input_file = args.input or (script_dir / 'unused_typedefs.json')

    try:
        with open(input_file, 'r', encoding='utf-8') as f:
            data = json.load(f)
    except FileNotFoundError:
        print(f"Error: Input file not found: {input_file}")
        print("Run collect_unused_typedefs.py first to generate the JSON file.")
        return 1
    except json.JSONDecodeError as e:
        print(f"Error: Invalid JSON in {input_file}: {e}")
        return 1

    print(f"Loaded: {input_file}")
    print(f"  Files to delete: {len(data.get('files_to_delete', []))}")
    print(f"  Files to modify: {len(data.get('files_to_modify', []))}")

    applier = TypedefCleanupApplier(
        src_dir=args.src_directory,
        dry_run=args.dry_run,
        verbose=args.verbose
    )

    result = applier.apply(data)

    # Print summary
    print_summary(result, args.dry_run)

    if args.dry_run:
        print("\nRun without --dry-run to apply changes")

    return 0


if __name__ == '__main__':
    exit(main())
