# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis/
#
# Copyright the Hypothesis Authors.
# Individual contributors are listed in AUTHORS.rst and the git log.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.

import contextlib
import random
import sys
from itertools import count
from weakref import WeakValueDictionary

import hypothesis.core
from hypothesis.errors import InvalidArgument

# This is effectively a WeakSet, which allows us to associate the saved states
# with their respective Random instances even as new ones are registered and old
# ones go out of scope and get garbage collected.  Keys are ascending integers.
_RKEY = count()
RANDOMS_TO_MANAGE: WeakValueDictionary = WeakValueDictionary({next(_RKEY): random})


class NumpyRandomWrapper:
    def __init__(self):
        assert "numpy" in sys.modules
        # This class provides a shim that matches the numpy to stdlib random,
        # and lets us avoid importing Numpy until it's already in use.
        import numpy.random

        self.seed = numpy.random.seed
        self.getstate = numpy.random.get_state
        self.setstate = numpy.random.set_state


NP_RANDOM = None


def register_random(r: random.Random) -> None:
    """Register the given Random instance for management by Hypothesis.

    You can pass ``random.Random`` instances (or other objects with seed,
    getstate, and setstate methods) to ``register_random(r)`` to have their
    states seeded and restored in the same way as the global PRNGs from the
    ``random`` and ``numpy.random`` modules.

    All global PRNGs, from e.g. simulation or scheduling frameworks, should
    be registered to prevent flaky tests.  Hypothesis will ensure that the
    PRNG state is consistent for all test runs, or reproducibly varied if you
    choose to use the :func:`~hypothesis.strategies.random_module` strategy.
    """
    if not (hasattr(r, "seed") and hasattr(r, "getstate") and hasattr(r, "setstate")):
        raise InvalidArgument(f"r={r!r} does not have all the required methods")
    if r not in RANDOMS_TO_MANAGE.values():
        RANDOMS_TO_MANAGE[next(_RKEY)] = r


def get_seeder_and_restorer(seed=0):
    """Return a pair of functions which respectively seed all and restore
    the state of all registered PRNGs.

    This is used by the core engine via `deterministic_PRNG`, and by users
    via `register_random`.  We support registration of additional random.Random
    instances (or other objects with seed, getstate, and setstate methods)
    to force determinism on simulation or scheduling frameworks which avoid
    using the global random state.  See e.g. #1709.
    """
    assert isinstance(seed, int) and 0 <= seed < 2**32
    states: dict = {}

    if "numpy" in sys.modules:
        global NP_RANDOM
        if NP_RANDOM is None:
            # Protect this from garbage-collection by adding it to global scope
            NP_RANDOM = RANDOMS_TO_MANAGE[next(_RKEY)] = NumpyRandomWrapper()

    def seed_all():
        assert not states
        for k, r in RANDOMS_TO_MANAGE.items():
            states[k] = r.getstate()
            r.seed(seed)

    def restore_all():
        for k, state in states.items():
            r = RANDOMS_TO_MANAGE.get(k)
            if r is not None:  # i.e., hasn't been garbage-collected
                r.setstate(state)
        states.clear()

    return seed_all, restore_all


@contextlib.contextmanager
def deterministic_PRNG(seed=0):
    """Context manager that handles random.seed without polluting global state.

    See issue #1255 and PR #1295 for details and motivation - in short,
    leaving the global pseudo-random number generator (PRNG) seeded is a very
    bad idea in principle, and breaks all kinds of independence assumptions
    in practice.
    """
    if hypothesis.core._hypothesis_global_random is None:  # pragma: no cover
        hypothesis.core._hypothesis_global_random = random.Random()
        register_random(hypothesis.core._hypothesis_global_random)

    seed_all, restore_all = get_seeder_and_restorer(seed)
    seed_all()
    try:
        yield
    finally:
        restore_all()
