import unittest

import numpy
import pytest

import cupy
from cupy import testing
from cupy.testing import _condition


@testing.parameterize(
    {'seed': None},
    {'seed': 0},
)
class TestPermutations(unittest.TestCase):

    def _xp_random(self, xp):
        if self.seed is None:
            return xp.random
        else:
            return xp.random.RandomState(seed=self.seed)

    # Test ranks

    # TODO(niboshi): Fix xfail
    @pytest.mark.xfail(reason='Explicit error types required')
    def test_permutation_zero_dim(self):
        for xp in (numpy, cupy):
            xp_random = self._xp_random(xp)
            a = testing.shaped_random((), xp)
            with pytest.raises(IndexError):
                xp_random.permutation(a)

    # Test same values

    @testing.for_all_dtypes(no_float16=True, no_bool=True, no_complex=True)
    def test_permutation_sort_1dim(self, dtype):
        cupy_random = self._xp_random(cupy)
        a = cupy.arange(10, dtype=dtype)
        b = cupy.copy(a)
        c = cupy_random.permutation(a)
        testing.assert_allclose(a, b)
        testing.assert_allclose(b, cupy.sort(c))

    @testing.for_all_dtypes(no_float16=True, no_bool=True, no_complex=True)
    def test_permutation_sort_ndim(self, dtype):
        cupy_random = self._xp_random(cupy)
        a = cupy.arange(15, dtype=dtype).reshape(5, 3)
        b = cupy.copy(a)
        c = cupy_random.permutation(a)
        testing.assert_allclose(a, b)
        testing.assert_allclose(b, cupy.sort(c, axis=0))

    # Test seed

    @testing.for_all_dtypes()
    def test_permutation_seed1(self, dtype):
        a = testing.shaped_random((10,), cupy, dtype)
        b = cupy.copy(a)

        cupy_random = self._xp_random(cupy)
        if self.seed is None:
            cupy_random.seed(0)
        pa = cupy_random.permutation(a)
        cupy_random = self._xp_random(cupy)
        if self.seed is None:
            cupy_random.seed(0)
        pb = cupy_random.permutation(b)

        testing.assert_allclose(pa, pb)


class TestShuffle(unittest.TestCase):

    # Test ranks

    def test_shuffle_zero_dim(self):
        for xp in (numpy, cupy):
            a = testing.shaped_random((), xp)
            with pytest.raises(TypeError):
                xp.random.shuffle(a)

    # Test same values

    @testing.for_all_dtypes(no_float16=True, no_bool=True, no_complex=True)
    def test_shuffle_sort_1dim(self, dtype):
        a = cupy.arange(10, dtype=dtype)
        b = cupy.copy(a)
        cupy.random.shuffle(a)
        testing.assert_allclose(cupy.sort(a), b)

    @testing.for_all_dtypes(no_float16=True, no_bool=True, no_complex=True)
    def test_shuffle_sort_ndim(self, dtype):
        a = cupy.arange(15, dtype=dtype).reshape(5, 3)
        b = cupy.copy(a)
        cupy.random.shuffle(a)
        testing.assert_allclose(cupy.sort(a, axis=0), b)

    # Test seed

    @testing.for_all_dtypes()
    def test_shuffle_seed1(self, dtype):
        a = testing.shaped_random((10,), cupy, dtype)
        b = cupy.copy(a)
        cupy.random.seed(0)
        cupy.random.shuffle(a)
        cupy.random.seed(0)
        cupy.random.shuffle(b)
        testing.assert_allclose(a, b)


@testing.parameterize(*(testing.product({
    'num': [0, 1, 100, 1000, 10000, 100000],
})))
class TestPermutationSoundness(unittest.TestCase):

    def setUp(self):
        a = cupy.random.permutation(self.num)
        self.a = a.get()

    # Test soundness

    @_condition.repeat(3)
    def test_permutation_soundness(self):
        assert (numpy.sort(self.a) == numpy.arange(self.num)).all()


@testing.parameterize(*(testing.product({
    'offset': [0, 17, 34, 51],
    'gap': [1, 2, 3, 5, 7],
    'mask': [1, 2, 4, 8, 16, 32, 64, 128],
})))
class TestPermutationRandomness(unittest.TestCase):

    num = 256

    def setUp(self):
        a = cupy.random.permutation(self.num)
        self.a = a.get()
        self.num_half = int(self.num / 2)

    # Simple bit proportion test

    # This test is to check kind of randomness of permutation.
    # An intuition behind this test is that, when you make a sub-array
    # by regularly extracting half elements from the permuted array,
    # the sub-array should also hold randomeness and accordingly
    # frequency of appearance of 0 and 1 at each bit position of
    # whole elements in the sub-array should become similar
    # when elements count of original array is 2^N.
    # Note that this is not an establishd method to check randomness.
    # TODO(anaruse): implement randomness check using some established methods.
    @_condition.repeat_with_success_at_least(5, 3)
    def test_permutation_randomness(self):
        if self.mask > self.num_half:
            return
        index = numpy.arange(self.num_half)
        index = (index * self.gap + self.offset) % self.num
        samples = self.a[index]
        ret = (samples & self.mask > 0)
        count = numpy.count_nonzero(ret)  # expectation: self.num_half / 2
        if count > self.num_half - count:
            count = self.num_half - count
        prob_le_count = self._calc_probability(count)
        if prob_le_count < 0.001:
            raise

    def _calc_probability(self, count):
        comb_all = self._comb(self.num, self.num_half)
        comb_le_count = 0
        for i in range(count + 1):
            tmp = self._comb(self.num_half, i)
            comb_i = tmp * tmp
            comb_le_count += comb_i
        prob = comb_le_count / comb_all
        return prob

    def _comb(self, N, k):
        val = numpy.float64(1)
        for i in range(k):
            val *= (N - i) / (k - i)
        return val
