# Source code for propertyestimator.workflow.decorators

    """
    A collection of descriptors used to mark-up elements in a workflow, such
    as the inputs or outputs of workflow protocols.
    """
    import abc
    import copy

    from enum import Enum

    from propertyestimator import unit
    from propertyestimator.utils.quantities import EstimatedQuantity
    from propertyestimator.workflow.typing import is_instance_of_type, is_supported_type
    from propertyestimator.workflow.utils import PlaceholderInput

    class UndefinedAttribute:
        """A custom type used to differentiate between ``None`` values,
        and an undeclared optional value."""

        def __eq__(self, other):
            return type(other) == UndefinedAttribute

        def __ne__(self, other):
            return not self.__eq__(other)

        def __getstate__(self):
            return {}

        def __setstate__(self, state):
            return

    UNDEFINED = UndefinedAttribute()

    class BaseMergeBehaviour(Enum):
        """A base class for enums which will describes how attributes should
        be handled when attempting to merge similar protocols.
        """
        pass

    class MergeBehaviour(BaseMergeBehaviour):
        """A enum which describes how attributes should be handled when
        attempting to merge similar protocols.

        This enum may take values of

        * ExactlyEqual: This attribute must be exactly equal between two protocols for
          them to be able to merge.
        """
        ExactlyEqual = 'ExactlyEqual'

    class InequalityMergeBehaviour(BaseMergeBehaviour):
        """A enum which describes how attributes which can be compared
        with inequalities should be merged.

        This enum may take values of

        * SmallestValue: When two protocols are merged, the smallest value of this
          attribute from either protocol is retained.
        * LargestValue: When two protocols are merged, the largest value of this
          attribute from either protocol is retained.
        """
        SmallestValue = 'SmallestValue'
        LargestValue = 'LargestValue'

    class BaseProtocolAttribute(abc.ABC):
        """A custom descriptor used to mark class attributes as being either
        a required input, or provided output of a protocol.

        Notes
        -----
        This decorator expects the protocol to have a matching private field
        in addition to the public attribute. For example if a protocol has
        an attribute `substance`, by default the protocol must also have a
        `_substance` field.
        """

        def __init__(self, docstring, type_hint):
            """Initializes a new BaseProtocolAttribute object.

            Parameters
            ----------
            docstring: str
                A docstring describing the attributes purpose. This will automatically
                be decorated with additional information such as type hints, default
                values, etc.
            type_hint: type, typing.Union
                The expected type of this attribute. This will be used to help the
                workflow engine ensure that expected input types match corresponding
                output values.
            """

            if not is_supported_type(type_hint):

                raise ValueError(f'The {type_hint} type is not supported by the '
                                 f'workflow type hinting system.')

            if hasattr(type_hint, '__qualname__'):

                if type_hint.__qualname__ == 'build_quantity_class.<locals>.Quantity':
                    typed_docstring = f'Quantity: {docstring}'
                elif type_hint.__qualname__ == 'build_quantity_class.<locals>.Unit':
                    typed_docstring = f'Unit: {docstring}'
                else:
                    typed_docstring = f'{type_hint.__qualname__}: {docstring}'

            elif hasattr(type_hint, '__name__'):
                typed_docstring = f'{type_hint.__name__}: {docstring}'
            else:
                typed_docstring = f'{str(type_hint)}: {docstring}'

            self.__doc__ = typed_docstring
            self.type_hint = type_hint

        def __set_name__(self, owner, name):
            self._private_attribute_name = '_' + name

        def __get__(self, instance, owner=None):

            if instance is None:
                # Handle the case where this is called on the class directly,
                # rather than an instance.
                return self

            try:
                return getattr(instance, self._private_attribute_name)
            except AttributeError:
                return UNDEFINED

        def __set__(self, instance, value):

            if (not is_instance_of_type(value, self.type_hint) and
                not isinstance(value, PlaceholderInput) and
                not value == UNDEFINED):

                raise ValueError(f'The {self._private_attribute_name[1:]} attribute can only accept '
                                 f'values of type {self.type_hint}')

            setattr(instance, self._private_attribute_name, value)

    class ProtocolInputAttribute(BaseProtocolAttribute):
        """A descriptor used to mark an attribute of a protocol as
        an input to that protocol.

        Examples
        ----------
        To mark an attribute as an input:

        >>> from propertyestimator.workflow.protocols import BaseProtocol
        >>> from propertyestimator.workflow.decorators import protocol_input
        >>>
        >>> class MyProtocol(BaseProtocol):
        >>>
        >>>     my_input = protocol_input(
        >>>         docstring='An input will be used.',
        >>>         type_hint=float,
        >>>         default_value=0.1
        >>>     )
        """

        def __init__(self, docstring, type_hint, default_value, optional=False,
                     merge_behavior=MergeBehaviour.ExactlyEqual):

            """Initializes a new protocol_input object.

            Parameters
            ----------
            default_value: Any
                The default value for this attribute.
            optional: bool
                Defines whether this is an optional input of a class. If true,
                the `default_value` must be set to `UNDEFINED`.
            merge_behavior: BaseMergeBehaviour
                An enum describing how this input should be handled when considering
                whether to, and actually merging two different protocols.
            """

            docstring = f'**Protocol Input** - {docstring}'

            if not isinstance(merge_behavior, BaseMergeBehaviour):
                raise ValueError('The merge behaviour must inherit from `BaseMergeBehaviour`')

            # Automatically extend the docstrings.
            if (isinstance(default_value, (int, float, str, unit.Quantity, EstimatedQuantity, Enum)) or
                (isinstance(default_value, (list, tuple, set, frozenset)) and len(default_value) <= 4)):

                docstring = f'{docstring} The default value of this attribute ' \
                            f'is ``{str(default_value)}``.'

            elif default_value == UNDEFINED:

                optional_string = '' if optional else ' and must be set by the user.'

                docstring = f'{docstring} The default value of this attribute ' \
                             f'is not set{optional_string}.'

            if (merge_behavior == InequalityMergeBehaviour.SmallestValue or
                merge_behavior == InequalityMergeBehaviour.LargestValue):

                merge_docstring = ''

                if merge_behavior == InequalityMergeBehaviour.SmallestValue:
                    merge_docstring = 'When two protocols are merged, the smallest value of ' \
                                            'this attribute from either protocol is retained.'

                if merge_behavior == InequalityMergeBehaviour.SmallestValue:
                    merge_docstring = 'When two protocols are merged, the largest value of ' \
                                      'this attribute from either protocol is retained.'

                docstring = f'{docstring} {merge_docstring}'

            if optional is True:
                docstring = f'{docstring} This input is *optional*.'

            super().__init__(docstring, type_hint)

            self.optional = optional
            self.merge_behavior = merge_behavior

            self._default_value = default_value

        def __get__(self, instance, owner=None):

            if instance is None:
                # Handle the case where this is called on the class directly,
                # rather than an instance.
                return self

            if not hasattr(instance, self._private_attribute_name):
                # Make sure to only ever pass a copy of the default value to ensure
                # mutable values such as lists don't get set by reference.
                setattr(instance, self._private_attribute_name, copy.deepcopy(self._default_value))

            return getattr(instance, self._private_attribute_name)

    class ProtocolOutputAttribute(BaseProtocolAttribute):
        """A descriptor used to mark an attribute of a protocol as
        an output of that protocol.

        Examples
        ----------
        To mark an attribute as an output:

        >>> from propertyestimator.workflow.protocols import BaseProtocol
        >>> from propertyestimator.workflow.decorators import protocol_output,
        >>>
        >>> class MyProtocol(BaseProtocol):
        >>>
        >>>     my_output = protocol_output(
        >>>         docstring='An output that will be filled.',
        >>>         type_hint=float
        >>>     )
        """

        def __init__(self, docstring, type_hint):
            """Initializes a new protocol_output object.
            """
            docstring = f'**Protocol Output** - {docstring}'
            super().__init__(docstring, type_hint)

    protocol_input = ProtocolInputAttribute
    protocol_output = ProtocolOutputAttribute