from typing import Dict, Tuple, Optional, List, final, Any
import os
import json
import xml.etree.ElementTree as Et
from omegaconf import OmegaConf, DictConfig

from pymasep.common.agent import Agent
from pymasep.common.state import State
from pymasep.common.action import Action
from pymasep.common.template import Template
from pymasep.common.initializer import Initializer
from pymasep.circular_head_list import CircularHeadList


class Game:
    """
    Represents the rules of the game.
    A game contains templates and initializers.

    :param cfg: DictConfig containing the configuration. See :ref:`config` for more information.
    """

    ACTION_NO_ACTION = 0
    ACTION_INIT_OBJECT = 1000
    ACTION_INTENTION = 2000

    # Turn by turn game
    MULTIPLAYER_COORDINATION_TURN = 1
    # Waiting all action before next state. Not implemented for interface agents at the moment.
    MULTIPLAYER_COORDINATION_WAITING_ALL = 2
    # Execute action when it arrives. Not implemented
    MULTIPLAYER_COORDINATION_FREE_FOR_ALL = 3

    def __init__(self, cfg: DictConfig = None) -> None:
        if cfg is None:
            cfg = OmegaConf.create()
        if cfg.get('game') is None:
            cfg['game'] = {}

        self.config = cfg
        self.initializers = dict()
        self.templates = dict()
        self.default_templates = dict()
        self.root_path = self.config['root_path']
        self.data_path = self.config.get('data_path', 'data')
        self.is_rewarded = None  # is the game rewards agents ?
        self.nb_episodes = 1
        self.add_templates()
        self.add_initializers()
        self.max_nb_players = self.config.game.get('max_nb_players', 0)
        self.possible_players = ['player' + str(p) for p in range(self.max_nb_players)]

        # agent coordination management
        self.coord_method = self.MULTIPLAYER_COORDINATION_TURN

    def add_templates(self) -> None:

        empty_intention_tmpl = Template(self, {"name": "EmptyIntentionTemplate",
                                               "created_class": "Intention"})
        self.add_template(empty_intention_tmpl)

        dflt_state_tmpl = Template(self, {"name": "DefaultStateTemplate",
                                          "created_class": "State"})
        self.add_template(dflt_state_tmpl)

        # System Object template
        agent_order_charac = Template(self, {"name": "AgentOrder",
                                             "created_class": "Characteristic",
                                             "value_type": "CircularHeadList",
                                             "default_value": CircularHeadList()})
        self.add_template(agent_order_charac)
        system_object_state_tmpl = Template(self, {"name": "SystemObjectStateTemplate",
                                                   "created_class": "ObjectState",
                                                   "subclass_templates": ['AgentOrder']})
        self.add_template(system_object_state_tmpl)
        system_object_tmpl = Template(self, {'name': 'SystemObjectTemplate',
                                             'name_base_object': 'system',
                                             'created_class': "Object",
                                             "subclass_templates": ["SystemObjectStateTemplate"]
                                             })
        self.add_template(system_object_tmpl)

        sys_state_tmpl = Template(self, {"name": "SystemStateTemplate",
                                         "created_class": "State",
                                         "subclass_templates": ["SystemObjectTemplate"]})
        self.add_template(sys_state_tmpl)

        template_filename = os.path.join(self.root_path, self.data_path, 'templates.json')
        if os.path.isfile(template_filename):
            with open(template_filename) as json_file:
                templates = json.load(json_file)
            for template_dict in templates['Templates']:
                tmpl = Template(self, template_dict)
                self.add_template(tmpl)

    def add_initializers(self):
        pass

    def init_state(self, environment) -> State:
        """
        Create the initial state of the game. Can be reimplemented in subclasses.

        :param environment: the environment
        :return: the initial state
        """
        tree = Et.parse(os.path.join(self.root_path, self.data_path, 'state.xml'))
        xml_node = tree.getroot()
        result = environment.create_object(xml_node=xml_node, parent=None)
        return result

    @final
    def initialize_state(self, environment) -> State:
        """
        Create the initial state of the game.

        :param environment: the environment
        :return: the initial state
        """
        result = self.init_state(environment)
        return result

    @staticmethod
    def set_system_value(state: State, key: str, value: Any) -> State:
        """
        Set a value to a system characteristic. The charac is set to "State.system" object
        :param state: The state where the characteristic have to be set. The state is modified
        :param key: the name of the characteristic
        :param value: the value of the characteristic
        :return: the state modified
        """
        state.objects['State.system'].object_state.characteristics[key].value = value
        return state

    @staticmethod
    def get_system_value(state: State, key: str) -> Any:
        """
        Get the value of a system characteristic.
        :param state: the state where to read the value
        :param key: the name of the characteric. The name must exist.
        :return: the value of the characteristic
        """
        return state.objects['State.system'].object_state.characteristics[key].value

    def create_external_agent(self, env, external_id: str) -> Agent:
        """
        Create an agent for an external thread. Initialized as best as can the game do without external inputs
        :param: env the environment
        :param: external_id: uuid of the external thread/sub_app
        :return the agent
        """
        existing_agents = [ag.name for ag in env.agents]
        agent_name = [x for x in self.possible_players if x not in existing_agents]
        tree = Et.parse(os.path.join(self.root_path, self.data_path, 'player_init.xml'))
        xml_node = tree.getroot()
        result = env.create_object(name=agent_name[0], xml_node=xml_node, parent=env.current_state)
        result.control = external_id
        return result

    def next_state(self, current_state: State, actions: Dict[str, Action]) -> Tuple[State, Dict]:
        """
        Calculates the new state according to the current state and the action of the agent.
        Must be reimplemented in sub classes.

        :param current_state: the current state
        :param actions: action of the agents
        :return: the new state and the additional information about the state (may be None)
        """
        raise NotImplementedError("Please Implement this method")

    def reward(self,
               previous_state: Optional[State],
               actions: Optional[Dict[str, Action]],
               next_state: State) -> Dict:
        """
        Return the reward for all agents depending on the previous_state, actions and the next state.
        This is the general form but each game car implement it as they want.

        :param previous_state: the previous state before actions
        :param actions: actions of the agents
        :param next_state: next state usually calculated by next_state()
        :return: a dictionary of the reward by agent.
        """
        raise NotImplementedError("Please Implement this method")

    def is_final(self, state: State) -> bool:
        """
        Is the state final ?

        :param state: the state
        :return: True if final, False if not
        """
        return False

    @staticmethod
    def observe_state(state: State) -> State:
        """
        State Observation function. calculate the partial state according to d from state pS
        :param state the state of the environment
        :return the observed state. The result is a state (possibly partial and noisy) and not a general observation.
        """
        observation = state
        return observation

    def add_initializer(self, initializer: Initializer) -> None:
        """
        Add an initializer into the initializers dictionary's game

        :param initializer: the test_initializer to add
        """
        self.initializers[initializer.name] = initializer

    def add_template(self, template: Template) -> None:
        """
        Add a template into the initializers dictionary's game

        :param template: the template to add
        """
        self.templates[template.name] = template

    def add_default_template(self, object_nature: str, template_name: str) -> None:
        """
        Add a default template into the initializers dictionary's game

        :param object_nature the type of object to associate
        :param template_name the template to associate (must be exists in game.templates
        """
        self.default_templates[object_nature] = self.templates[template_name]

    @staticmethod
    def order_agents(current_order: List[str], new_agent) -> List[str]:
        """
        implement rules to determine the order of playing agent. Usefull only for MULTIPLAYER_COORDINATION_TURN
        At the moment, first in, first in order is implemented

        :param current_order: the current order of agent
        :param new_agent: the new agent  to add
        :return: an ordered list of agents name that can be used by next_state() to set the next agent that can play
        """

        result = current_order
        result.append(new_agent.get_fullname())
        return result
