import queue
import uuid
from typing import List, Optional
from threading import Thread
from logging import Logger

from omegaconf import DictConfig

from pymasep.application.sub_app_state import SubAppState
from pymasep.application.connected_sub_app_info import ConnectedSubAppInfo
from pymasep.communication.message import Message
from pymasep.communication.frame import Frame
from pymasep.common.environment import Environment
from pymasep.utils import setup_logger, close_logger


class SubApp(Thread):
    """
    Part of the application as a Thread. Usually a sub application is the engine or the interface

    :param app: main application
    :param received_q_id: id of the queue used by the sub app to receive messages.
    :param cfg: configuration of the sub application
    """

    def __init__(self, app, received_q_id: int, cfg: DictConfig) -> None:

        Thread.__init__(self, name=self.__class__.__name__)
        self.id: str = str(uuid.uuid4())
        self.config = cfg
        self.app = app
        self.messages_received_q_id = received_q_id
        self.messages_received: queue.Queue = self.app.queues[received_q_id]
        self.connected_sub_app = {}
        self.current_event: Optional[Message] = None
        # State management
        self.app_states = {}
        self.current_app_states: List[SubAppState] = []
        # Logger
        self.logger: Logger = setup_logger(name=self.__class__.__name__,
                                           path='logs',
                                           log_filename=self.__class__.__name__ + '-' + str(self.id) + '.log')
        self.environment: Optional[Environment] = None
        self.resources = {}
        self.remote_communication = None

    def __del__(self):
        """
        delete the instance. Close the logger
        """
        close_logger(self.logger)

    def register_connected_sub_app(self, sub_app_id: str, role: str, sent_q: Optional[queue.Queue] = None) -> None:
        """
        Register a sub application connected to self with a rale, and optionally a queue (if it's the same process)

        :param sub_app_id: id of the connected sub app
        :param role: role of the connected sub app. See ConnectedSubAppInfo.
        :param sent_q: Queue of the connected sub app used to send message (for sub apps on the same process)
        """
        sa_info = ConnectedSubAppInfo(sub_app_id=sub_app_id, role=role, queue=sent_q)
        self.connected_sub_app[sub_app_id] = sa_info

    def get_connected_id_from_agent_fname(self, agent_fname: str) -> str:
        """
        get the connected sub app that handle an agent

        :param agent_fname: the agent full name (ex. state.agent_name)
        :return: the id of the sub app associated to the agent
        """
        result = None
        list_id = [csai.id for csai in self.connected_sub_app.values() if csai.agent_fname == agent_fname]
        if list_id:
            result = list_id[0]
        return result

    def send_message(self, message: Message, subapp_id: str) -> None:
        """
        Send message to a subapp from its id. Send by queue or by socket according to
        the connection between the tow subapp

        :param message: message to send
        :param subapp_id: destination subapp id
        """

        if self.connected_sub_app[subapp_id].queue is not None:
            self.send_message_to_queue(message, self.connected_sub_app[subapp_id].queue)
        else:
            self.remote_communication.send_frame_to(subapp_id, Frame(message.to_bytes()))
        self.logger.info('Sent message ' + str(message.to_bytes()))

    @staticmethod
    def send_message_to_queue(message: Message, q: queue.Queue) -> None:
        """
        Send a message to a queue. The message is put in the queue as byte

        :param message the message to send
        :param q the queue
        """
        q.put(message.to_bytes())

    def wait_message(self, wanted_messages: List[int], block: bool = True) -> Message:
        """
        Wait for a message of certain types. The wait can be blocked or not.
        The received message is put in self.current_event

        :param wanted_messages: list of type of wanted message.
        :param block: True if the method blocks until a message arrives
        :return: the received message
        """
        try:
            msg_byte = self.messages_received.get(block=block)
            msg = Message(0, None, self.id)
            msg.from_bytes(msg_byte)
            if msg.msg_type in wanted_messages:
                self.current_event = msg
                self.logger.info('Received message: ' + str(msg.msg_type))
            else:
                self.current_event = None
                self.logger.warning('Expected:' + str(wanted_messages) + ' instead have ' + str(msg.msg_type))
        except queue.Empty:
            self.current_event = None
        return self.current_event

    # Game state management (kind of)

    def get_current_app_state(self):
        """
        Get the current sub app state

        :return: the current sub app state as SubAppState
        """
        return self.current_app_states[-1]

    def push_state(self, sbapp_state: SubAppState) -> None:
        """
        Push a new state in the head state. Pause the current state and init the new one.

        :param sbapp_state: new state to push
        """
        if self.current_app_states:
            self.current_app_states[-1].pause()
        self.current_app_states.append(sbapp_state)
        self.current_app_states[-1].init()

    def set_state(self, sbapp_state: SubAppState) -> None:
        """
        Set a new state. Remove the ald current and push the new one.

        :param sbapp_state: new state to set
        """
        self.pop_state()
        self.push_state(sbapp_state)

    def pop_state(self) -> None:
        """
        Clean and remove the current state
        """
        if self.current_app_states:
            self.current_app_states[-1].clean()
            self.current_app_states.pop()
            self.current_app_states[-1].pause()
