import re


class AgentID:
    """
    Class to store agent unique ID. This ID follows a hierarchy structure
    The format is [root].[agent_id]

    This class is hashable.
    """

    def __init__(self, root_id, agent_identifier):
        assert isinstance(root_id, int) and isinstance(agent_identifier, int)
        self._root_id = root_id
        self._agent_identifier = agent_identifier
        self._identifier = '{}.{}'.format(root_id, agent_identifier)

    @property
    def root_id(self):
        """
        Return root id
        """
        return self._root_id

    @property
    def agent_identifier(self):
        """
        Return agent identifier
        """
        return self._agent_identifier

    def __eq__(self, other):
        """
        Equals if root and agent identifier matches
        """
        return (
            isinstance(other, AgentID)
            and self._root_id == other.root_id
            and self._agent_identifier == other.agent_identifier
        )

    def __hash__(self):
        return hash(self._root_id) ^ hash(self._agent_identifier)

    def __repr__(self):
        """
        Returns the identifier as str, that follows the format [root].[agent_id]
        """
        return self._identifier

    @staticmethod
    def from_str(agent_id):
        result = re.search(r'(\d+)\.(\d+)', agent_id)
        if not result:
            raise ValueError(
                'Expecting format [root_id as int].[identifier as int]. Received Invalid ID {}'.format(agent_id)
            )

        return AgentID(int(result.group(1)), int(result.group(2)))


class AgentIDManager:
    """
    Class responsible to generate new identifier
    Disclaimer: this class is a bit error prone. You need to commit after
    a new agent id is generated. Otherwise the next ID will be the same
    """

    def __init__(self, root_id):
        """
        Initializes AgentIDManager. agent ID starts at 1.
        0 is reserved for the agent manager

        Args:
        - root_id: root id that will be used to generate identifiers
        """
        self._root_id = root_id
        self._identifier = 1
        self._identifiers = set()
        self._invalid_commit = True

    def next_available_id(self):
        """
        Return next available ID.

        You need to invoke commit function if hte ID will be used
        """
        self._invalid_commit = False
        return AgentID(self._root_id, self._identifier)

    def commit(self):
        """
        Function to tell manager that the ID will be used
        """
        if self._invalid_commit:
            return

        self._identifiers.add(AgentID(self._root_id, self._identifier))
        self._identifier += 1
        self._invalid_commit = True

    @property
    def identifiers(self):
        """
        Return set of identifiers generated by the manager
        """
        return set(self._identifiers)

    def __repr__(self):
        return f'{self.__class__.__name__}({self._root_id}.{self._agent_identifier})'
