/*
   IGraph library.
   Copyright (C) 2022  The igraph development team <igraph@igraph.org>

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

#include <igraph.h>

#include "test_utilities.h"

/* Basic check: Are means within tol*sigma from the expected value?
 * This is meant to catch gross bugs while changing RNG/sampler code. */
void stats() {
    igraph_integer_t k;
    const igraph_integer_t n = 100000;
    igraph_real_t m, tm, tsd;
    igraph_real_t tol = 3;

    printf("\n");

    /* Binary trials with success of probability p. Counting successes. */
    {
        igraph_real_t p = 0.1;
        m = 0;
        for (k = 0; k < n; k++) {
            if (RNG_UNIF01() < p) {
                m += 1;
            }
        }
        tm  = n*p;
        tsd = sqrt(n*p*(1-p));
        printf("binary trials: %g; expected: %g; std. dev.: %g\n", m, tm, tsd);
        IGRAPH_ASSERT(tm - tol*tsd < m && m < tm + tol*tsd);
    }

    /* Mean of Poisson distributed values. */
    {
        tm = 2;
        m = 0;
        for (k = 0; k < n; k++) {
            m += RNG_POIS(tm);
        }
        m /= n;
        tsd = sqrt(tm) / sqrt(n);
        printf("pois: %g; expected: %g; std. dev.: %g\n", m, tm, tsd);
        IGRAPH_ASSERT(tm - tol*tsd < m && m < tm + tol*tsd);
    }

    /* Mean of geometrically distributed values. */
    {
        igraph_real_t p = 0.25;
        m = 0;
        for (k = 0; k < n; k++) {
            m += RNG_GEOM(p);
        }
        m /= n;
        tm  = (1-p) / p;
        tsd = sqrt(1 - p) / p / sqrt(n);
        printf("geom: %g; expected: %g; std. dev.: %g\n", m, tm, tsd);
        IGRAPH_ASSERT(tm - tol*tsd < m && m < tm + tol*tsd);
    }

    /* Mean of binomially distributed values. */
    {
        igraph_real_t p = 0.33;
        igraph_integer_t nn = 77;
        m = 0;
        for (k = 0; k < n; k++) {
            m += RNG_BINOM(nn, p);
        }
        m /= n;
        tm  = nn * p;
        tsd = sqrt(nn*p*(1-p)) / sqrt(n);
        printf("binom: %g; expected: %g; std. dev.: %g\n", m, tm, tsd);
        IGRAPH_ASSERT(tm - tol*tsd < m && m < tm + tol*tsd);
    }

    /* Mean of exponentially distributed values. */
    {
        tm = 3;
        m = 0;
        for (k = 0; k < n; k++) {
            m += RNG_EXP(1 / tm);
        }
        m /= n;
        tsd = tm / sqrt(n);
        printf("exp: %g; expected: %g; std. dev.: %g\n", m, tm, tsd);
        IGRAPH_ASSERT(tm - tol*tsd < m && m < tm + tol*tsd);
    }

    /* Mean of gamma distributed values. */
    {
        igraph_real_t shape = 1.5, scale = 3.5;
        m = 0;
        for (k = 0; k < n; k++) {
            m += RNG_GAMMA(shape, scale);
        }
        m /= n;
        tm  = shape*scale;
        tsd = sqrt(shape) * scale / sqrt(n);
        printf("gamma: %g; expected: %g; std. dev.: %g\n", m, tm, tsd);
        IGRAPH_ASSERT(tm - tol*tsd < m && m < tm + tol*tsd);
    }

    /* Mean of normally distributed values. */
    {
        tm = 3.0; tsd = 2.0;
        m = 0;
        for (k = 0; k < n; k++) {
            m += RNG_NORMAL(tm, tsd);
        }
        m /= n;
        tsd /= sqrt(n);
        printf("norm: %g; expected: %g; std. dev.: %g\n", m, tm, tsd);
        IGRAPH_ASSERT(tm - tol*tsd < m && m < tm + tol*tsd);
    }
}

/* These is merely a smoke test for various random samplers.
 * It does not verify the correctness of the result, except
 * for some special edge cases. */

void sample() {
    igraph_integer_t i;
    igraph_real_t x;

    i = RNG_INTEGER(-100, 100);
    printf("integer: %" IGRAPH_PRId "\n", i);
    IGRAPH_ASSERT(-100 <= i && i <= 100);

    i = RNG_INTEGER(2, 2);
    printf("integer: %" IGRAPH_PRId "\n", i);
    IGRAPH_ASSERT(i == 2);

    i = RNG_INTEGER(-5, -5);
    printf("integer: %" IGRAPH_PRId "\n", i);
    IGRAPH_ASSERT(i == -5);

    x = RNG_UNIF(-100, 100);
    printf("unif: %g\n", x);
    IGRAPH_ASSERT(-100 <= x && x < 100);

    x = RNG_UNIF(3, 3);
    printf("unif: %g\n", x);
    IGRAPH_ASSERT(x == 3);

    x = RNG_UNIF(-4.5, -4.5);
    printf("unif: %g\n", x);
    IGRAPH_ASSERT(x == -4.5);

    x = RNG_UNIF01();
    printf("unif01: %g\n", x);
    IGRAPH_ASSERT(0 <= x);
    IGRAPH_ASSERT(x < 1);

    x = RNG_BINOM(10, 0.3);
    printf("binom: %g\n", x);
    IGRAPH_ASSERT(0 <= x && x <= 10);

    x = RNG_BINOM(5, 0);
    IGRAPH_ASSERT(x == 0);

    x = RNG_BINOM(5, 1);
    IGRAPH_ASSERT(x == 5);

    x = RNG_BINOM((1LL << 31) - 1, 0.5);
    IGRAPH_ASSERT(!igraph_is_nan(x));
    IGRAPH_ASSERT(0 <= x && x <= (1LL << 31) - 1);

#if IGRAPH_INTEGER_SIZE > 32
    x = RNG_BINOM((1LL << 31), 0.5);
    IGRAPH_ASSERT(!igraph_is_nan(x));
    IGRAPH_ASSERT(0 <= x && x <= (1LL << 31) - 1);
#endif

    x = RNG_GEOM(0.2);
    printf("geom: %g\n", x);
    IGRAPH_ASSERT(0 <= x);

    x = RNG_GEOM(1);
    IGRAPH_ASSERT(x == 0);

    x = RNG_GEOM(0);
    IGRAPH_ASSERT(igraph_is_nan(x));

    x = RNG_NORMAL(0, 1);
    printf("normal: %g\n", x);
    IGRAPH_ASSERT(IGRAPH_FINITE(x));

    x = RNG_EXP(1);
    printf("exp: %g\n", x);
    IGRAPH_ASSERT(0 <= x);

    x = RNG_EXP(0);
    IGRAPH_ASSERT(igraph_is_nan(x));

    x = RNG_GAMMA(1, 1);
    printf("gamma: %g\n", x);
    IGRAPH_ASSERT(0 <= x);

    /* Note: Some systems would reject 0 as a parameter value instead of returning 0 */
    x = RNG_GAMMA(0, 1);
    IGRAPH_ASSERT(x == 0);

    x = RNG_GAMMA(1, 0);
    IGRAPH_ASSERT(x == 0);

    x = RNG_POIS(10);
    printf("poisson: %g\n", x);
    IGRAPH_ASSERT(0 <= x && IGRAPH_FINITE(x));

    x = RNG_POIS(0);
    IGRAPH_ASSERT(x == 0);

    x = RNG_POIS(-1);
    IGRAPH_ASSERT(igraph_is_nan(x));

    x = RNG_POIS((1LL << 31) - 1);
    IGRAPH_ASSERT(0 <= x && IGRAPH_FINITE(x));

    x = RNG_POIS((1LL << 31));
    IGRAPH_ASSERT(0 <= x && IGRAPH_FINITE(x));

#if IGRAPH_INTEGER_SIZE > 32
    x = RNG_POIS((1LL << 32));
    IGRAPH_ASSERT(0 <= x && IGRAPH_FINITE(x));
#endif

}

void test_and_destroy(igraph_rng_type_t *rng_type) {
    igraph_rng_t *def = igraph_rng_default();
    igraph_rng_t rng;

    igraph_error_handler_t *oldhandler = igraph_set_error_handler(&igraph_error_handler_printignore);
    igraph_error_t err = igraph_rng_init(&rng, rng_type);
    switch (err) {
    case IGRAPH_SUCCESS:
        break;
    case IGRAPH_UNIMPLEMENTED:
        return;
    default:
        IGRAPH_FATAL("Error while initializing RNG.");
    }
    igraph_set_error_handler(oldhandler);

    printf("\n%s\n\n", igraph_rng_name(&rng));

    igraph_rng_set_default(&rng);
    igraph_rng_seed(igraph_rng_default(), 137);

    sample();
    stats();

    igraph_rng_set_default(def);
    igraph_rng_destroy(&rng);
}

int main() {
    igraph_rng_type_t rng_types[] = {
        igraph_rngtype_glibc2,
        igraph_rngtype_mt19937,
        igraph_rngtype_pcg32,
        igraph_rngtype_pcg64
    };

    printf("Default\n\n");
    igraph_rng_seed(igraph_rng_default(), 709);

    sample();
    stats();

    for (size_t i = 0; i < sizeof(rng_types) / sizeof(rng_types[0]); i++) {
        test_and_destroy(&rng_types[i]);
    }

    return 0;
}
