#############################################################################
##
#W  random.gi                     GAP library                    Frank Lübeck
#W                                                             Max Neunhöffer
##
##
#Y  Copyright (C) 2006 The GAP Group
##
##  This file implements the basic operations for some types of random
##  sources.
##

###########################################################################
##  Generic methods for random sources.
##  
# The generic initializer of a random source creates a dummy object of the
# right type and then calls 'Init'.
InstallMethod(RandomSource, [IsOperation, IsObject], function(rep, seed)
  local res;
  res := Objectify(NewType(RandomSourcesFamily, rep), rec());
  return Init (res, seed);
end);
InstallMethod(RandomSource, [IsOperation], function(rep)
  return RandomSource(rep, 1);
end);

# A generic Reset if no seed is given (then seed=1 is chosen).
InstallMethod(Reset, [IsRandomSource], function(rs)
  return Reset(rs, 1);
end);

# Generic fallback methods, such that it is sufficient to install Random for 
# lists or for pairs of integers.
InstallMethod(Random, [IsRandomSource, IsInt, IsInt], function(rs, a, b)
  local  d, x, r, y;
  d := b - a;
  if d < 0  then
    return fail;
  elif a = b  then
    return a;
  else
    x := LogInt( d, 2 ) + 1;
    r := 0;
    while 0 < x  do
        y := Minimum( 10, x );
        x := x - y;
        r := r * 2 ^ y + Random( rs, [ 0 .. (2 ^ y - 1) ] );
    od;
    if d < r  then
        return Random( rs, a, b );
    else
        return a + r;
    fi;
  fi;
end);

InstallMethod(Random, [IsRandomSource, IsList], function(rs, list)
  return list[Random(rs, 1, Length(list))];
end);

# A print method.
InstallMethod(PrintObj, [IsRandomSource], function(rs)
  local cat;
  cat := Difference(CategoriesOfObject(rs), [ "IsRandomSource" ]);
  Print("<RandomSource in ", JoinStringsWithSeparator(cat, " and "), ">");
end);

############################################################################
##  We provide the "classical" GAP random generator via a random source.
##
if IsHPCGAP then
BindThreadLocalConstructor("GlobalRandomSource", {} ->
    Objectify(NewType(RandomSourcesFamily, IsGlobalRandomSource), rec()));
else
InstallValue(GlobalRandomSource,
    Objectify(NewType(RandomSourcesFamily, IsGlobalRandomSource), rec()));
fi;

InstallMethod(Init, [IsGlobalRandomSource, IsObject], function(rs, seed)
  if IsInt(seed) then
    RANDOM_SEED(seed);
  else
    R_N := seed[1];
    R_X := ShallowCopy(seed[2]);
  fi;
  return GlobalRandomSource;
end);
Init(GlobalRandomSource, 1);

InstallMethod(State, [IsGlobalRandomSource], function(rs)
  return [R_N, ShallowCopy(R_X)];
end);

InstallMethod(Reset, [IsGlobalRandomSource, IsObject], function(rs, seed)
  local old;
  old := State(GlobalRandomSource);
  Init(rs, seed);
  return old;
end);

InstallMethod(Random, [IsGlobalRandomSource, IsList], function(rs, l)
  if Length(l) < 2^28 then
    return RANDOM_LIST(l);
  else
    return l[Random(rs, 1, Length(l))];
  fi;
end);

############################################################################
##  The classical GAP random generator as independent random sources.
##  
InstallMethod(Init, [IsGAPRandomSource, IsObject], function(rs, seed)
  local old;
  if seed = 1 then
    rs!.R_N := 45;
    rs!.R_X  :=   [  66318732,  86395905,  22233618,   21989103,  237245480,
    264566285,  240037038, 264902875,  9274660,  180361945, 94688010,  24032135,
    106293216,  27264613,  126456102,  243761907, 80312412,  2522186,  59575208,
    70682510, 228947516,  173992210, 175178224, 250788150,  73030390, 210575942,
    128491926, 194508966,  201311350, 63569414, 185485910,  62786150, 213986102,
    88913350,  94904086, 252860454,  247700982, 233113990,  75685846, 196780518,
    74570934,  7958751,  130274620,  247708693, 183364378,  82600777,  28385464,
    184547675,  20423483, 75041763,  235736203,  54265107, 49075195,  100648387,
    114539755 ];
  elif IsInt(seed) then
    old := Reset(GlobalRandomSource, seed);
    rs!.R_N := R_N;
    rs!.R_X := ShallowCopy(R_X);
    Reset(GlobalRandomSource, old);
  else
    rs!.R_N := seed[1];
    rs!.R_X := ShallowCopy(seed[2]);
  fi;
  return rs;
end);

InstallMethod(State, [IsGAPRandomSource], function(rs)
  return [rs!.R_N, ShallowCopy(rs!.R_X)];
end);

InstallMethod(Reset, [IsGAPRandomSource, IsObject], function(rs, seed)
  local old;
  old := State(rs);
  Init(rs, seed);
  return old;
end);

InstallMethod(Random, [IsGAPRandomSource, IsList], function(rs, list)
  local rx, rn;
  if Length(list) < 2^28 then
    # we need to repeat the code of RANDOM_LIST
    rx := rs!.R_X;
    rn := rs!.R_N mod 55 + 1;
    rs!.R_N := rn;
    rx[rn] := (rx[rn] + rx[(rn+30) mod 55+1]) mod R_228;
    return list[ QUO_INT( rx[rn] * LEN_LIST(list), -R_228 ) + 1 ];
  else
    return list[Random(rs, 1, Length(list))];
  fi;
end);


##############################################################################
##  Random source using the Mersenne twister kernel functions.
##  
InstallMethod(Init, [IsMersenneTwister, IsObject], function(rs, seed)
  local st, endianseed, endiansys, perm, tmp, i;
  if IsPlistRep(seed) and IsString(seed[1]) and Length(seed[1]) = 2504 then
    st := ShallowCopy(seed[1]);
    # maybe adjust endianness if seed comes from different machine:
    endianseed := st{[2501..2504]};
    endiansys := GlobalMersenneTwister!.state{[2501..2504]};
    if endianseed <> endiansys then
      perm := List(endiansys, c-> Position(endianseed, c));
      tmp := "";
      for i in [0..625] do
        tmp{[4*i+1..4*i+4]} := st{4*i+perm};
      od;
      st := tmp;
    fi;
    rs!.state := st;
  else
    if not IsString(seed) then
      seed := ShallowCopy(String(seed));
      # padding such that length is positive and divisible by 4
      while Length(seed) = 0 or Length(seed) mod 4 <> 0 do
        Add(seed, CHAR_INT(0));
      od;
    fi;
    rs!.state := InitRandomMT(seed);
  fi;
  return rs;
end);

InstallMethod(State, [IsMersenneTwister], function(rs)
  return [ShallowCopy(rs!.state)];
end);

InstallMethod(Reset, [IsMersenneTwister, IsObject], function(rs, seed)
  local old;
  old := State(rs);
  Init(rs, seed);
  return old;
end);

InstallMethod(Random, [IsMersenneTwister, IsList], function(rs, list)
  return list[Random(rs, 1, Length(list))];
end);

InstallMethod(Random, [IsMersenneTwister, IsInt, IsInt], function(rs, a, b)
  local d, nrbits, res;
  d := b-a+1;
  if d < 0 then
    return fail;
  elif d = 0 then
    return a;
  fi;
  nrbits := Log2Int(d) + 1;
  repeat
    res := RandomIntegerMT(rs!.state, nrbits);
  until res < d;
  return res + a;
end);

# One global Mersenne twister random source, can be used to overwrite
# the library Random(list) and Random(a,b) methods.
if IsHPCGAP then
BindThreadLocalConstructor("GlobalMersenneTwister", {} ->
    RandomSource(IsMersenneTwister, String(GET_RANDOM_SEED_COUNTER())));
else
InstallValue(GlobalMersenneTwister, RandomSource(IsMersenneTwister, "1"));
fi;

# default random method for lists and pairs of integers using the Mersenne
# twister
InstallMethod( Random, "for an internal list",
    [ IsList and IsInternalRep ], 100, function(l) 
  return l[Random(GlobalMersenneTwister, 1, Length(l))]; 
end );

InstallMethod( Random,
    "for two integers",
    IsIdenticalObj,
    [ IsInt,
      IsInt ],
    0,
function(low, high)
  return Random(GlobalMersenneTwister, low, high);
end );


(function()
    local func;
    func := function(installType)
        return function(args...)
            local str, filterpos, filtercopy, i, func, info;

            # Check we understand arguments
            # Second value must be an info string
            if not IsString(args[2]) then
                ErrorNoReturn("Second argument must be an info string");
            fi;

            # Info strings always tend to begin 'for ', and here we want
            # to be able to edit it, so we check.
            if args[2]{[1..23]} <> "for a random source and" then
                ErrorNoReturn("Info string must begin 'for a random source and'");
            fi;

            # Filters must start with 'IsRandomSource'
            for i in [1..Length(args)] do
                if IsList(args[i]) and args[i][1] = IsRandomSource then
                    filterpos := i;
                fi;
            od;

            if not IsBound(filterpos) then
                ErrorNoReturn("Must use a list of filters beginning 'IsRandomSource'");
            fi;

            # Last argument must be the actual method
            if not IsFunction(args[Length(args)]) then
                ErrorNoReturn("Argument list must end with the method");
            fi;

            # Install
            CallFuncList(installType, args);

            # Install random, wrapping random source argument

            # Remove 'IsRandomSource' from the filter list
            Remove(args[filterpos], 1);

            # Correct info string by removing 'a random source and'
            info := "for";
            APPEND_LIST(info, args[2]{[24..Length(args[2])]});
            args[2] := info;

            func := Remove(args);
            if Length(args[filterpos]) = 1 then
                Add(args, x -> func(GlobalMersenneTwister,x));
            elif Length(args[filterpos]) = 2 then
                Add(args, {x,y} -> func(GlobalMersenneTwister,x,y));
            else
                Error("Only 2 or 3 argument methods supported");
            fi;

            CallFuncList(installType, args);
        end;
    end;
    InstallGlobalFunction("InstallMethodWithRandomSource", func(InstallMethod));
    InstallGlobalFunction("InstallOtherMethodWithRandomSource", func(InstallOtherMethod));
end)();

# This method must rank below Random(SomeRandomSource, IsList)
# for any random source SomeRandomSource, to avoid an infinite loop.
InstallMethodWithRandomSource( Random, "for a random source and a (finite) collection",
    [ IsRandomSource, IsCollection and IsFinite ], -8,
    {rs, C} -> RandomList(rs, Enumerator( C ) ) );
