import unittest

import numpy
import pytest

import cupy
from cupy import testing


@testing.parameterize(*testing.product({
    'shape': [(7,), (2, 3), (4, 3, 2)],
    'n_vals': [0, 1, 3, 15],
}))
class TestPlace(unittest.TestCase):

    # NumPy 1.9 don't wraps values.
    # https://github.com/numpy/numpy/pull/5821
    @testing.for_all_dtypes()
    @testing.numpy_cupy_array_equal()
    def test_place(self, xp, dtype):
        a = testing.shaped_arange(self.shape, xp, dtype)
        if self.n_vals == 0:
            mask = xp.zeros(self.shape, dtype=numpy.bool_)
        else:
            mask = testing.shaped_random(self.shape, xp, numpy.bool_)
        vals = testing.shaped_random((self.n_vals,), xp, dtype)
        xp.place(a, mask, vals)
        return a


@testing.parameterize(*testing.product({
    'shape': [(7,), (2, 3), (4, 3, 2)],
}))
class TestPlaceRaises(unittest.TestCase):

    # NumPy 1.9 performs illegal memory access.
    # https://github.com/numpy/numpy/pull/5821
    @testing.with_requires('numpy>=1.10')
    @testing.for_all_dtypes()
    def test_place_empty_value_error(self, dtype):
        for xp in (numpy, cupy):
            a = testing.shaped_arange(self.shape, xp, dtype)
            mask = testing.shaped_arange(self.shape, xp, int) % 2 == 0
            vals = testing.shaped_random((0,), xp, dtype)
            with pytest.raises(ValueError):
                xp.place(a, mask, vals)

    # Before NumPy 1.12 it was TypeError.
    # https://github.com/numpy/numpy/pull/7003
    @testing.for_all_dtypes()
    def test_place_shape_unmatch_error(self, dtype):
        for xp in (numpy, cupy):
            a = testing.shaped_arange(self.shape, xp, dtype)
            mask = testing.shaped_random((3, 4), xp, numpy.bool_)
            vals = testing.shaped_random((1,), xp, dtype)
            with pytest.raises(ValueError):
                xp.place(a, mask, vals)


@testing.parameterize(*testing.product({
    'shape': [(7,), (2, 3), (4, 3, 2)],
    'mode': ['raise', 'wrap', 'clip'],
    'n_vals': [0, 1, 3, 4, 5],
}))
class TestPut(unittest.TestCase):

    @testing.for_all_dtypes()
    @testing.numpy_cupy_array_equal()
    def test_put(self, xp, dtype):
        a = testing.shaped_arange(self.shape, xp, dtype)
        # Take care so that actual indices don't overlap.
        if self.mode == 'raise':
            inds = xp.array([2, -1, 3, 0])
        else:
            inds = xp.array([2, -8, 3, 7])
        vals = testing.shaped_random((self.n_vals,), xp, dtype)
        xp.put(a, inds, vals, self.mode)
        return a


@testing.parameterize(*testing.product({
    'shape': [(7,), (2, 3), (4, 3, 2)],
}))
class TestPutScalars(unittest.TestCase):

    @testing.numpy_cupy_array_equal()
    def test_put_index_scalar(self, xp):
        dtype = cupy.float32
        a = testing.shaped_arange(self.shape, xp, dtype)
        inds = 4
        vals = testing.shaped_random((4,), xp, dtype)
        xp.put(a, inds, vals)
        return a

    @testing.numpy_cupy_array_equal()
    def test_put_values_scalar(self, xp):
        dtype = cupy.float32
        a = testing.shaped_arange(self.shape, xp, dtype)
        # Take care so that actual indices don't overlap.
        inds = xp.array([2, 3, 5])
        vals = 3.0
        xp.put(a, inds, vals)
        return a


@testing.parameterize(*testing.product({
    'shape': [(7,), (2, 3)],
}))
class TestPutRaises(unittest.TestCase):

    @testing.for_all_dtypes()
    def test_put_inds_underflow_error(self, dtype):
        for xp in (numpy, cupy):
            a = testing.shaped_arange(self.shape, xp, dtype)
            inds = xp.array([2, -8, 3, 0])
            vals = testing.shaped_random((4,), xp, dtype)
            with pytest.raises(IndexError):
                xp.put(a, inds, vals, mode='raise')

    @testing.for_all_dtypes()
    def test_put_inds_overflow_error(self, dtype):
        for xp in (numpy, cupy):
            a = testing.shaped_arange(self.shape, xp, dtype)
            inds = xp.array([2, -1, 3, 7])
            vals = testing.shaped_random((4,), xp, dtype)
            with pytest.raises(IndexError):
                xp.put(a, inds, vals, mode='raise')

    @testing.with_requires('numpy>=1.19')
    @testing.for_all_dtypes()
    def test_put_mode_error(self, dtype):
        for xp in (numpy, cupy):
            a = testing.shaped_arange(self.shape, xp, dtype)
            inds = xp.array([2, -1, 3, 0])
            vals = testing.shaped_random((4,), xp, dtype)
            with pytest.raises(ValueError):
                xp.put(a, inds, vals, mode='unknown')


@testing.parameterize(
    *testing.product(
        {'shape': [(0,), (1,), (2, 3), (2, 3, 4)]}))
class TestPutmaskSameShape(unittest.TestCase):

    @testing.for_all_dtypes()
    @testing.numpy_cupy_array_equal()
    def test_putmask(self, xp, dtype):
        a = testing.shaped_random(self.shape, xp, dtype=dtype, seed=0)
        mask = testing.shaped_random(self.shape, xp, dtype=numpy.bool_, seed=1)
        values = testing.shaped_random(self.shape, xp, dtype=dtype, seed=2)
        ret = xp.putmask(a, mask, values)
        assert ret is None
        return a


@testing.parameterize(
    *testing.product(
        {'shape': [(0,), (1,), (2, 3), (2, 3, 4)],
         'values_shape': [(2,), (3, 1), (5,)]}))
class TestPutmaskDifferentShapes(unittest.TestCase):

    @testing.for_all_dtypes()
    @testing.numpy_cupy_array_equal()
    def test_putmask(self, xp, dtype):
        a = testing.shaped_random(self.shape, xp, dtype=dtype, seed=3)
        mask = testing.shaped_random(self.shape, xp, dtype=numpy.bool_, seed=4)
        values = testing.shaped_random(self.values_shape,
                                       xp, dtype=dtype, seed=5)
        ret = xp.putmask(a, mask, values)
        assert ret is None
        return a


class TestPutmask(unittest.TestCase):

    @testing.numpy_cupy_array_equal()
    def test_putmask_scalar_values(self, xp):
        shape = (2, 3)
        a = testing.shaped_arange(shape, xp)
        xp.putmask(a, a > 1, 30)
        return a

    def test_putmask_non_equal_shape_raises(self):
        for xp in (numpy, cupy):
            a = xp.array([1, 2, 3])
            mask = xp.array([True, False])
            with pytest.raises(ValueError):
                xp.putmask(a, mask, a**2)

    @testing.numpy_cupy_array_equal()
    def test_putmask_int_mask_scalar_values(self, xp):
        a = xp.array([1, 2, 3, 3])
        mask = xp.array([0, 1, 0, 2])
        xp.putmask(a, mask, 0)
        return a


class TestPutmaskDifferentDtypes(unittest.TestCase):

    @testing.for_all_dtypes_combination(names=['a_dtype', 'val_dtype'])
    def test_putmask_differnt_dtypes_raises(self, a_dtype, val_dtype):
        shape = (2, 3)
        for xp in (numpy, cupy):
            a = testing.shaped_random(shape, xp, dtype=a_dtype)
            mask = testing.shaped_random(shape, xp, dtype=numpy.bool_)
            values = testing.shaped_random((3,), xp, dtype=val_dtype)
            if not numpy.can_cast(val_dtype, a_dtype):
                with pytest.raises(TypeError):
                    xp.putmask(a, mask, values)

    @testing.for_all_dtypes()
    @testing.numpy_cupy_array_equal()
    def test_putmask_differnt_dtypes_mask(self, xp, dtype):
        shape = (2, 3)
        a = testing.shaped_random(shape, xp, dtype=numpy.int64)
        mask = testing.shaped_random(shape, xp, dtype=dtype)
        values = testing.shaped_random((3,), xp, dtype=numpy.int64)
        xp.putmask(a, mask, values)
        return a


@testing.parameterize(*testing.product({
    'shape': [(3, 3), (2, 2, 2), (3, 5), (5, 3)],
    'val': [1, 0, (2,), (2, 2)],
    'wrap': [True, False],
}))
class TestFillDiagonal(unittest.TestCase):

    def _compute_val(self, xp):
        if type(self.val) is int:
            return self.val
        else:
            return xp.arange(numpy.prod(self.val)).reshape(self.val)

    @testing.for_all_dtypes()
    @testing.numpy_cupy_array_equal()
    def test_fill_diagonal(self, xp, dtype):
        a = testing.shaped_arange(self.shape, xp, dtype)
        val = self._compute_val(xp)
        xp.fill_diagonal(a, val=val, wrap=self.wrap)
        return a

    @testing.for_all_dtypes()
    @testing.numpy_cupy_array_equal()
    def test_columnar_slice(self, xp, dtype):  # see cupy#2970
        if self.shape == (2, 2, 2):
            pytest.skip(
                'The length of each dimension must be the same after slicing')
        a = testing.shaped_arange(self.shape, xp, dtype)
        val = self._compute_val(xp)
        xp.fill_diagonal(a[:, 1:], val=val, wrap=self.wrap)
        return a

    @testing.for_all_dtypes()
    def test_1darray(self, dtype):
        for xp in (numpy, cupy):
            a = testing.shaped_arange((5,), xp, dtype)
            val = self._compute_val(xp)
            with pytest.raises(ValueError):
                xp.fill_diagonal(a, val=val, wrap=self.wrap)


@testing.parameterize(*testing.product({
    'n': [2, 4, -3, 0],
    'ndim': [2, 3, 1, 0, -2],
}))
class TestDiagIndices(unittest.TestCase):

    @testing.numpy_cupy_array_equal()
    def test_diag_indices(self, xp):
        return xp.diag_indices(self.n, self.ndim)


@testing.parameterize(*testing.product({
    'n': [-3, 0],
    'ndim': [1, 0, -2],
}))
class TestDiagIndicesInvalidValues(unittest.TestCase):

    @testing.numpy_cupy_array_equal()
    def test_diag_indices(self, xp):
        return xp.diag_indices(self.n, self.ndim)


@testing.parameterize(*testing.product({
    'shape': [(3, 3), (0, 0), (2, 2, 2)],
}))
class TestDiagIndicesFrom(unittest.TestCase):

    @testing.numpy_cupy_array_equal()
    def test_diag_indices_from(self, xp):
        arr = testing.shaped_arange(self.shape, xp)
        return xp.diag_indices_from(arr)


@testing.parameterize(*testing.product({
    'shape': [(3, 5), (3, 3, 4), (5,), (0,), (-1,)],
}))
class TestDiagIndicesFromRaises(unittest.TestCase):

    def test_non_equal_dims(self):
        for xp in (numpy, cupy):
            arr = testing.shaped_arange(self.shape, xp)
            with pytest.raises(ValueError):
                xp.diag_indices_from(arr)
