# -*- coding: utf-8 -*-
"""
Unit library parse extension.

The methods here parse a string containing physical
quantities to numerical value and unit. The unit
manipulation and conversion is done using
Astropy unit library.
"""
import warnings

from astropy import units as u
from astropy.units import cds
from astropy.units import Quantity
from numpy import inf

__author__ = "Deepansh J. Srivastava"
__email__ = "srivastava.89@osu.edu"
__all__ = ["string_to_quantity", "ScalarQuantity"]

NUMERIC = "0123456789-+.eE*/j^ ()"

tr = u.def_unit(["tr", "turn", "revolution"], 1.0 * u.cycle)
u.add_enabled_units([cds.ppm, tr])

convert = {
    "Å": "Angstrom",
    "°C": "deg_C",
    "°F": "deg_F",
    "°": "deg",
    "µ": "u",
    "ℏ": "/h",
    "Ω": "Ohm",
}


def get_start_index_of_quantity_unit(string):
    """Return the index where the unit of the quantity starts."""
    string = string.strip() + " "

    for i, c in enumerate(string):
        if c not in NUMERIC:
            break

    j = 1
    for j in range(1, i + 1):
        if string[:i][-j] == "(":
            break
        if string[:i][-j] == ")":
            j = j - 1
            break

    index = i if i == j else i - j
    return index


def string_to_quantity(string, dtype=float):
    """
    Parse the string and return a ``Quantity`` object.

    The string must contain a physical quantity. The method parse the string
    for numerical value and unit.

    :returns: ``Quantity`` object
    """
    index = get_start_index_of_quantity_unit(string)

    # the string has numerical value and unit
    if index != -1:
        try:
            number = eval(string[:index])
        except ZeroDivisionError:
            number = inf
        except Exception as e:
            raise ValueError(e)
    # the string is unit only
    else:
        index = 0
        number = 1.0

    unit = string[index:].strip()
    if unit != "" and unit != "()":
        if unit[0] == "(" and unit[-1] == ")":
            unit = unit[1:-1]

    for key in convert:
        unit = unit.replace(key, convert[key])
        unit_multiplier = 1
    try:
        unit_qt = u.Unit(unit) * unit_multiplier
        analysis = dtype(number) * unit_qt
        return analysis
    except BaseException as e:
        raise BaseException(e)


def scalar_quantity_format(quantity, numerical_value=True):
    """Convert unit to value object."""
    string = quantity.unit.to_string("fits").strip()
    for key in convert.keys():
        string = string.replace(convert[key], key)
    lst = {"10**-6": "ppm", "10**-2": "%"}
    for key in lst.keys():
        string = string.replace(key, lst[key])

    if numerical_value:
        cat_string = [str(quantity.value)]
    else:
        cat_string = [""]
    subunits = string.split(" ")
    for item in subunits:
        power = False
        if item.find("-") == -1:
            for i, c in enumerate(item):
                if c.isnumeric():
                    if i == 0:
                        break
                    cat_string.append(item[:i] + "^" + item[i:] + " *")
                    power = True
                    break
            if not power:
                cat_string.append(item + " *")
        else:
            l, r = item.split("-")
            cat_string.append(l + "^-" + r + " *")
    string = " ".join(cat_string)[:-2]
    string = string.replace("* / *", "/")
    return string.strip()


# def unit_to_latex(unit):
#     """NotImplemented."""
#     string = unit.to_string("fits").strip()
#     convert_tex = {
#         "Angstrom": "\\AA",
#         "deg_C": "$^\\circ$C",
#         "deg_F": "$^\\circ$C",
#         "deg": "$^\\circ$",
#         "u": "$\\mu$",
#         "/h": "$\\hbar$",
#         "Ohm": "$\\Ohm$",
#         "10**-6": "ppm",
#     }
#     for key in convert_tex:
#         string = string.replace(key, convert_tex[key])
#     lst = {"10**-6": "ppm", "10**-2": "%"}
#     for key in lst.keys():
#         string = string.replace(key, lst[key])

#     cat_string = []
#     subunits = string.split(" ")
#     for item in subunits:
#         power = False
#         if item.find("-") == -1:
#             for i, c in enumerate(item):
#                 if c.isnumeric():
#                     if i == 0:
#                         break
#                     cat_string.append(item[:i] + "$^{" + item[i:] + "}$ ")
#                     power = True
#                     break
#             if not power:
#                 cat_string.append(item + " ")
#         else:
#             l, r = item.split("-")
#             cat_string.append(l + "$^{-" + r + "}$ ")
#     string = " ".join(cat_string)
#     return string


class ScalarQuantity:

    """
    A ScalarQuantity class compliant with the CSDM standards.

    @params: quantity: Return the quantity object from astropy.units
                       library.
    """

    __slots__ = "quantity"

    def __init__(self, quantity_string=None, unit=None):
        self.quantity = self.quantity_object(quantity_string, unit)

    @classmethod
    def quantity_object(cls, quantity_string, unit):
        if isinstance(quantity_string, ScalarQuantity):
            return quantity_string.quantity

        if isinstance(quantity_string, Quantity):
            return quantity_string

        lst = [None, ""]
        if quantity_string in lst and unit in lst:
            return 0.0 * u.Unit("")

        if quantity_string in lst and unit not in lst:
            return 0.0 * unit

        if isinstance(quantity_string, str):
            quantity = string_to_quantity(quantity_string)
            if unit is not None:
                return check_unit_consistency(quantity, unit)
            else:
                return quantity

    def __str__(self):
        return self.__format__()

    def __format__(self, format="quantity"):
        """Format the units according to csdmpy recommendation.

        :ivar: format: The value can either be 'quantity' or 'unit'

        .. doctest::

            >>> a = ScalarQuantity('5 kg m^2 /s')
            >>> print(a.quantity)
            5.0 kg m2 / s
            >>> print(str(ScalarQuantity(a.quantity)))
            5.0 kg * m^2 * s^-1
            >>> print(ScalarQuantity(a.quantity).__format__('unit'))
            kg * m^2 * s^-1
        """

        element = _default_units(self.quantity)
        if format == "unit":
            return scalar_quantity_format(element, numerical_value=False)
        if format == "quantity":
            return scalar_quantity_format(element)


def check_unit_consistency(element, unit):
    if isinstance(unit, str):
        unit = ScalarQuantity(unit).quantity.unit
    if element.unit.physical_type != unit.physical_type:
        options = [
            str(element.unit),
            str(element.unit.physical_type),
            str(unit),
            unit.physical_type,
        ]
        message = (
            "Validation Failed: The unit '{0}' ({1}) is inconsistent "
            "with the unit '{2}' ({3})."
        )
        raise Exception(message.format(*options))
    return element


def _default_units(element):
    if element.unit.physical_type == "frequency":
        element = element.to("Hz")
    return element


def check_quantity_name(element, unit):
    if element is None:
        element = unit.physical_type
        return element

    if unit.physical_type == "unknown":
        warnings.warn(
            f"The physical quantity name associated with the unit, {str(unit)}, is not"
            f" defined in astropy.units package. Continuing with '{element}' as the "
            f"physical quantity name."
        )
        return element

    if element.lower() != unit.physical_type:
        warnings.warn(
            f"The physical quantity name, '{element}', is not defined in the "
            f"astropy.units package. Continuing with '{element}' as the physical "
            f"quantity name for unit {str(unit)}."
        )

    return element.lower()


def frequency_ratio(offset):
    return [
        (u.Hz, u.cds.ppm, lambda x: (x / offset.to("Hz")) * 1e6, lambda x: x * offset)
    ]
