import decimal
import math
import numbers
import random as _rng

import numpy


class Generator:
    rng = None

    def __init__(self, seed):
        self.rng = _rng.Random(seed)

    def random(self, start, stop, step=0, max_precision=7):
        precision = 1
        if step:
            """ Generate a factor to use stepping with randrange"""
            d = decimal.Decimal(step)
            precision = 10 ** d.as_tuple().exponent
        else:
            """ Analyse the given parameter to determine the step size """
            step = 1
            start_d = decimal.Decimal(start)
            stop_d = decimal.Decimal(stop)
            precision = 10 ** max(start_d.as_tuple().exponent, stop_d.as_tuple().exponent)

        # Avoid issues with values like 0.1 and ensure that the precision does not become
        # 1e-55 with the resulting floating point error.
        # See: https://docs.python.org/3/tutorial/floatingpoint.html
        if precision < 10 ** -max_precision:
            precision = 10 ** -max_precision

        f = 1 / precision

        f_start = math.floor(start * f)
        f_end = math.floor(stop * f) + 1
        f_step = math.floor(step * f)

        v = self.rng.randrange(f_start, f_end, f_step) / f

        # Return an integer value if the value is already a float
        if round(v) == v:
            return int(v)

        return v

    def choice(self, l):
        return self.rng.choice(l)

    def sample(self, s, l):
        return self.rng.sample(s, l)

    def round(self, n, p):
        # Move the number the next valid number which will cause round to round in the right direction, even if the
        # number is not directly representable.
        # Ref: https://www.py4u.net/discuss/139408
        if isinstance(n, numbers.Number):
            n = numpy.nextafter(n, math.copysign(math.inf, n))
            return round(n, p)

        num = decimal.Decimal(n)
        prec = decimal.Decimal('0.1') ** p
        return num.quantize(prec, decimal.ROUND_HALF_UP)



