# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Name:        _pydecimalfp
# Purpose:     Decimal fixed-point arithmetic (Python implementation)
#
# Author:      Michael Amrhein (michael@adrhinum.de)
#
# Copyright:   (c) 2001 ff. Michael Amrhein
#              Portions adopted from FixedPoint.py written by Tim Peters
# License:     This program is part of a larger application. For license
#              details please read the file LICENSE.TXT provided together
#              with the application.
# ----------------------------------------------------------------------------
# $Source: src/decimalfp/_pydecimalfp.py $
# $Revision: 2021-07-02T16:40:27+02:00 $


"""Decimal fixed-point arithmetic."""

from __future__ import annotations

import locale
import operator
import sys
from abc import abstractmethod
from decimal import Decimal as _StdLibDecimal
from fractions import Fraction
from functools import reduce
from math import ceil, floor, gcd, log10
from numbers import Complex, Integral, Rational, Real
from typing import (
    Any, Callable, Generator, Optional, Sequence, SupportsFloat,
    SupportsInt, Tuple, Union, overload,
    )

if sys.version_info >= (3, 8):
    from typing import Protocol, runtime_checkable
else:
    from typing_extensions import Protocol, runtime_checkable

from .rounding import ROUNDING

__name__ = 'decimalfp'  # for pickling

MAX_DEC_PRECISION = 65535

# parse functions
import re  # noqa: I100, I202

# parse for a Decimal
# [+|-]<int>[.<frac>][<e|E>[+|-]<exp>] or
# [+|-].<frac>[<e|E>[+|-]<exp>].
_pattern = r"""
            \s*
            (?P<sign>[+|-])?
            (
                (?P<int>\d+)(\.(?P<frac>\d*))?
                |
                \.(?P<onlyfrac>\d+)
            )
            ([eE](?P<exp>[+|-]?\d+))?
            \s*$
            """
_parse_dec_string = re.compile(_pattern, re.VERBOSE).match

# parse for a format specifier
# [[fill]align][sign][0][minimumwidth][,][.precision][type]
_pattern = r"""
            \A
            (?:
                (?P<fill>.)?
                (?P<align>[<>=^])
            )?
            (?P<sign>[-+ ])?
            (?P<zeropad>0)?
            (?P<minimumwidth>(?!0)\d+)?
            (?P<thousands_sep>,)?
            (?:\.(?P<precision>0|(?!0)\d+))?
            (?P<type>[fFn%])?
            \Z
            """
_parse_format_spec = re.compile(_pattern, re.VERBOSE).match
del re, _pattern


@runtime_checkable
class SupportsAsIntegerRatio(Protocol):
    """Number which is convertable to a ratio of integers."""

    @abstractmethod
    def as_integer_ratio(self) -> Tuple[int, int]:
        """Return integer ratio equal to `self`."""


SupportsIntOrFloat = Union[SupportsInt, SupportsFloat]
DecValueT = Union[SupportsAsIntegerRatio, SupportsInt, SupportsFloat, str,
                  None]
Integer = Union[int, Integral]


class Decimal(Rational):
    # noinspection PyUnresolvedReferences
    """Decimal number with a given number of fractional digits.

    Args:
        value: numerical value (default: None)
        precision: number of fractional digits (default: None)

    If `value` is given, it must either be a string, an instance of
    `int`, `number.Rational` (for example `fractions.Fraction`),
    `decimal.Decimal`, a finite instance of `numbers.Real` (for example
    `float`) or be convertable to a `float` or an `int`.

    If a string is given as value, it must be a string in one of two formats:

    * [+|-]<int>[.<frac>][<e|E>[+|-]<exp>] or
    * [+|-].<frac>[<e|E>[+|-]<exp>].

    If given value is `None`, Decimal(0) is returned.

    Returns:
        :class:`Decimal` instance derived from `value` according
            to `precision`

    The value is always adjusted to the given precision or the precision is
    calculated from the given value, if no precision is given.

    Raises:
        TypeError: `precision` is given, but not of type `int`.
        TypeError: `value` is not an instance of the types listed above and
            not convertable to `float` or `int`.
        ValueError: `precision` is given, but not >= 0.
        ValueError: `precision` is given, but not <= `MAX_DEC_PRECISION`.
        ValueError: `value` can not be converted to a `Decimal` (with a number
            of fractional digits <= `MAX_DEC_PRECISION`).

    :class:`Decimal` instances are immutable.

    """

    __slots__ = ('_value', '_precision',
                 # used for caching values only:
                 '_hash', '_numerator', '_denominator'
                 )

    _value: int
    _precision: int
    _hash: int
    _numerator: int
    _denominator: int

    def __new__(cls, value: DecValueT = None,
                precision: Optional[Integer] = None) -> Decimal:
        """Create and return new `Decimal` instance."""
        self: Decimal = object.__new__(cls)

        if precision is None:
            if value is None:
                self._value = 0
                self._precision = 0
                return self
            prec = 0
        else:
            if not isinstance(precision, Integral):
                raise TypeError(
                    "Precision must be of type 'numbers.Integral'.")
            prec = int(precision)
            if prec < 0:
                raise ValueError("Precision must be >= 0.")
            if prec > MAX_DEC_PRECISION:
                raise ValueError("Precision limit exceeded.")
            if value is None:
                self._value = 0
                self._precision = prec
                return self

        # Decimal
        if isinstance(value, Decimal):
            v, p = value._value, value._precision
            if precision is None or prec == p:
                self._value = v
                self._precision = p
            else:
                self._value = _vp_adjust_to_prec(v, p, prec)
                self._precision = prec
            return self

        # String
        if isinstance(value, str):
            parsed = _parse_dec_string(value)
            if parsed is None:
                raise ValueError("Can't convert %s to Decimal." % repr(value))
            sign_n_digits = parsed.group('sign') or ''
            s_exp = parsed.group('exp')
            if s_exp:
                exp = int(s_exp)
            else:
                exp = 0
            s_int = parsed.group('int')
            if s_int:
                s_frac = parsed.group('frac')
                if s_frac:
                    sign_n_digits += s_int + s_frac
                    n_frac = len(s_frac)
                else:
                    sign_n_digits += s_int
                    n_frac = 0
            else:
                s_frac = parsed.group('onlyfrac')
                n_frac = len(s_frac)
                sign_n_digits += s_frac
            if precision is None:
                prec = max(0, n_frac - exp)
                if prec > MAX_DEC_PRECISION:
                    raise ValueError("Precision limit exceeded.")
            self._precision = prec
            shift10 = prec - n_frac + exp
            if shift10 == 0:
                self._value = int(sign_n_digits)
            elif shift10 > 0:
                self._value = int(sign_n_digits) * 10 ** shift10
            else:
                self._value = _floordiv_rounded(int(sign_n_digits),
                                                10 ** -shift10)
            return self

        # Integer
        if isinstance(value, Integral):
            self._precision = prec
            value = int(value)
            if prec == 0:
                self._value = value
            else:
                self._value = value * 10 ** prec
            return self

        # Decimal (from standard library)
        if isinstance(value, _StdLibDecimal):
            if value.is_finite():
                sign, digits, exp = value.as_tuple()
                coeff = (-1) ** sign * reduce(lambda x, y: x * 10 + y, digits)
                if precision is None:
                    if exp > 0:
                        self._value = coeff * 10 ** exp
                    else:
                        self._value = coeff
                        prec = abs(exp)
                        if prec > MAX_DEC_PRECISION:
                            raise ValueError("Precision limit exceeded.")
                else:
                    shift10 = exp + prec
                    if shift10 == 0:
                        self._value = coeff
                    elif shift10 > 0:
                        self._value = coeff * 10 ** shift10
                    else:
                        self._value = _floordiv_rounded(coeff, 10 ** -shift10)
                self._precision = prec
                return self
            else:
                raise ValueError("Can't convert %s to Decimal." % repr(value))

        # Real (incl. Rational)
        if isinstance(value, Real):
            try:
                # noinspection PyUnresolvedReferences
                num, den = value.numerator, value.denominator  # type: ignore
            except AttributeError:
                try:
                    # noinspection PyUnresolvedReferences
                    num, den = value.as_integer_ratio()  # type: ignore
                except (ValueError, OverflowError, AttributeError):
                    raise ValueError("Can't convert %s to Decimal."
                                     % repr(value))
            if precision is None:
                v, p, rem = _approx_rational(num, den)
                if rem:
                    raise ValueError("Can't convert %s exactly to Decimal."
                                     % repr(value))
                if p > MAX_DEC_PRECISION:
                    raise ValueError("Precision limit exceeded.")
                self._value = v
                self._precision = p
            else:
                self._value = _floordiv_rounded(num * 10 ** prec, den)
                self._precision = prec
            return self

        # Others
        # If there's a float or int equivalent to value, use it
        ev = None
        try:
            ev = float(value)  # type: ignore
        except (TypeError, ValueError):
            try:
                ev = int(value)  # type: ignore
            except (TypeError, ValueError):
                pass
        if ev == value:  # do we really have the same value?
            return Decimal(ev, precision)

        # unable to create Decimal
        raise TypeError("Can't convert %s to Decimal." % repr(value))

    # to be compatible to fractions.Fraction
    @classmethod
    def from_float(cls, f: Union[float, int]) -> Decimal:
        """Convert a finite float (or int) to a :class:`Decimal`.

        Args:
            f: number to be converted to a `Decimal`

        Returns:
            :class:`Decimal` instance derived from `f`

        Raises:
            TypeError: `f` is neither a `float` nor an `int`.
            ValueError: `f` can not be converted to a :class:`Decimal` with
                a precision <= `MAX_DEC_PRECISION`.

        Beware that Decimal.from_float(0.3) != Decimal('0.3').

        """
        if not isinstance(f, (float, int)):
            raise TypeError("%s is not a float." % repr(f))
        # noinspection PyArgumentList
        return cls(f)

    # to be compatible to fractions.Fraction
    @classmethod
    def from_decimal(cls, d: Union[Decimal, int, _StdLibDecimal]) \
            -> Decimal:
        """Convert a finite decimal number to a :class:`Decimal`.

        Args:
            d: decimal number to be converted to a :class:`Decimal`

        Returns:
            :class:`Decimal` instance derived from `d`

        Raises:
            TypeError: `d` is not an instance of the types listed above.
            ValueError: `d` can not be converted to a :class:`Decimal`.

        """
        if not isinstance(d, (Decimal, int, _StdLibDecimal)):
            raise TypeError("%s is not a Decimal." % repr(d))
        # noinspection PyArgumentList
        return cls(d)

    @classmethod
    def from_real(cls, r: Real, exact: bool = True) -> Decimal:
        """Convert a finite Real number to a :class:`Decimal`.

        Args:
            r: number to be converted to a :class:`Decimal`
            exact: `True` if `r` shall exactly be represented by
                the resulting :class:`Decimal`

        Returns:
            :class:`Decimal` instance derived from `r`

        Raises:
            TypeError: `r` is not an instance of `numbers.Real`.
            ValueError: `exact` is `True` and `r` can not exactly be converted
                to a :class:`Decimal` with a precision <= `MAX_DEC_PRECISION`.

        If `exact` is `False` and `r` can not exactly be represented by a
        `Decimal` with a precision <= `MAX_DEC_PRECISION`, the result is
        rounded to a precision = `MAX_DEC_PRECISION`.

        """
        if not isinstance(r, Real):
            raise TypeError("%s is not a Real." % repr(r))
        try:
            # noinspection PyArgumentList
            return cls(r)
        except ValueError:
            if exact:
                raise
            else:
                # noinspection PyArgumentList
                return cls(r, MAX_DEC_PRECISION)

    @property
    def precision(self) -> int:
        """Return precision of `self`."""
        return self._precision

    @property
    def magnitude(self) -> int:
        """Return magnitude of `self` in terms of power to 10.

        I.e. the largest integer exp so that 10 ** exp <= self.

        """
        sv = self._value
        if sv == 0:
            raise OverflowError("Result would be '-Infinity'.")
        return floor(log10(abs(sv))) - self._precision

    @property
    def numerator(self) -> int:
        """Return the normalized numerator of `self`.

        I. e. the numerator from the pair of integers with the smallest
        positive denominator, whose ratio is equal to `self`.

        """
        try:
            return self._numerator
        except AttributeError:
            # noinspection PyAttributeOutsideInit
            self._numerator, self._denominator = self.as_integer_ratio()
            return self._numerator

    @property
    def denominator(self) -> int:
        """Return the normalized denominator of 'self'.

        I. e. the smallest positive denominator from the pairs of integers,
        whose ratio is equal to `self`.

        """
        try:
            return self._denominator
        except AttributeError:
            # noinspection PyAttributeOutsideInit
            self._numerator, self._denominator = self.as_integer_ratio()
            return self._denominator

    @property
    def real(self) -> Decimal:
        """Return real part of `self`.

        Returns `self` (Real numbers are their real component).

        """
        return self

    @property
    def imag(self) -> int:
        """Return imaginary part of `self`.

        Returns 0 (Real numbers have no imaginary component).

        """
        return 0

    def adjusted(self, precision: Optional[int] = None,
                 rounding: Optional[ROUNDING] = None) -> Decimal:
        """Return adjusted copy of `self`.

        Args:
            precision: number of fractional digits (default: None)
            rounding: rounding mode (default: None)

        Returns:
            :class:`Decimal` instance derived from `self`, adjusted
                to the given `precision`, using the given `rounding` mode

        If no `precision` is given, the result is adjusted to the minimum
        precision preserving x == x.adjusted().

        If no `rounding` mode is given, the current default rounding mode is
        used.

        If the given `precision` is less than the precision of `self`, the
        result is rounded and thus information may be lost.

        """
        if precision is None:
            adj: Decimal = object.__new__(Decimal)
            adj._value, adj._precision = _vp_normalize(self._value,
                                                       self._precision)
        else:
            if not isinstance(precision, int):
                raise TypeError("Precision must be of type 'int'.")
            to_prec = int(precision)
            if abs(to_prec) > MAX_DEC_PRECISION:
                raise ValueError("Precision limit exceeded.")
            p = self._precision
            if to_prec == p:
                return self
            adj = object.__new__(Decimal)
            adj._value = _vp_adjust_to_prec(self._value, p, to_prec, rounding)
            adj._precision = max(0, to_prec)
        return adj

    def quantize(self, quant: SupportsAsIntegerRatio,
                 rounding: Optional[ROUNDING] = None) -> Rational:
        """Return integer multiple of `quant` closest to `self`.

        Args:
            quant: quantum to get a multiple from
            rounding: rounding mode (default: None)

        If no `rounding` mode is given, the current default rounding mode is
        used.

        Returns:
            :class:`Decimal` instance that is the integer multiple of `quant`
                closest to `self` (according to `rounding` mode); if result
                can not be represented as :class:`Decimal`, an instance of
                `Fraction` is returned

        Raises:
            TypeError: `quant` is not a number or does not support
                `as_integer_ratio`
            ValueError: `quant` is not convertable to a `Rational`

        """
        try:
            num, den = quant.numerator, quant.denominator  # type: ignore
        except AttributeError:
            try:
                num, den = quant.as_integer_ratio()
            except AttributeError:
                raise TypeError("Can't quantize to a '%s': %s."
                                % (quant.__class__.__name__, quant)) \
                    from None
            except (OverflowError, ValueError):
                raise ValueError("Can't quantize to '%r'." % quant) \
                    from None
        mult = _floordiv_rounded(self._value * den,
                                 10 ** self._precision * num,
                                 rounding)
        return Decimal(mult) * quant

    def as_tuple(self) -> Tuple[int, int, int]:
        """Return a tuple (sign, coeff, exp) equivalent to `self`.

        self == sign * coeff * 10 ** exp.

        sign in (-1, 0, 1), for self < 0, = 0, > 0.
        coeff = 0 only if self = 0.

        """
        v = self._value
        if v == 0:
            sign = coeff = exp = 0
        else:
            if v > 0:
                sign = 1
                coeff = v
            else:
                sign = -1
                coeff = abs(v)
            exp = -self._precision
            # normalize coeff / exp
            q, r = divmod(coeff, 10)
            while r == 0:
                coeff = q
                exp += 1
                q, r = divmod(coeff, 10)
        return sign, coeff, exp

    def as_fraction(self) -> Fraction:
        """Return an instance of `Fraction` equal to `self`.

        Returns the `Fraction` with the smallest positive denominator, whose
        ratio is equal to `self`.

        """
        return Fraction(self._value, 10 ** self._precision)

    def as_integer_ratio(self) -> Tuple[int, int]:
        """Return a pair of integers whose ratio is equal to `self`.

        Returns the pair of numerator and denominator with the smallest
        positive denominator, whose ratio is equal to `self`.

        """
        n, d = self._value, 10 ** self._precision
        g = gcd(n, d)
        return n // g, d // g

    def __copy__(self) -> Decimal:
        """Return self (Decimal instances are immutable)."""
        return self

    def __deepcopy__(self, memo: Any) -> Decimal:
        """Return self (Decimal instances are immutable)."""
        return self.__copy__()

    # string representation
    def __repr__(self) -> str:
        """repr(self)"""  # noqa: D400
        sv = self._value
        sp = self._precision
        rv, rp = _vp_normalize(sv, sp)
        if rp == 0:
            s = str(rv)
        else:
            s = str(abs(rv))
            n = len(s)
            if n > rp:
                s = "'%s%s.%s'" % ((rv < 0) * '-', s[0:-rp], s[-rp:])
            else:
                s = "'%s0.%s%s'" % ((rv < 0) * '-', (rp - n) * '0', s)
        if sp == rp:
            return "Decimal(%s)" % s
        else:
            return "Decimal(%s, %s)" % (s, sp)

    def __str__(self) -> str:
        """str(self)"""  # noqa: D400
        sp = self._precision
        if sp == 0:
            return '%i' % self._value
        else:
            s, v = self._value < 0, abs(self._value)
            lit = f"{v}"
            if len(lit) > sp:
                return f"{s * '-'}{lit[:-sp]}.{lit[-sp:]}"
            else:
                # here an f-string would be slower because of nested width
                return '%s0.%0*i' % (s * '-', sp, v)

    def __bytes__(self) -> bytes:
        """bytes(self)"""  # noqa: D400
        sp = self._precision
        if sp == 0:
            return b'%i' % self._value
        else:
            s, v = self._value < 0, abs(self._value)
            lit = b'%i' % v
            if len(lit) > sp:
                return b'%s%s.%s' % (s * b'-', lit[:-sp], lit[-sp:])
            else:
                return b'%s0.%0*i' % (s * b'-', sp, v)

    def __format__(self, fmt_spec: str) -> str:
        """Return `self` converted to a string according to `fmt_spec`.

        Args:
            fmt_spec: a standard format specifier for a number

        Returns:
            str: `self` converted to a string according to `fmt_spec`

        """
        (fmt_fill, fmt_align, fmt_sign, fmt_min_width, fmt_thousands_sep,
         fmt_grouping, fmt_decimal_point, fmt_precision,
         fmt_type) = _get_format_params(fmt_spec)
        n_to_fill = fmt_min_width
        sv = self._value
        sp = self._precision
        if fmt_precision is None:
            fmt_precision = sp
        if fmt_type == '%':
            percent_sign = '%'
            n_to_fill -= 1
            xtra_shift = 2
        else:
            percent_sign = ''
            xtra_shift = 0
        v = _vp_adjust_to_prec(sv, sp, fmt_precision + xtra_shift)
        if v < 0:
            sign = '-'
            n_to_fill -= 1
            v = abs(v)
        elif fmt_sign == '-':
            sign = ''
        else:
            sign = fmt_sign
            n_to_fill -= 1
        raw_digits = format(v, '>0%i' % (fmt_precision + 1))
        if fmt_precision:
            decimal_point: str = fmt_decimal_point
            raw_digits, frac_part = (raw_digits[:-fmt_precision],
                                     raw_digits[-fmt_precision:])
            n_to_fill -= fmt_precision + 1
        else:
            decimal_point = ''
            frac_part = ''
        if fmt_align == '=':
            int_part = _pad_digits(raw_digits, max(0, n_to_fill), fmt_fill,
                                   fmt_thousands_sep, fmt_grouping)
            return sign + int_part + decimal_point + frac_part + percent_sign
        else:
            int_part = _pad_digits(raw_digits, 0, fmt_fill,
                                   fmt_thousands_sep, fmt_grouping)
            raw = sign + int_part + decimal_point + frac_part + percent_sign
            if n_to_fill > len(int_part):
                fmt = "%s%s%i" % (fmt_fill, fmt_align, fmt_min_width)
                return format(raw, fmt)
            else:
                return raw

    __getstate__ = __bytes__

    def __setstate__(self, state: bytes) -> None:
        """Set state of `self` from `state`."""
        lit = state.decode('ascii')
        int_lit, _, frac_lit = lit.partition('.')
        self._precision = len(frac_lit)
        self._value = int(int_lit + frac_lit)

    def _compare(self, other: Any, cmp: Callable[[Any, Any], bool]) -> bool:
        """Compare `self` and `other` using operator `cmp`."""
        sv = self._value
        sp = self._precision

        if isinstance(other, Decimal):
            ov = other._value
            op = other._precision
            # if sp == op, we are done, otherwise we adjust the value with the
            # lesser precision
            if sp < op:
                sv *= 10 ** (op - sp)
            elif sp > op:
                ov *= 10 ** (sp - op)
            return cmp(sv, ov)

        elif isinstance(other, int):
            ov = int(other) * 10 ** sp
            return cmp(sv, ov)

        elif isinstance(other, Rational):
            # cross-wise product of numerator and denominator
            sv *= other.denominator
            ov = other.numerator * 10 ** sp
            return cmp(sv, ov)

        # float, Real, standard lib Decimal
        try:
            num, den = other.as_integer_ratio()
        except AttributeError:
            # fall through
            pass
        except (ValueError, OverflowError):
            # 'nan' and 'inf'
            return cmp(0, other)
        else:
            # cross-wise product of numerator and denominator
            sv *= den
            ov = num * 10 ** sp
            return cmp(sv, ov)

        if isinstance(other, Complex):
            if cmp in (operator.eq, operator.ne):
                if other.imag == 0:
                    return self._compare(other.real, cmp)
                else:
                    return False if cmp is operator.eq else True

        # don't know how to compare
        return NotImplemented

    def __eq__(self, other: Any) -> bool:
        """self == other"""  # noqa: D400, D403
        return self._compare(other, operator.eq)

    def __lt__(self, other: Any) -> bool:
        """self < other"""  # noqa: D400, D403
        return self._compare(other, operator.lt)

    def __le__(self, other: Any) -> bool:
        """self <= other"""  # noqa: D400, D403
        return self._compare(other, operator.le)

    def __gt__(self, other: Any) -> bool:
        """self > other"""  # noqa: D400, D403
        return self._compare(other, operator.gt)

    def __ge__(self, other: Any) -> bool:
        """self >= other"""  # noqa: D400, D403
        return self._compare(other, operator.ge)

    def __hash__(self) -> int:
        """hash(self)"""  # noqa: D400
        try:
            return self._hash
        except AttributeError:
            sv, sp = self._value, self._precision
            if sp == 0:  # if self == int(self),
                return hash(sv)  # same hash as int
            else:  # otherwise same hash as equivalent fraction
                return hash(Fraction(sv, 10 ** sp))

    # return 0 or 1 for truth-value testing
    def __bool__(self) -> bool:
        """bool(self)"""  # noqa: D400
        return self._value != 0

    # return integer portion as int
    def __int__(self) -> int:
        """math.trunc(self)"""  # noqa: D400
        return _vp_to_int(self._value, self._precision)

    __trunc__ = __int__

    # convert to float (may loose precision!)
    def __float__(self) -> float:
        """float(self)"""  # noqa: D400
        return self._value / 10 ** self._precision  # type: ignore

    def __pos__(self) -> Decimal:
        """+self"""  # noqa: D400
        return self

    def __neg__(self) -> Decimal:
        """-self"""  # noqa: D400
        result: Decimal = object.__new__(Decimal)
        result._value = -self._value
        result._precision = self._precision
        return result

    def __abs__(self) -> Decimal:
        """abs(self)"""  # noqa: D400
        result: Decimal = object.__new__(Decimal)
        result._value = abs(self._value)
        result._precision = self._precision
        return result

    @overload
    def __add__(self, other: int) -> Decimal:
        ...

    @overload
    def __add__(self, other: Decimal) -> Decimal:
        ...

    @overload
    def __add__(self, other: SupportsAsIntegerRatio) -> Rational:
        ...

    def __add__(self, other: SupportsAsIntegerRatio) -> Rational:
        """self + other"""  # noqa: D400, D403
        if isinstance(other, Decimal):
            p = self._precision - other._precision
            if p == 0:
                result = Decimal(self)
                result._value += other._value
            elif p > 0:
                result = Decimal(self)
                result._value += other._value * 10 ** p
            else:
                result = Decimal(other)
                result._value += self._value * 10 ** -p
            return result
        elif isinstance(other, int):
            p = self._precision
            result = Decimal(self)
            result._value += int(other) * 10 ** p
            return result
        elif isinstance(other, Rational):
            onum, oden = (other.numerator, other.denominator)
        elif isinstance(other, SupportsAsIntegerRatio):
            try:
                onum, oden = other.as_integer_ratio()
            except (ValueError, OverflowError):
                raise ValueError("Unsupported operand: %s" % repr(other))
        elif isinstance(other, _StdLibDecimal):
            return self + Decimal(other)
        else:
            return NotImplemented
        # handle Rational and Real
        sden = 10 ** self._precision
        num = self._value * oden + sden * onum
        den = oden * sden
        min_prec = self._precision
        # return num / den as Decimal or as Fraction
        return _div(num, den, min_prec)

    @overload
    def __radd__(self, other: int) -> Decimal:
        ...

    @overload
    def __radd__(self, other: SupportsAsIntegerRatio) -> Rational:
        ...

    def __radd__(self, other: SupportsAsIntegerRatio) -> Rational:
        """other + self"""  # noqa: D400, D403
        return self + other

    @overload
    def __sub__(self, other: int) -> Decimal:
        ...

    @overload
    def __sub__(self, other: Decimal) -> Decimal:
        ...

    @overload
    def __sub__(self, other: SupportsAsIntegerRatio) -> Rational:
        ...

    def __sub__(self, other: SupportsAsIntegerRatio) -> Rational:
        """self - other"""  # noqa: D400, D403
        if isinstance(other, Decimal):
            p = self._precision - other._precision
            if p == 0:
                result = Decimal(self)
                result._value -= other._value
            elif p > 0:
                result = Decimal(self)
                result._value -= other._value * 10 ** p
            else:
                result = Decimal(other)
                result._value = self._value * 10 ** -p - other._value
            return result
        elif isinstance(other, int):
            p = self._precision
            result = Decimal(self)
            result._value -= int(other) * 10 ** p
            return result
        elif isinstance(other, Rational):
            onum, oden = (other.numerator, other.denominator)
        elif isinstance(other, SupportsAsIntegerRatio):
            try:
                onum, oden = other.as_integer_ratio()
            except (ValueError, OverflowError):
                raise ValueError("Unsupported operand: %s" % repr(other))
        elif isinstance(other, _StdLibDecimal):
            return self - Decimal(other)
        else:
            return NotImplemented
        # handle Rational and Real
        sden = 10 ** self._precision
        num = self._value * oden - sden * onum
        den = oden * sden
        min_prec = self._precision
        # return num / den as Decimal or as Fraction
        return _div(num, den, min_prec)

    @overload
    def __rsub__(self, other: int) -> Decimal:
        ...

    @overload
    def __rsub__(self, other: SupportsAsIntegerRatio) -> Rational:
        ...

    def __rsub__(self, other: SupportsAsIntegerRatio) -> Rational:
        """other - self"""  # noqa: D400, D403
        return -self + other

    @overload
    def __mul__(self, other: int) -> Decimal:
        ...

    @overload
    def __mul__(self, other: Decimal) -> Decimal:
        ...

    @overload
    def __mul__(self, other: SupportsAsIntegerRatio) -> Rational:
        ...

    def __mul__(self, other: SupportsAsIntegerRatio) -> Rational:
        """self * other"""  # noqa: D400, D403
        if isinstance(other, Decimal):
            result = Decimal(self)
            result._value *= other._value
            result._precision += other._precision
            if result._precision > MAX_DEC_PRECISION:
                return Fraction(result._value, 10 ** result._precision)
            return result
        elif isinstance(other, int):
            result = Decimal(self)
            result._value *= other
            return result
        elif isinstance(other, Rational):
            onum, oden = (other.numerator, other.denominator)
        elif isinstance(other, SupportsAsIntegerRatio):
            try:
                onum, oden = other.as_integer_ratio()
            except (ValueError, OverflowError):
                raise ValueError("Unsupported operand: %s" % repr(other))
        elif isinstance(other, _StdLibDecimal):
            return self * Decimal(other)
        else:
            return NotImplemented
        # handle Rational and Real
        num = self._value * onum
        den = oden * 10 ** self._precision
        min_prec = self._precision
        # return num / den as Decimal or as Fraction
        return _div(num, den, min_prec)

    @overload
    def __rmul__(self, other: int) -> Decimal:
        ...

    @overload
    def __rmul__(self, other: SupportsAsIntegerRatio) -> Rational:
        ...

    def __rmul__(self, other: SupportsAsIntegerRatio) -> Rational:
        """other * self"""
        return self * other

    def __truediv__(self, other: SupportsAsIntegerRatio) -> Rational:
        """self / other"""  # noqa: D400, D403
        if isinstance(other, Decimal):
            if other._value == 0:
                raise ZeroDivisionError("division by zero")
            xp, yp = self._precision, other._precision
            if xp == yp:
                min_prec = xp
                num, den = self._value, other._value
            elif xp > yp:
                min_prec = xp - yp
                num = self._value
                den = other._value * 10 ** (xp - yp)
            else:
                min_prec = 0
                num = self._value * 10 ** (yp - xp)
                den = other._value
            # return num / den as Decimal or as Fraction
            return _div(num, den, min_prec)
        elif isinstance(other, Rational):  # includes int
            onum, oden = (other.numerator, other.denominator)
        elif isinstance(other, SupportsAsIntegerRatio):
            try:
                onum, oden = other.as_integer_ratio()
            except (ValueError, OverflowError):
                raise ValueError("Unsupported operand: %s" % repr(other))
        elif isinstance(other, _StdLibDecimal):
            return self / Decimal(other)
        else:
            return NotImplemented
        # handle Rational and Real
        if onum == 0:
            raise ZeroDivisionError("division by zero")
        num = self._value * oden
        den = onum * 10 ** self._precision
        min_prec = self._precision
        # return num / den as Decimal or as Fraction
        return _div(num, den, min_prec)

    def __rtruediv__(self, other: SupportsAsIntegerRatio) -> Rational:
        """other / self"""  # noqa: D400, D403
        if isinstance(other, Rational):
            onum, oden = (other.numerator, other.denominator)
        elif isinstance(other, SupportsAsIntegerRatio):
            try:
                onum, oden = other.as_integer_ratio()
            except (ValueError, OverflowError):
                raise ValueError("Unsupported operand: %s" % repr(other))
        elif isinstance(other, _StdLibDecimal):
            return Decimal(other) / self
        else:
            return NotImplemented
        # handle Rational and Real
        if self._value == 0:
            raise ZeroDivisionError("division by zero")
        num = onum * 10 ** self._precision
        den = self._value * oden
        min_prec = self._precision
        # return num / den as Decimal or as Fraction
        return _div(num, den, min_prec)

    def __divmod__(self, other: SupportsAsIntegerRatio) \
            -> Tuple[int, Rational]:
        """self // other, self % other"""  # noqa: D400, D403
        # noinspection DuplicatedCode
        if isinstance(other, Decimal):
            sp, op = self._precision, other._precision
            if sp >= op:
                r = Decimal(precision=sp)
                sv = self._value
                ov = other._value * 10 ** (sp - op)
            else:
                r = Decimal(precision=op)
                sv = self._value * 10 ** (op - sp)
                ov = other._value
            q = sv // ov
            r._value = sv - q * ov
            return q, r
        elif isinstance(other, int):
            sp = self._precision
            r = Decimal(precision=sp)
            sv = self._value
            ov = other * 10 ** sp
            q = sv // ov
            r._value = sv - q * ov
            return q, r
        elif isinstance(other, _StdLibDecimal):
            return self.__divmod__(Decimal(other))
        else:
            return self // other, self % other

    def __rdivmod__(self, other: SupportsAsIntegerRatio) \
            -> Tuple[int, Rational]:
        """other // self, other % self"""  # noqa: D400, D403
        # noinspection DuplicatedCode
        if isinstance(other, int):
            sp = self._precision
            r = Decimal(precision=sp)
            sv = self._value
            ov = other * 10 ** sp
            q = ov // sv
            r._value = ov - q * sv
            return q, r
        elif isinstance(other, _StdLibDecimal):
            return Decimal(other).__divmod__(self)
        else:
            return other // self, other % self

    def __floordiv__(self, other: SupportsAsIntegerRatio) -> int:
        """self // other"""  # noqa: D400, D403
        if isinstance(other, Decimal):
            sp, op = self._precision, other._precision
            if sp >= op:
                sv = self._value
                ov: int = other._value * 10 ** (sp - op)
            else:
                sv = self._value * 10 ** (op - sp)
                ov = other._value
            return sv // ov
        elif isinstance(other, int):
            sv = self._value
            ov = other * 10 ** self._precision
            return sv // ov
        elif isinstance(other, _StdLibDecimal):
            return self // Decimal(other)
        else:
            return floor(self / other)

    def __rfloordiv__(self, other: SupportsAsIntegerRatio) -> int:
        """other // self"""  # noqa: D400, D403
        if isinstance(other, int):
            sv = self._value
            ov: int = other * 10 ** self._precision
            return ov // sv
        elif isinstance(other, _StdLibDecimal):
            return Decimal(other) // self
        else:
            return floor(other / self)

    def __mod__(self, other: SupportsAsIntegerRatio) -> Rational:
        """self % other"""  # noqa: D400, D403
        # noinspection DuplicatedCode
        if isinstance(other, Decimal):
            sp, op = self._precision, other._precision
            if sp >= op:
                r = Decimal(precision=sp)
                sv = self._value
                ov = other._value * 10 ** (sp - op)
            else:
                r = Decimal(precision=op)
                sv = self._value * 10 ** (op - sp)
                ov = other._value
            r._value = sv - (sv // ov) * ov
            return r
        elif isinstance(other, int):
            sp = self._precision
            r = Decimal(precision=sp)
            sv = self._value
            ov = other * 10 ** sp
            r._value = sv - (sv // ov) * ov
            return r
        elif isinstance(other, _StdLibDecimal):
            return self % Decimal(other)
        else:
            return self - other * Decimal(self // other)  # type: ignore

    def __rmod__(self, other: SupportsAsIntegerRatio) -> Rational:
        """other % self"""  # noqa: D400, D403
        if isinstance(other, int):
            sp = self._precision
            r = Decimal(precision=sp)
            sv = self._value
            ov = other * 10 ** sp
            r._value = ov - (ov // sv) * sv
            return r
        elif isinstance(other, _StdLibDecimal):
            return Decimal(other) % self
        else:
            return other - self * Decimal(other // self)

    def __pow__(self, other: SupportsIntOrFloat, mod: Any = None) -> Complex:
        """self ** other

        If other is an integer (or a Rational with denominator = 1), the
        result will be a Decimal or a Fraction. Otherwise, the result will be
        a float or a complex.

        `mod` must always be None (otherwise a `TypeError` is raised).

        """
        if mod is not None:
            raise TypeError("3rd argument not allowed unless all arguments "
                            "are integers")
        # SupportsInt is not runtime_checkable in Python 3.7, so check directly
        if (hasattr(other.__class__, "__int__") or
                hasattr(other.__class__, "__trunc__")):
            try:
                exp = int(other)  # type: ignore
            except (ValueError, OverflowError):
                raise ValueError("Unsupported operand: %s" % repr(other)) \
                    from None
            else:
                if exp == other:
                    if exp >= 0:
                        result = Decimal()
                        result._value = self._value ** exp
                        result._precision = self._precision * exp
                        if result._precision > MAX_DEC_PRECISION:
                            raise ValueError("Precision limit exceeded.")
                        return result
                    else:
                        # 1 / x ** -y
                        exp = -exp
                        prec = self._precision
                        return _div(10 ** (prec * exp), self._value ** exp,
                                    prec)
        # SupportsFloat is not runtime_checkable in Python 3.7, so check
        # directly
        if hasattr(other.__class__, "__float__"):
            # fractional exponent => fallback to float
            return float(self) ** float(other)  # type: ignore
        return NotImplemented

    def __rpow__(self, other: Any, mod: Any = None) -> Complex:
        """other ** self

        `mod` must always be None (otherwise a `TypeError` is raised).
        """
        if mod is not None:  # pragma: no cover
            raise TypeError("3rd argument not allowed unless all arguments "
                            "are integers")
        if self.denominator == 1:
            return other ** self.numerator  # type: ignore
        return other ** float(self)  # type: ignore

    def __floor__(self) -> int:
        """math.floor(self)"""  # noqa: D400
        n: int
        d: int
        n, d = self._value, 10 ** self._precision
        return n // d

    def __ceil__(self) -> int:
        """math.ceil(self)"""  # noqa: D400
        n: int
        d: int
        n, d = self._value, 10 ** self._precision
        return -(-n // d)

    @overload
    def __round__(self, ndigits: None = ...) -> int:
        ...

    @overload
    def __round__(self, ndigits: int) -> Decimal:
        ...

    def __round__(self, ndigits: Optional[int] = None) \
            -> Union[int, Decimal]:
        """round(self [, n_digits])

        Round `self` to a given precision in decimal digits (default 0).
        `n_digits` may be negative.

        This method is called by the built-in `round` function. It returns an
        `int` when called with one argument, otherwise a :class:`Decimal`.
        """
        if ndigits is None:
            # return integer
            return int(self.adjusted(0))
        # otherwise return Decimal
        return self.adjusted(ndigits)


# helper functions for formatting:

_dflt_format_params = {
    'fill': ' ',
    'align': '>',
    'sign': '-',
    # 'zeropad': '',
    'minimumwidth': 0,
    'thousands_sep': '',
    'grouping': [3, 0],
    'decimal_point': '.',
    'precision': None,
    'type': 'f'
    }


def _get_format_params(format_spec: str) -> Tuple[Any, ...]:
    m = _parse_format_spec(format_spec)
    if m is None:
        raise ValueError("Invalid format specifier: " + format_spec)
    fill = m.group('fill')
    zeropad = m.group('zeropad')
    if fill:  # fill overrules zeropad
        fmt_fill = fill
        fmt_align = m.group('align')
    elif zeropad:  # zeropad overrules align
        fmt_fill = '0'
        fmt_align = "="
    else:
        fmt_fill = _dflt_format_params['fill']  # type: ignore
        fmt_align = \
            m.group('align') or _dflt_format_params['align']  # type: ignore
    fmt_sign = m.group('sign') or _dflt_format_params['sign']
    minimumwidth = m.group('minimumwidth')
    if minimumwidth:
        fmt_min_width = int(minimumwidth)
    else:
        fmt_min_width = _dflt_format_params['minimumwidth']  # type: ignore
    fmt_type = m.group('type') or _dflt_format_params['type']
    if fmt_type == 'n':
        lconv = locale.localeconv()
        fmt_thousands_sep = (m.group('thousands_sep') and
                             lconv['thousands_sep'])
        fmt_grouping = lconv['grouping']
        fmt_decimal_point = lconv['decimal_point']
    else:
        fmt_thousands_sep = (m.group('thousands_sep') or
                             _dflt_format_params[
                                 'thousands_sep'])  # type: ignore
        fmt_grouping = _dflt_format_params['grouping']  # type: ignore
        fmt_decimal_point = _dflt_format_params[
            'decimal_point']  # type: ignore
    precision = m.group('precision')
    if precision:
        fmt_precision: Optional[int] = int(precision)
    else:
        fmt_precision = None
    return (fmt_fill, fmt_align, fmt_sign, fmt_min_width, fmt_thousands_sep,
            fmt_grouping, fmt_decimal_point, fmt_precision, fmt_type)


def _pad_digits(digits: str, min_width: int, fill: str,
                sep: Optional[str] = None,
                grouping: Optional[Sequence[int]] = None) -> str:
    n_digits = len(digits)
    if sep and grouping:
        slices = []
        i = j = 0
        limit = max(min_width, n_digits) if fill == '0' else n_digits
        for k in _iter_grouping(grouping):
            j = min(i + k, limit)
            slices.append((i, j))
            if j >= limit:
                break
            i = j
            limit = max(limit - 1, n_digits, i + 1)
        if j < limit:
            slices.append((j, limit))
        digits = (limit - n_digits) * fill + digits
        raw = sep.join([digits[limit - j: limit - i]
                        for i, j in reversed(slices)])
        return (min_width - len(raw)) * fill + raw
    else:
        return (min_width - n_digits) * fill + digits


def _iter_grouping(grouping: Sequence[int]) -> Generator[int, None, None]:
    # From Python docs: 'grouping' is a sequence of numbers specifying which
    # relative positions the 'thousands_sep' is expected. If the sequence is
    # terminated with CHAR_MAX, no further grouping is performed. If the
    # sequence terminates with a 0, the last group size is repeatedly used.
    k = None
    for i in grouping[:-1]:
        yield i
        k = i
    i = grouping[-1]
    if i == 0:
        while k:
            yield k
    elif i != locale.CHAR_MAX:  # pragma: no cover
        yield i


# helper functions for decimal arithmetic


def _vp_adjust_to_prec(v: int, p: int, to_prec: int,
                       rounding: Optional[ROUNDING] = None) -> int:
    # Return internal tuple (v, p) adjusted to precision `to_prec` using given
    # rounding mode (or default mode if none is given).
    # Assumes p != to_prec.
    dp = to_prec - p
    if dp >= 0:
        # increase precision -> increase internal value
        sh: int = 10 ** dp
        return v * sh
    # decrease precision -> decrease internal value -> rounding
    elif to_prec >= 0:
        # resulting precision >= 0 -> just return adjusted internal value
        return _floordiv_rounded(v, 10 ** -dp, rounding)
    else:
        # result to be rounded to a power of 10 -> two steps needed:
        # 1) round internal value to requested precision
        # 2) adjust internal value to precison 0 (because internal precision
        # must be >= 0)
        sh = 10 ** -to_prec
        return _floordiv_rounded(v, 10 ** -dp, rounding) * sh


def _vp_normalize(v: int, p: int) -> Tuple[int, int]:
    # Reduce v, p to the smallest precision >= 0 without loosing value.
    # I. e. return rv, rp so that rv // 10 ** rp == v // 10 ** p and
    # (rv % 10 != 0 or rp == 0)
    if v == 0:
        return 0, 0
    while p > 0 and v % 10 == 0:
        p -= 1
        v = v // 10
    return v, p


def _floordiv_rounded(x: int, y: int,
                      rounding: Optional[ROUNDING] = None) -> int:
    # Return x // y, rounded using given rounding mode (or default mode
    # if none is given)
    quot, rem = divmod(x, y)
    if rem == 0:  # no need for rounding
        return quot
    else:
        if rounding is None:
            rounding = get_dflt_rounding_mode()
        if rounding == ROUNDING.ROUND_HALF_UP:
            # Round 5 up (away from 0)
            # |remainder| > |divisor|/2 or
            # |remainder| = |divisor|/2 and quotient >= 0
            # => add 1
            ar, ay = abs(2 * rem), abs(y)
            if ar > ay or (ar == ay and quot >= 0):
                return quot + 1
            else:
                return quot
        elif rounding == ROUNDING.ROUND_HALF_EVEN:
            # Round 5 to even, rest to nearest
            # |remainder| > |divisor|/2 or
            # |remainder| = |divisor|/2 and quotient not even
            # => add 1
            ar, ay = abs(2 * rem), abs(y)
            if ar > ay or (ar == ay and quot % 2 != 0):
                return quot + 1
            else:
                return quot
        elif rounding == ROUNDING.ROUND_HALF_DOWN:
            # Round 5 down
            # |remainder| > |divisor|/2 or
            # |remainder| = |divisor|/2 and quotient < 0
            # => add 1
            ar, ay = abs(2 * rem), abs(y)
            if ar > ay or (ar == ay and quot < 0):
                return quot + 1
            else:
                return quot
        elif rounding == ROUNDING.ROUND_DOWN:
            # Round towards 0 (aka truncate)
            # quotient negativ
            # => add 1
            if quot < 0:
                return quot + 1
            else:
                return quot
        elif rounding == ROUNDING.ROUND_UP:
            # Round away from 0
            # quotient not negativ
            # => add 1
            if quot >= 0:
                return quot + 1
            else:
                return quot
        elif rounding == ROUNDING.ROUND_CEILING:
            # Round up (not away from 0 if negative)
            # => always add 1
            return quot + 1
        elif rounding == ROUNDING.ROUND_FLOOR:
            # Round down (not towards 0 if negative)
            # => never add 1
            return quot
        elif rounding == ROUNDING.ROUND_05UP:
            # Round down unless last digit is 0 or 5
            # quotient not negativ and
            # quotient divisible by 5 without remainder or
            # quotient negativ and
            # (quotient + 1) not divisible by 5 without remainder
            # => add 1
            if (quot >= 0 and quot % 5 == 0 or
                    quot < 0 and (quot + 1) % 5 != 0):
                return quot + 1
            else:
                return quot
    raise ValueError(f"Invalid rounding mode: {rounding!r}.")


def _vp_to_int(v: int, p: int) -> int:
    # Return integral part of shifted decimal.
    if p == 0:
        return v
    if v == 0:
        return v
    if p > 0:
        sh: int = 10 ** p
        if v > 0:
            return v // sh
        else:
            return -(-v // sh)
    else:  # shouldn't happen!
        sh = 10 ** -p
        return v * sh  # pragma: no cover


def _approx_rational(num: int, den: int, min_prec: int = 0) \
        -> Tuple[int, int, int]:
    # Approximate num / den as internal Decimal representation.
    # Returns v, p, r, so that
    # v * 10 ** -p + r == num / den
    # and p <= MAX_DEC_PRECISION and r -> 0.
    if num == 0:
        return 0, min_prec, 0
    accel = 1
    p = max(min_prec, ceil(log10(abs(den)) - log10(abs(num))))
    while True:
        v, r = divmod(num * 10 ** p, den)
        if p >= MAX_DEC_PRECISION or r == 0:
            break
        accel += 1
        p = min(p + accel ** 2, MAX_DEC_PRECISION)
    while v % 10 == 0 and p > 0:
        p -= 1
        v //= 10
    return v, p, r


def _div(num: int, den: int, min_prec: int) -> Union[Decimal, Fraction]:
    # Return num / den as Decimal,
    # if possible with precision <= max(minPrec, MAX_DEC_PRECISION),
    # otherwise as Fraction
    v, p, r = _approx_rational(num, den, min_prec)
    if r == 0:
        dec = object.__new__(Decimal)
        dec._value = v
        dec._precision = p
        return dec
    else:
        return Fraction(num, den)


# get / set default rounding mode

_dflt_rounding_mode = ROUNDING.ROUND_HALF_EVEN


def get_dflt_rounding_mode() -> ROUNDING:
    """Return default rounding mode."""
    global _dflt_rounding_mode
    return _dflt_rounding_mode


def set_dflt_rounding_mode(rounding: ROUNDING) -> None:
    """Set default rounding mode.

    Args:
        rounding (ROUNDING): rounding mode to be set as default

    Raises:
        TypeError: given 'rounding' is not a valid rounding mode
    """
    global _dflt_rounding_mode
    if not isinstance(rounding, ROUNDING):
        raise TypeError(f"Illegal rounding mode: {rounding!r}")
    _dflt_rounding_mode = rounding


# Some Decimal constants
ZERO = Decimal(0)
ONE = Decimal(1)

__all__ = [
    'Decimal',
    'ZERO',
    'ONE',
    'ROUNDING',
    'get_dflt_rounding_mode',
    'set_dflt_rounding_mode',
    ]
