/*
 * Adapted from: https://github.com/KilianB/pcg-java/blob/master/src/main/java/com/github/kilianB/pcg/fast/PcgRSFast.java
 *
 * MIT License
 *
 * Copyright (c) 2018
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package io.sentry.util;

import java.util.concurrent.atomic.AtomicLong;
import org.jetbrains.annotations.ApiStatus;

/**
 * A simplified replacement for {@link java.util.Random}, based on pcg-java that we use for
 * sampling, which is much faster than {@link java.security.SecureRandom}. This is necessary so that
 * some security tools do not flag our Random usage as potentially insecure.
 */
@ApiStatus.Internal
@SuppressWarnings({"unused", "UnnecessaryParentheses", "OperatorPrecedence"})
public final class Random implements java.io.Serializable {

  private static final long serialVersionUID = -4257915988930727506L;

  static final AtomicLong UNIQUE_SEED = new AtomicLong(System.nanoTime());

  /** Linear congruential constant. Same as MMIX by Donald Knuth and Newlib, Musl */
  private static final long MULT_64 = 6364136223846793005L;

  // static final variables are inlined by default
  private static final double DOUBLE_MASK = 1L << 53;
  private static final float FLOAT_UNIT = (float) (1 << 24);
  private static final long INTEGER_MASK = 0xFFFFFFFFL;

  // 64 version
  /** 64 bit internal state */
  private long state;

  /** Stream number of the rng. */
  private long inc;

  private boolean gausAvailable;
  private double nextGaus;

  // private static final int INTEGER_MASK_SIGNED = 0xFFFFFFFF;

  /**
   * Create a PcgRSFast instance seeded with with 2 longs generated by xorshift*. The values chosen
   * are very likely not used as seeds in any other non argument constructor of any of the classes
   * provided in this library.
   */
  public Random() {
    this(getRandomSeed(), getRandomSeed());
  }

  /**
   * Create a random number generator with the given seed and stream number. The seed defines the
   * current state in which the rng is in and corresponds to seeds usually found in other RNG
   * instances. RNGs with different seeds are able to catch up after they exhaust their period and
   * produce the same numbers. (2^63).
   *
   * <p>Different stream numbers alter the increment of the rng and ensure distinct state sequences
   *
   * <p>Only generators with the same seed AND stream numbers will produce identical values
   *
   * <p>
   *
   * @param seed used to compute the starting state of the RNG
   * @param streamNumber used to compute the increment for the lcg.
   */
  public Random(long seed, long streamNumber) {
    setSeed(seed, streamNumber);
  }

  private Random(long initialState, long increment, boolean dummy) {
    setState(initialState);
    setInc(increment);
  }

  /**
   * Sets the seed of this random number generator using . The general contract of setSeed is that
   * it alters the state of this random number generator object so as to be in exactly the same
   * state as if it had just been created with the argument seed as a seed.
   *
   * <p>Only generators with the same seed AND stream numbers will produce identical values
   *
   * <p>
   *
   * @param seed used to compute the starting state of the RNG
   * @param streamNumber used to compute the increment for the lcg.
   */
  public void setSeed(long seed, long streamNumber) {
    // TODO synchronize?
    state = 0;
    inc = (streamNumber << 1) | 1; // 2* + 1
    state = (state * MULT_64) + inc;
    state += seed;
    // Due to access to inlined vars the fast implementation is one step ahead of
    // the ordinary rngs. To get same results we can skip the state update

    // state = (state * MULT_64) + inc;
  }

  public byte nextByte() {
    state = (state * MULT_64) + inc;
    return (byte) ((((state >>> 22) ^ state) >>> ((state >>> 61) + 22)) >>> 24);
  }

  public void nextBytes(byte[] b) {
    for (int i = 0; i < b.length; i++) {
      state = (state * MULT_64) + inc;
      b[i] = (byte) ((((state >>> 22) ^ state) >>> ((state >>> 61) + 22)) >>> 24);
    }
  }

  public char nextChar() {
    state = (state * MULT_64) + inc;
    // Why should we cast it to an int first can't we mask it to a char directly?
    return (char) ((((state >>> 22) ^ state) >>> ((state >>> 61) + 22)) >>> 16);
  }

  public short nextShort() {
    state = (state * MULT_64) + inc;
    return (short) ((((state >>> 22) ^ state) >>> ((state >>> 61) + 22)) >>> 16);
  }

  /**
   * Returns the next pseudorandom, uniformly distributed {@code int} value from this random number
   * generator's sequence. The general contract of {@code nextInt} is that one {@code int} value is
   * pseudorandomly generated and returned. All 2<sup>32</sup> possible {@code int} values are
   * produced with (approximately) equal probability.
   *
   * @return the next pseudorandom, uniformly distributed {@code int} value from this random number
   *     generator's sequence
   */
  public int nextInt() {
    // we miss a single state and keep an old value around. but this does not alter
    // The produced number but shifts them 1 back.
    state = (state * MULT_64) + inc;
    // long oldState = state;
    return (int) (((state >>> 22) ^ state) >>> ((state >>> 61) + 22));
  }

  /**
   * Returns a pseudorandom, uniformly distributed {@code int} value between 0 (inclusive) and the
   * specified value (exclusive), drawn from this random number generator's sequence.
   *
   * @param n the upper bound (exclusive). Must be positive.
   * @return the next pseudorandom, uniformly distributed {@code int} value between zero (inclusive)
   *     and {@code bound} (exclusive) from this random number generator's sequence
   */
  public int nextInt(int n) {
    state = (state * MULT_64) + inc;
    int r = (int) (((state >>> 22) ^ state) >>> ((state >>> 61) + 22)) >>> 1; // Unsigned!
    int m = n - 1;
    if ((n & m) == 0) // i.e., bound is a power of 2
    r = (int) ((n * (long) r) >> 31);
    else {
      for (int u = r; u - (r = u % n) + m < 0; ) {
        state = (state * MULT_64) + inc;
        u = (int) (((state >>> 22) ^ state) >>> ((state >>> 61) + 22)) >>> 1;
      }
    }
    return r;
  }
  ;

  /**
   * Returns the next pseudorandom, uniformly distributed {@code boolean} value from this random
   * number generator's sequence. The general contract of {@code nextBoolean} is that one {@code
   * boolean} value is pseudorandomly generated and returned. The values {@code true} and {@code
   * false} are produced with (approximately) equal probability.
   *
   * @return the next pseudorandom, uniformly distributed {@code boolean} value from this random
   *     number generator's sequence
   */
  @SuppressWarnings("OperatorPrecedence")
  public boolean nextBoolean() {
    // Two choices either take the low bit or get a range 2 int and make an if
    state = (state * MULT_64) + inc;
    return (((((state >>> 22) ^ state) >>> (state >>> 61) + 22) & INTEGER_MASK) >>> 31) != 0;
  }

  @SuppressWarnings("UnnecessaryParentheses")
  public boolean nextBoolean(double probability) {
    if (probability < 0.0 || probability > 1.0)
      throw new IllegalArgumentException("probability must be between 0.0 and 1.0 inclusive.");

    // Borrowed from https://cs.gmu.edu/~sean/research/mersenne/MersenneTwister.java
    if (probability == 0.0) return false;
    if (probability == 1.0) return true;

    state = (state * MULT_64) + inc;
    long l = ((((state >>> 22) ^ state) >>> ((state >>> 61) + 22))) & INTEGER_MASK;

    state = (state * MULT_64) + inc;

    return (((l >>> 6) << 27)
                + (((((state >>> 22) ^ state) >>> ((state >>> 61) + 22)) & INTEGER_MASK) >>> 5))
            / DOUBLE_MASK
        < probability;
  }

  public long nextLong() {

    state = (state * MULT_64) + inc;
    // No need to mask if we shift by 32 bits
    long l = (((state >>> 22) ^ state) >>> ((state >>> 61) + 22));

    state = (state * MULT_64) + inc;
    long j = (((state >>> 22) ^ state) >>> ((state >>> 61) + 22));

    // Long keep consistent with the random definition of keeping the lower word
    // signed,
    // But should this really be the case? Why don't we mask the sign bit?
    return (l << 32) + (int) j;
  }

  public long nextLong(long n) {
    if (n == 0) throw new IllegalArgumentException("n has to be greater than 0");

    long bits;
    long val;
    do {
      state = (state * MULT_64) + inc;
      // No need to mask if we shift by 32 bits
      long l = (((state >>> 22) ^ state) >>> ((state >>> 61) + 22));

      state = (state * MULT_64) + inc;
      long j = (((state >>> 22) ^ state) >>> ((state >>> 61) + 22));

      bits = ((l << 32) + (int) j >>> 1);
      val = bits % n;
    } while (bits - val + (n - 1) < 0);
    return val;
  }

  public double nextDouble() {
    state = (state * MULT_64) + inc;
    long l = ((((state >>> 22) ^ state) >>> ((state >>> 61) + 22))) & INTEGER_MASK;
    state = (state * MULT_64) + inc;
    return (((l >>> 6) << 27)
            + (((((state >>> 22) ^ state) >>> ((state >>> 61) + 22)) & INTEGER_MASK) >>> 5))
        / DOUBLE_MASK;
  }

  public double nextDouble(boolean includeZero, boolean includeOne) {
    double d = 0.0;
    do {
      state = (state * MULT_64) + inc;
      long l = ((((state >>> 22) ^ state) >>> ((state >>> 61) + 22))) & INTEGER_MASK;
      state = (state * MULT_64) + inc;
      d =
          (((l >>> 6) << 27)
                  + (((((state >>> 22) ^ state) >>> ((state >>> 61) + 22)) & INTEGER_MASK) >>> 5))
              / DOUBLE_MASK;

      // grab a value, initially from half-open [0.0, 1.0)
      if (includeOne) {
        // Only generate the boolean if it really is the case or we scramble the state
        state = (state * MULT_64) + inc;
        if ((((((state >>> 22) ^ state) >>> (state >>> 61) + 22) & INTEGER_MASK) >>> 31) != 0) {
          d += 1.0;
        }
      }

    } while ((d > 1.0)
        || // everything above 1.0 is always invalid
        (!includeZero && d == 0.0)); // if we're not including zero, 0.0 is invalid
    return d;
  }

  public float nextFloat() {
    state = (state * MULT_64) + inc;
    return (((((state >>> 22) ^ state) >>> ((state >>> 61) + 22)) & INTEGER_MASK) >>> 8)
        / FLOAT_UNIT;
  }

  public float nextFloat(boolean includeZero, boolean includeOne) {
    float d = 0.0f;
    do {
      state = (state * MULT_64) + inc;
      d =
          (((((state >>> 22) ^ state) >>> ((state >>> 61) + 22)) & INTEGER_MASK) >>> 8)
              / FLOAT_UNIT; // grab a
      // value,
      // initially
      // from
      // half-open
      // [0.0f,
      // 1.0f)
      if (includeOne) {
        // Only generate the boolean if it really is the case or we scramble the state
        state = (state * MULT_64) + inc;
        if ((((((state >>> 22) ^ state) >>> (state >>> 61) + 22) & INTEGER_MASK) >>> 31) != 0) {
          d += 1.0f;
        }
      }
    } while ((d > 1.0f)
        || // everything above 1.0f is always invalid
        (!includeZero && d == 0.0f)); // if we're not including zero, 0.0f is invalid
    return d;
  }

  private void setInc(long increment) {
    if (increment == 0 || increment % 2 == 0) {
      throw new IllegalArgumentException("Increment may not be 0 or even. Value: " + increment);
    }
    this.inc = increment;
  }

  private void setState(long state) {
    this.state = state;
  }

  private static long getRandomSeed() {
    // xorshift64*
    for (; ; ) {
      long current = UNIQUE_SEED.get();
      long next = current;
      next ^= next >> 12;
      next ^= next << 25; // b
      next ^= next >> 27; // c
      next *= 0x2545F4914F6CDD1DL;
      if (UNIQUE_SEED.compareAndSet(current, next)) return next;
    }
  }
}
