########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Lib/Random.py,v 1.5 2005/10/17 00:52:36 mbrown Exp $
"""
Thread-safe random number generation

We consider a physical source of entropy to be better than a simulated
one, so we will try to use one (/dev/urandom on posix, for example).
If a pseudo-random number generator is to be used, it should be seeded
from the physical source, if possible, rather than the usual custom of
seeding it from the system clock. Random number sources that are not
known to be thread-safe should be polled serially.

Copyright 2005 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""

__all__ = ['dev_rand_fd', 'pseudo_rng',
           'GetRandomBytes', 'Random']

import os, sys, random, threading

pseudo_rng = random.Random()
lock = threading.Lock()
py22 = sys.version_info[0:2] < (2,3)

dev_rand_fd = None
if os.name == 'posix':
    try:
        dev_rand_fd = os.open('/dev/urandom', os.O_RDONLY)
    except IOError:
        pass


def GetRandomBytes(numBytes):
    """Returns numBytes random bytes from the best RNG available"""
    bytes = ''
    if dev_rand_fd is not None:
        lock.acquire()
        while len(bytes) < numBytes:
            bytes += os.read(dev_rand_fd, numBytes - len(bytes))
        lock.release()
    else:
        for i in xrange(numBytes):
            if py22:
                lock.acquire()
            byte = pseudo_rng.randrange(0, 256)
            if py22:
                lock.release()
            bytes += chr(byte)
    return bytes

# The pseudo-RNG will be seeded from the system clock, normally,
# but if we have dev_rand_fd, we can seed it more randomly. In this
# situation, we probably won't be using the pseudo-RNG, ourselves,
# so this is just for the benefit of whatever else might end up
# using it.
if dev_rand_fd is not None:
    # the idea here is to be between 0L and 27814431486575L
    # which is what the Wichmann-Hill pseudo-RNG needs (Py 2.2-)
    pseudo_rng.seed(reduce(lambda a,b: ((a+1)*(b+1))%27814431486576L -1,
                    map(ord,GetRandomBytes(6))))


def Random():
    """Returns a random float, n, where 0 <= n < 1"""
    if dev_rand_fd is not None:
        # is there a more efficient, mathematical way to do this?
        s = '0.' + reduce(lambda a,b: str(a) + str(b % 10),
                          map(lambda b: int(ord(b)/25.6), GetRandomBytes(18)))
        n = float(s)
    else:
        if py22:
            lock.acquire()
        n = pseudo_rng.random()
        if py22:
            lock.release()
    return n
