import os
from pyboy import PyBoy, WindowEvent
import time
from datetime import datetime
import pandas as pd

class PokemonRedBot:
    """
    A comprehensive bot for automating Pokémon Red gameplay including:
    - Automated training/grinding
    - Path recording and following
    - Pokémon Center visits
    - Battle management
    """
         
    def __init__(self, rom_path, state_path):
        # Memory addresses for game state
        self.MEMORY_ADDRESSES = {
            'party_size': 0xD163,
            'x_pos': 0xD362,
            'y_pos': 0xD361,
            'map_n': 0xD35E,
            'battle_flag': 0xD057,
            'waiting_for_input': 0xCFC4,
            'current_menu_item': 0xCC26,
            'current_menu_y': 0xCC24,
            'current_menu_x': 0xCC25,
            'audio_track': 0xC026,
            'evolution_audio_bank': 0xE5,
            'battle_hp_slot1': 0xD016,
            'battle_pp_slot11': 0xD02D,
            'battle_menu_cursor': 0xCC30,
            'battle_enemy_hp_high': 0xCFE7,
            'battle_enemy_hp_low': 0xCFE6,
            'index_pokemon_out': 0xCC2F
            
        }
        
        # Array addresses for party data
        self.ARRAY_ADDRESSES = {
            'levels': [0xD18C, 0xD1B8, 0xD1E4, 0xD210, 0xD23C, 0xD268],
            'party': [0xD164, 0xD165, 0xD166, 0xD167, 0xD168, 0xD169],
            'pp_slot1': [0xD188, 0xD1B4, 0xD1E0, 0xD20C, 0xD238, 0xD264],
            'hp': [0xD16D, 0xD199, 0xD1C5, 0xD1F1, 0xD21D, 0xD249],
            'max_hp': [0xD18E, 0xD1BA, 0xD1E6, 0xD212, 0xD23E, 0xD26A]
        }
        
        # Training configuration
        self.TRAINING_CONFIG = {
            # 'target_x': 14,
            # 'target_y': 6,
            # 'target_map_id': 12,
            'level_target': 28, # Target level for training
            'tick_range': 10
        }
        
        # Load target position from the first line of the log file
        self.load_target_from_file("log_path_coord_result.txt")
        
        # Ensure all keys exist
        self.TRAINING_CONFIG.setdefault('target_x', 14)
        self.TRAINING_CONFIG.setdefault('target_y', 6)
        self.TRAINING_CONFIG.setdefault('target_map_id', 12)
        
                
        # File paths for configuration and logging
        self.FILE_PATHS = {
            'log_coord': "log_path_coord_result.txt",
            'pokecenter_path': "log_path_coord_poke_center.txt",
            'pokecenter_path_out': "log_path_coord_poke_center_out.txt",
            'level_log': "level_log.txt"
        }
        
        # Initialize emulator
        self.pyboy = PyBoy(rom_path, window_type="SDL2")
        
        # Load game state
        with open(state_path, "rb") as f:
            self.pyboy.load_state(f)
        
        # Bot state variables
        self.last_x_pos = self.read_memory('x_pos')
        self.start_x_pos = self.last_x_pos
        self.stuck_counter = 0
        self.moving_left = False
        self.last_pos = None
        self.step_count = 0
        self.last_log_time = None
        self.log_cleared = False
        
    def load_target_from_file(self, filepath="log_path_coord_result.txt"):
        """Load target_x, target_y, target_map_id from the first line of a file."""
        try:
            with open(filepath, "r") as f:
                first_line = f.readline().strip()
            if first_line:
                x, y, map_id = map(int, first_line.split(","))
                self.TRAINING_CONFIG['target_x'] = x
                self.TRAINING_CONFIG['target_y'] = y
                self.TRAINING_CONFIG['target_map_id'] = map_id
                print(f"[Config] Loaded target from {filepath}: ({x}, {y}, {map_id})")
            else:
                print(f"[Warning] File '{filepath}' is empty.")
        except FileNotFoundError:
            print(f"[Error] File '{filepath}' not found. Using default target values.")
        except Exception as e:
            print(f"[Error] Failed to parse target from '{filepath}': {e}")
    
    def clear_log_file(self, filepath=None):
        """Clear the contents of a log file (default: log_path_coord_result.txt)"""
        if filepath is None:
            filepath = self.FILE_PATHS['log_coord']  # Default to coordinate log
        try:
            with open(filepath, "w") as f:
                f.truncate(0)  # Clear all contents
            print(f"[Log] Cleared log file: {filepath}")
        except Exception as e:
            print(f"[Error] Failed to clear log file '{filepath}': {e}")
    
    def log_coordinates(self,command):
        with open(self.FILE_PATHS['log_coord'], "a") as log_file:
            log_file.write(command + "\n")
            
    def listen_for_manual_input(self,last_pos):
        """Listen for manual input to update coordinates"""
        
        x = self.read_memory('x_pos')
        y = self.read_memory('y_pos')
        map_id = self.read_memory('map_n')
        current_pos = (x, y, map_id)

        if current_pos != last_pos:
            print(f'log current position: {current_pos}')
            self.log_coordinates(f"{x},{y},{map_id}")
            return current_pos  # return the new value

        return last_pos
          
    def read_memory(self, address_key):
        """Read a single memory value by key name"""
        return self.pyboy.get_memory_value(self.MEMORY_ADDRESSES[address_key])
    
    def read_array_memory(self, array_key, size=None):
        """Read an array of memory values by key name"""
        addresses = self.ARRAY_ADDRESSES[array_key]
        if size:
            addresses = addresses[:size]
        return [self.pyboy.get_memory_value(addr) for addr in addresses]
    
    def read_config_file(self, filename):
        """Read configuration from text file (y/n format)"""
        try:
            with open(filename, "r") as f:
                return f.readline().strip().lower() == "y"
        except Exception as e:
            print(f"Error reading {filename}: {e}")
            return False
    
    def update_config_file(self, filename, old_value, new_value):
        """Update configuration file by replacing values"""
        try:
            with open(filename, "r") as f:
                content = f.read()
            content = content.replace(old_value, new_value)
            with open(filename, "w") as f:
                f.write(content)
        except Exception as e:
            print(f"Error updating {filename}: {e}")
    
    def get_enemy_hp(self):
        """Calculate enemy HP from high and low bytes"""
        if self.read_memory('battle_flag') == 1:
            high = self.read_memory('battle_enemy_hp_high')
            low = self.read_memory('battle_enemy_hp_low')
            return (high << 8) | low
        return -1
    
    def get_battle_pokemon_index(self, hp_values):
        """Get index of first Pokémon with HP > 0"""
        indices = [i for i, hp in enumerate(hp_values) if hp > 0]
        return indices[0] if indices else 0
    
    def is_evolving(self):
        """Check if a Pokémon is currently evolving"""
        return self.read_memory('audio_track') == self.MEMORY_ADDRESSES['evolution_audio_bank']
    
    def advance_frames(self, duration=None):
        """Advance emulator by n frames or self.tick_range"""
        if duration is None:
            duration = self.TRAINING_CONFIG['tick_range']
        for _ in range(duration):
            self.pyboy.tick()
    
    def press_and_release_button(self, button_event, duration=None):
        """Press and release a button with specified duration"""
        duration = duration or self.TRAINING_CONFIG['tick_range']
        
        
        self.pyboy.send_input(button_event)
        
        #advance frames
        self.advance_frames(duration)
        
        # Get release event (assumes press events have corresponding release events)
        release_map = {
            WindowEvent.PRESS_BUTTON_A: WindowEvent.RELEASE_BUTTON_A,
            WindowEvent.PRESS_BUTTON_B: WindowEvent.RELEASE_BUTTON_B,
            WindowEvent.PRESS_BUTTON_START: WindowEvent.RELEASE_BUTTON_START,
            WindowEvent.PRESS_ARROW_UP: WindowEvent.RELEASE_ARROW_UP,
            WindowEvent.PRESS_ARROW_DOWN: WindowEvent.RELEASE_ARROW_DOWN,
            WindowEvent.PRESS_ARROW_LEFT: WindowEvent.RELEASE_ARROW_LEFT,
            WindowEvent.PRESS_ARROW_RIGHT: WindowEvent.RELEASE_ARROW_RIGHT,
        }
        
        if button_event in release_map:
            self.pyboy.send_input(release_map[button_event])
            #advance frames
            self.advance_frames(duration)
    
    def press_only_button(self, button_event, duration=None):
        """Press a button and hold it for specified duration"""
        duration = duration or self.TRAINING_CONFIG['tick_range']
        
        self.pyboy.send_input(button_event)
        #advance frames
        self.advance_frames()
        
    def release_only_button(self, button_event, duration=None):
        """Release a button with specified duration"""
        duration = duration or self.TRAINING_CONFIG['tick_range']
        
        # Map press events to their corresponding release events
        release_map = {
            WindowEvent.PRESS_BUTTON_A: WindowEvent.RELEASE_BUTTON_A,
            WindowEvent.PRESS_BUTTON_B: WindowEvent.RELEASE_BUTTON_B,
            WindowEvent.PRESS_BUTTON_START: WindowEvent.RELEASE_BUTTON_START,
            WindowEvent.PRESS_ARROW_UP: WindowEvent.RELEASE_ARROW_UP,
            WindowEvent.PRESS_ARROW_DOWN: WindowEvent.RELEASE_ARROW_DOWN,
            WindowEvent.PRESS_ARROW_LEFT: WindowEvent.RELEASE_ARROW_LEFT,
            WindowEvent.PRESS_ARROW_RIGHT: WindowEvent.RELEASE_ARROW_RIGHT,
        }
        
        if button_event in release_map:
            self.pyboy.send_input(release_map[button_event])
            #advance frames
            self.advance_frames()
            
    def exit_prompts(self):
        """Press buttons to exit any prompts"""
        # Press B to exit any menus
        for _ in range(3):
            self.press_and_release_button(WindowEvent.PRESS_BUTTON_B, self.TRAINING_CONFIG['tick_range'] )
        
        self.advance_frames()
        
    def release_all_inputs(self):
        """Release all button inputs"""
        release_events = [
            WindowEvent.RELEASE_ARROW_UP,
            WindowEvent.RELEASE_ARROW_DOWN,
            WindowEvent.RELEASE_ARROW_LEFT,
            WindowEvent.RELEASE_ARROW_RIGHT,
            WindowEvent.RELEASE_BUTTON_A,
            WindowEvent.RELEASE_BUTTON_B,
            WindowEvent.RELEASE_BUTTON_START,
            WindowEvent.RELEASE_BUTTON_SELECT
        ]
        
        for event in release_events:
            self.pyboy.send_input(event)
            
        # Advance a few frames to ensure inputs are processed
        self.advance_frames(self.TRAINING_CONFIG['tick_range'] * 3)
    
    def run_from_battle(self):
        """Attempt to run away from battle"""
        if self.read_memory('battle_flag') != 1:
            return
        
        print("[Battle] Attempting to run away...")
        
        # Press B to exit any menus
        self.exit_prompts()
        
        # Navigate to RUN option in battle menu
        battle_menu = self.read_memory('battle_menu_cursor')
        
        # Move to RUN option (navigate through battle menu)
        if battle_menu == 0xC1:  # FIGHT
            self.press_and_release_button(WindowEvent.PRESS_ARROW_RIGHT, self.TRAINING_CONFIG['tick_range'] * 3)
            self.press_and_release_button(WindowEvent.PRESS_ARROW_DOWN, self.TRAINING_CONFIG['tick_range'] * 3)
        
        battle_menu = self.read_memory('battle_menu_cursor')
        if battle_menu == 0xC7:  # POKEMON
            self.press_and_release_button(WindowEvent.PRESS_ARROW_DOWN, self.TRAINING_CONFIG['tick_range'] * 3)
        
        battle_menu = self.read_memory('battle_menu_cursor')
        if battle_menu == 0xE9:  # ITEM
            self.press_and_release_button(WindowEvent.PRESS_ARROW_RIGHT, self.TRAINING_CONFIG['tick_range'] * 3)
        
        # Confirm RUN
        battle_menu = self.read_memory('battle_menu_cursor')
        if battle_menu == 0xEF:  # RUN
            self.press_and_release_button(WindowEvent.PRESS_BUTTON_A, self.TRAINING_CONFIG['tick_range'] * 3)
            self.press_and_release_button(WindowEvent.PRESS_BUTTON_B, self.TRAINING_CONFIG['tick_range'] * 3)
    
    def calculate_movement_direction(self, current_pos, target_pos):
        """Calculate movement direction between two positions"""
        if target_pos[2] != current_pos[2]:  # Different maps
            return None
        
        tx, ty, _ = target_pos
        cx, cy, _ = current_pos
        
        if tx > cx:
            return "right"
        elif tx < cx:
            return "left"
        elif ty > cy:
            return "down"
        elif ty < cy:
            return "up"
        return None
    
    def move_to_position(self, target_x, target_y, target_map_id, direction=None):
        """Move to specified coordinates"""
        print(f"[Movement] Moving to ({target_x}, {target_y}, {target_map_id})")
        
        def move_until_coordinate(coord_getter, target_value, press_event, max_attempts=10):
            """Move in direction until coordinate matches target"""
            attempts = 0
            while coord_getter() != target_value and attempts < max_attempts:
                self.run_from_battle()
                self.press_and_release_button(press_event, self.TRAINING_CONFIG['tick_range'])
                attempts += 1
            
            if attempts >= max_attempts:
                print(f"[Error] Timeout moving to {target_value}")
                # Press B to exit any menus
                self.exit_prompts()
                #release all keys
                self.release_all_inputs()
        
        current_x = self.read_memory('x_pos')
        current_y = self.read_memory('y_pos')
        current_map = self.read_memory('map_n')
        
        print(f"[Movement] Current: ({current_x}, {current_y}, {current_map})")
        print(f"[Movement] Target: ({target_x}, {target_y}, {target_map_id},{direction})\n")
        
        # Handle map transitions
        if current_map != target_map_id and direction:
            if direction in ['up', 'down']:
                event = WindowEvent.PRESS_ARROW_UP if direction == 'up' else WindowEvent.PRESS_ARROW_DOWN
                move_until_coordinate(lambda: self.read_memory('y_pos'), target_y, event)
            elif direction in ['left', 'right']:
                event = WindowEvent.PRESS_ARROW_LEFT if direction == 'left' else WindowEvent.PRESS_ARROW_RIGHT
                move_until_coordinate(lambda: self.read_memory('x_pos'), target_x, event)
        else:
            # Move horizontally
            if current_x < target_x:
                move_until_coordinate(lambda: self.read_memory('x_pos'), target_x, WindowEvent.PRESS_ARROW_RIGHT)
            elif current_x > target_x:
                move_until_coordinate(lambda: self.read_memory('x_pos'), target_x, WindowEvent.PRESS_ARROW_LEFT)
            
            # Move vertically
            if current_y < target_y:
                move_until_coordinate(lambda: self.read_memory('y_pos'), target_y, WindowEvent.PRESS_ARROW_DOWN)
            elif current_y > target_y:
                move_until_coordinate(lambda: self.read_memory('y_pos'), target_y, WindowEvent.PRESS_ARROW_UP)
    
    def follow_path_from_file(self, filename, reverse=False):
        """Follow a recorded path from file"""
        print(f"\n following path: {filename} \n")
        try:
            with open(filename, "r") as f:
                lines = f.readlines()
            
            if reverse:
                lines = lines[::-1]
            
            path = []
            for line in lines:
                parts = line.strip().split(",")
                if len(parts) == 3:
                    x, y, map_id = map(int, parts)
                    path.append((x, y, map_id))
            
            for i in range(len(path) - 1):
                current_pos = path[i]
                target_pos = path[i + 1]
                
                direction = None
                if i > 0:
                    prev_pos = path[i - 1]
                    if current_pos[2] == target_pos[2]:  # Same map
                        direction = self.calculate_movement_direction(current_pos, target_pos)
                        # print (f"current = {current_pos}, target = {target_pos}, heading={direction}")
                    else:  # Map transition
                        direction = self.calculate_movement_direction(prev_pos, current_pos)
                        # print (f"current = {current_pos}, target = {target_pos}, heading={direction}")
                
                self.move_to_position(target_pos[0], target_pos[1], target_pos[2], direction)
                
        except Exception as e:
            print(f"[Error] Following path from {filename}: {e}")
    
    def pokemon_center_in(self):
        """Enter Pokémon Center"""  
        
        #advance frames
        self.advance_frames(self.TRAINING_CONFIG['tick_range'] * 6)
              
        # Enter Pokémon Center        
        self.press_and_release_button(WindowEvent.PRESS_ARROW_UP,self.TRAINING_CONFIG['tick_range'] * 3)
        
        #advance frames
        self.advance_frames(self.TRAINING_CONFIG['tick_range'] * 6)
        
        #follow pokémon center path
        self.follow_path_from_file(self.FILE_PATHS['pokecenter_path'])
        
        #advance frames
        self.advance_frames(self.TRAINING_CONFIG['tick_range'] * 6)
        
    def pokemon_center_out(self):
        """Exit Pokémon Center"""
        
        #advance frames
        self.advance_frames(self.TRAINING_CONFIG['tick_range'] * 6)
        
        # Exit Pokémon Center
        self.follow_path_from_file(self.FILE_PATHS['pokecenter_path'], reverse=True)
        
        #advance frames
        self.advance_frames(self.TRAINING_CONFIG['tick_range'] * 6)
        
        # Exit Pokémon Center        
        print("[Pokémon Center] Opening door, going outside...")
        self.press_and_release_button(WindowEvent.PRESS_ARROW_DOWN, self.TRAINING_CONFIG['tick_range'] * 3)
          
        #advance frames
        self.advance_frames(self.TRAINING_CONFIG['tick_range'] * 6)
        
    def get_hp_values(self, pokemon_index=0):
        """Get HP values of all Pokémon in party"""
               
        party_size = self.read_memory('party_size')
        if party_size > 0:
            hp_values = self.read_array_memory('hp', self.read_memory('party_size'))
            party_hp = hp_values[pokemon_index] 
            battle_hp = self.read_memory('battle_hp_slot1')
            return (party_hp, battle_hp)
        else:
            print("[Error] No Pokémon in party")
        return []

    def get_menu_position(self):
        menu_y = self.read_memory('current_menu_y')
        menu_x = self.read_memory('current_menu_x')
        current_menu_item = self.read_memory('current_menu_item') 
        
        return (menu_x, menu_y, current_menu_item)
        
        
    
    def visit_pokemon_center(self):
        """Complete Pokémon Center visit sequence"""
        print("[Pokémon Center] Starting path...")
        
        # Go to Pokémon Center Entry Door
        self.follow_path_from_file(self.FILE_PATHS['log_coord'])
            
        #Enter pokemon center
        print("[Pokémon Center] Entering...")
        self.pokemon_center_in()    
                       
        # Interact with nurse
        self.press_and_release_button(WindowEvent.PRESS_BUTTON_A)

        # Wait for dialogue and keep pressing A
        while self.read_memory('waiting_for_input') == 1:
            self.press_and_release_button(WindowEvent.PRESS_BUTTON_A)
            print("[Pokémon Center] Talking to nurse...")          
              
         
        #read hp
        party_hp, battle_hp = self.get_hp_values()
        # min_hp = min(party_hp, battle_hp)
        
        #advance frames
        self.advance_frames(self.TRAINING_CONFIG['tick_range'] * 6)
         
        # while  min_hp == 0 :
        #     # Interact with nurse
        #     self.press_and_release_button(WindowEvent.PRESS_BUTTON_A)

        #     # Wait for dialogue and keep pressing A
        #     while self.read_memory('waiting_for_input') == 1:
        #         self.press_and_release_button(WindowEvent.PRESS_BUTTON_A)
        #         # print("[Pokémon Center] Talking to nurse...") 
                
        #     #read hp
        #     party_hp, battle_hp = self.get_hp_values() 
            
        #     print(f"Party HP: {party_hp},Battle HP: {battle_hp}")
            
        #     #advance frames
        #     self.advance_frames(self.TRAINING_CONFIG['tick_range'] * 6)

            
        #Go out of Pokémon Center
        print("[Pokémon Center] Exiting...")
        self.pokemon_center_out()
        

        
        
        # Return to training location
        self.follow_path_from_file(self.FILE_PATHS['log_coord'], reverse=True)
       
             
        print("[Pokémon Center] Visit complete!")
    
    def switch_pokemon_order(self, new_slot,old_slot=None, battle_flag=None, hp = None):
        print(f'old_slot: {old_slot}, new_slot: {new_slot}, battle_flag: {battle_flag}, hp: {hp}')
        """Switch the order of Pokémon in party"""        
        
        battle_flag = self.read_memory('battle_flag')
        
        if battle_flag == 0: # Not in battle
            print(f"[Party] Switching Pokémon from slot {old_slot} to {new_slot}")
        
            # Exit any current menus
            print(f"[Party] Exiting any current menus...")
            for _ in range(3):
                self.press_and_release_button(WindowEvent.PRESS_BUTTON_B, self.TRAINING_CONFIG['tick_range'] * 3)
            
            # Open menu
            print(f"[Party] Opening menu...")
            self.press_and_release_button(WindowEvent.PRESS_BUTTON_START, self.TRAINING_CONFIG['tick_range'] * 3)
            
            # Navigate to POKEMON option
            print(f"[Party] Navigating to POKEMON menu...")
            if self.read_memory('current_menu_item') != 0x01:
                self.press_and_release_button(WindowEvent.PRESS_ARROW_DOWN, self.TRAINING_CONFIG['tick_range'] * 3)
            
            # Select POKEMON MENU
            print(f"[Party] Selecting POKEMON menu...")
            self.press_and_release_button(WindowEvent.PRESS_BUTTON_A, self.TRAINING_CONFIG['tick_range'] * 3)
        
            # Select first Pokémon
            print(f"[Party] Selecting first Pokémon...")
            while self.read_memory('current_menu_item') != old_slot:
                self.press_and_release_button(WindowEvent.PRESS_ARROW_DOWN, self.TRAINING_CONFIG['tick_range'] * 3)
            # Press A to select the Pokémon
            print(f"[Party] Confirming selection of Pokémon in slot {old_slot}...")
            self.press_and_release_button(WindowEvent.PRESS_BUTTON_A, self.TRAINING_CONFIG['tick_range'] * 3)
            
            #navigate to "switch" in the menu
            #current menu item will be 0x00 for STATS, 0x01 for SWITCH and 0x02 for CANCEL
            print(f"[Party] Navigating to SWITCH option...")
            while self.read_memory('current_menu_item') != 0x01: #switch                        
                if self.read_memory('current_menu_item') == 0x00: #stats
                    self.press_and_release_button(WindowEvent.PRESS_ARROW_DOWN, self.TRAINING_CONFIG['tick_range'] * 3)
                elif self.read_memory('current_menu_item') == 0x02: #cancel
                    self.press_and_release_button(WindowEvent.PRESS_ARROW_UP, self.TRAINING_CONFIG['tick_range'] * 3)
            # press SWITCH
            print(f"[Party] Confirming SWITCH option...")
            self.press_and_release_button(WindowEvent.PRESS_BUTTON_A, self.TRAINING_CONFIG['tick_range'] * 3)
            
            # Select target Pokémon
            print(f"[Party] Selecting target Pokémon in slot {new_slot}...")
            while self.read_memory('current_menu_item') != new_slot:
                self.press_and_release_button(WindowEvent.PRESS_ARROW_DOWN, self.TRAINING_CONFIG['tick_range'] * 3)
            
            # Confirm switch
            print(f"[Party] Confirming switch to Pokémon in slot {new_slot}...")
            self.press_and_release_button(WindowEvent.PRESS_BUTTON_A, self.TRAINING_CONFIG['tick_range'] * 3)
            
            # Exit menus
            print(f"[Party] Exiting menus...")
            for _ in range(5):
                self.press_and_release_button(WindowEvent.PRESS_BUTTON_B, self.TRAINING_CONFIG['tick_range'] * 3)
                
                
        else:  # In battle
            
            if hp == 0:
                print(f"[Party] Switching Pokémon with HP = 0 to {new_slot}")
                
                #read HP
                party_hp, battle_hp = self.get_hp_values(new_slot) 
                print(f"[Party] Party HP: {party_hp}, Battle HP: {battle_hp}")
                
                menu_x,menu_y,current_selection = self.get_menu_position()
                print(f"[Party] Current Menu Position: ({menu_x}, {menu_y},{current_selection})")
                
                
                attempts=0
                max_attempts = 10
                while (menu_y != 0x01) and (menu_x != 0x00):
                    attempts+=1
                    print(f"[Party]Press A until pokemon list is open...{attempts}")
                    self.press_and_release_button(WindowEvent.PRESS_BUTTON_A, self.TRAINING_CONFIG['tick_range'] * 3)
                    menu_x,menu_y,current_selection = self.get_menu_position() 
                    
                    if attempts >= max_attempts:
                        print("[Party][Error] Failed to open Pokémon list after multiple attempts.")
                        return 
                    
                if (menu_y == 0x01) and (menu_x == 0x00):              
                    print("[Party] Pokémon list is open, selecting Pokémon...")

                    # Select target Pokémon
                    menu_x,menu_y,current_selection = self.get_menu_position()                    
                    print(f"[Party] Current Menu Position: ({menu_x}, {menu_y},{current_selection})")
                    
                    attempts=0
                    max_attempts = 10
                    while current_selection != new_slot:
                        self.press_and_release_button(WindowEvent.PRESS_ARROW_UP, self.TRAINING_CONFIG['tick_range'] * 3)
                        current_selection = self.read_memory('current_menu_item')
                        # #advance frames 
                        self.advance_frames(self.TRAINING_CONFIG['tick_range'] * 3)
                        
                        if attempts >= max_attempts:
                            print("[Party][Error] Failed to select target Pokémon after multiple attempts.")
                            return
                        
                        
                    # Press A to select the Pokémon
                    print("[Party]Confirm target Pokémon...")
                    self.press_and_release_button(WindowEvent.PRESS_BUTTON_A, self.TRAINING_CONFIG['tick_range'] * 3)
                                
                #read HP
                party_hp, battle_hp = self.get_hp_values(new_slot) 
                print(f"[Party]Party HP: {party_hp}, Battle HP: {battle_hp}")
                
  
            else:
                print(f"[Party] Cannor Switch. Need more conditions.")

            
    
    def log_pokemon_levels(self, levels):
        """Log Pokémon levels with timestamp"""
        now = datetime.now()
        
        if self.last_log_time is None or (now - self.last_log_time).total_seconds() >= 60:
            log_line = f"{now.strftime('%Y-%m-%d %H:%M:%S')}, {levels}\n"
            with open(self.FILE_PATHS['level_log'], "a") as f:
                f.write(log_line)
            self.last_log_time = now
            print(f"[Log] Levels logged: {levels}")
    
    def training_movement(self,duration =None):
        """Handle left-right training movement"""
        current_x = self.read_memory('x_pos')
        print(f"[Movement] Current X Position: {current_x}, Start X Position: {self.start_x_pos}")
        
        # Determine movement direction
        if self.moving_left:
            print("[Movement] Moving left")
            self.press_only_button(WindowEvent.PRESS_ARROW_LEFT)   
            self.release_only_button(WindowEvent.PRESS_ARROW_RIGHT)         
        else:
            print("[Movement] Moving right")
            self.press_only_button(WindowEvent.PRESS_ARROW_RIGHT)
            self.release_only_button(WindowEvent.PRESS_ARROW_LEFT)
                    
        # Check bounds and reverse direction
        if current_x <= self.start_x_pos - 10:
            print("[Movement] Reached left boundary, reversing direction")
            self.moving_left = False            
            
        elif current_x >= self.start_x_pos + 10:
            print("[Movement] Reached right boundary, reversing direction")
            self.moving_left = True
            
        
        # Detect if stuck
        if current_x == self.last_x_pos:
            print("[Movement] Stuck in place, reversing direction")
            self.stuck_counter += 1
            if self.stuck_counter > 10:
                print("[Movement] Stuck for too long, reversing direction")
                self.moving_left = not self.moving_left
                self.stuck_counter = 0
        else:
            print("[Movement] Moving forward, resetting stuck counter")
            self.stuck_counter = 0
        
        print(f"[Movement] Stuck Counter: {self.stuck_counter}")
        self.last_x_pos = current_x
    
    def handle_battle(self, enemy_hp, battle_pp, battle_hp, party_hp, is_evolving,battle_pokemon_index):
        """Handle battle logic"""
        
        if self.step_count % 8 == 0 and self.step_count > 0:
            print(f"[Battle Debug] Enemy HP: {enemy_hp}, Battle HP: {battle_hp},Party HP: {party_hp}, Battle PP: {battle_pp}")
            print(f"[Battle Debug] Step count: {self.step_count}, Button cycle: {self.step_count % 8}")
        
        #read hp
        party_hp, battle_hp = self.get_hp_values(battle_pokemon_index)
        print(f"[Battle] Party HP: {party_hp}, Battle HP: {battle_hp}")
        min_hp = min(party_hp, battle_hp)
         
        # Run if out of PP = 0
        if battle_pp == 0 :
            print("[Battle] Out of PP, attempting to run...")
            
            #run from battle
            self.run_from_battle()           
            return
        
        # Run if out of HP
        if party_hp == 0: 
            print("[Battle] Out of HP, switching to next Pokémon...")  
                                             
            #get number of pokemon in party
            party_size = self.read_memory('party_size')
            
            #get HP values of all Pokémon in party
            hp_values = self.read_array_memory('hp', party_size) if party_size > 0 else []
                        
            #get index of the first Pokémon with HP > 0
            new_index = min([i for i, hp_values in enumerate(hp_values) if hp_values >0 and i >0]) #assuming index 0 is the one that fainted                           
            print(f"[Battle]HP values: {hp_values} | new index: {new_index}")
            
            #switch to the next Pokémon with HP > 0
            self.switch_pokemon_order(new_slot=new_index,battle_flag=1,hp=party_hp)
            
            return
        
        # Battle actions based on enemy state
        if enemy_hp > 0 or (enemy_hp == 0 and is_evolving):
            # Attack (improved button timing)
            button_cycle = self.step_count % 8
            
            if button_cycle == 0:
                self.pyboy.send_input(WindowEvent.PRESS_BUTTON_A)
            elif button_cycle == 4:
                self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_A)
        else:
            # Enemy defeated, advance dialogue (improved timing)
            button_cycle = self.step_count % 8
            
            if button_cycle == 0:
                self.pyboy.send_input(WindowEvent.PRESS_BUTTON_B)
            elif button_cycle == 4:
                self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_B)
    
    def display_status(self):
        """Display current bot status"""
        try:
            # Clear screen
            os.system('cls' if os.name == 'nt' else 'clear')
            
            # Read current state
            x_pos = self.read_memory('x_pos')
            y_pos = self.read_memory('y_pos')
            map_n = self.read_memory('map_n')
            party_size = self.read_memory('party_size')
            
            levels = self.read_array_memory('levels', party_size) if party_size > 0 else []
            hp_values = self.read_array_memory('hp', party_size) if party_size > 0 else []
            pp_values = self.read_array_memory('pp_slot1', party_size) if party_size > 0 else []
            
            battle_flag = self.read_memory('battle_flag')
            battle_hp = self.read_memory('battle_hp_slot1')
            battle_pp = self.read_memory('battle_pp_slot11')
            enemy_hp = self.get_enemy_hp()
            
            print(f"=== Pokémon Red Training Bot ===")
            print(f"Step: {self.step_count}")
            print(f"Position: ({x_pos}, {y_pos}) | Map: {map_n}")
            print(f"Initial Target: ({self.TRAINING_CONFIG['target_x']}, {self.TRAINING_CONFIG['target_y']}) | Map: {self.TRAINING_CONFIG['target_map_id']}")
            print()
            
            print("--- Pokémon Party ---")
            print(f"Party Size: {party_size}")
            if party_size > 0:
                print(f"Levels: {levels} (Total: {sum(levels)})")
                print(f"HP:     {hp_values}")
                print(f"PP:     {pp_values}")
            print()
            
            print("--- Battle Status ---")
            print(f"In Battle: {'Yes' if battle_flag == 1 else 'No'}")
            print(f"Enemy HP: {enemy_hp if enemy_hp >= 0 else 'N/A'}")
            print(f"Battle HP: {battle_hp }")
            print(f"Battle PP: {battle_pp }")
            print()
            
            print("--- Bot Configuration ---")
            print(f"Training Enabled: {self.read_config_file('bot_train_enabled.txt')}")
            print(f"Path Logging: {self.read_config_file('log_path_enabled.txt')}")
            print(f"Target Level: {self.TRAINING_CONFIG['level_target']}")
            print()
            
            
        except Exception as e:
            print(f"Error displaying status: {e}")
    
    def save_state_with_datetime_and_level(self):
        """Save the current emulator state with a filename containing datetime and target level."""
        # Get current datetime
        now = datetime.now()
        date_str = now.strftime("%Y%m%d_%H%M")  # Format: YYYYMMDD_HHMM
        
        # Get target level
        level = self.TRAINING_CONFIG.get('level_target', 15)  # Default to 15 if not found
        
        # Create filename
        filename = fr"C:\Users\TSCVI\Documents\Python Scripts\pokemon\my scripts\state files\saved_state_{date_str}_level{level}.state"
        
        try:
            # Save state
            with open(filename, "wb") as f:
                self.pyboy.save_state(f)
            print(f"[State] Saved to '{filename}'")
        except Exception as e:
            print(f"[Error] Failed to save state: {e}")
    
    
    def run(self):
        """Main bot loop"""
        print("=== Pokémon Red Training Bot Started ===")
        
        # Initialize last position
        x = self.read_memory('x_pos')
        y = self.read_memory('y_pos')
        map_id = self.read_memory('map_n')
        self.last_pos = (x, y, map_id)
        
        #flag to allow only one "release button event" when "not bot_train_enabled"
        flag_enter_bot_train = False
        
        while not self.pyboy.tick():
            self.step_count += 1
            
            # Read configuration flags
            bot_train_enabled = self.read_config_file('bot_train_enabled.txt')
            if not bot_train_enabled and flag_enter_bot_train:
                # If bot training is disabled, release all inputs only once                            
                # Press B to exit any menus
                self.exit_prompts()
                #release all keys
                self.release_all_inputs()  
                
                #set flag to initial state
                flag_enter_bot_train=False                             
            
            #read configuration files
            log_path_enabled = self.read_config_file('log_path_enabled.txt')
            # bot_execute_path_enabled = self.read_config_file('bot_execute_path_enabled.txt')
            bot_go_to_position_enabled = self.read_config_file('bot_go_to_position_enabled.txt')
            bot_follow_path_enabled = self.read_config_file('follow_path_enabled.txt')
            
            
            # Read game state
            battle_flag = self.read_memory('battle_flag')
            party_size = self.read_memory('party_size')
            
            if party_size > 0:
                levels = self.read_array_memory('levels', party_size)
                hp_values = self.read_array_memory('hp', party_size)
                pp_values = self.read_array_memory('pp_slot1', party_size)
                battle_pokemon_index = self.get_battle_pokemon_index(hp_values)
            else:
                levels = []
                hp_values = []
                pp_values = []
                battle_pokemon_index = 0
                
                
                
            if log_path_enabled and battle_flag == 0:

                if not self.log_cleared:
                    self.clear_log_file(self.FILE_PATHS['log_coord'])
                    print(f"[Log] Cleared {self.FILE_PATHS['log_coord']}")
                    self.log_cleared = True
                
                                
                # listen_for_manual_input(last_pos)
                self.last_pos = self.listen_for_manual_input(self.last_pos)
            
            # Bot actions based on flags and state
            if bot_follow_path_enabled:
                self.visit_pokemon_center()
                                
                # self.visit_pokemon_center()
                self.update_config_file("follow_path_enabled.txt", "y", "n")
            
            elif bot_go_to_position_enabled:
                self.move_to_position(
                    self.TRAINING_CONFIG['target_x'],
                    self.TRAINING_CONFIG['target_y'],
                    self.TRAINING_CONFIG['target_map_id']
                )
                self.update_config_file("bot_go_to_position_enabled.txt", "y", "n")
            
            elif bot_train_enabled:
                
                flag_enter_bot_train = True
                
                
                if battle_flag == 0:  # Not in battle
                    self.log_pokemon_levels(levels)
                    
                    # Check if need to go to Pokémon Center
                    if (party_size > 0 and 
                        (min(hp_values) == 0 or pp_values[battle_pokemon_index] == 0)):
                        
                        current_x = self.read_memory('x_pos')
                        current_y = self.read_memory('y_pos')
                        current_map = self.read_memory('map_n')
                        
                        if (current_x == self.TRAINING_CONFIG['target_x'] and 
                            current_y == self.TRAINING_CONFIG['target_y'] and 
                            current_map == self.TRAINING_CONFIG['target_map_id']):
                            
                            self.visit_pokemon_center()
                            self.update_config_file("bot_train_enabled.txt", "n", "y")
                        else:
                            self.move_to_position(
                                self.TRAINING_CONFIG['target_x'],
                                self.TRAINING_CONFIG['target_y'],
                                self.TRAINING_CONFIG['target_map_id']
                            )
                    
                    # Check if need to switch Pokémon
                    elif party_size > 0 and levels:
                        min_level = min(levels)
                        if (min_level < self.TRAINING_CONFIG['level_target'] and 
                            levels[battle_pokemon_index] >= self.TRAINING_CONFIG['level_target']):
                            
                            new_index = min([i for i, lvl in enumerate(levels) 
                                           if lvl < self.TRAINING_CONFIG['level_target']])
                            # Switch Pokémon
                            print(f"[Training] Switching Pokémon to level {min_level}...")
                            self.switch_pokemon_order(new_slot=new_index,old_slot=battle_pokemon_index)
                        
                        elif min_level >= self.TRAINING_CONFIG['level_target']:
                            print("[Training] All Pokémon reached target level!")
                            # Press B to exit any menus
                            self.exit_prompts()
                            #release all keys
                            self.release_all_inputs()
                            #save state
                            self.save_state_with_datetime_and_level()
                            self.update_config_file("bot_train_enabled.txt", "y", "n")
                        
                        else:
                            # Continue training movement
                            self.training_movement()
                
                else:  # In battle
                    enemy_hp = self.get_enemy_hp()
                    battle_pokemon_index = self.read_memory('index_pokemon_out')
                    battle_pp = self.read_memory('battle_pp_slot11')
                    party_hp, battle_hp = self.get_hp_values(battle_pokemon_index)
                    is_evolving = self.is_evolving()
                    hp_values = self.read_array_memory('hp', party_size)
                    party_hp = hp_values[battle_pokemon_index]
                    
                     
                    
                    # self.handle_battle(enemy_hp, battle_pp, battle_hp, is_evolving)
                    self.handle_battle(enemy_hp, battle_pp, battle_hp,party_hp, is_evolving,battle_pokemon_index)
                    
            # else:
                # Bot disabled, release all inputs
                # self.release_all_inputs()
            
            # Display status every 30 steps
            if self.step_count % 30 == 0:
                self.display_status()
        
        print("=== Bot Stopped ===")
        self.pyboy.stop()


# Usage example
if __name__ == "__main__":
    ROM_PATH = r"C:\Users\TSCVI\Documents\Python Scripts\pokemon\my scripts\rom\Pokemon_red.gb"
    STATE_PATH = r"C:\Users\TSCVI\Documents\Python Scripts\pokemon\my scripts\rom\Pokemon_red.gb.state"
    
    bot = PokemonRedBot(ROM_PATH, STATE_PATH)
    bot.run()