//This file is part of necsim project which is released under MIT license.
//See file **LICENSE.txt** or visit https://opensource.org/licenses/MIT) for full license details.

/**
 * @author Sam Thompson, based on a functionally equivalent example from David Blackman and Sebastiano Vigna
 * @file RngXorishi256.h
 * @brief Contains a generic random number generator based on the Xoroshi256+ method.
 *
 * Modified from code by David Blackman and Sebastiano Vigna (vigna@acm.org)
 *
 * To the extent possible under law, the author has dedicated all copyright and related and neighboring rights to this
 * software to the public domain worldwide. This software is distributed without any warranty.
 * See <http://creativecommons.org/publicdomain/zero/1.0/>.
 *
 * Samuel Thompson (thomsonsed@gmail.com).
 *
 * The definitions for the constants defined here should not be altered.
 * @copyright <a href="https://opensource.org/licenses/MIT"> MIT Licence.</a>
 */

#ifndef NECSIM_XOROSHIRO256PLUS_H
#define NECSIM_XOROSHIRO256PLUS_H

#include <cstdint>
#include <array>

/**
 * @brief Rotate the value a specified number of times.
 * @param value the value to rotate
 * @param bits the number of bits to shift
 * @return the rotated and shifted value
 */
static inline uint64_t rotl(const uint64_t &value, int bits)
{
    return (value << bits) | (value >> (64 - bits));
}

/**
 * @brief Converts a uniform integer on [0, UINT64_MAX) to a double
 * @param i the integer to convert
 * @return double equivalent of the integer
 */
static inline double intToDouble(const uint64_t &i)
{
    union un
    {
        uint64_t i;
        double d;
    };
    const un u = {.i = UINT64_C(0x3FF) << 52 | i >> 12};
    return u.d - 1.0;
}

/**
 * @brief A random number generator using the splitmix64 algorithm - this is provided for generating the shuffle table
 * within the main Xoroshiro256+ algorithm.
 */
class SplitMix64
{
private:
    uint64_t x; /* The state can be seeded with any value. */

public:

    /**
     * @brief Default constructor, taking the RNG seed.
     * @param seed the seed to use
     */
    explicit SplitMix64(uint64_t seed) : x(seed)
    { }

    /**
     * @brief Generates the next random integer
     * @return a random integer in the range of 0 to 2^64
     */
    uint64_t next()
    {
        uint64_t z = (x += UINT64_C(0x9E3779B97F4A7C15));
        z = (z ^ (z >> 30)) * UINT64_C(0xBF58476D1CE4E5B9);
        z = (z ^ (z >> 27)) * UINT64_C(0x94D049BB133111EB);
        return z ^ (z >> 31);
    }

    /**
     * @brief Shuffle the random number 8 times.
     */
    void shuffle()
    {
        for(unsigned int i = 0; i < 8; i++)
        {
            next();
        }
    }

    /**
     * @brief Gets a random double in the range [0, 1)
     * @return a random double
     */
    double d01()
    {
        return intToDouble(next());
    }
};

/**
 * @brief A random number generator using the xoroshiro
 *
 * @note From the authors of the algorthim:
 * This is xoshiro256+ 1.0, our best and fastest generator for floating-point numbers. We suggest to use its upper bits
 * for floating-point generation, as it is slightly faster than xoshiro256**. It passes all tests we are aware of except
 * for the lowest three bits, which might fail linearity tests (and just those), so if low linear complexity is not
 * considered an issue (as it is usually the case) it can be used to generate 64-bit outputs, too. We suggest to use a
 * sign test to extract a random Boolean value, and right shifts to extract subsets of bits. The state must be seeded so
 * that it is not everywhere zero. If you have a 64-bit seed, we suggest to seed a splitmix64 generator and use its
 * output to fill the shuffle table.
 */
class Xoroshiro256plus
{
protected:
    std::array<uint64_t, 4> shuffle_table;
public:

    Xoroshiro256plus() : shuffle_table()
    {

    }

    /**
     * @brief Explicit constructor which sets the rng seed.
     * @param seed the random seed
     */
    explicit Xoroshiro256plus(uint64_t seed) : shuffle_table()
    {
        setSeed(seed);
    }

    virtual void setSeed(uint64_t seed)
    {
        SplitMix64 seed_generator(seed);
        // Shuffle the seed generator 8 times
        seed_generator.shuffle();
        for(auto &item : shuffle_table)
        {
            item = seed_generator.next();
        }
    }

    /**
     * @brief Generates the next random integer.
     * @return a random integer from 0 to max of 2^64
     */
    uint64_t next()
    {
        const uint64_t result_plus = shuffle_table[0] + shuffle_table[3];

        const uint64_t t = shuffle_table[1] << 17;

        shuffle_table[2] ^= shuffle_table[0];
        shuffle_table[3] ^= shuffle_table[1];
        shuffle_table[1] ^= shuffle_table[2];
        shuffle_table[0] ^= shuffle_table[3];

        shuffle_table[2] ^= t;

        shuffle_table[3] = rotl(shuffle_table[3], 45);

        return result_plus;

    }

    /**
     * @brief Generates a random number in the range [0, 1)
     * @return a random double
     */
    double d01()
    {
        return intToDouble(next());
    }

    /**
     * @brief Jumps the generator forwards by the equivalent of 2^128 calls of next() - useful for parallel
     * computations where different random number sequences are required.
     */
    void jump()
    {
        static const uint64_t JUMP[] = {0x180ec6d33cfd0aba, 0xd5a61266f0c9392c, 0xa9582618e03fc9aa, 0x39abdc4529b1661c};

        uint64_t s0 = 0;
        uint64_t s1 = 0;
        uint64_t s2 = 0;
        uint64_t s3 = 0;
        for(unsigned long i : JUMP)
            for(int b = 0; b < 64; b++)
            {
                if(i & UINT64_C(1) << b)
                {
                    s0 ^= shuffle_table[0];
                    s1 ^= shuffle_table[1];
                    s2 ^= shuffle_table[2];
                    s3 ^= shuffle_table[3];
                }
                next();
            }

        shuffle_table[0] = s0;
        shuffle_table[1] = s1;
        shuffle_table[2] = s2;
        shuffle_table[3] = s3;
    }

    /**
     * @brief Writes the random number generator to an output stream.
     * @param os the output stream to write to
     * @param x the random number generator to save
     * @return
     */
    friend ostream &operator<<(ostream &os, const Xoroshiro256plus &x)
    {
        for(const auto& item: x.shuffle_table)
        {
            os << item << ",";
        }
        return os;
    }

    /**
     * @brief Inputs the NRrand object from the input stream.
     * Used for reading the NRrand object from a file.
     * @param is the input stream.
     * @param r the NRrand object to input to.
     * @return the input stream.
     */
    friend istream &operator>>(istream &is, Xoroshiro256plus &x)
    {
        char delim;
        for(auto &i : x.shuffle_table)
        {
            is >> i >> delim;
        }
        return is;
    }
};

#endif //NECSIM_XOROSHIRO256PLUS_H
