#define PERL_NO_GET_CONTEXT

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include "ppport.h"

#include "pcg.h"

MODULE = Math::Random::PCG32		PACKAGE = Math::Random::PCG32

PROTOTYPES: ENABLE

pcg32_random_t *
new(CLASS, initstate, initseq)
    char *CLASS
    UV initstate
    UV initseq
    CODE:
        Newxz(RETVAL, 1, pcg32_random_t);
        if (RETVAL == NULL) croak("Could not allocate state memory");
        pcg32_srandom_r(RETVAL, initstate, initseq);
    OUTPUT:
        RETVAL

UV
coinflip(pcg32_random_t *rng)
    CODE:
        RETVAL = pcg32_random_r(rng) & 1;
    OUTPUT:
        RETVAL

UV
decay(pcg32_random_t *rng, uint32_t odds, uint32_t min, uint32_t max)
    PREINIT:
        uint32_t count;
    CODE:
        count = min;
        while (count < max) {
            if (pcg32_random_r(rng) < odds) break;
            count++;
        }
        RETVAL = count;
    OUTPUT:
        RETVAL

UV
irand(pcg32_random_t *rng)
    CODE:
        RETVAL = pcg32_random_r(rng);
    OUTPUT:
        RETVAL

UV
irand64(pcg32_random_t *rng)
    CODE:
        RETVAL = ((uint64_t) pcg32_random_r(rng) << 32) | pcg32_random_r(rng);
    OUTPUT:
        RETVAL

UV
irand_in(pcg32_random_t *rng, uint32_t min, uint32_t max)
    CODE:
        if (max == min)     RETVAL = min;
        else if (min > max) croak("max must be greater than min");
        else RETVAL = min + pcg32_random_r(rng) % (max - min + 1);
    OUTPUT:
        RETVAL

void
irand_way(pcg32_random_t *rng, int32_t x1, int32_t y1, int32_t x2, int32_t y2)
    PREINIT:
        int32_t dx, dy, magx;
    PPCODE:
        if (x1 == x2 && y1 == y2) XSRETURN_UNDEF;
        EXTEND(SP, 2);
        dx = x2 - x1;
        dy = y2 - y1;
        if (dx == 0)      goto MOVE_Y;
        else if (dy == 0) goto MOVE_X;
        magx = abs(dx);
        if (pcg32_random_r(rng) % (magx + abs(dy)) < magx) {
            MOVE_X:
            mPUSHs(newSViv(x1 + (dx > 0 ? 1 : -1)));
            mPUSHs(newSViv(y1));
        } else {
            MOVE_Y:
            mPUSHs(newSViv(x1));
            mPUSHs(newSViv(y1 + (dy > 0 ? 1 : -1)));
        }

double
rand(pcg32_random_t *rng, ...)
    PROTOTYPE: $;$
    PREINIT:
        double factor;
    CODE:
        if (items > 1) {
            if (!SvIOK(ST(1)) && !SvNOK(ST(1)))
              croak("factor must be a number");
            factor = SvNV(ST(1));
        } else factor = 1.0;
        /* "binary floating point constant for 2^-32" from PCG blog
         * post "Efficiently Generating a Number in a Range" (fast,
         * but biased) */
        RETVAL = 0x1.0p-32 * pcg32_random_r(rng) * factor;
    OUTPUT:
        RETVAL

SV *
rand_elm(pcg32_random_t *rng, avref)
    AV *avref;
    PREINIT:
        SSize_t len;
        SV **svp;
    CODE:
        len = av_len(avref) + 1;
        if (len == 0) XSRETURN_UNDEF;
        svp = av_fetch(avref, pcg32_random_r(rng) % len, FALSE);
        SvREFCNT_inc(*svp);
        RETVAL = *svp;
    OUTPUT:
        RETVAL

SV *
rand_from(pcg32_random_t *rng, avref)
    AV *avref;
    PREINIT:
        SSize_t i, len, rnd;
        SV *dunno, **src, **dst;
    CODE:
        len = av_len(avref) + 1;
        if (len == 0) XSRETURN_UNDEF;
        rnd = pcg32_random_r(rng) % len;
        dunno = av_delete(avref, rnd, 0);
        if (rnd != len - 1) {
            dst = &AvARRAY(avref)[rnd];
            src = dst + 1;
            for (i = rnd; i < len - 1; i++) *dst++ = *src++;
            AvFILLp(avref) -= 1;
            AvMAX(avref) -= 1;
        }
        SvREFCNT_inc(dunno);
        RETVAL = dunno;
    OUTPUT:
        RETVAL

UV
rand_idx(pcg32_random_t *rng, avref)
    AV *avref;
    PREINIT:
        SSize_t len;
    CODE:
        len = av_len(avref) + 1;
        if (len == 0) XSRETURN_UNDEF;
        else          RETVAL = pcg32_random_r(rng) % len;
    OUTPUT:
        RETVAL

UV
roll(pcg32_random_t *rng, uint32_t count, uint32_t sides)
    PREINIT:
        uint32_t sum;
    CODE:
        if (count == 0) croak("count must be positive");
        if (sides == 0) croak("sides must be positive");
        sum = count;
        while (count--) sum += pcg32_random_r(rng) % sides;
        RETVAL = sum;
    OUTPUT:
        RETVAL

void
sample(pcg32_random_t *rng, uint32_t count, avref)
    AV *avref;
    PREINIT:
        AV* smpl;
        SSize_t len, i;
        uint32_t total;
    PPCODE:
        smpl = newAV();
        len = av_len(avref) + 1;
        if (len == 0 || count == 0) goto DONE;
        if (count >= len) {
            av_extend(smpl, len - 1);
            for (i = 0; i < len; i++) {
                SV *sv = *av_fetch(avref, i, FALSE);
                av_push(smpl, sv);
            }
        } else {
            total = len;
            av_extend(smpl, count - 1);
            for (i = 0; i < len; i++) {
                if (pcg32_random_r(rng) < ( count * ( UINT32_MAX / total ) )) {
                    SV *sv = *av_fetch(avref, i, FALSE);
                    av_push(smpl, sv);
                    if (--count == 0) break;
                }
                total--;
            }
        }
    DONE:
        ST(0) = sv_2mortal( newRV_inc((SV *) smpl) );
        XSRETURN(1);

void
DESTROY(pcg32_random_t *rng)
    PPCODE:
        Safefree(rng);