"""UnitDbl module."""

import functools
import operator

from matplotlib import _api


class UnitDbl:
    """Class UnitDbl in development."""

    # Unit conversion table.  Small subset of the full one but enough
    # to test the required functions.  First field is a scale factor to
    # convert the input units to the units of the second field.  Only
    # units in this table are allowed.
    allowed = {
        "m": (0.001, "km"),
        "km": (1, "km"),
        "mile": (1.609344, "km"),

        "rad": (1, "rad"),
        "deg": (1.745329251994330e-02, "rad"),

        "sec": (1, "sec"),
        "min": (60.0, "sec"),
        "hour": (3600, "sec"),
        }

    _types = {
        "km": "distance",
        "rad": "angle",
        "sec": "time",
        }

    def __init__(self, value, units):
        """
        Create a new UnitDbl object.

        Units are internally converted to km, rad, and sec.  The only
        valid inputs for units are [m, km, mile, rad, deg, sec, min, hour].

        The field UnitDbl.value will contain the converted value.  Use
        the convert() method to get a specific type of units back.

        = ERROR CONDITIONS
        - If the input units are not in the allowed list, an error is thrown.

        = INPUT VARIABLES
        - value     The numeric value of the UnitDbl.
        - units     The string name of the units the value is in.
        """
        data = _api.getitem_checked(self.allowed, units=units)
        self._value = float(value * data[0])
        self._units = data[1]

    def convert(self, units):
        """
        Convert the UnitDbl to a specific set of units.

        = ERROR CONDITIONS
        - If the input units are not in the allowed list, an error is thrown.

        = INPUT VARIABLES
        - units     The string name of the units to convert to.

        = RETURN VALUE
        - Returns the value of the UnitDbl in the requested units as a floating
          point number.
        """
        if self._units == units:
            return self._value
        data = _api.getitem_checked(self.allowed, units=units)
        if self._units != data[1]:
            raise ValueError(f"Error trying to convert to different units.\n"
                             f"    Invalid conversion requested.\n"
                             f"    UnitDbl: {self}\n"
                             f"    Units:   {units}\n")
        return self._value / data[0]

    def __abs__(self):
        """Return the absolute value of this UnitDbl."""
        return UnitDbl(abs(self._value), self._units)

    def __neg__(self):
        """Return the negative value of this UnitDbl."""
        return UnitDbl(-self._value, self._units)

    def __bool__(self):
        """Return the truth value of a UnitDbl."""
        return bool(self._value)

    def _cmp(self, op, rhs):
        """Check that *self* and *rhs* share units; compare them using *op*."""
        self.checkSameUnits(rhs, "compare")
        return op(self._value, rhs._value)

    __eq__ = functools.partialmethod(_cmp, operator.eq)
    __ne__ = functools.partialmethod(_cmp, operator.ne)
    __lt__ = functools.partialmethod(_cmp, operator.lt)
    __le__ = functools.partialmethod(_cmp, operator.le)
    __gt__ = functools.partialmethod(_cmp, operator.gt)
    __ge__ = functools.partialmethod(_cmp, operator.ge)

    def _binop_unit_unit(self, op, rhs):
        """Check that *self* and *rhs* share units; combine them using *op*."""
        self.checkSameUnits(rhs, op.__name__)
        return UnitDbl(op(self._value, rhs._value), self._units)

    __add__ = functools.partialmethod(_binop_unit_unit, operator.add)
    __sub__ = functools.partialmethod(_binop_unit_unit, operator.sub)

    def _binop_unit_scalar(self, op, scalar):
        """Combine *self* and *scalar* using *op*."""
        return UnitDbl(op(self._value, scalar), self._units)

    __mul__ = functools.partialmethod(_binop_unit_scalar, operator.mul)
    __rmul__ = functools.partialmethod(_binop_unit_scalar, operator.mul)

    def __str__(self):
        """Print the UnitDbl."""
        return f"{self._value:g} *{self._units}"

    def __repr__(self):
        """Print the UnitDbl."""
        return f"UnitDbl({self._value:g}, '{self._units}')"

    def type(self):
        """Return the type of UnitDbl data."""
        return self._types[self._units]

    @staticmethod
    def range(start, stop, step=None):
        """
        Generate a range of UnitDbl objects.

        Similar to the Python range() method.  Returns the range [
        start, stop) at the requested step.  Each element will be a
        UnitDbl object.

        = INPUT VARIABLES
        - start     The starting value of the range.
        - stop      The stop value of the range.
        - step      Optional step to use.  If set to None, then a UnitDbl of
                      value 1 w/ the units of the start is used.

        = RETURN VALUE
        - Returns a list containing the requested UnitDbl values.
        """
        if step is None:
            step = UnitDbl(1, start._units)

        elems = []

        i = 0
        while True:
            d = start + i * step
            if d >= stop:
                break

            elems.append(d)
            i += 1

        return elems

    def checkSameUnits(self, rhs, func):
        """
        Check to see if units are the same.

        = ERROR CONDITIONS
        - If the units of the rhs UnitDbl are not the same as our units,
          an error is thrown.

        = INPUT VARIABLES
        - rhs     The UnitDbl to check for the same units
        - func    The name of the function doing the check.
        """
        if self._units != rhs._units:
            raise ValueError(f"Cannot {func} units of different types.\n"
                             f"LHS: {self._units}\n"
                             f"RHS: {rhs._units}")
