#!/usr/bin/env python3

"""
    pint-convert
    ~~~~~~~~~~~~

    :copyright: 2020 by Pint Authors, see AUTHORS for more details.
    :license: BSD, see LICENSE for more details.
"""

from __future__ import annotations

import argparse
import contextlib
import re

from pint import UnitRegistry

parser = argparse.ArgumentParser(description="Unit converter.", usage=argparse.SUPPRESS)
parser.add_argument(
    "-s",
    "--system",
    metavar="sys",
    default="SI",
    help="unit system to convert to (default: SI)",
)
parser.add_argument(
    "-p",
    "--prec",
    metavar="n",
    type=int,
    default=12,
    help="number of maximum significant figures (default: 12)",
)
parser.add_argument(
    "-u",
    "--prec-unc",
    metavar="n",
    type=int,
    default=2,
    help="number of maximum uncertainty digits (default: 2)",
)
parser.add_argument(
    "-U",
    "--no-unc",
    dest="unc",
    action="store_false",
    help="ignore uncertainties in constants",
)
parser.add_argument(
    "-C",
    "--no-corr",
    dest="corr",
    action="store_false",
    help="ignore correlations between constants",
)
parser.add_argument(
    "fr", metavar="from", type=str, help="unit or quantity to convert from"
)
parser.add_argument("to", type=str, nargs="?", help="unit to convert to")
try:
    args = parser.parse_args()
except SystemExit:
    parser.print_help()
    raise

ureg = UnitRegistry()
ureg.auto_reduce_dimensions = True
ureg.autoconvert_offset_to_baseunit = True
ureg.enable_contexts("Gau", "ESU", "sp", "energy", "boltzmann")
ureg.default_system = args.system


def _set(key: str, value):
    obj = ureg._units[key].converter
    object.__setattr__(obj, "scale", value)


if args.unc:
    import uncertainties

    # Measured constants subject to correlation
    #  R_i: Rydberg constant
    #  g_e: Electron g factor
    #  m_u: Atomic mass constant
    #  m_e: Electron mass
    #  m_p: Proton mass
    #  m_n: Neutron mass
    R_i = (ureg._units["R_inf"].converter.scale, 0.0000000000021e7)
    g_e = (ureg._units["g_e"].converter.scale, 0.00000000000035)
    m_u = (ureg._units["m_u"].converter.scale, 0.00000000050e-27)
    m_e = (ureg._units["m_e"].converter.scale, 0.00000000028e-30)
    m_p = (ureg._units["m_p"].converter.scale, 0.00000000051e-27)
    m_n = (ureg._units["m_n"].converter.scale, 0.00000000095e-27)
    if args.corr:
        # Correlation matrix between measured constants (to be completed below)
        #          R_i       g_e       m_u       m_e       m_p       m_n
        corr = [
            [1.0, -0.00206, 0.00369, 0.00436, 0.00194, 0.00233],  # R_i
            [-0.00206, 1.0, 0.99029, 0.99490, 0.97560, 0.52445],  # g_e
            [0.00369, 0.99029, 1.0, 0.99536, 0.98516, 0.52959],  # m_u
            [0.00436, 0.99490, 0.99536, 1.0, 0.98058, 0.52714],  # m_e
            [0.00194, 0.97560, 0.98516, 0.98058, 1.0, 0.51521],  # m_p
            [0.00233, 0.52445, 0.52959, 0.52714, 0.51521, 1.0],
        ]  # m_n
        (R_i, g_e, m_u, m_e, m_p, m_n) = uncertainties.correlated_values_norm(
            [R_i, g_e, m_u, m_e, m_p, m_n], corr
        )
    else:
        R_i = uncertainties.ufloat(*R_i)
        g_e = uncertainties.ufloat(*g_e)
        m_u = uncertainties.ufloat(*m_u)
        m_e = uncertainties.ufloat(*m_e)
        m_p = uncertainties.ufloat(*m_p)
        m_n = uncertainties.ufloat(*m_n)

    _set("R_inf", R_i)
    _set("g_e", g_e)
    _set("m_u", m_u)
    _set("m_e", m_e)
    _set("m_p", m_p)
    _set("m_n", m_n)

    # Measured constants with zero correlation
    _set(
        "gravitational_constant",
        uncertainties.ufloat(
            ureg._units["gravitational_constant"].converter.scale, 0.00015e-11
        ),
    )

    _set(
        "d_220",
        uncertainties.ufloat(ureg._units["d_220"].converter.scale, 0.000000032e-10),
    )

    _set(
        "K_alpha_Cu_d_220",
        uncertainties.ufloat(
            ureg._units["K_alpha_Cu_d_220"].converter.scale, 0.00000022
        ),
    )

    _set(
        "K_alpha_Mo_d_220",
        uncertainties.ufloat(
            ureg._units["K_alpha_Mo_d_220"].converter.scale, 0.00000019
        ),
    )

    _set(
        "K_alpha_W_d_220",
        uncertainties.ufloat(
            ureg._units["K_alpha_W_d_220"].converter.scale, 0.000000098
        ),
    )

    ureg._root_units_cache = {}
    ureg._build_cache()


def convert(u_from, u_to=None, unc=None, factor=None):
    q = ureg.Quantity(u_from)
    fmt = f".{args.prec}g"
    if unc:
        q = q.plus_minus(unc)
    if u_to:
        nq = q.to(u_to)
    else:
        nq = q.to_base_units()
    if factor:
        q *= ureg.Quantity(factor)
        nq *= ureg.Quantity(factor).to_base_units()
    prec_unc = use_unc(nq.magnitude, fmt, args.prec_unc)
    if prec_unc > 0:
        fmt = f".{prec_unc}uS"
    else:
        with contextlib.suppress(Exception):
            nq = nq.magnitude.n * nq.units

    fmt = "{:" + fmt + "} {:~P}"
    print(("{:} = " + fmt).format(q, nq.magnitude, nq.units))


def use_unc(num, fmt, prec_unc):
    unc = 0
    with contextlib.suppress(Exception):
        if isinstance(num, uncertainties.UFloat):
            full = ("{:" + fmt + "}").format(num)
            unc = re.search(r"\+/-[0.]*([\d.]*)", full).group(1)
            unc = len(unc.replace(".", ""))

    return max(0, min(prec_unc, unc))


def main():
    convert(args.fr, args.to)


if __name__ == "__main__":
    main()
