"""
Defines the ActionManager, which manages the scheduling and execution of actions in a game.
It implements pseudo code from the book "Artificial Intelligence for Games", Second Edtion, by
Ian Millington and John Funge, Chapter 5, Section 5.11.3, 2009.

Exported Classes:
    ActionManager -- A class that manages the scheduling and execution of game actions.

Exported Exceptions:
    None
    
Exceptions raised:
    ExcessiveRepeatActionScheduleError -- Raised when the same type of action is scheduled too many times in a row.
 
Exported Functions:
    None
"""


# stanard imports

# local imports
from pyGameAIFoundation.game_action import GameAction
from pyGameAIFoundation.exceptions import ExcessiveRepeatActionScheduleError, ActionCannotAchieveGoalError


class ActionManager(object):
    """
    This class manages a collection of actions that can be performed in the game. It schedules and
    exectues the actions.
    """
    def __init__(self):
        """
        Initialize the ActionManager with an empty list of actions.
        """
        # List to store pending actions
        self._action_list = []
        # List of currently executing actions
        self._active = []
        # The current time, a simple counter
        self._currentTime = 0
        # The maxium number of times the same type of action can be pending or active at the same time.
        # If we exceed this number, we will raise an exception. Intended to prevent Game AI from
        # getting stuck in a loop of scheduling the same action over and over again.
        # See sheduleAction method.
        self._max_num_times = 1

    @property
    def currentTime(self):
        """
        Return the current time. Time starts at 0, and is incremented by 1 each time the execute() method is called.
        :return: The current time, as int.
        """
        return self._currentTime

    def __len__(self):
        """
        Return the number of pending plus active actions.
        :return: The number of pending plus active actions, as int.
        """
        return len(self._action_list) + len(self._active)

    def scheduleAction(self, action=None):
        """
        Add an action to be executed to the collection.
        :param action: The action to be scheduled, as GameAction object.
        :raises AssertionError: If the action is not an instance of GameAction.
        :raises ExcessiveRepeatActionScheduleError: If the same type of action is scheduled too many times in a row.
        :return: None
        """
        assert(isinstance(action, GameAction))
        # Count the number of times an action of the same type appears in the action list.
        count = len([act for act in self._action_list if type(act)==type(action)])
        # Add to the count the number of times an action of the same type appears in the active list.
        count += len([act for act in self._active if type(act)==type(action)])
        # If count is less than the maximum, then we add the action to the list of pending actions.
        if count < self._max_num_times:
            self._action_list.append(action)
            # Sort the list of pending actions in descending order of priority.
            self._action_list.sort(key=lambda x: x.priority, reverse=True)
        else:
            raise ExcessiveRepeatActionScheduleError(action_type=type(action),
                                                     num_times=count)
        return None

    def getHighestPriorityActive(self):
        """
        Return the priority level of the highest priority action in the active collection.
        :return: The priority level of the highest priority action in the active collection, as int.
        Note: Returns 0 if there are no active actions.
        """
        if len(self._active)==0:
            return 0
        else:
            # Sort now,just to make sure.
            self._active.sort(key=lambda x: x.priority, reverse=True)
            # Note: We are assuming that the active list is sorted in descending order of priority.
            return self._active[0].priority
    
    def execute(self):
        """
        Process the ActionManager.
        Generally implements the pseudo code from "Artificial Intelligence for Games", Second Edition,
                by Ian Millington and John Funge, Chapter 5, Section 5.11.3, 2009. However, catches
                ActionCannotAchieveGoalError exceptions raised by actions when they are exectued,
                and removes those actions.
        :return: None
        """

        # Update the time
        self._currentTime+=1

        # Go through the queue to find interrupters
        for action in list(self._action_list):

            # If we drop below the active priority, give up
            if action.priority <= self.getHighestPriorityActive():
                break

            # If we have an interrupter, then interrupt
            if action.canInterrupt():
                # Empty the active list
                self._active.clear()
                # Add the action to the active list, and sort the active list, to maintain priority order
                self._active.append(action)
                self._active.sort(key=lambda x: x.priority, reverse=True)
                # Remove the activeated action from the action list
                self._action_list.remove(action)

        # Try to add as many actions as possible to the active list
        for pending in list(self._action_list):
                
            # Check if the action has timed out
            if pending.expiry_time < self._currentTime:
                # Remove it from the pending action list   
                self._action_list.remove(pending)
                # Move on to the next pending action
                continue
                    
            # If there are no currently active actions, then we can add the pending action to the active list
            if len(self._active) == 0:
                # First, remove the pending action from the action list
                self._action_list.remove(pending)
                # Add it to the active list
                self._active.append(pending)
                # Sort the active list to maintain priority order
                self._active.sort(key=lambda x: x.priority, reverse=True)
            else:
            # There are currently active actions, so check if we can combine the pending action with all active actions
                canCombine=True
                for activeAction in self._active:
                    if not pending.canDoBoth(activeAction):
                        canCombine=False
                        break
                if canCombine:
                    # If we can combine, remove pending action from the action list and add it to the active list
                    self._action_list.remove(pending)
                    self._active.append(pending)
                    # Sort the active list to maintain priority order
                    self._active.sort(key=lambda x: x.priority, reverse=True)
 
        # Process the active list
        for action in list(self._active):
            
            if action.isComplete():
                # Remove completed actions
                self._active.remove(action)
            else:
                # Execute others
                try:
                    action.execute()
                except ActionCannotAchieveGoalError as e:
                    # If the action cannot achieve the goal, remove it from the active list
                    self._active.remove(action)
                    # And re-raise the exception, so that the caller is notified too
                    raise e
    
        return None

    def __str__(self):
        """
        Return a string representation of the ActionManager.
        :return: A string representation of the ActionManager.
        """
        return f"ActionManager(currentTime={self._currentTime}, " \
               f"action_list={self._action_list}, active={self._active})"






