"""Module for handling basic unit manipulation, note that this does not handle
any negative values.
"""
from dmd import common
from pint.errors import DimensionalityError, UndefinedUnitError
from tokenize import TokenError
from enum import Enum
import pint
import re
import pprint as pp


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class _UnitParser(object):
    """The base class for the unit parsing
    """
    LABEL = "UNIT"
    REGEX_STRING = ""
    REGEX = re.compile(REGEX_STRING, flags=re.VERBOSE)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def DIGIT_DEFINITION(x, named_group=True):
        """A regular expression definition for a digit (`str`)

        Parameters
        ----------
        x : `int`
            The number suffix for the name match. So ``1`` would give a regular
            expression string with it captured by the group name ``VALUE1``,
            ``2`` would be ``VALUE2`` and ``N`` would be ``VALUEN``. There is
            nothing stopping the use of a ``str`` for ``x``.

        Returns
        -------
        regexp_str : `str`
            A regular expression string representing a value, i.e. 3 or 1.5

        Notes
        -----
        This will also match non-prefixed digits such as .5 (as opposed to
        0.5).
        """
        base = r"(?:^\d*\.\d+)|(?:\s+\.\d+)|(?:\d+(\.\d+)?)"
        # base = r"(?:\d*\.\d+)|(?:\d+(\.\d+)?)"
        # return r'(?P<VALUE{0}>(?:\d*\.\d+)|(?:\d+(\.\d+)?))'.format(x)
        if named_group is True:
            return r"(?P<VALUE{0}>{1})".format(x, base)
        return r"({0})".format(base)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def UNIT_DEFINITION(x, named_group=True):
        """A regular expression definition for a unit string (`str`)
        """
        base = r'[a-zA-Z]{1,15}'
        if named_group is True:
            return r'(?P<UNIT{0}>{1})'.format(x, base)
        return r'({0})'.format(base)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def is_empty(cls, test_str):
        """Determine if a regular expression match contains a group name.

        Parameters
        ----------
        regexp_match : `re.Match`
            The regular expression match to check.
        group_name : `str`
            The group name to check for.

        Returns
        -------
        has_group : `bool`
            ``True`` if the regexp match has the group ``False`` if not.
        """
        if test_str in ['', None]:
            return True
        return False

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def has_group(cls, regex_match, group_name):
        """Determine if a regular expression match contains a group name.

        Parameters
        ----------
        regexp_match : `re.Match`
            The regular expression match to check.
        group_name : `str`
            The group name to check for.

        Returns
        -------
        has_group : `bool`
            ``True`` if the regexp match has the group ``False`` if not.
        """
        return cls.is_empty(regex_match.group(group_name))

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def extract_from_regex(cls, unit_match):
        """Extract the data in a regular expression match into unit objects.

        Parameters
        ----------
        unit_match : `re.Match`
            A regular expression match to extract unit objects from.

        Raises
        ------
        NotImplementedError
            You should override this.
        """
        raise NotImplementedError("override this method")

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def extract_units(cls, unit_str):
        """Attempt to extract the units from a prescription string.

        Parameters
        ----------
        unit_str : `str`
            A string potentially containing some units.

        Returns
        -------
        unit_search : `re.Match`
            A regular expression match.
        unit_match : `dmd.units.Unit`
            The extracted units.
        """
        found_units = []
        for unit_search in cls.REGEX.finditer(unit_str):
            try:
                found_units.append(cls.extract_from_regex(unit_search))
                unit_str = common.splice_spaces(
                    unit_str, unit_search.start(), unit_search.end()
                )
            except IndexError:
                print(unit_str)
                raise
        return found_units, unit_str


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class UnitAreaParser(_UnitParser):
    """Designed to parse any area units, so 1x1m or 1mX1m, so the units are
    optional in the first position but must be in the second position. If the
    units are missing in the first position the second position units are used.
    """
    LABEL = "AREA_UNIT"
    """The type of the unit parser (`str`)
    """
    # 1 cm X 2m or 1X3cm or 1cmX3
    REGEX_STRING = r'''
    \b
    {0}\s*           # The first digit
    ({1}(\ssq)?)?\s* # An optional first unit (i.e. cm)
    [xX]\s*          # Multiplication symbol
    {2}\s*           # The second digit
    {3}              # The mandatory second unit (i.e. mm)
    \b'''.format(
        _UnitParser.DIGIT_DEFINITION(1),
        _UnitParser.UNIT_DEFINITION(1),
        _UnitParser.DIGIT_DEFINITION(2),
        _UnitParser.UNIT_DEFINITION(2)
    )
    """The regular expression syntax to extract units such as ``1 cm X 2m`` or
    ``1X3cm`` (`str`)
    """
    REGEX = re.compile(REGEX_STRING, flags=re.VERBOSE | re.IGNORECASE)
    """The compiled regular expression of ``REGEX_STRING`` this is compiled
    with the ``re.VERBOSE`` and  ``re.IGNORECASE`` flags (`re.Pattern`)
    """

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def extract_from_regex(cls, unit_match):
        """Extract the data in a regular expression match into unit objects.

        Parameters
        ----------
        unit_match : `re.Match`
            A regular expression match to extract unit objects from.

        Returns
        -------
        len_unit : `dmd.unit.Unit`
            The length unit.
        width_unit : `dmd.unit.Unit`
            The width unit.
        """
        v1, u1, v2, u2 = "VALUE1", "UNIT1", "VALUE2", "UNIT2"

        low_units = unit_match.group(u1)
        high_units = unit_match.group(u2)

        # From the regexp we are guaranteed to have either low or high units
        if cls.is_empty(low_units):
            low_units = high_units
        elif cls.is_empty(high_units):
            # Some area units are suffixed with 1 mm sq |? so remove that
            re.sub(r'\ssq', low_units)
            high_units = low_units

        return (
            Unit(num_value=unit_match.group(v1), num_unit=low_units,
                 regex=unit_match, label=cls.LABEL),
            Unit(num_value=unit_match.group(v2), num_unit=high_units,
                 regex=unit_match, label=cls.LABEL)
        )


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class UnitVolumeParser(_UnitParser):
    """Designed to parse volume units such as 1x1x4m or 2mX3mx5m, the units are
    optional in the first two positions length and width but must be present in
    the third position (depth). Any missing units are replaced with the depth
    unit.
    """
    LABEL = "VOLUME_UNIT"
    """The type of the unit parser (`str`)
    """
    # 32mmx10mmx76mm or 32x10x76mm
    REGEX_STRING = r"""
    \b
    {0}\s*    # The first digit
    ({1})?\s* # An optional first unit (i.e. cm)
    [xX]\s*   # Multiplication symbol
    {2}\s*    # The second digit
    ({3})?\s* # An optional second unit (i.e. cm)
    [xX]\s*   # Multiplication symbol
    {4}\s*    # The second digit
    {5}       # An optional second unit (i.e. cm)
    \b
    """.format(
        _UnitParser.DIGIT_DEFINITION(1),
        _UnitParser.UNIT_DEFINITION(1),
        _UnitParser.DIGIT_DEFINITION(2),
        _UnitParser.UNIT_DEFINITION(2),
        _UnitParser.DIGIT_DEFINITION(3),
        _UnitParser.UNIT_DEFINITION(3)
    )
    """The regular expression syntax to extract units such as
    ``32mmx10mmx76mm`` or ``32x10x76mm`` (`str`)
    """
    REGEX = re.compile(REGEX_STRING, re.VERBOSE | re.IGNORECASE)
    """The compiled regular expression of ``REGEX_STRING`` this is compiled
    with the ``re.VERBOSE`` and  ``re.IGNORECASE`` flags (`re.Pattern`)
    """

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def extract_from_regex(cls, unit_match):
        """Extract the data in a regular expression match into unit objects.

        Parameters
        ----------
        unit_match : `re.Match`
            A regular expression match to extract unit objects from.

        Returns
        -------
        len_unit : `dmd.unit.Unit`
            The length unit.
        width_unit : `dmd.unit.Unit`
            The width unit.
        depth_unit : `dmd.unit.Unit`
            The depth unit.

        Notes
        -----
        This is designed to accept a match of the form ``32mmx10mmx76mm`` or
        ``32x10x76mm``. In the later case the units of the end match are
        extrapolated to the former two units. So ``32x10x76mm`` would become
        ``32mmx10mmx76mm``.
        """
        possible_units = [unit_match.group('UNIT1'), unit_match.group('UNIT2')]
        last_units = unit_match.group('UNIT3')

        for i in range(len(possible_units)):
            if cls.is_empty(possible_units[i]):
                possible_units[i] = last_units

        return (
            Unit(num_value=unit_match.group('VALUE1'), regex=unit_match,
                 num_unit=possible_units[0], label=cls.LABEL),
            Unit(num_value=unit_match.group('VALUE2'), regex=unit_match,
                 num_unit=possible_units[1], label=cls.LABEL),
            Unit(num_value=unit_match.group('VALUE3'), regex=unit_match,
                 num_unit=last_units, label=cls.LABEL)
        )


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class UnitCombinedParser(_UnitParser):
    """The combined unit parser aims to tackle the issue of having two unit
    definitions with a common denominator that is inferred to both the
    numerators, such as ``100mg with 1.1mg/5ml`` which should be ``100mg/5ml``
    and ``1.1mg/5ml``.
    """
    LABEL = "COMBINED_UNIT"
    """The type of the unit parser (`str`)
    """
    # 100mgwith1.1mg/5ml or 100mg + 1.1mg/5ml
    REGEX_STRING = r'''
    \b
    {0}\s*          # The first mandatory value (i.e. .5)
    {1}\s*          # The first mandatory units (i.e. cm)
    (?:with|\+)\s*  # The combination delimiter with or +
    {2}\s*          # The first mandatory value (i.e. 1.5)
    {3}             # The second mandatory unit (i.e. mm)
    (
    (?:with|\+)\s*  # The combination delimiter with or +
    {4}\s*          # The first mandatory value (i.e. 1.5)
    {5}             # The second mandatory unit (i.e. mm)
    )?
    (?:
    \s*/            # The denominator delimiter with optional space before
    \s*{6}?\s*
    {7}
    )?
    \b              # The denominator value and units are optional
    '''.format(
        _UnitParser.DIGIT_DEFINITION(1),
        _UnitParser.UNIT_DEFINITION(1),
        _UnitParser.DIGIT_DEFINITION(2),
        _UnitParser.UNIT_DEFINITION(2),
        _UnitParser.DIGIT_DEFINITION(3),
        _UnitParser.UNIT_DEFINITION(3),
        _UnitParser.DIGIT_DEFINITION(4),
        _UnitParser.UNIT_DEFINITION(4)
    )
    """The regular expression syntax to extract units such as
    ``100mgwith1.1mg/5ml`` or ``100mg + 1.1mg/5ml`` (`str`)
    """
    REGEX = re.compile(REGEX_STRING, re.VERBOSE)
    """The compiled regular expression of ``REGEX_STRING`` this is compiled
    with the ``re.VERBOSE`` and  ``re.IGNORECASE`` flags (`re.Pattern`)
    """

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def extract_from_regex(cls, unit_match):
        """Extract the data in a regular expression match into unit objects.

        Parameters
        ----------
        unit_match : `re.Match`
            A regular expression match to extract unit objects from.

        Returns
        -------
        first_unit : `dmd.unit.Unit`
            The first combined unit.
        second_unit : `dmd.unit.Unit`
            The second combined unit.

        Notes
        -----
        This is designed to accept a match of the form ``100mg with 1.1mg/5ml``
        or 100mgwith1.1mg. In the former case the ``5ml`` denominator value and
        units are applied to the ``100mg`` as well.
        """
        combined = [
            Unit(num_value=unit_match.group('VALUE1'), label=cls.LABEL,
                 num_unit=unit_match.group('UNIT1'), regex=unit_match,
                 den_value=unit_match.group('VALUE4'),
                 den_unit=unit_match.group('UNIT4')),
            Unit(num_value=unit_match.group('VALUE2'), label=cls.LABEL,
                 num_unit=unit_match.group('UNIT2'), regex=unit_match,
                 den_value=unit_match.group('VALUE4'),
                 den_unit=unit_match.group('UNIT4'))
        ]
        try:
            combined.append(
                Unit(num_value=unit_match.group('VALUE3'), label=cls.LABEL,
                     num_unit=unit_match.group('UNIT3'), regex=unit_match,
                     den_value=unit_match.group('VALUE4'),
                     den_unit=unit_match.group('UNIT4'))
            )
        except IndexError:
            # No third group
            pass
        return tuple(combined)




# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class UnitPercentParser(_UnitParser):
    """The percent unit parser deals with the specialised ``%``, ``%v/v``,
    ``%w/v`` and bracketed versions of these, i.e. ``%(w/v)``. These are
    converted to more meaningful units based on ``ml/100ml`` ``(v/v)`` or
    ``mg/100mg`` ``(w/w)`` or ``ml/100mg`` (``v/w`` - not sure if this actually
    exists!?) or ``mg/100ml`` ``(w/v)``.
    """
    LABEL = "PERCENT_UNIT"
    """The type of the unit parser (`str`)
    """
    # 0.1 % or 0.1 % (w/v) or 0.1%w/v
    REGEX_STRING = r"\b{0}\s*%(?P<UNIT1>\s*\(?[wv]/[wv]\)?)?".format(
        _UnitParser.DIGIT_DEFINITION(1)
    )
    """The regular expression syntax to extract units such as
    ``%`` or ``%v/v`` (`str`)
    """
    REGEX = re.compile(REGEX_STRING, flags=re.IGNORECASE)
    """The compiled regular expression of ``REGEX_STRING`` this is compiled
    with the ``re.IGNORECASE`` flag (`re.Pattern`)
    """

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def extract_from_regex(cls, unit_match):
        """Extract the data in a regular expression match into a unit object.

        Parameters
        ----------
        unit_match : `re.Match`
            A regular expression match to extract unit objects from.

        Returns
        -------
        rate_unit : `dmd.unit.Unit`
            The unit object with the regular expression and label stored
            within.
        """
        try:
            # Remove any brackets and spaces
            unit_def = re.sub(
                r"[\s+\(\)]", '', unit_match.group('UNIT1').lower()
            )
        except AttributeError:
            if unit_match.group('UNIT1') is not None:
                raise

            return (
                Unit(
                    num_value=unit_match.group('VALUE1'), label=cls.LABEL,
                    num_unit='ml', regex=unit_match,
                    den_value=100,
                    den_unit='ml'
                ),
            )

        num_unit = 'ml'
        den_value = 100
        den_unit = 'ml'
        if unit_def.startswith('w'):
            num_unit = 'mg'
        if unit_def.endswith('w'):
            den_unit = 'mg'

        return (
            Unit(
                num_value=unit_match.group('VALUE1'), label=cls.LABEL,
                num_unit=num_unit, regex=unit_match,
                den_value=den_value,
                den_unit=den_unit
            ),
        )


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class UnitRangeParser(_UnitParser):
    """Capture unit ranges, note that the later such as ``1-2m`` or ``1cm-3cm``
    or ``1m-2``. Note that the later one is problematic as it could capture
    unintended downstream characters - so the units should be validated.
    """
    LABEL = "RANGE_UNIT"
    """The type of the unit parser (`str`)
    """
    # 0.1 cm - 0.2 or 0.1 cm - 0.2 or 0.1 cm - 0.2 cm
    REGEX_STRING = r"""
    \b
    (?:
    {0}\s*                  # The first digit
    {1}\s*                  # An mandatory first unit (i.e. cm)
    -\s*                    # Range symbol
    {2}\s?                  # The second digit
    (?P<UNIT2>[A-Z]{{1,5}})?  # The optional second unit (i.e. mm)
    )
    |
    (?:
    {3}\s*                     # The first digit
    (?P<UNIT3>[A-Z]{{1,5}})?     # An mandatory first unit (i.e. cm)
    \S*-\s*                    # Range symbol
    {4}\s*                     # The second digit
    {5}
    )
    \b
    """.format(
        _UnitParser.DIGIT_DEFINITION(1),
        _UnitParser.UNIT_DEFINITION(1),
        _UnitParser.DIGIT_DEFINITION(2),
        _UnitParser.DIGIT_DEFINITION(3),
        _UnitParser.DIGIT_DEFINITION(4),
        _UnitParser.UNIT_DEFINITION(4),
    )
    """The regular expression syntax to extract units such as ``1-2m`` or
    ``1cm-3cm`` or ``1m-2`` (`str`)
    """
    REGEX = re.compile(REGEX_STRING, flags=re.VERBOSE | re.IGNORECASE)
    """The compiled regular expression of ``REGEX_STRING`` this is compiled
    with the ``re.VERBOSE`` and  ``re.IGNORECASE`` flags (`re.Pattern`)
    """

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def extract_from_regex(cls, unit_match):
        """Extract the data in a regular expression match into unit objects.

        Parameters
        ----------
        unit_match : `re.Match`
            A regular expression match to extract unit objects from.

        Returns
        -------
        low_unit : `dmd.unit.Unit`
            The low range unit.
        high_unit : `dmd.unit.Unit`
            The high range unit.
        """
        low_units = unit_match.group("UNIT1") or unit_match.group("UNIT2")
        high_units = unit_match.group("UNIT2") or unit_match.group("UNIT1")

        if low_units is None:
            low_units = unit_match.group("UNIT3") or unit_match.group("UNIT4")
            high_units = unit_match.group("UNIT4") or unit_match.group("UNIT3")

        low_value = unit_match.group("VALUE1") or unit_match.group("VALUE3")
        high_value = unit_match.group("VALUE2") or unit_match.group("VALUE4")

        return (
            Unit(num_value=low_value, num_unit=low_units,
                 regex=unit_match, label=cls.LABEL),
            Unit(num_value=high_value, num_unit=high_units,
                 regex=unit_match, label=cls.LABEL)
        )


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class UnitRateParser(_UnitParser):
    """The rate unit parser aims to parse units with a numerator unit and a
    denominator unit (i.e. the numerator is expressed as a rate) i.e.
    100mg/ml. The denominator may have an optional value as well i.e.
    ``100mg/5ml``.
    """
    LABEL = "RATE_UNIT"
    """The type of the unit parser (`str`)
    """
    REGEX_STRING = r"""
    \b
    {0}\s*  # The first digit
    {1}\s*  # The first units i.e. cm
    /\s{{0,2}}    # The unit divider
    ({2}?)  # The second optional digits
    \s{{0,2}}{3}  # The second units
    \b
    """.format(
        _UnitParser.DIGIT_DEFINITION(1),
        _UnitParser.UNIT_DEFINITION(1),
        _UnitParser.DIGIT_DEFINITION(2),
        _UnitParser.UNIT_DEFINITION(2)
    )
    """The regular expression syntax to extract units such as
    ``1.1mg/5ml`` or ``1.1mg/ml`` (`str`)
    """
    REGEX = re.compile(REGEX_STRING, flags=re.VERBOSE | re.IGNORECASE)
    """The compiled regular expression of ``REGEX_STRING`` this is compiled
    with the ``re.VERBOSE`` and  ``re.IGNORECASE`` flags (`re.Pattern`)
    """

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def extract_from_regex(cls, unit_match):
        """Extract the data in a regular expression match into a unit object.

        Parameters
        ----------
        unit_match : `re.Match`
            A regular expression match to extract unit objects from.

        Returns
        -------
        rate_unit : `dmd.unit.Unit`
            The unit object with the regular expression and label stored
            within.
        """
        return (
            Unit(
                num_value=unit_match.group('VALUE1'), label=cls.LABEL,
                num_unit=unit_match.group('UNIT1'), regex=unit_match,
                den_value=unit_match.group('VALUE2'),
                den_unit=unit_match.group('UNIT2')
            ),
        )


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class UnitInternationalParser(_UnitParser):
    """Used to extract international unit definitions that mat be confused
    with rate units in some circumstances. i.e. I/U but sometimes may be IU.
    """
    LABEL = "INTERNATIONAL_UNIT"
    """The type of the unit parser (`str`)
    """
    REGEX_STRING = r"""
    \b
    {0}\s*              # The digit
    (i\s?u|i\s?/\s?u)   # Different ways of saying IU
    \b
    """.format(
        _UnitParser.DIGIT_DEFINITION(1)
    )
    """The regular expression syntax to extract units such as
    ``1 I/U`` or ``1 IU`` (`str`)
    """
    REGEX = re.compile(REGEX_STRING, flags=re.VERBOSE | re.IGNORECASE)
    """The compiled regular expression of ``REGEX_STRING`` this is compiled
    with the ``re.VERBOSE`` and  ``re.IGNORECASE`` flags (`re.Pattern`)
    """

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def extract_from_regex(cls, unit_match):
        """Extract the data in a regular expression match into a unit object.

        Parameters
        ----------
        unit_match : `re.Match`
            A regular expression match to extract unit objects from.

        Returns
        -------
        international_unit : `dmd.unit.Unit`
            The unit object with the regular expression and label stored
            within.
        """
        return (
            Unit(
                num_value=unit_match.group('VALUE1'), label=cls.LABEL,
                num_unit="IU", regex=unit_match
            ),
        )


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class UnitRatioParser(_UnitParser):
    """Capture unit ratios. The matches from this are either NUM1/NUM2 or
    NUM1:NUM2. Note this is more generally one number divided by another
    number. If both numbers add up to 100 and the divider is a ``:`` then this
    is treated as a true ratio and the denominator becomes NUM1+NUM2 instead of
    just NUM2.
    """
    LABEL = "RATIO_UNIT"
    """The type of the unit parser (`str`)
    """
    # (
    #         # 1/160 or 10:90
    #         r'\b{0}\s*(?P<SEP>[/:])\s*{1}\b'.format(
    #             _UnitParser.DIGIT_DEFINITION(1),
    #             _UnitParser.DIGIT_DEFINITION(2)
    #         ),
    #         _get_ratio_unit,
    #         RATIO_UNIT_LABEL
    # )
    # 1/160 or 10:90
    REGEX_STRING = r"""
    \b
    {0}\s*(?P<SEP>[/:])\s*{1}
    \b
    """.format(
        _UnitParser.DIGIT_DEFINITION(1),
        _UnitParser.DIGIT_DEFINITION(2)
    )
    """The regular expression syntax to extract units such as ``1-2m`` or
    ``1cm-3cm`` or ``1m-2`` (`str`)
    """
    REGEX = re.compile(REGEX_STRING, flags=re.VERBOSE | re.IGNORECASE)
    """The compiled regular expression of ``REGEX_STRING`` this is compiled
    with the ``re.VERBOSE`` and  ``re.IGNORECASE`` flags (`re.Pattern`)
    """

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def extract_from_regex(cls, unit_match):
        """Extract and process ratio-like units.

        Parameters
        ----------
        unit_match : `re.Match`
            A regexp that has been matched against 3D units.

        Returns
        -------
        ratio_unit : `dmd.unit.Unit`
            With the numerator and denominator values set.

        Notes
        -----
        The matches from this are either NUM1/NUM2 or NUM1:NUM2. Note this is
        more generally one number divided by another number. If both numbers
        add up to 100 and the divider is a ``:`` then this is treated as a true
        ratio and the denominator becomes NUM1+NUM2 instead of just NUM2.
        """
        num_value = float(unit_match.group('VALUE1'))
        den_value = float(unit_match.group('VALUE2'))
        if unit_match.group('SEP') == ':' and num_value + den_value == 100:
            return (
                Unit(num_value=num_value, den_value=num_value + den_value,
                     regex=unit_match, label=cls.LABEL),
            )

        return (
            Unit(num_value=num_value, den_value=den_value,
                 regex=unit_match, label=cls.LABEL),
        )


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class UnitExtract(object):
    """A class to aid in the extraction of unit information from strings.
    """
    # TODO: Implement 250mg.5ml
    UNIT_EXTRACT_ORDER = [
        UnitVolumeParser, UnitAreaParser, UnitCombinedParser,
        UnitInternationalParser, UnitRateParser, UnitPercentParser,
        UnitRangeParser, UnitRatioParser
    ]
    """The various unit definitions, order is important here, the more
    complex units appear before the least complex units as the least
    complex are often a sub component of the more complex. Each class here
    should have an ``extract_units`` method that accepts a string and returns
    a list of unit matches and a string with the units removed. The unit
    matches should contain tuples of ``dmd.units.Unit`` objects the length of
    each tuple will differ depending in the unit type, for example volume
    units will have three ``dmd.units.Unit`` objects in their tuple.
    (`list` of `dmd.units._UnitParser`)
    """

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def extract_units(cls, unit_str):
        """Attempt to extract the units from a prescription string.

        Parameters
        ----------
        unit_str : `str`
            A string potentially containing some units.

        Yields
        ------
        found_units : `tuple` of `dmd.units.Unit`
            The extracted units. The length of the tuple will differ depending
            on the unit type, for example volume units will have three
            ``dmd.units.Unit`` objects in their tuple. The ``dmd.units.Unit``
            objects, will contain the regex match and the unit label.
        """
        found = False
        for parser in cls.UNIT_EXTRACT_ORDER:
            found_units, unit_str = parser.extract_units(unit_str)
            for u in found_units:
                yield u
                found = True

        if found is False:
            raise KeyError("no units found: '{0}'".format(unit_str))


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class Unit(object):
    """A simple representation of a unit because everyone needs one.

    Parameters
    ----------
    num_value : `float`
        The value for the numerator.
    num_unit : `str` or `NoneType`, optional, default: `NoneType`
        The units for the numerator.
    den_value : `float` or `NoneType`, optional, default: `NoneType`
        The value for the denominator.
    den_unit : `str` or `NoneType`, optional, default: `NoneType`
        The units for the denominator.
    """
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self, num_value=None, num_unit=None, den_value=None,
                 den_unit=None, ureg=None, label=None, regex=None):
        self.num_value = num_value
        self.num_unit = num_unit
        self.den_value = den_value
        self.den_unit = den_unit
        self.ureg = ureg
        self.label = label
        self.regex = regex
        self.num_inferred = False
        self.standard = True

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __eq__(self, other):
        return self.__hash__ == other.__hash__

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

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __hash__(self):
        return hash(
            (self.num_value, self.num_unit, self.den_value, self.den_unit)
        )

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def fingerprint(self):
        return hash(
            (self.num_value, self.num_unit, self.den_value, self.den_unit)
        )

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __repr__(self):
        """Pretty printing
        """
        pint_val = None
        if self.ureg is not None:
            pint_val = self.pint

        return (
            "<{0}(num_value={1}, num_unit='{2}',"
            " den_value={3}, den_unit='{4}', pint='{5}')".format(

                self.__class__.__name__,
                self.num_value,
                self.num_unit,
                self.den_value,
                self.den_unit,
                pint_val
            )
        )

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @property
    def pint(self):
        """
        """
        num_unit_def, standard = get_pint_unit(
            self.ureg, self.num_value, self.num_unit
        )
        self.standard = self.standard & standard

        den_unit_def, standard = get_pint_unit(
            self.ureg, self.den_value, self.den_unit
        )
        self.standard = self.standard & standard
            # print("recursion error")
            # return None

        try:
            return num_unit_def/den_unit_def
        except TypeError:
            if num_unit_def is None:
                return None
            return num_unit_def
        except Exception:
            # '0.1%w/w'
            return None


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_pint_unit(ureg, val, unit):
    """
    """
    if val is None and unit is None:
        return None, False
    if val is not None:
        unit = unit or 'unknown'

    standard = True
    try:
        return (val * ureg(unit)), standard
    except TypeError:
        # No numerator value but will check to be sure
        if val is not None:
            print("VALUE: {0}".format(val))
            print("UNIT: {0}".format(unit))
            raise
        val = 1
        return get_pint_unit(ureg, val, unit)
    except (UndefinedUnitError, AttributeError):
        standard = False
        unit_str = re.sub(r'\s+', '_', unit)
        if unit_str == '%':
            unit_str = 'percent'
        ureg.define('{0} = 1'.format(unit_str))
        try:
            unitdef, _ = get_pint_unit(ureg, val, unit_str)
        except Exception:
            # Fudge for '0.1%w/w' to stop infinite recursion
            return None, False
    except TokenError:
        # tokenize.TokenError: ('EOF in multi-line statement', (2, 0)):
        # {'bnfchapter': 'Oestrogens And Hrt',
        #  'bnfcode': '6040101',
        #  'dmdcode': '7.43050010000271E+016',
        #  'drugsubstance': 'Estradiol Valerate/Norethisterone',
        #  'formulation': 'Tablets',
        #  'gemscriptcode': '57503020',
        #  'prodcode': '9413',
        #  'productname': 'Estradiol valerate and (estradiol valerate with '
        #                 'norethisterone) 1mg with (1mg with 1mg) tablets',
        #  'route': 'Oral',
        #  'strength': '1mg + (1mg + 1mg)'}
        return None, False

    return unitdef, standard

# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class UnitErrorMsg(object):
    """Handle unit error messages.
    """
    MINUS_ERROR_MSG = (
        "unsupported operand type(s) for -: 'ParserHelper' and 'ParserHelper'"
    )
    """Error message when a - is in the unit name (`str`)
    """
    SCALING_ERROR_MSG = (
        "Unit expression cannot have a scaling factor."
    )
    """Error message when a unit expression has a number in it
    i.e. mg/100 ml (`str`)
    """
    VALUE_NONE_ERROR_MSG = (
        "unsupported operand type(s) for *: 'NoneType' and 'Quantity'"
    )
    """Error message when one value in a unit expression is NoneType (`str`)
    """
    REG_PARSE_ERROR = "'NoneType' object has no attribute 'evaluate'"
    """
    """

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def process_exception(cls, e, *units_on_test):
        """Attempt to understand any unit errors and give a set error code

        Parameters
        ----------
        e : `Exception`
            An exception to process.
        *units_on_test
            Any units that were being tested when the exception happened.

        Returns
        -------
        unit_eval : `dmd.units.UnitEval`
            A unit error code.
        """
        if e.__class__ == TypeError and \
           e.args[0] == cls.MINUS_ERROR_MSG:
            return UnitEval.SCALING_ERROR
        elif e.__class__ == ValueError and \
           e.args[0] == cls.SCALING_ERROR_MSG:
            return UnitEval.MINUS_ERROR
        elif e.__class__ == TypeError and \
           e.args[0] == cls.VALUE_NONE_ERROR_MSG:
            return UnitEval.VALUE_NONE_ERROR
        elif e.__class__ == UndefinedUnitError:
            return UnitEval.UNDEF_UNIT
        elif e.__class__ == AttributeError and \
             e.args[0] == cls.REG_PARSE_ERROR:
            return UnitEval.BAD_PARSE
        elif e.__class__ == DimensionalityError:
            return UnitEval.NO_COMPARE
        else:
            raise e


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class UnitEval(Enum):
    """Codes for determine the state of a unit.
    """
    OK = 1
    MISSING = 2
    MINUS_ERROR = 3
    SCALING_ERROR = 4
    VALUE_NONE_ERROR = 5
    UNDEF_UNIT = 6
    BAD_PARSE = 7
    NO_COMPARE = 8

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_pint_unit_registry():
    """Return a copy of the pint unit registry with DM+D units loaded into it.
    """
    return pint.UnitRegistry()



# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# def parse_num_den(unit):
#     """Return a copy of the pint unit registry with DM+D units loaded into it.
#     """
    

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# def test_unit(ureg, i):
#     try:
#         in_ureg = i in ureg
#         if in_ureg is True:
#             return UnitEval.OK
#         else:
#             return UnitEval.MISSING
#         # print("{0} -> {1}".format(i, i in ureg))
#     except TypeError as e:
#         if e.args[0] == _MINUS_ERROR:
#             return UnitEval.MINUS_ERROR
#     except ValueError as e:
#         if e.args[0] == _SCALING_ERROR:
#             return UnitEval.SCALING_ERROR


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def comparable_units(ureg, first_units, second_units):
    """Test if two sets of units are comparable.

    Parameters
    ----------
    ureg : `pint.UnitRegistry`
        A unit registry used to attempt to normalise units.
    first_units : `str`
        A set of units to compare, i.e. ``mg``
    second_units : `str`
        A second set of units to compare, i.e. ``milligram``

    Returns
    -------
    unit_eval_code : `UnitEval`
        A unit evaluation code enumerator.
    """
    try:
        (
            (1 * ureg(first_units)) + (1 * ureg(second_units))
        )
        return UnitEval.OK
    except Exception as e:
        return UnitErrorMsg.process_exception(e)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def unit_diff(first, second):
    """Calculate the numeric difference between comparable unit values and also
    the proportional overlap.

    Parameters
    ----------
    ureg : `pint.UnitRegistry`
        A unit registry used to attempt to normalise units.
    first_value : `float`
        The value for the 
    first_units : `str`
        A set of units for the ``first_value`` i.e. ``mg``
    second_value
    second_units : `str`
        A set of units for the ``second_value`` i.e. ``milligram``

    Notes
    -----
    The assumption is that the units for both are comparable.
    """
    try:
        diff = first - second
        # print(diff)
        perc_denom = max(first, second)
    except UndefinedUnitError:
        return None, None
    except DimensionalityError:
        return None, None
    except TypeError:
        return None, None
    perc_sim = 1 - abs(diff / perc_denom)
    return diff, perc_sim

# # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# def unit_diff(ureg, first_value, first_units, second_value, second_units):
#     """Calculate the numeric difference between comparable unit values and also
#     the proportional overlap.

#     Parameters
#     ----------
#     ureg : `pint.UnitRegistry`
#         A unit registry used to attempt to normalise units.
#     first_value : `float`
#         The value for the 
#     first_units : `str`
#         A set of units for the ``first_value`` i.e. ``mg``
#     second_value
#     second_units : `str`
#         A set of units for the ``second_value`` i.e. ``milligram``

#     Notes
#     -----
#     The assumption is that the units for both are comparable.
#     """
#     try:
#         diff = (first_value * getattr(ureg, first_units)) - \
#             (second_value * getattr(ureg, second_units))

#         perc_denom = max(
#             (first_value * getattr(ureg, first_units)),
#             (second_value * getattr(ureg, second_units))
#         )
#     except UndefinedUnitError:
#         if first_units == second_units:
#             diff = first_value - second_value
#             perc_denom = max(first_value, second_value)
#         else:
#             return 1000, 0

#     perc_sim = 1 - abs(diff / perc_denom)
#     return diff, perc_sim
