# Copyright (C) 2001 Christopher Craig
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#

"""

This module is mostly compatible with the Python's random module, but uses
Linux or BSD's /dev/urandom device to generate numbers, thus yielding more
random output than the default Python module.

Incompatibilities are as follows:

getstate, setstate, jumpahead :

Because the core entropy generator is a stateless random number generator and
not a pseudo-random number generator these are not supported.  They will
raise an exception if they are run.

randrange, randint :

This module was intended to be used for cryptography so the 32 bit range limit
on the default randrange was not sufficient.  These functions have been
modified to return longints and support the full range of the longint type if
Bos' real.py module is installed.  If real.py is not installed and the range
of the function exceeds the maximum float, these functions will fail without
notice.

randrange is probabilistic and does not guarantee a return (though it has a
greater than 1-(1/2**n) chance of returning within n attempts).  See the doc
string of randrange for more information.

a setmode function was added accepting the values "strong" or "secure".  See
doc string for details.

"""

__version__ = "$Revision: 1.2 $"

import sys
import random
try: from real import log                # use the real module if it exists
except ImportError: from math import log # else use math

   
RandomError="ccRandomError"

class Random(random.Random):
    def __init__(self, mode="strong"):
        self.file = None
        self.setmode(mode)

    def setmode(self, newmode):
        """
        sets the mode to either "strong" or "secure"

        "strong" (default) will use a fill requests even if it runs out of
                 entropy in the entropy pool.  This can theoretically be
                 compromised (though no attack is known)

        "secure" will return only bytes within the estimated entropy of the
                 entropy pool.  This means that even if the algorithm is
                 broken in theory the number generator cannot be compromised.
                 This has the major drawback that it will block indefinately
                 if there is insufficient entropy.
        """
        if self.file: self.file.close()
        if newmode=="secure":
            self.file=open("/dev/random")
            self.mode="secure"
        elif newmode=="strong":
            self.file=open("/dev/urandom")
            self.mode="strong"
        else:
            raise RandomError, "Incorrect arguments"

    def randrange(self, start, stop=None, step=1):
        """randrange(start, stop=None, step=1) -> long

        Choose a random item from range(start, stop[, step]).

        Follows randrange from the default random module fairly closely.
        Differs mainly in that it allows for long integers and guarantees no
        loss of entropy from entropy source. (The default gets its entropy
        from a call to random() which only allows for 52bits of entropy)

        If Jurjen N.E. Bos's "real" module is not installed, then large spans
        (where the size of the range is greater than the maximum floating
        point value on this machine) will fail without notice.

        ------------------------
        Notes on non-determinism
        ------------------------

        The point of this generator was to have something to build my large
        random keys for cryptosystems, which meant that I wanted probability
        of any integer in the specified range to be _exactly_ the same as any
        other integer.  This meant that I could not use a normal mapping
        function to get my random numbers because that would make some
        integers more probable than others.  A simple example of this would be
        to try to map the set range(8) into the set range(5), but it holds
        true for mapping 2**52 (the size of the significand on a double
        precision float) to any number that does not divide 2**52.  My
        solution to this was to drop the most signicant bits down to the
        minimum bit space that could contain the requested range and then
        discard all generated numbers that were greater than the requested
        range.

        This means that this function isn't guaranteed to stop, but it has a
        probability greater than 1-(1/2**n) of returning before having to
        generate n integers. (Ignoring negligable rounding errors in
        exceptionally large ranges) """

        if stop==None:
            stop = start
            start = 0

        if step > 0: size = (stop - start + step - 1) / step
        elif step < 0: size = (stop - start + step + 1) / step
        else: ValueError, "zero step for randrange()"

        if size <= 0: raise ValueError, "empty range for randrange()"

        numbits = long(log(size)/log(2))+1 # the +1 is for rounding errors
        numbytes, t = divmod(numbits, 8)
        bitshift = -t % 8
        if t: numbytes += 1
        while 1:
            rval = 0L
            data = self.randbytes(numbytes)
            for byte in data:
                rval = (rval << 8) + ord(byte)
            rval >>= bitshift
            if rval < size: break

        return start + step*rval

    def random(self):
        """random() -> float

        returns a random float on the interval [0,1)
        """

        # IEEE double precision floats have 6 bytes of precision,
        # so to be sure this uses 12
        data = self.randbytes(12)

        rval = 0.0
        for byte in data:
            rval = (rval / 256) + ord(byte)

        return rval / 256

    def choice(self, seq):
        """choice(seq) -> choice

        returns a random element from the sequence
        """
        return seq[self.randrange(len(seq))]


    def shuffle(self, x):
        """x, random=random.random -> shuffle list x in place; return None.

        Optional arg random is a 0-argument function returning a random
        float in [0.0, 1.0); by default, the standard random.random.

        Note that for even rather small len(x), the total number of
        permutations of x is larger than the period of most random number
        generators; this implies that "most" permutations of a long
        sequence can never be generated.
        """

        for i in xrange(len(x)-1, 0, -1):
            # pick an element in x[:i+1] with which to exchange x[i]
            j = randrange(i+1)
            x[i], x[j] = x[j], x[i]

    def randbytes(self, n):
        """rand(n) -> string

        returns n random bytes
        """
        return self.file.read(n)

    #------- Do nothing functions --------
    # these functions are included only to maintain compatibility with
    # random
    def seed(self, a):
        return
    def setstate(self, state):
        raise RandomError, "ccrandom does not support state setting"
    def getstate(self):
        raise RandomError, "ccrandom does not support state setting"
    def jumpahead(self, n):
        raise RandomError, "ccrandom does not support state setting"
    


__all__ = ["Random","setmode","seed","random","uniform","randint","choice",
           "randrange","shuffle","normalvariate","lognormvariate",
           "cunifvariate","expovariate","vonmisesvariate","gammavariate",
           "stdgamma","gauss","betavariate","paretovariate","weibullvariate",
           "getstate","setstate","jumpahead"]

_inst = Random()
seed = _inst.seed
random = _inst.random
uniform = _inst.uniform
randint = _inst.randint
choice = _inst.choice
randrange = _inst.randrange
shuffle = _inst.shuffle
normalvariate = _inst.normalvariate
lognormvariate = _inst.lognormvariate
cunifvariate = _inst.cunifvariate
expovariate = _inst.expovariate
vonmisesvariate = _inst.vonmisesvariate
gammavariate = _inst.gammavariate
stdgamma = _inst.stdgamma
gauss = _inst.gauss
betavariate = _inst.betavariate
paretovariate = _inst.paretovariate
weibullvariate = _inst.weibullvariate
getstate = _inst.getstate
setstate = _inst.setstate
jumpahead = _inst.jumpahead
setmode = _inst.setmode

