from typing import Optional, Any
from xml.etree.ElementTree import Element, tostring

from pymasep.common.exception import CreationException
from pymasep.utils import native_type_to_xml, native_type_from_xml, native_xml_types, method_exists


class Action:
    """
    This class represents an action that can be done by an agent.

    :param environment: the environment where the action is created.
    :param action_type: type of the action. None if xml_node.
    :param xml_node: xml used to create the Action.
    """

    def __init__(self, environment, action_type: Optional[int] = None, xml_node: Element = None) -> None:
        self.environment = environment
        if xml_node is None and action_type is None:
            raise CreationException('Need action_type or xml_node')
        self.type: Optional[int] = action_type
        self.params = {}

    def add_parameter(self, name: str, value: Any) -> None:
        """
        Add a parameter to the action.

        :param name: name of the parameter.
        :param value: value of the parameter. Must be Xml serializable.
        """
        self.params[name] = value

    def id_str(self) -> str:
        """
        Create an str unique ID of the Action.
        Depends on the content of the Action (parameters).

        :return: the string unique ID
        """
        params_str = ''
        ordered_params = sorted(self.params.keys())
        for p in ordered_params:
            if method_exists(self.params[p], 'id_str'):
                id_str_params = self.params[p].id_str()
            else:
                id_str_params = str(self.params[p]).replace(' ', '')
            params_str += p + id_str_params
        return 'action' + str(self.type) + params_str

    def to_xml(self) -> Element:
        """
        Transform the Action to xml. See :ref:`serialization` for more information.

        :return: xml version of the Action
        """
        result = Element(self.__class__.__name__)
        result.set('type', str(self.type))
        for name, param in self.params.items():
            param_xml = Element('ActionParameter')
            param_xml.set('name', name)
            if method_exists(param, 'to_xml'):
                param_xml.append(param.to_xml())
            else:
                param_xml.append(native_type_to_xml(param))
            result.append(param_xml)
        return result

    def from_xml(self, environment, xml_node: Element) -> None:
        """
        Transform a xml to an Action. The instance must be created before.
        See :ref:`serialization` for more information.

        :param environment: the environment where the Action is created.
        :param xml_node: the xml node containing the Action values and parameters.

        """
        self.environment = environment
        self.type = int(xml_node.attrib['type'])
        for p in xml_node:
            name = p.attrib['name']
            if p[0].tag not in native_xml_types:
                if p[0].tag == 'Action':
                    one_param = environment.create_action(xml_node=p[0])
                else:
                    one_param = environment.create_object(xml_node=p[0])
            else:
                value, _ = native_type_from_xml(p[0])
                one_param = value
            self.add_parameter(name, one_param)

    def __str__(self) -> str:
        return tostring(self.to_xml()).decode("utf-8")
