"""
Binary file discovery for IDA Pro batch decompiler.
"""

import os
from pathlib import Path
from typing import List, Optional, Tuple, Dict


# IDA Pro 要处理的文件类型
FILE_TYPES = {
    'executable': {
        'description': '可执行文件 (Mach-O, ELF, PE)',
        'extensions': ['', '.out', '.exe', '.elf', '.bin'],
        'magic_bytes': {
            b'\x7fELF': 'ELF',
            b'\xCF\xFA\xED\xFE': 'Mach-O 64-bit',  # 0xFEEDFACE reversed
            b'\xCE\xFA\xED\xFE': 'Mach-O 32-bit',
            b'\x4D\x5A': 'PE/DOS executable',
        }
    },
    'sharedlib': {
        'description': '共享库/动态库 (Mach-O dylib, ELF so, DLL)',
        'extensions': ['.so', '.dylib', '.dll'],
        'magic_bytes': {
            b'\x7fELF': 'ELF shared library',
            b'\xCF\xFA\xED\xFE': 'Mach-O 64-bit dylib',
            b'\xCE\xFA\xED\xFE': 'Mach-O 32-bit dylib',
            b'\x4D\x5A': 'PE DLL',
        }
    }
}


class BinaryScanner:
    """
    Binary file scanner for identifying executable and shared library files.
    """

    # Files and directories to skip
    EXCLUDED_DIRS = {
        '.git', '.svn', '.idea', '.vscode',
        '__pycache__', 'node_modules',
        '.cache', 'build', 'dist'
    }

    EXCLUDED_EXTENSIONS = {
        '.py', '.pyc', '.txt', '.md', '.json', '.xml', '.yaml',
        '.yml', '.log', '.cfg', '.ini', '.conf', '.html', '.css',
        '.js', '.ts', '.java', '.c', '.cpp', '.h', '.hpp'
    }

    def __init__(self):
        """Initialize the binary scanner."""
        pass

    def detect_file_type(self, file_path: Path) -> Optional[Tuple[str, str]]:
        """
        Detect the type of a binary file.

        Args:
            file_path: Path to the file

        Returns:
            Tuple of (file_category, file_description) or None
        """
        try:
            with open(file_path, 'rb') as f:
                # Read first 4 bytes for magic number
                magic = f.read(4)

                for file_type, config in FILE_TYPES.items():
                    for magic_bytes, desc in config['magic_bytes'].items():
                        if magic.startswith(magic_bytes):
                            return (file_type, desc)

        except (IOError, PermissionError):
            pass

        return None

    def is_binary_file(self, file_path: Path) -> bool:
        """
        Check if a file is an executable or shared library.

        Args:
            file_path: Path to the file

        Returns:
            True if the file is a binary, False otherwise
        """
        try:
            # Check excluded extensions
            if file_path.suffix.lower() in self.EXCLUDED_EXTENSIONS:
                return False

            # Check file size
            if file_path.stat().st_size < 64:
                return False

            # Check for binary content
            with open(file_path, 'rb') as f:
                chunk = f.read(8192)

                # Check for null bytes - binaries typically contain them
                if b'\x00' in chunk:
                    return True

                # Check if it's plain text
                try:
                    chunk.decode('utf-8')
                    # Plain text, check for shebang (scripts)
                    if chunk.startswith(b'#!'):
                        return True  # Scripts might need processing
                except UnicodeDecodeError:
                    return True

            # Check magic numbers
            with open(file_path, 'rb') as f:
                magic = f.read(4)

            for config in FILE_TYPES.values():
                if magic[:2] in (b'\x7fE', b'\xCF\xFA', b'\xCE\xFA', b'\x4D\x5A'):
                    return True

            # Check extensions
            for config in FILE_TYPES.values():
                if any(file_path.suffix.lower() == ext for ext in config['extensions']):
                    return True

        except (IOError, OSError, PermissionError):
            return False

        return False

    def find_binaries(self, directory: Path) -> List[Path]:
        """
        Recursively find all binary files in a directory.

        Args:
            directory: The directory to scan

        Returns:
            List of binary file paths
        """
        binaries = []

        for root, dirs, files in os.walk(directory):
            # Skip excluded directories
            dirs[:] = [d for d in dirs if d not in self.EXCLUDED_DIRS]

            for filename in files:
                file_path = Path(root) / filename

                # Quick initial screening
                if not self.is_binary_file(file_path):
                    continue

                # Detailed detection
                file_type = self.detect_file_type(file_path)
                if file_type:
                    binaries.append(file_path)

        return binaries

    def scan_directory(self, directory: Path) -> List[Path]:
        """
        Scan a directory for binary files.

        This is an alias for find_binaries() with a more descriptive name.

        Args:
            directory: The directory to scan

        Returns:
            List of binary file paths
        """
        return self.find_binaries(directory)

    def scan_and_group(self, directory: Path) -> Dict[str, List[Path]]:
        """
        Scan directory and group files by type.

        Args:
            directory: The directory to scan

        Returns:
            Dictionary mapping file types to lists of files
        """
        result = {ftype: [] for ftype in FILE_TYPES.keys()}

        for binary in self.find_binaries(directory):
            file_type = self.detect_file_type(binary)
            if file_type:
                result[file_type[0]].append(binary)

        return result
