//! A seedable, Owen-scrambled Sobol sequence.
//!
//! This implementation is limited to `2^16` samples, and will loop back to
//! the start of the sequence after that limit.

#![allow(clippy::unreadable_literal)]

mod wide;
use wide::Int4;

// This `include` provides `MAX_DIMENSION` and `REV_VECTORS`.
// See the build.rs file for how this included file is generated.
include!(concat!(env!("OUT_DIR"), "/vectors.inc"));

pub const MAX_DIMENSION_SET: u32 = MAX_DIMENSION / 4;

/// Compute four dimensions of a single sample in the Sobol sequence.
///
/// `sample_index` specifies which sample in the Sobol sequence to compute.
///
/// `dimension_set` specifies which four dimensions to compute. `0` yields the
/// first four dimensions, `1` the second four dimensions, and so on.
///
/// `seed` produces statistically independent Sobol sequences.  Passing two
/// different seeds will produce two different sequences that are only randomly
/// associated, with no stratification or correlation between them.
#[inline]
pub fn sample_4d(sample_index: u32, dimension_set: u32, seed: u32) -> [f32; 4] {
    assert!(dimension_set < MAX_DIMENSION_SET);
    let vecs = &REV_VECTORS[dimension_set as usize];

    // Shuffle the index using the given seed to produce a unique statistically
    // independent Sobol sequence.  This index shuffling approach is due to
    // Brent Burley.
    let shuffled_rev_index = lk_scramble(sample_index.reverse_bits(), seed);

    // Compute the Sobol sample with reversed bits.
    let mut sobol_rev = Int4::zero();
    let mut index = shuffled_rev_index & 0xffff0000; // Only use the top 16 bits.
    let mut i = 0;
    while index != 0 {
        let j = index.leading_zeros();
        sobol_rev ^= vecs[(i + j) as usize].into();
        i += j + 1;
        index <<= j;
        index <<= 1;
    }

    // Do Owen scrambling on the reversed-bits Sobol sample.
    let sobol_owen_rev = lk_scramble_int4(sobol_rev, dimension_set ^ seed);

    // Un-reverse the bits and convert to floating point in [0, 1).
    sobol_owen_rev.reverse_bits().to_norm_floats()
}

//----------------------------------------------------------------------

/// Scrambles `n` using the Laine Karras hash.  This is equivalent to Owen
/// scrambling, but on reversed bits.
#[inline]
fn lk_scramble(mut n: u32, scramble: u32) -> u32 {
    // This uses essentially the same technique as presented in the paper
    // "Stratified Sampling for Stochastic Transparency" by Laine and Karras,
    // but with a faster, higher quality hash function.
    //
    // The basic idea is that we're running a special kind of hash function
    // that only allows avalanche to happen upwards (i.e. a bit is only
    // affected by the bits lower than it).  This is achieved by only doing
    // mixing via operations that also adhere to that property.
    //
    // Normally this would be considered a poor hash function, because normally
    // you want all bits to have an equal chance of affecting all other bits.
    // But in this case that only-upward behavior is exactly what we want,
    // because it ends up being equivalent to Owen scrambling on
    // reverse-ordered bits.

    n = n.wrapping_add(hash(scramble, 2));

    n ^= 0xdc967795;
    n = n.wrapping_mul(0x97b754b7);
    n ^= 0x866350b1;
    n = n.wrapping_mul(0x9e3779cd);

    n
}

/// Same as `lk_scramble()`, except does it on 4 integers at a time.
#[inline(always)]
fn lk_scramble_int4(mut n: Int4, scramble: u32) -> Int4 {
    n += hash_int4([scramble; 4].into(), 2);

    n ^= [0xdc967795; 4].into();
    n *= [0x97b754b7; 4].into();
    n ^= [0x866350b1; 4].into();
    n *= [0x9e3779cd; 4].into();

    n
}

/// A simple 32-bit hash function.  Its quality can be tuned with
/// the number of rounds used.
#[inline(always)]
fn hash(n: u32, rounds: u32) -> u32 {
    let mut hash = n ^ 0x79c68e4a;
    for _ in 0..rounds {
        hash = hash.wrapping_mul(0x736caf6f);
        hash ^= hash.wrapping_shr(16);
    }
    hash
}

/// Same as `hash()` except performs hashing on four numbers at once.
///
/// Each of the four numbers gets a different hash, so even if all input
/// numbers are the same, the outputs will still be different for each of them.
#[inline(always)]
fn hash_int4(n: Int4, rounds: u32) -> Int4 {
    let mut hash = n;
    hash ^= [0x912f69ba, 0x174f18ab, 0x691e72ca, 0xb40cc1b8].into();
    for _ in 0..rounds {
        hash *= [0x736caf6f; 4].into();
        hash ^= hash.shr16();
    }
    hash
}
