from __future__ import (absolute_import, division, print_function)

from .constants import Constants, ConversionFactors


def _apply_conv_fact(var, vartype, var_unit, dest_unit):
    """Return the variable converted to different units using a conversion
    factor.

    Args:

        var (:class:`xarray.DataArray` or :class:`numpy.ndarray`): A
            variable.

        vartype (:obj:`str`): The type of variable.  Choices are: 'wind',
            'pressure', 'temp', or 'height'.

        var_unit (:obj:`str`): The variable's current units.

        dest_unit (:obj:`str`): The desired units.

    Returns:

        :class:`xarray.DataArray` or :class:`numpy.ndarray`: The variable in
        the desired units.

    """
    if var_unit == dest_unit:
        return var

    # Note, case where var_unit and dest_unit are base unit, should be
    # handled above
    if var_unit == _BASE_UNITS[vartype]:
        return var*(_CONV_FACTORS[vartype]["to_dest"][dest_unit])
    else:
        if dest_unit == _BASE_UNITS[vartype]:
            return var*(_CONV_FACTORS[vartype]["to_base"][var_unit])
        else:
            return var*(_CONV_FACTORS[vartype]["to_base"][var_unit] *
                        _CONV_FACTORS[vartype]["to_dest"][dest_unit])


def _to_kelvin(var, var_unit):
    """Return the variable in Kelvin.

    Args:

        var (:class:`xarray.DataArray` or :class:`numpy.ndarray`): A
            variable.

        var_unit (:obj:`str`): The variable's current units.

    Returns:

        :class:`xarray.DataArray` or :class:`numpy.ndarray`: The variable in
        Kelvin.

    """
    if var_unit == "c":
        return var + Constants.CELKEL
    elif var_unit == "f":
        return (var - 32.0) * (5.0/9.0) + Constants.CELKEL


def _k_to_c(var):
    """Return the variable in Celsius.

    Args:

        var (:class:`xarray.DataArray` or :class:`numpy.ndarray`): A
            variable in units of Kelvin.

    Returns:

        :class:`xarray.DataArray` or :class:`numpy.ndarray`: The variable in
        Celsius.

    """
    return var - Constants.CELKEL


def _k_to_f(var):
    """Return the variable in Fahrenheit.

    Args:

        var (:class:`xarray.DataArray` or :class:`numpy.ndarray`): A
            variable in units of Kelvin.

    Returns:

        :class:`xarray.DataArray` or :class:`numpy.ndarray`: The variable in
        Fahrenheit.

    """
    return 1.8 * _k_to_c(var) + 32.0


def _apply_temp_conv(var, var_unit, dest_unit):
    """Return the variable converted to different units using a temperature
    conversion algorithm.

    Args:

        var (:class:`xarray.DataArray` or :class:`numpy.ndarray`): A
            variable.

        var_unit (:obj:`str`): The variable's current units.

        dest_unit (:obj:`str`): The desired units.

    Returns:

        :class:`xarray.DataArray` or :class:`numpy.ndarray`: The variable in
        the desired units.

    """
    if dest_unit == var_unit:
        return var

    if var_unit != _BASE_UNITS["temp"]:
        tk = _to_kelvin(var, var_unit)
        if dest_unit == _BASE_UNITS["temp"]:
            return tk
        else:
            return (_TEMP_CONV_METHODS[dest_unit])(tk)
    else:
        return (_TEMP_CONV_METHODS[dest_unit])(var)


# A mapping of unit names to their dictionary key names
_UNIT_ALIASES = {"mps": "m s-1",
                 "m/s": "m s-1",
                 "ms-1": "m s-1",
                 "meters_per_second": "m s-1",
                 "metres_per_second": "m s-1",
                 "knots": "kt",
                 "knot": "kt",
                 "kts": "kt",
                 "kn": "kt",
                 "miles_per_hour": "mi h-1",
                 "mih-1": "mi h-1",
                 "mph": "mi h-1",
                 "mi/h": "mi h-1",
                 "kmph": "km h-1",
                 "kmh-1": "km h-1",
                 "km/h": "km h-1",
                 "kilometers_per_hour": "km h-1",
                 "kilometres_per_hour": "km h-1",
                 "ft/s": "ft s-1",
                 "ft/sec": "ft s-1",
                 "fps": "ft s-1",
                 "fs-1": "ft s-1",
                 "feet_per_second": "ft s-1",
                 "pascal": "pa",
                 "pascals": "pa",
                 "hecto_pascal": "hpa",
                 "hecto_pascals": "hpa",
                 "millibar": "mb",
                 "millibars": "mb",
                 "mbar": "mb",
                 "kelvin": "k",
                 "degree_kelvin": "k",
                 "degrees_kelvin": "k",
                 "degree_k": "k",
                 "degrees_k": "k",
                 "degreek": "k",
                 "degreesk": "k",
                 "degk": "k",
                 "degsk": "k",
                 "deg_k": "k",
                 "degs_k": "k",
                 "deg k": "k",
                 "degs k": "k",
                 "celsius": "c",
                 "degree_celsius": "c",
                 "degrees_celsius": "c",
                 "degree_c": "c",
                 "degrees_c": "c",
                 "degreec": "c",
                 "degreesc": "c",
                 "degc": "c",
                 "degsc": "c",
                 "deg_c": "c",
                 "degs_c": "c",
                 "deg c": "c",
                 "degs c": "c",
                 "fahrenheit": "f",
                 "degree_fahrenheit": "f",
                 "degrees_fahrenheit": "f",
                 "degree_f": "f",
                 "degrees_f": "f",
                 "degreef": "f",
                 "degreesf": "f",
                 "degf": "f",
                 "degsf": "f",
                 "deg_f": "f",
                 "degs_f": "f",
                 "deg f": "f",
                 "degs f": "f",
                 "meter": "m",
                 "meters": "m",
                 "metre": "m",
                 "metres": "m",
                 "kilometer": "km",
                 "kilometers": "km",
                 "dekameter": "dm",
                 "dekameters": "dm",
                 "decameter": "dm",
                 "decameters": "dm",
                 "dekametre": "dm",
                 "dekametres": "dm",
                 "decametre": "dm",
                 "decametres": "dm",
                 "dam": "dm",
                 "dkm": "dm",
                 "feet": "ft",
                 "foot": "ft",
                 "mile": "mi",
                 "miles": "mi"
                 }

# A mapping of unit types to the avaible units
_VALID_UNITS = {"wind": ["m s-1", "kt", "mi h-1", "km h-1", "ft s-1"],
                "pressure": ["pa", "hpa", "mb", "torr", "mmhg", "atm"],
                "temp": ["k", "f", "c"],
                "height": ["m", "km", "dm", "ft", "mi"]
                }

# Conversion factor map for wind from base units
_WIND_BASE_FACTORS = {"kt": ConversionFactors.MPS_TO_KTS,
                      "km h-1": ConversionFactors.MPS_TO_KMPH,
                      "mi h-1": ConversionFactors.MPS_TO_MPH,
                      "ft s-1": ConversionFactors.MPS_TO_FPS
                      }

# Conversion factor map to base units
_WIND_TOBASE_FACTORS = {"kt": 1.0/ConversionFactors.MPS_TO_KTS,
                        "km h-1": 1.0/ConversionFactors.MPS_TO_KMPH,
                        "mi h-1": 1.0/ConversionFactors.MPS_TO_MPH,
                        "ft s-1": 1.0/ConversionFactors.MPS_TO_FPS
                        }

# Conversion factor map for pressure from base units
_PRES_BASE_FACTORS = {"hpa": ConversionFactors.PA_TO_HPA,
                      "mb": ConversionFactors.PA_TO_HPA,
                      "torr": ConversionFactors.PA_TO_TORR,
                      "mmhg": ConversionFactors.PA_TO_MMHG,
                      "atm": ConversionFactors.PA_TO_ATM
                      }

# Conversion factor map for pressure to base units
_PRES_TOBASE_FACTORS = {"hpa": 1.0/ConversionFactors.PA_TO_HPA,
                        "mb": 1.0/ConversionFactors.PA_TO_HPA,
                        "torr": 1.0/ConversionFactors.PA_TO_TORR,
                        "mmhg": 1.0/ConversionFactors.PA_TO_MMHG,
                        "atm": 1.0/ConversionFactors.PA_TO_ATM
                        }

# Conversion factor map for height from base units
_HEIGHT_BASE_FACTORS = {"km": ConversionFactors.M_TO_KM,
                        "dm": ConversionFactors.M_TO_DM,
                        "ft": ConversionFactors.M_TO_FT,
                        "mi": ConversionFactors.M_TO_MILES
                        }

# Conversion factor map for height to base units
_HEIGHT_TOBASE_FACTORS = {"km": 1.0/ConversionFactors.M_TO_KM,
                          "dm": 1.0/ConversionFactors.M_TO_DM,
                          "ft": 1.0/ConversionFactors.M_TO_FT,
                          "mi": 1.0/ConversionFactors.M_TO_MILES
                          }

# Mapping of unit type to base unit type
_BASE_UNITS = {"wind": "m s-1",
               "pressure": "pa",
               "temp": "k",
               "height": "m"
               }

# A mapping of unit type to a mapping of to/from base conversion factors
_CONV_FACTORS = {"wind": {"to_dest": _WIND_BASE_FACTORS,
                          "to_base": _WIND_TOBASE_FACTORS},
                 "pressure": {"to_dest": _PRES_BASE_FACTORS,
                              "to_base": _PRES_TOBASE_FACTORS},
                 "height": {"to_dest": _HEIGHT_BASE_FACTORS,
                            "to_base": _HEIGHT_TOBASE_FACTORS}
                 }

# A mapping of temperature type to the conversion function
_TEMP_CONV_METHODS = {"c": _k_to_c,
                      "f": _k_to_f
                      }


def dealias_and_clean_unit(unit):
    """Return the properly cleaned and dealiased unit name.

    Args:

        unit (:obj:`str`): The unit name.

    Returns:

        :obj:`str`: A unit name suitable for dictionary key lookups.

    """
    cleaned_unit = " ".join(unit.lower().split())
    dealiased = _UNIT_ALIASES.get(cleaned_unit, None)

    return cleaned_unit if dealiased is None else dealiased


def check_units(unit, unit_type):
    """Raise an exception if the unit name is invalid.

    Args:

        unit (:obj:`str`):  The unit name.

        unit_type (:obj:`str`): The type of unit.

    Returns:

        None

    Raises:

        :class:`ValueError`: Raised when the unit name is invalid.

    """
    u_cleaned = dealias_and_clean_unit(unit)
    if u_cleaned not in _VALID_UNITS[unit_type]:
        raise ValueError("invalid unit type '{}'".format(unit))


def do_conversion(var, vartype, var_unit, dest_unit):
    """Return the variable converted to different units.

    Args:

        var (:class:`xarray.DataArray` or :class:`numpy.ndarray`): A
            variable.

        vartype (:obj:`str`): The type of variable.  Choices are: 'wind',
            'pressure', 'temp', or 'height'.

        var_unit (:obj:`str`): The variable's current units.

        dest_unit (:obj:`str`): The desired units.

    Returns:

        :class:`xarray.DataArray` or :class:`numpy.ndarray`: The variable in
        the desired units.

    """
    u_cleaned = dealias_and_clean_unit(dest_unit)
    if vartype != "temp":
        return _apply_conv_fact(var, vartype, var_unit.lower(), u_cleaned)
    else:
        return _apply_temp_conv(var, var_unit.lower(), u_cleaned)
