"""
Defines the abstract base class, GameAction. Concrete implementations of this class represent actions that
can be performed in a game. Also defines GameAction child classes, GameActionCombination, representing
combinations of actions (exectued together), and GameActionSequence, representing sequences of actions
(exectued in order).

It generally implements pseudo code from the book "Artificial Intelligence for Games", Second Edtion, by
Ian Millington and John Funge, Chapter 5, Section 5.11.4, 2009. The possibility of actions being able to
read and write data to a blackboard is additionally included, following the blackboard architecture pattern
described in Section 5.9 of the same book.

Concrete implementation child classes of GameAction
    must: 
        (1) Implement the method execute(), which is called to enact the specific behavior of the action.
        (2) Override the method isComplete(), which is called to determine if the action is completed.
    may/should:
        (3) (may) Override the method canInterrupt(), which is called to determine if this action can
            interrupt other actions, e.g., in the active collection of an ActionManager object.
        (4) (may) Override the method canDoBoth(), which is called to determine of this action can be executed
            at the same time as another action, e.g., in the active collection of an ActionManager object.
        (5) (should) Override the method getGoalChange(), which is called to determine how this action affects
            the insistence of a specific goal, e.g., in the goal collection maintained by a Gob object. The method
            should return a negative integer, indicating a reduction in the insistence of the goal when the action
            is executed.

Exported Classes:
    GameAction -- An abstract base class representing a game action.
    GameActionCombination -- A class representing a combination of game actions that can be performed together.
    GameActionSequence -- A class representing a sequence of game actions that must be performed in order.

Exported Exceptions:
    None
    
Exceptions raised:
    ActionCannotAchieveGoalError -- Raised by GameActionCombination.execute() and
                                    GameActionSequence.execute() when an action's execute() is called
                                    and cannot achieve its intended goal.
 
Exported Functions:
    None
"""

# Standard imports


# Local imports
from pyGameAIFoundation.game_goal import GameGoal, GoalInsistence
from pyGameAIFoundation.exceptions import ActionCannotAchieveGoalError


class GameAction(object):
    """
    This class represents a game action that can be performed in a game.
    """

    def __init__(self, expiry_time=0, priority=0, read_blackboard=None, write_blackboard=None):
        """
        :parameter expiry_time: The time in an arbitrary count-up from zero until the action expires, as int
                                Each execution cycle of the ActionManager increments the time count by 1.
        :parameter priority: The priority of the action. Higher numbers indicate higher priority. As int
        :parameter read_blackboard: A function to read from the blackboard of a blackboard architecture, as callable
            Signature: read_blackboard(key: str) -> any
        :parameter write_blackboard: A function to write to the blackboard of a blackboard architecture, as callable
            Signature: write_blackboard(key: str, value: any) -> None
        """
        self._expiry_time=expiry_time
        self._priority=priority
        self._read_blackboard = read_blackboard
        if read_blackboard:
            assert(callable(read_blackboard))
        self._write_blackboard = write_blackboard
        if write_blackboard:
            assert(callable(write_blackboard))

    @property
    def expiry_time(self):
        """
        Return the expiration time of the action. Expiration time is an arbitrary count-up from zero.
        :return: The expiration time of the action, as int.
        """
        return self._expiry_time

    @expiry_time.setter
    def expiry_time(self, expire_at):
        """
        Set the expiration time of the action.
        :parameter expire_at: The time in an arbitrary count-up from zero until the action expires, as int.
        """
        self._expiry_time = expire_at

    @property
    def priority(self):
        """
        Return the priority of the action. Higher numbers indicate higher priority.
        :return: The priority of the action, as int.
        """
        return self._priority

    def canInterrupt(self):
        """
        Return whether this action can interrupt other actions.
        :return: True if this action can interrupt other actions, False otherwise, as boolean.
        """
        # At this base class level, we assume that this action cannot interrupt other actions.
        return False

    def canDoBoth(self, other_action):
        """
        Return whether this action can be done at the same time as another action.
        :param other_action: The other action to check against, as GameAction object.
        :return: True if both actions can be done at the same time, False otherwise, as boolean.
        """
        assert(isinstance(other_action, GameAction))
        # At this base class level, we assume that all actions cannot be done at the same time.
        return False

    def isComplete(self):
        """
        Return whether this action is complete.
        :return: True if the action is complete, False otherwise, as boolean.
        """
        # At this base class level, we assume that all actions are not complete.
        return False

    def execute(self):
        """
        Execute the action. This method must be implemented by concrete subclasses to provide specific action behavior.
        Raises NotImplementedError if called and not implemented by a subclass.
        :return: None
        """
        raise NotImplementedError("Subclasses of GameAction must implement this method.")
        return None

    def getGoalChange(self, goal=None):
        """
        Return the goal insistence change associated with this action. This method should be overridden by subclasses to
            provide specific goal insistence change behavior.
        :param goal: The goal to check against, as GameGoal object.
        :return: The goal insistence change associated with this action, as int.
        Note: By convention, a negative integer indicates a reduction in the insistence of the goal when the action is executed.
        """
        assert(isinstance(goal, GameGoal))
        # By default, here in the base class, we assume that the action does not change any goal.
        return GoalInsistence.ZERO


class GameActionCombination(GameAction):
    """
    This class represents a combination of game actions that can be performed together.
    """
    def __init__(self, expiry_time=0, priority=0, combo_acts=[]):
        """
        :parameter expiry_time: The time in an arbitrary count-up from zero until the action expires, as int
                                Each execution cycle of the ActionManager increments the time count by 1.
        :parameter priority: The priority of the action. Higher numbers indicate higher priority. As int
        :parameter combo_acts: A list of GameAction objects that make up the combination.
        """
        super().__init__(expiry_time, priority)
        self._action_list = [] # List of GameActions in the combination
        for act in combo_acts:
            assert(isinstance(act, GameAction))
            self._action_list.append(act)

    def canInterrupt(self):
        """
        Return whether this combination of actions can be interrupted by another action.
        :return: True if the combination of actions can be interrupted, False otherwise, as boolean.
        """
        # The combination can be interrupted if any of its subactions can be interrupted.
        for act in self._action_list:
            if act.canInterrupt():
                return True
        return False

    def canDoBoth(self, other_action):
        """
        Return whether this combination action can be done at the same time as another action.
        :param other_action: The other action to check against, as GameAction object.
        :return: True if both actions can be done at the same time, False otherwise, as boolean.
        """
        assert(isinstance(other_action, GameAction))
        # Combination is possible if it is possible for all subactions.
        for act in self._action_list:
            if not act.canDoBoth(other_action):
                return False
        return True

    def isComplete(self):
        """
        Return whether this combination action is complete.
        :return: True if the combination action is complete, False otherwise, as boolean.
        """
        # Combination is complete if all subactions are complete.
        for act in self._action_list:
            if not act.isComplete():
                return False
        return True

    def execute(self):
        """
        Execute all subactions in the combination.
        Raises ActionCannotAchieveGoalError if any subaction's execute() cannot achieve its intended goal.
        :return: None
        """
        for act in self._action_list:
            try:
                act.execute()
            except ActionCannotAchieveGoalError as e:
                # Re-raise the exception with the current action and parent action set to this combination
                raise ActionCannotAchieveGoalError(action=e._action, parent_action=self)
        return None

    def getGoalChange(self, goal=None):
        """
        Return the goal insistence change associated with this combination action.
        :param goal: The goal to check against, as GameGoal object.
        :return: The goal insistence change associated with this combination action, as int.
        """
        assert(isinstance(goal, GameGoal))
        # Add up the contributions of all subactions to the goal change, and return the negative of this total
        total = 0
        for act in self._action_list:
            total += act.getGoalChange(goal)
        return total


class GameActionSequence(GameAction):
    """
    This class represents a sequence of game actions that must be performed in order.
    """
    def __init__(self, expiry_time=0, priority=0, seq_acts=[]):
        """
        :parameter expiry_time: The time in an arbitrary count-up from zero until the action expires, as int
                                Each execution cycle of the ActionManager increments the time count by 1.
        :parameter priority: The priority of the action. Higher numbers indicate higher priority. As int
        :parameter seq_acts: A list of GameAction objects that make up the ordered sequence.
        """
        super().__init__(expiry_time, priority)
        self._action_list = [] # List of GameActions in the sequence
        for act in seq_acts:
            assert(isinstance(act, GameAction))
            self._action_list.append(act)
        if len(self._action_list) >= 0:
            self._activeIndex=0 # The index of the currently active (executing) action in the sequence
        else:
            self._activeIndex=-1
        self._blackboard={} # A dictionary used to pass information between actions in the sequence

    def addAction(self, action):
        """
        Add an action to the sequence.
        :param action: The GameAction object to add to the sequence.
        :return: None
        """
        assert(isinstance(action, GameAction))
        self._action_list.append(action)
        # If we are adding the first action to the sequence, set the active index to 0
        if self._activeIndex < 0:
            self._activeIndex = 0
        return None
        
    def canInterrupt(self):
        """
        Return whether this sequence of actions can be interrupted by another action.
        :return: True if the sequence of actions can be interrupted, False otherwise, as boolean.
        """
        # The sequence can be interrupted if the active action can be interrupted. 
        return self._action_list[self._activeIndex].canInterrupt()

    def canDoBoth(self, other_action):
        """
        Return whether this sequence action can be done at the same time as another action.
        :param other_action: The other action to check against, as GameAction object.
        :return: True if both actions can be done at the same time, False otherwise, as boolean.
        """
        assert(isinstance(other_action, GameAction))
        # We can do both if all subactions that have not already been exectued can do both
        for i in range(self._activeIndex, len(self._action_list)):
            if not self._action_list[i].canDoBoth(other_action):
                return False
        return True

    def isComplete(self):
        """
        Return whether this sequence action is complete.
        :return: True if the sequence action is complete, False otherwise, as boolean.
        """
        #  Sequence is complete if all subactions are complete.
        return self._activeIndex >= len(self._action_list)

    def execute(self):
        """
        Execute subactions in the sequence one at a time and in order.
        :return: None
        """
        # If there are no actions in the sequence, do nothing
        if self._activeIndex < 0:
            return
        
        # Execute the current action in the sequence
        try:
            self._action_list[self._activeIndex].execute()
        except ActionCannotAchieveGoalError as e:
            # Re-raise the exception with the current action and parent action set to this sequence
            raise ActionCannotAchieveGoalError(action=e._action, parent_action=self)

        # If current action is complete, move to the next action in the sequence
        if self._action_list[self._activeIndex].isComplete():
            self._activeIndex += 1

        return None

    def getGoalChange(self, goal=None):
        """
        Return the goal insistence change associated with this action sequence.
        :param goal: The goal to check against, as GameGoal object.
        :return: The goal insistence change associated with this action sequence, as int.
        """
        assert(isinstance(goal, GameGoal))
        # Add up the contributions of all subactions to the goal change, and return the negative of this total
        total = 0
        for act in self._action_list:
            total += act.getGoalChange(goal)
        return total

    def writeToBlackBoard(self, key, value):
        """
        Write a value to the blackboard architecture blackboard for this sequence.
        :param key: The key to write the value under, as str.
        :param value: The value to write, as any type.
        :return: None
        """
        self._blackboard[key] = value
        return None

    def readFromBlackBoard(self, key):
        """
        Read a value from the blackboard architecture blackboard for this sequence.
        :param key: The key to read the value from, as str.
        :return: The value associated with the key, or None if the key does not exist.
        """
        return self._blackboard.get(key, None)