%---------------------------------------------------------------------------%
% vim: ft=mercury ts=4 sts=4 sw=4 et
%---------------------------------------------------------------------------%
% Copyright (C) 2019, 2022 The Mercury team.
% This file is distributed under the terms specified in COPYING.LIB.
%---------------------------------------------------------------------------%
%
% File: random.sfc16.m
% Main author: Mark Brown
%
% 16-bit Small Fast Counting generator, by Chris Doty-Humphrey.
%
% http://pracrand.sourceforge.net/
%
% From the above:
% "[A] good small chaotic RNG driven by a bad smaller linear RNG. The
% combination gives it the strengths of each - good chaotic behavior,
% but enough structure to avoid short cycles."
%
%---------------------------------------------------------------------------%
%---------------------------------------------------------------------------%

:- module random.sfc16.
:- interface.

%---------------------------------------------------------------------------%

    % A fast, 16-bit SFC generator.
    %
:- type random.

:- instance random(random).

    % Initialise a 16-bit SFC generator with the default seed. The
    % resulting generator produces the same sequence every time.
    %
:- func init = random.

    % Initialise a 16-bit SFC generator with the given seed.
    %
:- func seed(uint64) = random.

    % Generate a uniformly distributed pseudo-random unsigned integer
    % of 8, 16, 32 or 64 bits, respectively.
    %
:- pred generate_uint8(uint8::out, random::in, random::out) is det.
:- pred generate_uint16(uint16::out, random::in, random::out) is det.
:- pred generate_uint32(uint32::out, random::in, random::out) is det.
:- pred generate_uint64(uint64::out, random::in, random::out) is det.

%---------------------------------------------------------------------------%
%---------------------------------------------------------------------------%

:- implementation.

:- import_module int.
:- import_module uint.
:- import_module uint8.
:- import_module uint16.
:- import_module uint32.
:- import_module uint64.

%---------------------------------------------------------------------------%

:- type random
    --->    random(uint64).

:- instance random(random) where [
    pred(generate_uint8/3) is sfc16.generate_uint8,
    pred(generate_uint16/3) is sfc16.generate_uint16,
    pred(generate_uint32/3) is sfc16.generate_uint32,
    pred(generate_uint64/3) is sfc16.generate_uint64
].

init = seed(0x6048_5623_5e79_371e_u64).

seed(Seed) = R :-
    skip(10, random(Seed), R).

:- pred skip(int::in, random::in, random::out) is det.

skip(N, !R) :-
    ( if N > 0 then
        sfc16.generate_uint16(_, !R),
        skip(N - 1, !R)
    else
        true
    ).

%---------------------------------------------------------------------------%

generate_uint8(N, !R) :-
    sfc16.generate_uint16(N0, !R),
    N1 = uint16.to_int(N0 >> 8),
    N = uint8.cast_from_int(N1).

generate_uint16(N, random(S0), random(S)) :-
    unpack_uint64(S0, A0, B0, C0, Counter0),
    N = A0 + B0 + Counter0,
    A = B0 `xor` (B0 >> 5),
    B = C0 + (C0 << 3),
    C = ((C0 << 6) \/ (C0 >> 10)) + N,
    Counter = Counter0 + 1u16,
    S = pack_uint64(A, B, C, Counter).

generate_uint32(N, !R) :-
    sfc16.generate_uint16(A0, !R),
    sfc16.generate_uint16(B0, !R),
    A = uint16.cast_to_uint(A0),
    B = uint16.cast_to_uint(B0),
    N = uint32.cast_from_uint(A + (B << 16)).

generate_uint64(N, !R) :-
    sfc16.generate_uint16(A, !R),
    sfc16.generate_uint16(B, !R),
    sfc16.generate_uint16(C, !R),
    sfc16.generate_uint16(D, !R),
    N = pack_uint64(A, B, C, D).

%---------------------------------------------------------------------------%

:- func pack_uint64(uint16, uint16, uint16, uint16) = uint64.

pack_uint64(P1, P2, P3, P4) =
    uint16.cast_to_uint64(P1) +
    (uint16.cast_to_uint64(P2) << 16) +
    (uint16.cast_to_uint64(P3) << 32) +
    (uint16.cast_to_uint64(P4) << 48).

:- pred unpack_uint64(uint64::in, uint16::out, uint16::out, uint16::out,
    uint16::out) is det.

unpack_uint64(S, P1, P2, P3, P4) :-
    Mask = 0xffffu64,
    P1 = uint16.cast_from_uint64(S /\ Mask),
    P2 = uint16.cast_from_uint64((S >> 16) /\ Mask),
    P3 = uint16.cast_from_uint64((S >> 32) /\ Mask),
    P4 = uint16.cast_from_uint64(S >> 48).

%---------------------------------------------------------------------------%
:- end_module random.sfc16.
%---------------------------------------------------------------------------%
