from typing import Optional, Dict, Set, Type, cast
from xml.etree.ElementTree import Element
import uuid

from pymasep.common.base_object import BaseObject
from pymasep.common.agent import Agent
from pymasep.common.action import Action
from pymasep.common.state import State
from pymasep.common.controller import Controller
from pymasep.common.template import Template
from pymasep.common.game import Game
from pymasep.utils import import_from_dotted_path, setup_logger, close_logger


class Environment:
    """
    Represents the environment of the game. This object keeps all dynamic value of the game.
    :param id_env: the id of the environment. If None, a new uuid will be set
    """

    def __init__(self, id_env: str = None) -> None:
        self.id: str = id_env if id_env is not None else str(uuid.uuid4())
        self.end_episode: bool = False
        self.current_step: int = 0
        self.current_episode: int = 0
        self.current_state: Optional[State] = None
        self.current_additional_info: dict = {}
        self.agents: Set[Agent] = set()
        self.controllers: Dict[str, Controller] = {}
        self._game: Optional[Game] = None
        self.next_action: Dict[str, Action] = {}
        self.logger = setup_logger(name=self.__class__.__name__,
                                   path='logs',
                                   log_filename=self.__class__.__name__ + '-' + str(self.id) + '.log')

    def __del__(self):
        close_logger(self.logger)

    @property
    def game(self) -> Game:
        return self._game

    @game.setter
    def game(self, g: Game) -> None:
        self._game = g

    def get_observation_for_agent(self) -> State:
        """
        get the current observation. current state at the moment

        :return: the current observation
        """
        return self.game.observe_state(self.current_state)

    def get_additional_info_for_agent(self) -> dict:
        """
        get the current additional information. Could be filtered in the future (by agent)

        :return: the current additional information
        """
        return self.current_additional_info

    def get_reward_for_agent(self, agent: Agent) -> dict:
        """
        get the current reward for an agent.

        :param agent: the agent to get the reward

        :return: the current reward of the agent
        """
        result = 0
        if 'reward' in self.current_additional_info:
            if agent.get_fullname() in self.current_additional_info['reward']:
                result = self.current_additional_info['reward'][agent.get_fullname()]
        return result

    def get_controller(self, agent_name: str, class_controller_: Type[Controller]) -> Controller:
        """
        get the controller of an agent. If the controller is not created, create it.
        (Maybe i should change the name of this method...)

        :param agent_name: the agent
        :param class_controller_: the class of the controller to create if it is not created
        :return: the instance of the agent's controller
        """
        controller = self.controllers.get(agent_name, class_controller_(environment=self))
        self.controllers[agent_name] = controller
        return controller

    def calculate_end_episode(self) -> None:
        """
        calculate the end of the episode. modify self.end_episode if the conditions of the end of episode are met
        """
        self.end_episode = self.game.is_final(self.current_state) or self.end_episode
        if self.end_episode:
            self.current_state.is_final = True

    def calculate_next_state(self) -> None:
        """
        Calculate the next state of the environment.
        Change the current state.
        Increase the step
        """
        self.current_step = self.current_step + 1
        self.current_state, self.current_additional_info = self.game.next_state(current_state=self.current_state,
                                                                                actions=self.next_action)

        self.current_state.step = self.current_step
        self.logger.info('Current state after next state' + str(self.current_state))

    def calculate_reward(self) -> None:
        """
        calculate the reward from the current_state.
        Support only (at the moment) R(s) and neither R(s,a) nor R(s,a,s')
        adding the result to additional info with key 'reward'
        """
        self.current_additional_info['reward'] = self.game.reward(previous_state=None,
                                                                  actions=None,
                                                                  next_state=self.current_state)

    def create_base_object(self,
                           name: Optional[str] = None,
                           template: Optional[Template] = None,
                           xml_node: Optional[Element] = None,
                           parent: Optional[BaseObject] = None):
        """
        create a base object from a string represent the type

        :param name: the identification name of the BaseObject.
        :param template: template of the created object. \
                If None and no xml is specified, will be the default template for this bo_type
        :param xml_node: xml node used for creating the base object
        :param parent: the parent of the current object. May be None
        :return: the BaseObject instance
        """
        # if no module is specified, the default is pymasep.common
        if xml_node is not None:
            bo_type = xml_node.tag
        else:
            bo_type = template.created_class
        if '.' not in bo_type:
            bo_type = 'pymasep.common.' + bo_type
        class_ = import_from_dotted_path(bo_type)
        if xml_node is not None:
            result = class_(environment=self, name=name, xml_node=xml_node, parent=parent)
        else:
            result = class_(environment=self, name=name, parent=parent, template=template)
        return result

    @staticmethod
    def get_type_controller(xml_node: Optional[Element] = None,
                            template: Optional[Template] = None,
                            control: Optional[str] = None) -> Type[Controller]:
        """
        Get the type of controller according to a template or (exclusive) a xml node.

        :param xml_node: Xml node used to get the controller type. Use the attribut "controller".
        :param template: The template used to get controller type.
        :param control: 'interface' if the agent is controlled by interface \
                (only inside engine, and if template is used)
        :return: the class of the controller to use
        """

        if template:
            class_controller_ = import_from_dotted_path(template.controller)
        else:
            class_controller_ = import_from_dotted_path(xml_node.attrib['controller'])
        return class_controller_

    def create_object(self,
                      name: Optional[str] = None,
                      template: Optional[Template] = None,
                      xml_node: Optional[Element] = None,
                      parent: Optional[BaseObject] = None,
                      control: Optional[str] = None):
        """
        Create an object. Add it into the object dictionary (if not already exists) and agent set if the object is Agent

        :param name: the identification name of the object. Not used if xml_node is used
        :param template: the template of the class to create the object. Not used if xml_node is used
        :param xml_node: xml node describing the object to create
        :param parent: the parent of the current object. Maybe None
        :param control: 'interface' if the agent is controlled by interface (only inside engine and if template is used)
        :return: the object instance
        """
        # the controller is not ExternalController ????
        result = self.create_base_object(name=name, template=template, xml_node=xml_node, parent=parent)
        if xml_node is not None:
            result.from_xml(self, xml_node)
        # self.base_objects[result.get_fullname()] = result
        if isinstance(result, Agent):
            class_controller_ = self.get_type_controller(xml_node=xml_node,
                                                         template=template,
                                                         control=control)
            result.controller = self.get_controller(result.name, class_controller_)
            if control:
                result.control = control
            self.agents.add(result)

            # put agent into ordered list if state contains system object
            parent = cast(State, parent)
            if parent is not None and 'State.system' in parent.objects:
                agent_play_order = self.game.get_system_value(parent, 'AgentOrder')
                self.game.set_system_value(parent, 'AgentOrder',
                                           self.game.order_agents(agent_play_order, result))
        return result

    def create_action(self, action_type: Optional[int] = None, xml_node: Element = None) -> Action:
        """
        create an action

        :param action_type: the type of the action (Constants in Action class)
        :param xml_node: xml to create Action

        :return: The Action created from xml or with just a type
        """
        result = Action(environment=self, action_type=action_type, xml_node=xml_node)
        if xml_node is not None:
            result.from_xml(self, xml_node)
        return result

    def choose_action(self) -> None:
        """
        tell all agents to choose their action
        """
        self.next_action.clear()
        if self.game.coord_method == Game.MULTIPLAYER_COORDINATION_TURN:
            current_ag_name_to_play = self.game.get_system_value(self.current_state, 'AgentOrder').head
            current_ag_to_play = [ag for ag in self.agents if ag.get_fullname() == current_ag_name_to_play][0]

            current_ag_to_play.choose_action()
            if current_ag_to_play.action:
                self.logger.info('choose_action() ' + current_ag_to_play.name + ' ' + str(current_ag_to_play.action))
            else:
                self.logger.info('choose_action() ' + current_ag_to_play.name + ' None')
            if current_ag_to_play.action is not None:
                self.next_action[current_ag_to_play.get_fullname()] = current_ag_to_play.action
        if self.game.coord_method == Game.MULTIPLAYER_COORDINATION_WAITING_ALL:
            for ag in self.agents:
                ag.choose_action()
                if ag.action is not None:
                    self.next_action[ag.get_fullname()] = ag.action
        self.logger.info(str([(a[0], str(a[1])) for a in self.next_action.items()]))

    def clear_all_state_elements(self) -> None:
        """
        Clear all in the environment
        """
        self.agents.clear()
        self.next_action.clear()
        self.current_state = None
        self.current_additional_info = dict()
        self.current_step = 0
        self.end_episode = False
