import logging
from typing import Any, List

from .consts import ObjectPositionType
from .event import EventArguments
from .action import Actions
from .struct import ObjectPosition
from ..utils import BaseModel


class EventFrame(BaseModel):
    """
    Event frame is a frame of events, contains events with their arguments, the
    object lists that will be triggered by the event arguments, and the
    action lists that has triggered.

    When one action is done, it will generate a new event frame, with various
    number of events, and append it to the event frame list. When there are
    events and no processing objects and actions, the first event will
    trigger objects. When there are triggered objects, the first object
    position will be popped and trigger actions. When there are triggered
    actions, the first action will be popped and trigger events, then add
    a new event frame. If an event frame has no events, no triggered objects
    and no triggered actions, it will be removed from the list.
    """

    events: List[EventArguments]
    processing_event: EventArguments | None = None
    triggered_objects: List[ObjectPosition] = []
    triggered_actions: List[Actions] = []


class EventController(BaseModel):
    frame_list: List[EventFrame] = []

    def has_event(self) -> bool:
        return len(self.frame_list) != 0

    def append(self, event_frame: EventFrame) -> None:
        self.frame_list.append(event_frame)

    def pop(self) -> EventFrame:
        """
        pop and return last event frame.
        """
        return self.frame_list.pop()

    def run_event_frame(self, match: Any) -> None:
        """
        run the event frame. if last event frame contains actions, do actions and
        return; otherwise,
        trigger next object; otherwise, trigger next event; otherwise, pop last frame;
        otherwise, clear trashbin and done triggering.
        When any actions has done, or no valid event frame exist, return.
        """
        while len(self.frame_list):
            event_frame = self.frame_list[-1]
            if len(event_frame.triggered_actions):
                self.act_action(event_frame, match)
                return
            elif len(event_frame.triggered_objects):
                self.get_action(event_frame, match)
            elif len(event_frame.events):
                self.trigger_event(event_frame, match)
            else:
                self.pop()
        # event frame cleared, clear trashbin
        match.trashbin.clear()

    def act_action(self, frame: EventFrame, match: Any) -> None:
        """
        Act the actions. If these actions are triggered by one object (i.e.,
        event_frame.processing_event is not None), do all actions immediately;
        otherwise, actions are generated by system, and only do one action.
        """
        if frame.processing_event is None:
            # do one action
            activated_action = frame.triggered_actions.pop(0)
            logging.info(f"Action activated: {activated_action}")
            event_args = match._act(activated_action)
            match.record_last_action_history()
            self.stack_events(event_args)
        else:
            # do all actions
            event_args = []
            for activated_action in frame.triggered_actions:
                logging.info(f"Action activated: {activated_action}")
                event_args += match._act(activated_action)
                match.record_last_action_history()
            frame.triggered_actions = []
            self.stack_events(event_args)

    def get_action(self, frame: EventFrame, match: Any) -> None:
        """
        Get actions from triggered objects.
        """
        event_arg = frame.processing_event
        assert event_arg is not None
        object_position = frame.triggered_objects.pop(0)
        obj = match.get_object(object_position, event_arg.type)
        handler_name = f"event_handler_{event_arg.type.name}"
        func = getattr(obj, handler_name, None)
        if func is not None:
            frame.triggered_actions = func(event_arg, match)
        self.frame_list[-1] = frame

    def trigger_event(self, event_frame: EventFrame, match: Any) -> None:
        """
        trigger new event to update triggered object lists of a EventFrame.
        it will take first event from events, put it into processing_event,
        and update triggered object lists.
        """
        event_arg = event_frame.events.pop(0)
        event_frame.processing_event = event_arg
        object_list = match.get_object_list()
        # add object in trashbin to list
        for obj in match.trashbin:
            if event_arg.type in obj.available_handler_in_trashbin:
                object_list.append(obj)
        handler_name = f"event_handler_{event_arg.type.name}"
        for obj in object_list:
            # for deck objects, check availability
            if obj.position.area == ObjectPositionType.DECK:
                if event_arg.type not in obj.available_handler_in_deck:
                    continue
            func = getattr(obj, handler_name, None)
            if func is not None:
                event_frame.triggered_objects.append(obj.position)

    def stack_event(self, event_arg: EventArguments) -> EventFrame:
        """
        stack a new event. It will wrap it into a list and call
        self.stack_events.
        """
        return self.stack_events([event_arg])

    def stack_actions(self, actions: List[Actions]) -> EventFrame:
        """
        stack new actions, these actions are not triggered by any object, so when any
        action in it is triggered, its event will be triggered immediately.
        """
        event_frame = EventFrame(events=[], triggered_actions=actions)
        self.frame_list.append(event_frame)
        return event_frame

    def stack_events(self, event_args: List[EventArguments]) -> EventFrame:
        """
        stack events. It will create a new EventFrame with events and
        append it into self.event_frames. Then it will return the event frame.
        """
        frame = EventFrame(
            events=event_args,
        )
        self.frame_list.append(frame)
        return frame
