"""
Memory Aim Assist Module
Provides functionality for automated aiming through memory manipulation.
"""

import pymem
import pymem.process
import math
import time
import json
from typing import Optional, Tuple, Dict, Any


class MemoryAimAssist:
    """
    A class for automated aiming assistance through game memory manipulation.
    
    Attributes:
        pm: Pymem instance for memory operations
        game_module: Game module reference
        client_dll: Base address of client DLL
        engine_dll: Base address of engine DLL
        is_running: Aim assist status flag
        config: Configuration settings
    """
    
    def __init__(self, config_path: str = "aim_config.json"):
        """
        Initialize the MemoryAimAssist with configuration.
        
        Args:
            config_path (str): Path to configuration file
        """
        self.pm = None
        self.game_module = None
        self.client_dll = None
        self.engine_dll = None
        self.is_running = False
        self.config = self.load_config(config_path)
        
    def load_config(self, config_path: str) -> Dict[str, Any]:
        """
        Load configuration from JSON file.
        
        Args:
            config_path (str): Path to configuration file
            
        Returns:
            Dict[str, Any]: Configuration dictionary
        """
        try:
            with open(config_path, 'r', encoding='utf-8') as f:
                return json.load(f)
        except FileNotFoundError:
            print(f"Configuration file {config_path} not found, using defaults")
            return self.get_default_config()
        except json.JSONDecodeError as e:
            print(f"Configuration file error: {e}, using defaults")
            return self.get_default_config()
    
    def get_default_config(self) -> Dict[str, Any]:
        """
        Get default configuration.
        
        Returns:
            Dict[str, Any]: Default configuration dictionary
        """
        return {
            "process_name": "csgo.exe",
            "client_module": "client.dll",
            "engine_module": "engine.dll",
            "aim_settings": {
                "fov": 3.0,
                "smoothness": 8.0,
                "aim_key": 0x05,  # Mouse button 4
                "trigger_key": 0x06,  # Mouse button 5
                "bone_selection": 8,  # Head bone
                "visibility_check": True
            },
            "memory_offsets": {
                "local_player": 0xDB35DC,
                "entity_list": 0x4DD245C,
                "view_matrix": 0x4DC45DC,
                "force_attack": 0x320DE94,
                "crosshair_id": 0x11838,
                "team_num": 0xF4,
                "health": 0x100,
                "dormant": 0xED,
                "bone_matrix": 0x26A8,
                "vec_origin": 0x138,
                "vec_view_offset": 0x108,
                "view_angles": 0x4D90
            }
        }
    
    def initialize(self) -> bool:
        """
        Initialize memory read/write operations.
        
        Returns:
            bool: True if initialization successful, False otherwise
        """
        try:
            process_name = self.config["process_name"]
            client_module = self.config["client_module"]
            engine_module = self.config["engine_module"]
            
            self.pm = pymem.Pymem(process_name)
            self.client_module = pymem.process.module_from_name(
                self.pm.process_handle, 
                client_module
            )
            self.engine_module = pymem.process.module_from_name(
                self.pm.process_handle, 
                engine_module
            )
            self.client_dll = self.client_module.lpBaseOfDll
            self.engine_dll = self.engine_module.lpBaseOfDll
            
            print(f"Successfully connected to {process_name}")
            return True
        except Exception as e:
            print(f"Initialization failed: {e}")
            return False

    def read_memory(self, address: int, data_type: str = 'int') -> Any:
        """
        Read data from memory.
        
        Args:
            address (int): Memory address to read from
            data_type (str): Type of data to read
            
        Returns:
            Any: Data read from memory or None if failed
        """
        try:
            if data_type == 'int':
                return self.pm.read_int(address)
            elif data_type == 'float':
                return self.pm.read_float(address)
            elif data_type == 'bytes':
                return self.pm.read_bytes(address, 4)
            elif data_type == 'vector':
                return [
                    self.pm.read_float(address),
                    self.pm.read_float(address + 0x4),
                    self.pm.read_float(address + 0x8)
                ]
            return None
        except Exception as e:
            return None

    def write_memory(self, address: int, value: Any, data_type: str = 'float') -> bool:
        """
        Write data to memory.
        
        Args:
            address (int): Memory address to write to
            value: Value to write
            data_type (str): Type of data to write
            
        Returns:
            bool: True if write successful, False otherwise
        """
        try:
            if data_type == 'float':
                self.pm.write_float(address, value)
            elif data_type == 'int':
                self.pm.write_int(address, value)
            elif data_type == 'vector':
                self.pm.write_float(address, value[0])
                self.pm.write_float(address + 0x4, value[1])
                self.pm.write_float(address + 0x8, value[2])
            return True
        except Exception as e:
            return False

    def get_local_player(self) -> int:
        """
        Get local player address.
        
        Returns:
            int: Local player address
        """
        offsets = self.config["memory_offsets"]
        return self.read_memory(self.client_dll + offsets["local_player"])

    def get_entity(self, index: int) -> int:
        """
        Get entity address by index.
        
        Args:
            index (int): Entity index
            
        Returns:
            int: Entity address
        """
        offsets = self.config["memory_offsets"]
        entity_list = self.client_dll + offsets["entity_list"]
        return self.read_memory(entity_list + (index * 0x10))

    def get_entity_health(self, entity: int) -> int:
        """
        Get entity health.
        
        Args:
            entity (int): Entity address
            
        Returns:
            int: Entity health
        """
        offsets = self.config["memory_offsets"]
        return self.read_memory(entity + offsets["health"])

    def get_entity_team(self, entity: int) -> int:
        """
        Get entity team.
        
        Args:
            entity (int): Entity address
            
        Returns:
            int: Entity team
        """
        offsets = self.config["memory_offsets"]
        return self.read_memory(entity + offsets["team_num"])

    def is_entity_dormant(self, entity: int) -> bool:
        """
        Check if entity is dormant.
        
        Args:
            entity (int): Entity address
            
        Returns:
            bool: True if entity is dormant
        """
        offsets = self.config["memory_offsets"]
        return self.read_memory(entity + offsets["dormant"]) == 1

    def get_bone_position(self, entity: int, bone_id: int) -> Optional[Tuple[float, float, float]]:
        """
        Get bone position in 3D space.
        
        Args:
            entity (int): Entity address
            bone_id (int): Bone ID
            
        Returns:
            Optional[Tuple[float, float, float]]: Bone position (x, y, z)
        """
        try:
            offsets = self.config["memory_offsets"]
            bone_matrix = self.read_memory(entity + offsets["bone_matrix"])
            return [
                self.read_memory(bone_matrix + 0x30 * bone_id + 0x0C, 'float'),
                self.read_memory(bone_matrix + 0x30 * bone_id + 0x1C, 'float'),
                self.read_memory(bone_matrix + 0x30 * bone_id + 0x2C, 'float')
            ]
        except:
            return None

    def world_to_screen(self, world_pos: Tuple[float, float, float]) -> Optional[Tuple[float, float]]:
        """
        Convert 3D world coordinates to 2D screen coordinates.
        
        Args:
            world_pos (Tuple[float, float, float]): World position (x, y, z)
            
        Returns:
            Optional[Tuple[float, float]]: Screen coordinates (x, y)
        """
        try:
            offsets = self.config["memory_offsets"]
            view_matrix = self.read_memory(self.client_dll + offsets["view_matrix"], 'bytes')
            
            # Convert bytes to float array
            matrix = []
            for i in range(16):
                matrix.append(self.pm.read_float(self.client_dll + offsets["view_matrix"] + i * 4))
            
            w = (matrix[3] * world_pos[0] + matrix[7] * world_pos[1] + 
                 matrix[11] * world_pos[2] + matrix[15])
            
            if w < 0.01:
                return None
            
            x = (matrix[0] * world_pos[0] + matrix[4] * world_pos[1] + 
                 matrix[8] * world_pos[2] + matrix[12]) / w
            y = (matrix[1] * world_pos[0] + matrix[5] * world_pos[1] + 
                 matrix[9] * world_pos[2] + matrix[13]) / w
            
            screen_x = (1920 / 2) * (1.0 + x)
            screen_y = (1080 / 2) * (1.0 - y)
            
            return screen_x, screen_y
        except:
            return None

    def calculate_angle(self, local_pos: Tuple[float, float, float], 
                       target_pos: Tuple[float, float, float]) -> Tuple[float, float]:
        """
        Calculate aim angles to target.
        
        Args:
            local_pos (Tuple[float, float, float]): Local player position
            target_pos (Tuple[float, float, float]): Target position
            
        Returns:
            Tuple[float, float]: Pitch and yaw angles
        """
        delta_x = target_pos[0] - local_pos[0]
        delta_y = target_pos[1] - local_pos[1]
        delta_z = target_pos[2] - local_pos[2]
        
        hypotenuse = math.sqrt(delta_x * delta_x + delta_y * delta_y)
        pitch = math.atan2(delta_z, hypotenuse) * 180 / math.pi
        yaw = math.atan2(delta_y, delta_x) * 180 / math.pi
        
        return pitch, yaw

    def get_best_target(self) -> Optional[int]:
        """
        Find the best target to aim at.
        
        Returns:
            Optional[int]: Best target entity address
        """
        local_player = self.get_local_player()
        if not local_player:
            return None
        
        local_team = self.get_entity_team(local_player)
        best_target = None
        best_fov = float('inf')
        aim_settings = self.config["aim_settings"]
        
        for i in range(1, 32):  # Max entities
            entity = self.get_entity(i)
            if not entity or entity == local_player:
                continue
            
            # Check if entity is valid
            if (self.get_entity_health(entity) <= 0 or 
                self.is_entity_dormant(entity) or
                self.get_entity_team(entity) == local_team):
                continue
            
            # Get target bone position
            bone_pos = self.get_bone_position(entity, aim_settings["bone_selection"])
            if not bone_pos:
                continue
            
            # Get local player position
            offsets = self.config["memory_offsets"]
            local_origin = self.read_memory(local_player + offsets["vec_origin"], 'vector')
            local_view_offset = self.read_memory(local_player + offsets["vec_view_offset"], 'vector')
            local_pos = [
                local_origin[0] + local_view_offset[0],
                local_origin[1] + local_view_offset[1],
                local_origin[2] + local_view_offset[2]
            ]
            
            # Calculate FOV
            pitch, yaw = self.calculate_angle(local_pos, bone_pos)
            current_pitch, current_yaw = self.get_current_angles()
            
            fov = math.sqrt(
                (pitch - current_pitch) ** 2 + 
                (yaw - current_yaw) ** 2
            )
            
            if fov < aim_settings["fov"] and fov < best_fov:
                best_fov = fov
                best_target = entity
        
        return best_target

    def get_current_angles(self) -> Tuple[float, float]:
        """
        Get current view angles.
        
        Returns:
            Tuple[float, float]: Current pitch and yaw angles
        """
        offsets = self.config["memory_offsets"]
        engine_pointer = self.read_memory(self.engine_dll + offsets["view_angles"])
        pitch = self.read_memory(engine_pointer, 'float')
        yaw = self.read_memory(engine_pointer + 0x4, 'float')
        return pitch, yaw

    def aim_at_target(self, target_entity: int):
        """
        Aim at the specified target entity.
        
        Args:
            target_entity (int): Target entity address
        """
        try:
            local_player = self.get_local_player()
            if not local_player:
                return
            
            # Get positions
            offsets = self.config["memory_offsets"]
            local_origin = self.read_memory(local_player + offsets["vec_origin"], 'vector')
            local_view_offset = self.read_memory(local_player + offsets["vec_view_offset"], 'vector')
            local_pos = [
                local_origin[0] + local_view_offset[0],
                local_origin[1] + local_view_offset[1],
                local_origin[2] + local_view_offset[2]
            ]
            
            bone_pos = self.get_bone_position(target_entity, 
                                            self.config["aim_settings"]["bone_selection"])
            if not bone_pos:
                return
            
            # Calculate angles
            pitch, yaw = self.calculate_angle(local_pos, bone_pos)
            
            # Apply smoothing
            current_pitch, current_yaw = self.get_current_angles()
            smoothness = self.config["aim_settings"]["smoothness"]
            
            smooth_pitch = current_pitch + (pitch - current_pitch) / smoothness
            smooth_yaw = current_yaw + (yaw - current_yaw) / smoothness
            
            # Write angles
            engine_pointer = self.read_memory(self.engine_dll + offsets["view_angles"])
            self.write_memory(engine_pointer, smooth_pitch, 'float')
            self.write_memory(engine_pointer + 0x4, smooth_yaw, 'float')
            
        except Exception as e:
            print(f"Aiming error: {e}")

    def start_aim_assist(self):
        """Start the aim assist process."""
        if not self.pm:
            print("Please initialize connection first")
            return
        
        self.is_running = True
        print("Starting aim assist...")
        print("Configuration loaded:")
        print(f"  FOV: {self.config['aim_settings']['fov']}")
        print(f"  Smoothness: {self.config['aim_settings']['smoothness']}")
        print(f"  Bone: {self.config['aim_settings']['bone_selection']}")
        
        try:
            while self.is_running:
                # Check if aim key is pressed
                if self.is_key_pressed(self.config['aim_settings']['aim_key']):
                    target = self.get_best_target()
                    if target:
                        self.aim_at_target(target)
                
                time.sleep(0.001)  # 1ms delay for performance
                
        except KeyboardInterrupt:
            print("\nAim assist stopped by user")
        except Exception as e:
            print(f"Aim assist error: {e}")

    def is_key_pressed(self, key_code: int) -> bool:
        """
        Check if key is pressed.
        
        Args:
            key_code (int): Virtual key code
            
        Returns:
            bool: True if key is pressed
        """
        import ctypes
        return ctypes.windll.user32.GetAsyncKeyState(key_code) & 0x8000 != 0

    def stop_aim_assist(self):
        """Stop the aim assist process."""
        self.is_running = False
        print("Aim assist stopped")

    def update_config(self, key: str, value: Any):
        """
        Update configuration value.
        
        Args:
            key (str): Configuration key
            value: New value
        """
        keys = key.split('.')
        config_ref = self.config
        for k in keys[:-1]:
            config_ref = config_ref[k]
        config_ref[keys[-1]] = value
        print(f"Updated {key} to {value}")

    def show_status(self):
        """Display current aim assist status and configuration."""
        print("Current aim assist status:")
        print(f"Running: {'Yes' if self.is_running else 'No'}")
        print("Configuration:")
        print(f"  FOV: {self.config['aim_settings']['fov']}")
        print(f"  Smoothness: {self.config['aim_settings']['smoothness']}")
        print(f"  Bone: {self.config['aim_settings']['bone_selection']}")


def main():
    """Main function to run the aim assist system."""
    aim_assist = MemoryAimAssist()
    
    # Initialize connection
    if not aim_assist.initialize():
        return
    
    # Interactive menu
    while True:
        print("\n=== Memory Aim Assist System ===")
        print("1. Start aim assist")
        print("2. Stop aim assist")
        print("3. Modify settings")
        print("4. Show status")
        print("5. Exit")
        
        choice = input("Please select an option: ").strip()
        
        if choice == '1':
            aim_assist.start_aim_assist()
        elif choice == '2':
            aim_assist.stop_aim_assist()
        elif choice == '3':
            print("\nAvailable settings:")
            print("  fov - Field of View")
            print("  smoothness - Aim smoothness")
            print("  bone_selection - Target bone")
            
            setting = input("Enter setting to modify: ").strip()
            try:
                if setting in ['fov', 'smoothness']:
                    value = float(input("Enter new value: "))
                    aim_assist.update_config(f'aim_settings.{setting}', value)
                elif setting == 'bone_selection':
                    value = int(input("Enter new bone ID: "))
                    aim_assist.update_config(f'aim_settings.{setting}', value)
                else:
                    print("Invalid setting")
            except ValueError:
                print("Please enter a valid number")
        elif choice == '4':
            aim_assist.show_status()
        elif choice == '5':
            aim_assist.stop_aim_assist()
            break
        else:
            print("Invalid selection")


if __name__ == "__main__":
    main()