# Source code for openff.evaluator.workflow.utils

    """A set of helper classes for manipulating and passing inputs between
    buildings blocks in a property estimation workflow.
    """

    from openff.evaluator.attributes import PlaceholderValue
    from openff.evaluator.utils import graph

    class ReplicatorValue(PlaceholderValue):
        """A placeholder value which will be set by a protocol replicator
        with the specified id.
        """

        def __init__(self, replicator_id=""):
            """Constructs a new ReplicatorValue object

            Parameters
            ----------
            replicator_id: str
                The id of the replicator which will set this value.
            """
            self.replicator_id = replicator_id

        def __getstate__(self):
            return {"replicator_id": self.replicator_id}

        def __setstate__(self, state):
            self.replicator_id = state["replicator_id"]

    class ProtocolPath(PlaceholderValue):
        """Represents a pointer to the output of another protocol.
        """

        # The character which separates protocol ids.
        path_separator = "/"
        # The character which separates the property name from the path.
        property_separator = "."

        @property
        def property_name(self):
            """str: The property name pointed to by the path."""
            return self._property_name

        @property
        def protocol_ids(self):
            """tuple of str: The ids of the protocols referenced by this
            object."""
            return self._protocol_ids

        @property
        def start_protocol(self):
            """str: The leading protocol id of the path."""
            return None if len(self._protocol_ids) == 0 else self._protocol_ids[0]

        @property
        def last_protocol(self):
            """str: The end protocol id of the path."""
            return None if len(self._protocol_ids) == 0 else self._protocol_ids[-1]

        @property
        def protocol_path(self):
            """str: The full path referenced by this object excluding the
            property name."""
            return self._protocol_path

        @property
        def full_path(self):
            """str: The full path referenced by this object."""
            return self._full_path

        @property
        def is_global(self):
            return self.start_protocol == "global"

        def __init__(self, property_name="", *protocol_ids):
            """Constructs a new ProtocolPath object.

            Parameters
            ----------
            property_name: str
                The property name referenced by the path.
            protocol_ids: str
                An args list of protocol ids in the order in which they will appear in the path.
            """

            if property_name is None:
                property_name = ""

            self._property_name = property_name
            self._protocol_ids = tuple(protocol_ids)

            self._protocol_path = None
            self._full_path = None

            self._update_string_paths()

        def _update_string_paths(self):
            """Combines the property name and protocol ids into string representations
            and stores them on the object.
            """

            self._protocol_path = ""

            if len(self._protocol_ids) > 0:
                self._protocol_path = ProtocolPath.path_separator.join(self._protocol_ids)

            property_name = "" if self._property_name is None else self._property_name

            self._full_path = (
                f"{self._protocol_path}{ProtocolPath.property_separator}{property_name}"
            )

        @classmethod
        def from_string(cls, existing_path_string: str):

            property_name, protocol_ids = ProtocolPath._to_components(existing_path_string)

            if any(x is None or len(x) == 0 for x in protocol_ids):
                raise ValueError("An invalid protocol id (either None or empty) was found.")

            return ProtocolPath(property_name, *protocol_ids)

        @staticmethod
        def _to_components(path_string):
            """Splits a protocol path string into the property
            name, and the individual protocol ids.

            Parameters
            ----------
            path_string: str
                The protocol path to split.

            Returns
            -------
            str, list of str
                A tuple of the property name, and a list of the protocol ids in the path.
            """
            path_string = path_string.lstrip().rstrip()
            property_name_index = path_string.find(ProtocolPath.property_separator)

            if property_name_index < 0:

                raise ValueError(
                    f"A protocol path must contain a {ProtocolPath.property_separator} "
                    f"followed by the property name this path represents"
                )

            property_name_index = path_string.find(ProtocolPath.property_separator)
            property_name = path_string[property_name_index + 1 :]

            protocol_id_path = path_string[:property_name_index]
            protocol_ids = protocol_id_path.split(ProtocolPath.path_separator)

            if len(protocol_id_path) == 0:
                protocol_ids = tuple()

            return property_name, protocol_ids

        def prepend_protocol_id(self, id_to_prepend):
            """Prepend a new protocol id onto the front of the path.

            Parameters
            ----------
            id_to_prepend: str
                The protocol id to prepend to the path
            """

            if len(self._protocol_ids) > 0 and self._protocol_ids[0] == id_to_prepend:
                return

            self._protocol_ids = (id_to_prepend, *self._protocol_ids)
            self._update_string_paths()

        def pop_next_in_path(self):
            """Pops and then returns the leading protocol id from the path.

            Returns
            -------
            str:
                The previously leading protocol id.
            """

            if len(self._protocol_ids) == 0:
                return None

            next_in_path = self._protocol_ids[0]

            self._protocol_ids = self._protocol_ids[1:]
            self._update_string_paths()

            return next_in_path

        def append_uuid(self, uuid):
            """Appends a uuid to each of the protocol id's in the path

            Parameters
            ----------
            uuid: str
                The uuid to append.
            """

            if self.is_global:
                # Don't append uuids to global paths.
                return

            self._protocol_ids = tuple(
                graph.append_uuid(x, uuid) for x in self._protocol_ids
            )
            self._update_string_paths()

        def replace_protocol(self, old_id, new_id):
            """Redirect the input to point at a new protocol.

            The main use of this method is when merging multiple protocols
            into one.

            Parameters
            ----------
            old_id : str
                The id of the protocol to replace.
            new_id : str
                The id of the new protocol to use.
            """
            self._protocol_ids = tuple(
                x.replace(old_id, new_id) for x in self._protocol_ids
            )
            self._update_string_paths()

        def copy(self):
            """Returns a copy of this path."""
            return ProtocolPath(self._property_name, *self._protocol_ids)

        def __str__(self):
            return self._full_path

        def __repr__(self):
            return f"<ProtocolPath full_path={self._full_path}>"

        def __hash__(self):
            """Returns the hash key of this ProtocolPath."""
            return hash(self._full_path)

        def __eq__(self, other):
            return type(self) == type(other) and self._full_path == other.full_path

        def __ne__(self, other):
            return not (self == other)

        def __getstate__(self):
            return {"full_path": self._full_path}

        def __setstate__(self, state):

            self._property_name, self._protocol_ids = ProtocolPath._to_components(
                state["full_path"]
            )

            self._update_string_paths()