"""A collection of utility functions for registering mappings between
ThermoML archive properties, and properties supported by the estimation
framework.
"""
import functools

from openff.evaluator.datasets.thermoml import ThermoMLDataSet


class _ThermoMLPlugin:
    """Represents a property which may be extracted from a ThermoML archive.
    """

    def __init__(self, string_identifier, conversion_function, supported_phases):
        """Constructs a new ThermoMLPlugin object.

        Parameters
        ----------
        string_identifier: str
            The ThermoML string identifier (ePropName) for this property.
        conversion_function: function
            A function which maps a `ThermoMLProperty` into a
            `PhysicalProperty`.
        supported_phases: PropertyPhase:
            An enum which encodes all of the phases for which this
            property supports being estimated in.
        """

        self.string_identifier = string_identifier
        self.conversion_function = conversion_function

        self.supported_phases = supported_phases


def _default_mapping(property_class, property_to_map):
    """

    Parameters
    ----------
    property_class: type of PhysicalProperty
        The class to map this property into.
    property_to_map: ThermoMLProperty
        The ThermoML property to map.
    """

    mapped_property = property_class()

    mapped_property.value = property_to_map.value

    if property_to_map.uncertainty is not None:
        mapped_property.uncertainty = property_to_map.uncertainty

    mapped_property.phase = property_to_map.phase

    mapped_property.thermodynamic_state = property_to_map.thermodynamic_state
    mapped_property.substance = property_to_map.substance

    return mapped_property


def register_thermoml_property(
    thermoml_string, supported_phases, property_class=None, conversion_function=None
):
    """A function used to map a property from the ThermoML archive
    to an internal `PhysicalProperty` object of the correct type.

    This function takes either a specific class (e.g. `Density`)
    which maps directly to the specified `thermoml_string`, or a
    a function which maps a `ThermoMLProperty` into a `PhysicalProperty`
    allowing fuller control.

    Parameters
    ----------
    thermoml_string: str
        The ThermoML string identifier (ePropName) for this property.
    supported_phases: PropertyPhase:
        An enum which encodes all of the phases for which this property
        supports being estimated in.
    property_class: type of PhysicalProperty, optional
        The class associated with this physical property. This argument
        is mutually exclusive with the `conversion_function` argument.
    conversion_function: function
        A function which maps a `ThermoMLProperty` into a `PhysicalProperty`.
        This argument is mutually exclusive with the `property_class` argument.
    """

    if (property_class is None and conversion_function is None) or (
        property_class is not None and conversion_function is not None
    ):
        raise ValueError(
            "Only one of the `property_class` and `conversion_function` must be set."
        )

    if conversion_function is None:
        conversion_function = functools.partial(_default_mapping, property_class)

    ThermoMLDataSet.registered_properties[thermoml_string] = _ThermoMLPlugin(
        thermoml_string, conversion_function, supported_phases
    )


def thermoml_property(thermoml_string, supported_phases):
    """A decorator which wraps around the `register_thermoml_property`
    method.

    Parameters
    ----------
    thermoml_string: str
        The ThermoML string identifier (ePropName) for this property.
    supported_phases: PropertyPhase:
        An enum which encodes all of the phases for which this
        property supports being estimated in.
    """

    def decorator(cls):
        register_thermoml_property(thermoml_string, supported_phases, cls)
        return cls

    return decorator
