import numpy
import pytest

import cupy
from cupy import testing
from cupy import cuda
from cupy.exceptions import AxisError


class TestJoin:

    @testing.for_all_dtypes(name='dtype1')
    @testing.for_all_dtypes(name='dtype2')
    @testing.numpy_cupy_array_equal()
    def test_column_stack(self, xp, dtype1, dtype2):
        a = testing.shaped_arange((4, 3), xp, dtype1)
        b = testing.shaped_arange((4,), xp, dtype2)
        c = testing.shaped_arange((4, 2), xp, dtype1)
        return xp.column_stack((a, b, c))

    def test_column_stack_wrong_ndim1(self):
        a = cupy.zeros(())
        b = cupy.zeros((3,))
        with pytest.raises(ValueError):
            cupy.column_stack((a, b))

    def test_column_stack_wrong_ndim2(self):
        a = cupy.zeros((3, 2, 3))
        b = cupy.zeros((3, 2))
        with pytest.raises(ValueError):
            cupy.column_stack((a, b))

    def test_column_stack_wrong_shape(self):
        a = cupy.zeros((3, 2))
        b = cupy.zeros((4, 3))
        with pytest.raises(ValueError):
            cupy.column_stack((a, b))

    @testing.for_all_dtypes(name='dtype')
    @testing.numpy_cupy_array_equal()
    def test_concatenate1(self, xp, dtype):
        a = testing.shaped_arange((2, 3, 4), xp, dtype)
        b = testing.shaped_reverse_arange((2, 3, 2), xp, dtype)
        c = testing.shaped_arange((2, 3, 3), xp, dtype)
        return xp.concatenate((a, b, c), axis=2)

    @testing.for_all_dtypes(name='dtype')
    @testing.numpy_cupy_array_equal()
    def test_concatenate2(self, xp, dtype):
        a = testing.shaped_arange((2, 3, 4), xp, dtype)
        b = testing.shaped_reverse_arange((2, 3, 2), xp, dtype)
        c = testing.shaped_arange((2, 3, 3), xp, dtype)
        return xp.concatenate((a, b, c), axis=-1)

    @testing.for_all_dtypes(name='dtype')
    @testing.numpy_cupy_array_equal()
    def test_concatenate_axis_none(self, xp, dtype):
        a = testing.shaped_arange((2, 3), xp, dtype)
        b = testing.shaped_reverse_arange((3, 5, 2), xp, dtype)
        c = testing.shaped_arange((7, ), xp, dtype)
        return xp.concatenate((a, b, c), axis=None)

    @testing.for_all_dtypes(name='dtype')
    @testing.numpy_cupy_array_equal()
    def test_concatenate_large_2(self, xp, dtype):
        a = testing.shaped_arange((2, 3, 4), xp, dtype)
        b = testing.shaped_reverse_arange((2, 3, 2), xp, dtype)
        c = testing.shaped_arange((2, 3, 3), xp, dtype)
        d = testing.shaped_arange((2, 3, 5), xp, dtype)
        e = testing.shaped_arange((2, 3, 2), xp, dtype)
        return xp.concatenate((a, b, c, d, e) * 2, axis=-1)

    @testing.for_all_dtypes(name='dtype')
    @testing.numpy_cupy_array_equal()
    def test_concatenate_large_3(self, xp, dtype):
        a = testing.shaped_arange((2, 3, 1), xp, dtype)
        b = testing.shaped_reverse_arange((2, 3, 1), xp, dtype)
        return xp.concatenate((a, b) * 10, axis=-1)

    @testing.for_all_dtypes(name='dtype')
    @testing.numpy_cupy_array_equal()
    def test_concatenate_large_4(self, xp, dtype):
        a = testing.shaped_arange((2, 3, 4), xp, dtype)
        b = testing.shaped_reverse_arange((2, 3, 4), xp, dtype)
        return xp.concatenate((a, b) * 10, axis=-1)

    @testing.for_all_dtypes(name='dtype')
    @testing.numpy_cupy_array_equal()
    def test_concatenate_large_5(self, xp, dtype):
        a = testing.shaped_arange((2, 3, 4), xp, dtype)
        b = testing.shaped_reverse_arange((2, 3, 4), xp, 'i')
        return xp.concatenate((a, b) * 10, axis=-1)

    @testing.multi_gpu(2)
    def test_concatenate_large_different_devices(self):
        arrs = []
        for i in range(10):
            with cuda.Device(i % 2):
                arrs.append(cupy.empty((2, 3, 4)))
        if cuda.runtime.deviceCanAccessPeer(0, 1) == 1:
            with pytest.warns(cupy._util.PerformanceWarning):
                cupy.concatenate(arrs)
        else:
            with pytest.raises(ValueError):
                cupy.concatenate(arrs)

    @testing.for_all_dtypes(name='dtype')
    @testing.numpy_cupy_array_equal()
    def test_concatenate_f_contiguous(self, xp, dtype):
        a = testing.shaped_arange((2, 3, 4), xp, dtype)
        b = testing.shaped_arange((2, 3, 2), xp, dtype).T
        c = testing.shaped_arange((2, 3, 3), xp, dtype)
        return xp.concatenate((a, b, c), axis=-1)

    @testing.for_all_dtypes(name='dtype')
    @testing.numpy_cupy_array_equal()
    def test_concatenate_large_f_contiguous(self, xp, dtype):
        a = testing.shaped_arange((2, 3, 4), xp, dtype)
        b = testing.shaped_arange((2, 3, 2), xp, dtype).T
        c = testing.shaped_arange((2, 3, 3), xp, dtype)
        d = testing.shaped_arange((2, 3, 2), xp, dtype).T
        e = testing.shaped_arange((2, 3, 2), xp, dtype)
        return xp.concatenate((a, b, c, d, e) * 2, axis=-1)

    @testing.numpy_cupy_array_equal()
    def test_concatenate_many_multi_dtype(self, xp):
        a = testing.shaped_arange((2, 1), xp, 'i')
        b = testing.shaped_arange((2, 1), xp, 'f')
        return xp.concatenate((a, b) * 1024, axis=1)

    @testing.slow
    def test_concatenate_32bit_boundary(self):
        a = cupy.zeros((2 ** 30,), dtype=cupy.int8)
        b = cupy.zeros((2 ** 30,), dtype=cupy.int8)
        ret = cupy.concatenate([a, b])
        del a
        del b
        del ret
        # Free huge memory for slow test
        cupy.get_default_memory_pool().free_all_blocks()

    def test_concatenate_wrong_ndim(self):
        a = cupy.empty((2, 3))
        b = cupy.empty((2,))
        with pytest.raises(ValueError):
            cupy.concatenate((a, b))

    def test_concatenate_wrong_shape(self):
        a = cupy.empty((2, 3, 4))
        b = cupy.empty((3, 3, 4))
        c = cupy.empty((4, 4, 4))
        with pytest.raises(ValueError):
            cupy.concatenate((a, b, c))

    @testing.for_all_dtypes(name='dtype')
    @testing.numpy_cupy_array_equal()
    def test_concatenate_out(self, xp, dtype):
        a = testing.shaped_arange((3, 4), xp, dtype)
        b = testing.shaped_reverse_arange((3, 4), xp, dtype)
        c = testing.shaped_arange((3, 4), xp, dtype)
        out = xp.zeros((3, 12), dtype=dtype)
        xp.concatenate((a, b, c), axis=1, out=out)
        return out

    @testing.numpy_cupy_array_equal()
    def test_concatenate_out_same_kind(self, xp):
        a = testing.shaped_arange((3, 4), xp, xp.float64)
        b = testing.shaped_reverse_arange((3, 4), xp, xp.float64)
        c = testing.shaped_arange((3, 4), xp, xp.float64)
        out = xp.zeros((3, 12), dtype=xp.float32)
        xp.concatenate((a, b, c), axis=1, out=out)
        return out

    def test_concatenate_out_invalid_shape(self):
        for xp in (numpy, cupy):
            a = testing.shaped_arange((3, 4), xp, xp.float64)
            b = testing.shaped_reverse_arange((3, 4), xp, xp.float64)
            c = testing.shaped_arange((3, 4), xp, xp.float64)
            out = xp.zeros((4, 10), dtype=xp.float64)
            with pytest.raises(ValueError):
                xp.concatenate((a, b, c), axis=1, out=out)

    def test_concatenate_out_invalid_shape_2(self):
        for xp in (numpy, cupy):
            a = testing.shaped_arange((3, 4), xp, xp.float64)
            b = testing.shaped_reverse_arange((3, 4), xp, xp.float64)
            c = testing.shaped_arange((3, 4), xp, xp.float64)
            out = xp.zeros((2, 2, 10), dtype=xp.float64)
            with pytest.raises(ValueError):
                xp.concatenate((a, b, c), axis=1, out=out)

    def test_concatenate_out_invalid_dtype(self):
        for xp in (numpy, cupy):
            a = testing.shaped_arange((3, 4), xp, xp.float64)
            b = testing.shaped_reverse_arange((3, 4), xp, xp.float64)
            c = testing.shaped_arange((3, 4), xp, xp.float64)
            out = xp.zeros((3, 12), dtype=xp.int64)
            with pytest.raises(TypeError):
                xp.concatenate((a, b, c), axis=1, out=out)

    @testing.for_all_dtypes_combination(names=['dtype1', 'dtype2'])
    @testing.numpy_cupy_array_equal()
    def test_concatenate_different_dtype(self, xp, dtype1, dtype2):
        a = testing.shaped_arange((3, 4), xp, dtype1)
        b = testing.shaped_arange((3, 4), xp, dtype2)
        return xp.concatenate((a, b))

    @testing.for_all_dtypes_combination(names=['dtype1', 'dtype2'])
    @testing.numpy_cupy_array_equal(accept_error=TypeError)
    def test_concatenate_out_different_dtype(self, xp, dtype1, dtype2):
        a = testing.shaped_arange((3, 4), xp, dtype1)
        b = testing.shaped_arange((3, 4), xp, dtype1)
        out = xp.zeros((6, 4), dtype=dtype2)
        return xp.concatenate((a, b), out=out)

    @testing.with_requires('numpy>=1.20.0')
    @testing.for_all_dtypes_combination(names=['dtype1', 'dtype2'])
    @testing.numpy_cupy_array_equal(accept_error=TypeError)
    def test_concatenate_dtype(self, xp, dtype1, dtype2):
        a = testing.shaped_arange((3, 4), xp, dtype1)
        b = testing.shaped_arange((3, 4), xp, dtype1)
        return xp.concatenate((a, b), dtype=dtype2)

    @testing.with_requires('numpy>=1.20.0')
    def test_concatenate_dtype_invalid_out(self):
        for xp in (numpy, cupy):
            a = testing.shaped_arange((3, 4), xp, xp.float64)
            b = testing.shaped_arange((3, 4), xp, xp.float64)
            out = xp.zeros((6, 4), dtype=xp.int64)
            with pytest.raises(TypeError):
                xp.concatenate((a, b), out=out, dtype=xp.int64)

    @testing.with_requires('numpy<2.0')
    @pytest.mark.filterwarnings('error::cupy.exceptions.ComplexWarning')
    @pytest.mark.parametrize('casting', [
        'no',
        'equiv',
        'safe',
        'same_kind',
        'unsafe',
    ])
    @testing.for_all_dtypes_combination(names=['dtype1', 'dtype2'])
    @testing.numpy_cupy_array_equal(
        accept_error=(TypeError, cupy.exceptions.ComplexWarning))
    def test_concatenate_casting(self, xp, dtype1, dtype2, casting):
        a = testing.shaped_arange((3, 4), xp, dtype1)
        b = testing.shaped_arange((3, 4), xp, dtype1)
        # may raise TypeError or ComplexWarning
        return xp.concatenate((a, b), dtype=dtype2, casting=casting)

    @testing.numpy_cupy_array_equal()
    def test_dstack(self, xp):
        a = testing.shaped_arange((1, 3, 2), xp)
        b = testing.shaped_arange((3,), xp)
        c = testing.shaped_arange((1, 3), xp)
        return xp.dstack((a, b, c))

    @testing.numpy_cupy_array_equal()
    def test_dstack_single_element(self, xp):
        a = testing.shaped_arange((1, 2, 3), xp)
        return xp.dstack((a,))

    @testing.numpy_cupy_array_equal()
    def test_dstack_single_element_2(self, xp):
        a = testing.shaped_arange((1, 2), xp)
        return xp.dstack((a,))

    @testing.numpy_cupy_array_equal()
    def test_dstack_single_element_3(self, xp):
        a = testing.shaped_arange((1,), xp)
        return xp.dstack((a,))

    @testing.numpy_cupy_array_equal()
    def test_hstack_vectors(self, xp):
        a = xp.arange(3)
        b = xp.arange(2, -1, -1)
        return xp.hstack((a, b))

    @testing.numpy_cupy_array_equal()
    def test_hstack_scalars(self, xp):
        a = testing.shaped_arange((), xp)
        b = testing.shaped_arange((), xp)
        c = testing.shaped_arange((), xp)
        return xp.hstack((a, b, c))

    @testing.numpy_cupy_array_equal()
    def test_hstack(self, xp):
        a = testing.shaped_arange((2, 1), xp)
        b = testing.shaped_arange((2, 2), xp)
        c = testing.shaped_arange((2, 3), xp)
        return xp.hstack((a, b, c))

    @testing.with_requires('numpy>=1.24.0')
    @testing.for_all_dtypes_combination(names=['dtype1', 'dtype2'])
    @testing.numpy_cupy_array_equal(accept_error=TypeError)
    def test_hstack_dtype(self, xp, dtype1, dtype2):
        a = testing.shaped_arange((3, 4), xp, dtype1)
        b = testing.shaped_arange((3, 4), xp, dtype1)
        return xp.hstack((a, b), dtype=dtype2)

    @testing.with_requires('numpy>=1.24.0', 'numpy<2.0')
    @pytest.mark.filterwarnings('error::cupy.exceptions.ComplexWarning')
    @pytest.mark.parametrize('casting', [
        'no',
        'equiv',
        'safe',
        'same_kind',
        'unsafe',
    ])
    @testing.for_all_dtypes_combination(names=['dtype1', 'dtype2'])
    @testing.numpy_cupy_array_equal(
        accept_error=(TypeError, cupy.exceptions.ComplexWarning))
    def test_hstack_casting(self, xp, dtype1, dtype2, casting):
        a = testing.shaped_arange((3, 4), xp, dtype1)
        b = testing.shaped_arange((3, 4), xp, dtype1)
        # may raise TypeError or ComplexWarning
        return xp.hstack((a, b), dtype=dtype2, casting=casting)

    @testing.numpy_cupy_array_equal()
    def test_vstack_vectors(self, xp):
        a = xp.arange(3)
        b = xp.arange(2, -1, -1)
        return xp.vstack((a, b))

    @testing.numpy_cupy_array_equal()
    def test_vstack_single_element(self, xp):
        a = xp.arange(3)
        return xp.vstack((a,))

    def test_vstack_wrong_ndim(self):
        a = cupy.empty((3,))
        b = cupy.empty((3, 1))
        with pytest.raises(ValueError):
            cupy.vstack((a, b))

    @testing.with_requires('numpy>=1.24.0')
    @testing.for_all_dtypes_combination(names=['dtype1', 'dtype2'])
    @testing.numpy_cupy_array_equal(accept_error=TypeError)
    def test_vstack_dtype(self, xp, dtype1, dtype2):
        a = testing.shaped_arange((3, 4), xp, dtype1)
        b = testing.shaped_arange((3, 4), xp, dtype1)
        return xp.vstack((a, b), dtype=dtype2)

    @testing.with_requires('numpy>=1.24.0', 'numpy<2.0')
    @pytest.mark.filterwarnings('error::cupy.exceptions.ComplexWarning')
    @pytest.mark.parametrize('casting', [
        'no',
        'equiv',
        'safe',
        'same_kind',
        'unsafe',
    ])
    @testing.for_all_dtypes_combination(names=['dtype1', 'dtype2'])
    @testing.numpy_cupy_array_equal(
        accept_error=(TypeError, cupy.exceptions.ComplexWarning))
    def test_vstack_casting(self, xp, dtype1, dtype2, casting):
        a = testing.shaped_arange((3, 4), xp, dtype1)
        b = testing.shaped_arange((3, 4), xp, dtype1)
        # may raise TypeError or ComplexWarning
        return xp.vstack((a, b), dtype=dtype2, casting=casting)

    @testing.numpy_cupy_array_equal()
    def test_stack(self, xp):
        a = testing.shaped_arange((2, 3), xp)
        b = testing.shaped_arange((2, 3), xp)
        c = testing.shaped_arange((2, 3), xp)
        return xp.stack((a, b, c))

    def test_stack_value(self):
        a = testing.shaped_arange((2, 3), cupy)
        b = testing.shaped_arange((2, 3), cupy)
        c = testing.shaped_arange((2, 3), cupy)
        s = cupy.stack((a, b, c))
        assert s.shape == (3, 2, 3)
        cupy.testing.assert_array_equal(s[0], a)
        cupy.testing.assert_array_equal(s[1], b)
        cupy.testing.assert_array_equal(s[2], c)

    @testing.numpy_cupy_array_equal()
    def test_stack_with_axis1(self, xp):
        a = testing.shaped_arange((2, 3), xp)
        return xp.stack((a, a), axis=1)

    @testing.numpy_cupy_array_equal()
    def test_stack_with_axis2(self, xp):
        a = testing.shaped_arange((2, 3), xp)
        return xp.stack((a, a), axis=2)

    def test_stack_with_axis_over(self):
        for xp in (numpy, cupy):
            a = testing.shaped_arange((2, 3), xp)
            with pytest.raises(ValueError):
                xp.stack((a, a), axis=3)

    def test_stack_with_axis_value(self):
        a = testing.shaped_arange((2, 3), cupy)
        s = cupy.stack((a, a), axis=1)

        assert s.shape == (2, 2, 3)
        cupy.testing.assert_array_equal(s[:, 0, :], a)
        cupy.testing.assert_array_equal(s[:, 1, :], a)

    @testing.numpy_cupy_array_equal()
    def test_stack_with_negative_axis(self, xp):
        a = testing.shaped_arange((2, 3), xp)
        return xp.stack((a, a), axis=-1)

    def test_stack_with_negative_axis_value(self):
        a = testing.shaped_arange((2, 3), cupy)
        s = cupy.stack((a, a), axis=-1)

        assert s.shape == (2, 3, 2)
        cupy.testing.assert_array_equal(s[:, :, 0], a)
        cupy.testing.assert_array_equal(s[:, :, 1], a)

    def test_stack_different_shape(self):
        for xp in (numpy, cupy):
            a = testing.shaped_arange((2, 3), xp)
            b = testing.shaped_arange((2, 4), xp)
            with pytest.raises(ValueError):
                xp.stack([a, b])

    def test_stack_out_of_bounds1(self):
        for xp in (numpy, cupy):
            a = testing.shaped_arange((2, 3), xp)
            with pytest.raises(ValueError):
                xp.stack([a, a], axis=3)

    def test_stack_out_of_bounds2(self):
        a = testing.shaped_arange((2, 3), cupy)
        with pytest.raises(AxisError):
            return cupy.stack([a, a], axis=3)

    @testing.for_all_dtypes(name='dtype')
    @testing.numpy_cupy_array_equal()
    def test_stack_out(self, xp, dtype):
        a = testing.shaped_arange((3, 4), xp, dtype)
        b = testing.shaped_reverse_arange((3, 4), xp, dtype)
        c = testing.shaped_arange((3, 4), xp, dtype)
        out = xp.zeros((3, 3, 4), dtype=dtype)
        xp.stack((a, b, c), axis=1, out=out)
        return out

    @testing.numpy_cupy_array_equal()
    def test_stack_out_same_kind(self, xp):
        a = testing.shaped_arange((3, 4), xp, xp.float64)
        b = testing.shaped_reverse_arange((3, 4), xp, xp.float64)
        c = testing.shaped_arange((3, 4), xp, xp.float64)
        out = xp.zeros((3, 3, 4), dtype=xp.float32)
        xp.stack((a, b, c), axis=1, out=out)
        return out

    def test_stack_out_invalid_shape(self):
        for xp in (numpy, cupy):
            a = testing.shaped_arange((3, 4), xp, xp.float64)
            b = testing.shaped_reverse_arange((3, 4), xp, xp.float64)
            c = testing.shaped_arange((3, 4), xp, xp.float64)
            out = xp.zeros((3, 3, 10), dtype=xp.float64)
            with pytest.raises(ValueError):
                xp.stack((a, b, c), axis=1, out=out)

    def test_stack_out_invalid_shape_2(self):
        for xp in (numpy, cupy):
            a = testing.shaped_arange((3, 4), xp, xp.float64)
            b = testing.shaped_reverse_arange((3, 4), xp, xp.float64)
            c = testing.shaped_arange((3, 4), xp, xp.float64)
            out = xp.zeros((3, 3, 3, 10), dtype=xp.float64)
            with pytest.raises(ValueError):
                xp.stack((a, b, c), axis=1, out=out)

    def test_stack_out_invalid_dtype(self):
        for xp in (numpy, cupy):
            a = testing.shaped_arange((3, 4), xp, xp.float64)
            b = testing.shaped_reverse_arange((3, 4), xp, xp.float64)
            c = testing.shaped_arange((3, 4), xp, xp.float64)
            out = xp.zeros((3, 3, 4), dtype=xp.int64)
            with pytest.raises(TypeError):
                xp.stack((a, b, c), axis=1, out=out)

    @testing.with_requires('numpy>=1.24.0')
    @testing.for_all_dtypes_combination(names=['dtype1', 'dtype2'])
    @testing.numpy_cupy_array_equal(accept_error=TypeError)
    def test_stack_dtype(self, xp, dtype1, dtype2):
        a = testing.shaped_arange((3, 4), xp, dtype1)
        b = testing.shaped_arange((3, 4), xp, dtype1)
        return xp.stack((a, b), dtype=dtype2)

    @testing.with_requires('numpy>=1.24.0', 'numpy<2.0')
    @pytest.mark.filterwarnings('error::cupy.exceptions.ComplexWarning')
    @pytest.mark.parametrize('casting', [
        'no',
        'equiv',
        'safe',
        'same_kind',
        'unsafe',
    ])
    @testing.for_all_dtypes_combination(names=['dtype1', 'dtype2'])
    @testing.numpy_cupy_array_equal(
        accept_error=(TypeError, cupy.exceptions.ComplexWarning))
    def test_stack_casting(self, xp, dtype1, dtype2, casting):
        a = testing.shaped_arange((3, 4), xp, dtype1)
        b = testing.shaped_arange((3, 4), xp, dtype1)
        # may raise TypeError or ComplexWarning
        return xp.stack((a, b), dtype=dtype2, casting=casting)

    @testing.with_requires("numpy<2.0")
    @testing.for_all_dtypes(name='dtype1')
    @testing.for_all_dtypes(name='dtype2')
    @testing.numpy_cupy_array_equal()
    def test_row_stack(self, xp, dtype1, dtype2):
        a = testing.shaped_arange((4, 3), xp, dtype1)
        b = testing.shaped_arange((3,), xp, dtype2)
        c = testing.shaped_arange((2, 3), xp, dtype1)
        return xp.row_stack((a, b, c))

    def test_row_stack_wrong_ndim1(self):
        a = cupy.zeros(())
        b = cupy.zeros((3,))
        with pytest.raises(ValueError):
            cupy.row_stack((a, b))

    def test_row_stack_wrong_ndim2(self):
        a = cupy.zeros((3, 2, 3))
        b = cupy.zeros((3, 2))
        with pytest.raises(ValueError):
            cupy.row_stack((a, b))

    def test_row_stack_wrong_shape(self):
        a = cupy.zeros((3, 2))
        b = cupy.zeros((4, 3))
        with pytest.raises(ValueError):
            cupy.row_stack((a, b))
