#
# LSST Data Management System
#
# This product includes software developed by the
# LSST Project (http://www.lsst.org/).
#
# See COPYRIGHT file at the top of the source tree.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the LSST License Statement and
# the GNU General Public License along with this program.  If not,
# see <https://www.lsstcorp.org/LegalNotices/>.
#
from __future__ import print_function, division
__all__ = ['Metric']
from past.builtins import basestring
import astropy.units as u
from .jsonmixin import JsonSerializationMixin
from .naming import Name
[docs]class Metric(JsonSerializationMixin):
    """Container for the definition of a metric.
    Metrics can either be instantiated programatically, or from a metric YAML
    file through `lsst.verify.MetricSet`.
    Parameters
    ----------
    name : `str`
        Name of the metric (e.g., ``'PA1'``).
    description : `str`
        Short description about the metric.
    unit : `str` or `astropy.units.Unit`
        Units of the metric. `~lsst.verify.Measurement`\ s of this metric must
        be in an equivalent (that is, convertable) unit. Argument can either be
        an `astropy.unit.Unit` instance, or a `~astropy.unit.Unit`-compatible
        string representation. Use an empty string, ``''``, or
        `astropy.units.dimensionless_unscaled` for a unitless quantity.
    tags : `list` of `str`
        Tags associated with this metric. Tags are user-submitted string
        tokens that are used to group metrics.
    reference_doc : `str`, optional
        The document handle that originally defined the metric
        (e.g., ``'LPM-17'``).
    reference_url : `str`, optional
        The document's URL.
    reference_page : `str`, optional
        Page where metric in defined in the reference document.
    """
    description = None
    """Short description of the metric (`str`)."""
    reference_doc = None
    """Name of the document that specifies this metric (`str`)."""
    reference_url = None
    """URL of the document that specifies this metric (`str`)."""
    reference_page = None
    """Page number in the document that specifies this metric (`int`)."""
    def __init__(self, name, description, unit, tags=None,
                 reference_doc=None, reference_url=None, reference_page=None):
        self.name = name
        self.description = description
        self.unit = u.Unit(unit)
        if tags is None:
            self.tags = set()
        else:
            # FIXME DM-8477 Need type checking that tags are actually strings
            # and are a set.
            self.tags = tags
        self.reference_doc = reference_doc
        self.reference_url = reference_url
        self.reference_page = reference_page
    @classmethod
[docs]    def deserialize(cls, name=None, description=None, unit=None,
                    tags=None, reference=None):
        """Create a Metric instance from a parsed YAML/JSON document.
        Parameters
        ----------
        kwargs : `dict`
            Keyword arguments that match fields from the `Metric.json`
            serialization.
        Returns
        -------
        metric : `Metric`
            A Metric instance.
        """
        # keyword args for Metric __init__
        args = {
            'unit': unit,
            'tags': tags,
            # Remove trailing newline from folded block description field.
            # This isn't necessary if the field is trimmed with `>-` in YAML,
            # but won't hurt either.
            'description': description.rstrip('\n')
        }
        if reference is not None:
            args['reference_doc'] = reference.get('doc', None)
            args['reference_page'] = reference.get('page', None)
            args['reference_url'] = reference.get('url', None)
        return cls(name, **args) 
    def __eq__(self, other):
        return ((self.name == other.name) and
                (self.unit == other.unit) and
                (self.tags == other.tags) and
                (self.description == other.description) and
                (self.reference == other.reference))
    def __ne__(self, other):
        return not self.__eq__(other)
    def __str__(self):
        # self.unit_str provides the astropy.unit.Unit's string representation
        # that can be used to create a new Unit. But for readability,
        # we use 'dimensionless_unscaled' (an member of astropy.unit) rather
        # than an empty string for the Metric's string representation.
        if self.unit_str == '':
            unit_str = 'dimensionless_unscaled'
        else:
            unit_str = self.unit_str
        return '{self.name!s} ({unit_str}): {self.description}'.format(
            self=self, unit_str=unit_str)
    @property
    def name(self):
        """Metric's name (`Name`)."""
        return self._name
    @name.setter
    def name(self, value):
        self._name = Name(metric=value)
    @property
    def unit(self):
        """The metric's unit (`astropy.units.Unit`)."""
        return self._unit
    @unit.setter
    def unit(self, value):
        if not isinstance(value, (u.UnitBase, u.FunctionUnitBase)):
            message = ('unit attribute must be an astropy.units.Unit-type. '
                       ' Currently type {0!s}.'.format(type(value)))
            if isinstance(value, basestring):
                message += (' Set the `unit_str` attribute instead for '
                            'assigning the unit as a string')
            raise ValueError(message)
        self._unit = value
    @property
    def unit_str(self):
        """The string representation of the metric's unit
        (`~astropy.units.Unit`-compatible `str`).
        """
        return str(self.unit)
    @unit_str.setter
    def unit_str(self, value):
        self.unit = u.Unit(value)
    @property
    def tags(self):
        """Tag labels (`set` of `str`)."""
        return self._tags
    @tags.setter
    def tags(self, t):
        # Ensure that tags is always a set.
        if isinstance(t, basestring):
            t = [t]
        self._tags = set(t)
    @property
    def reference(self):
        """Documentation reference as human-readable text (`str`, read-only).
        Uses `reference_doc`, `reference_page`, and `reference_url`, as
        available.
        """
        ref_str = ''
        if self.reference_doc and self.reference_page:
            ref_str = '{doc}, p. {page:d}'.format(doc=self.reference_doc,
                                                  page=self.reference_page)
        elif self.reference_doc:
            ref_str = self.reference_doc
        if self.reference_url and self.reference_doc:
            ref_str += ', {url}'.format(url=self.reference_url)
        elif self.reference_url:
            ref_str = self.reference_url
        return ref_str
    @property
    def json(self):
        """`dict` that can be serialized as semantic JSON, compatible with
        the SQUASH metric service.
        """
        ref_doc = {
            'doc': self.reference_doc,
            'page': self.reference_page,
            'url': self.reference_url}
        return JsonSerializationMixin.jsonify_dict({
            'name': str(self.name),
            'description': self.description,
            'unit': self.unit_str,
            'tags': self.tags,
            'reference': ref_doc})
[docs]    def check_unit(self, quantity):
        """Check that a `~astropy.units.Quantity` has equivalent units to
        this metric.
        Parameters
        ----------
        quantity : `astropy.units.Quantity`
            Quantity to be tested.
        Returns
        -------
        is_equivalent : `bool`
            `True` if the units are equivalent, meaning that the quantity
            can be presented in the units of this metric. `False` if not.
        """
        if not quantity.unit.is_equivalent(self.unit):
            return False
        else:
            return True